@open-mercato/ui 0.6.6-develop.5876.1.6a45c370ac → 0.6.6-develop.5903.1.33a2be39eb

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.
@@ -5,6 +5,13 @@ import { DayPicker, useDayPicker } from "react-day-picker";
5
5
  import { ChevronLeft, ChevronRight } from "lucide-react";
6
6
  import { format } from "date-fns/format";
7
7
  import { cn } from "@open-mercato/shared/lib/utils";
8
+ const navButtonClassName = cn(
9
+ "h-9 w-9 inline-flex items-center justify-center rounded-md shrink-0",
10
+ "border border-border bg-background text-muted-foreground transition-colors",
11
+ "hover:bg-accent hover:text-accent-foreground hover:border-input",
12
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
13
+ "disabled:opacity-30 disabled:pointer-events-none disabled:hover:bg-background"
14
+ );
8
15
  function MonthNavButton({
9
16
  direction,
10
17
  locale
@@ -23,18 +30,12 @@ function MonthNavButton({
23
30
  onClick: () => {
24
31
  if (target && dayPicker.goToMonth) dayPicker.goToMonth(target);
25
32
  },
26
- className: cn(
27
- "h-9 w-9 inline-flex items-center justify-center rounded-md shrink-0",
28
- "border border-border bg-background text-muted-foreground transition-colors",
29
- "hover:bg-accent hover:text-accent-foreground hover:border-input",
30
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
31
- "disabled:opacity-30 disabled:pointer-events-none disabled:hover:bg-background"
32
- ),
33
+ className: navButtonClassName,
33
34
  children: /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4", "aria-hidden": "true" })
34
35
  }
35
36
  );
36
37
  }
37
- function buildMonthCaption(locale, totalMonths) {
38
+ function buildMonthCaption(locale, totalMonths, onOpenMonthGrid) {
38
39
  return function MonthCaption({
39
40
  calendarMonth,
40
41
  displayIndex
@@ -43,9 +44,23 @@ function buildMonthCaption(locale, totalMonths) {
43
44
  const index = typeof displayIndex === "number" ? displayIndex : 0;
44
45
  const showPrev = index === 0;
45
46
  const showNext = index === totalMonths - 1;
47
+ const labelInteractive = onOpenMonthGrid !== null && index === 0;
46
48
  return /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 mb-3", children: [
47
49
  showPrev ? /* @__PURE__ */ jsx(MonthNavButton, { direction: "prev", locale }) : /* @__PURE__ */ jsx("div", { className: "h-9 w-9 shrink-0", "aria-hidden": "true" }),
48
- /* @__PURE__ */ jsx(
50
+ labelInteractive ? /* @__PURE__ */ jsx(
51
+ "button",
52
+ {
53
+ type: "button",
54
+ onClick: onOpenMonthGrid ?? void 0,
55
+ "aria-label": `${label} \u2013 open month and year navigation`,
56
+ className: cn(
57
+ "flex-1 flex items-center justify-center h-9 rounded-md bg-muted px-3 text-sm font-medium",
58
+ "transition-colors hover:bg-accent hover:text-accent-foreground",
59
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
60
+ ),
61
+ children: label
62
+ }
63
+ ) : /* @__PURE__ */ jsx(
49
64
  "div",
50
65
  {
51
66
  className: "flex-1 flex items-center justify-center h-9 rounded-md bg-muted px-3 text-sm font-medium",
@@ -57,6 +72,92 @@ function buildMonthCaption(locale, totalMonths) {
57
72
  ] });
58
73
  };
59
74
  }
75
+ function MonthGrid({
76
+ initialYear,
77
+ selectedMonth,
78
+ locale,
79
+ onSelectMonth,
80
+ onClose
81
+ }) {
82
+ const [year, setYear] = React.useState(initialYear);
83
+ const today = /* @__PURE__ */ new Date();
84
+ const monthLabels = React.useMemo(
85
+ () => Array.from(
86
+ { length: 12 },
87
+ (_, monthIndex) => format(new Date(year, monthIndex, 1), "MMM", locale ? { locale } : void 0)
88
+ ),
89
+ [year, locale]
90
+ );
91
+ const yearLabel = format(new Date(year, 0, 1), "yyyy", locale ? { locale } : void 0);
92
+ return /* @__PURE__ */ jsxs(
93
+ "div",
94
+ {
95
+ className: "absolute inset-0 z-10 flex flex-col rounded-md bg-popover p-3",
96
+ role: "dialog",
97
+ "aria-label": "Select month and year",
98
+ children: [
99
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 mb-3", children: [
100
+ /* @__PURE__ */ jsx(
101
+ "button",
102
+ {
103
+ type: "button",
104
+ "aria-label": `Go to previous year: ${year - 1}`,
105
+ onClick: () => setYear((current) => current - 1),
106
+ className: navButtonClassName,
107
+ children: /* @__PURE__ */ jsx(ChevronLeft, { className: "h-4 w-4", "aria-hidden": "true" })
108
+ }
109
+ ),
110
+ /* @__PURE__ */ jsx(
111
+ "button",
112
+ {
113
+ type: "button",
114
+ onClick: onClose,
115
+ "aria-label": `${yearLabel} \u2013 back to day selection`,
116
+ className: cn(
117
+ "flex-1 flex items-center justify-center h-9 rounded-md bg-muted px-3 text-sm font-medium",
118
+ "transition-colors hover:bg-accent hover:text-accent-foreground",
119
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
120
+ ),
121
+ children: yearLabel
122
+ }
123
+ ),
124
+ /* @__PURE__ */ jsx(
125
+ "button",
126
+ {
127
+ type: "button",
128
+ "aria-label": `Go to next year: ${year + 1}`,
129
+ onClick: () => setYear((current) => current + 1),
130
+ className: navButtonClassName,
131
+ children: /* @__PURE__ */ jsx(ChevronRight, { className: "h-4 w-4", "aria-hidden": "true" })
132
+ }
133
+ )
134
+ ] }),
135
+ /* @__PURE__ */ jsx("div", { className: "grid grid-cols-3 gap-2", children: monthLabels.map((monthLabel, monthIndex) => {
136
+ const isSelected = selectedMonth.getFullYear() === year && selectedMonth.getMonth() === monthIndex;
137
+ const isCurrentMonth = today.getFullYear() === year && today.getMonth() === monthIndex;
138
+ return /* @__PURE__ */ jsx(
139
+ "button",
140
+ {
141
+ type: "button",
142
+ "aria-pressed": isSelected,
143
+ onClick: () => onSelectMonth(new Date(year, monthIndex, 1)),
144
+ className: cn(
145
+ "h-9 rounded-md text-sm font-normal transition-colors",
146
+ "inline-flex items-center justify-center",
147
+ "hover:bg-accent hover:text-accent-foreground",
148
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
149
+ isCurrentMonth && !isSelected && "font-semibold text-primary",
150
+ isSelected && "!bg-primary !text-primary-foreground hover:!bg-primary"
151
+ ),
152
+ children: monthLabel
153
+ },
154
+ monthIndex
155
+ );
156
+ }) })
157
+ ]
158
+ }
159
+ );
160
+ }
60
161
  function Calendar({
61
162
  className,
62
163
  classNames,
@@ -66,85 +167,135 @@ function Calendar({
66
167
  components,
67
168
  numberOfMonths = 1,
68
169
  pagedNavigation = true,
170
+ month,
171
+ defaultMonth,
172
+ onMonthChange,
69
173
  ...props
70
174
  }) {
71
- const monthCaption = React.useMemo(
72
- () => buildMonthCaption(locale, numberOfMonths),
73
- [locale, numberOfMonths]
175
+ const monthGridEnabled = numberOfMonths === 1;
176
+ const [displayMonth, setDisplayMonth] = React.useState(
177
+ () => month ?? defaultMonth ?? /* @__PURE__ */ new Date()
74
178
  );
75
- return /* @__PURE__ */ jsx(
76
- DayPicker,
77
- {
78
- showOutsideDays,
79
- fixedWeeks,
80
- pagedNavigation,
179
+ const [showMonthGrid, setShowMonthGrid] = React.useState(false);
180
+ React.useEffect(() => {
181
+ if (month) setDisplayMonth(month);
182
+ }, [month]);
183
+ const handleMonthChange = React.useCallback(
184
+ (next) => {
185
+ setDisplayMonth(next);
186
+ onMonthChange?.(next);
187
+ },
188
+ [onMonthChange]
189
+ );
190
+ const handleSelectMonth = React.useCallback(
191
+ (next) => {
192
+ handleMonthChange(next);
193
+ setShowMonthGrid(false);
194
+ },
195
+ [handleMonthChange]
196
+ );
197
+ const monthCaption = React.useMemo(
198
+ () => buildMonthCaption(
81
199
  locale,
82
200
  numberOfMonths,
83
- className: cn("p-3", className),
84
- classNames: {
85
- months: "flex flex-col sm:flex-row gap-4",
86
- month: "space-y-2",
87
- month_caption: "",
88
- caption_label: "sr-only",
89
- nav: "sr-only",
90
- month_grid: "w-full border-collapse",
91
- weekdays: "flex",
92
- weekday: "text-muted-foreground rounded-md w-9 font-normal text-xs",
93
- weeks: "w-full border-collapse",
94
- week: "flex w-full mt-1",
95
- day: "h-9 w-9 text-center text-sm p-0 relative focus-within:relative focus-within:z-20",
96
- day_button: cn(
97
- "h-9 w-9 p-0 font-normal aria-selected:opacity-100",
98
- "inline-flex items-center justify-center rounded-md text-sm",
99
- "transition-colors focus:outline-none focus-visible:outline-none disabled:pointer-events-none",
100
- // Focus indicator is a soft accent fill instead of a ring overlay — keyboard
101
- // users get a visible cue, but mouse-click focus does not leave a stuck ring
102
- // on top of the selected cell.
103
- "hover:bg-accent hover:text-accent-foreground",
104
- "focus-visible:bg-accent focus-visible:text-accent-foreground"
105
- ),
106
- // React-day-picker v9 applies `classNames.selected` / `range_*` to the day
107
- // CELL (`<td>`) wrapper, not to the inner `<button>`. To keep the parent
108
- // fill visible through interaction, we (a) paint the cell with the desired
109
- // bg/text, and (b) force the inner button to render transparent — so the
110
- // button's own hover/focus-visible bg overrides cannot cover the cell fill.
111
- selected: cn(
112
- "!bg-primary !text-primary-foreground rounded-md",
113
- "[&_button]:!bg-transparent [&_button]:!text-primary-foreground",
114
- "[&_button:hover]:!bg-transparent [&_button:hover]:!text-primary-foreground",
115
- "[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-primary-foreground"
116
- ),
117
- range_start: cn(
118
- "!bg-primary !text-primary-foreground rounded-l-md !rounded-r-none",
119
- "[&_button]:!bg-transparent [&_button]:!text-primary-foreground",
120
- "[&_button:hover]:!bg-transparent [&_button:hover]:!text-primary-foreground",
121
- "[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-primary-foreground"
122
- ),
123
- range_end: cn(
124
- "!bg-primary !text-primary-foreground rounded-r-md !rounded-l-none",
125
- "[&_button]:!bg-transparent [&_button]:!text-primary-foreground",
126
- "[&_button:hover]:!bg-transparent [&_button:hover]:!text-primary-foreground",
127
- "[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-primary-foreground"
128
- ),
129
- range_middle: cn(
130
- "!bg-accent !text-accent-foreground !rounded-none",
131
- "[&_button]:!bg-transparent [&_button]:!text-accent-foreground",
132
- "[&_button:hover]:!bg-transparent [&_button:hover]:!text-accent-foreground",
133
- "[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-accent-foreground"
134
- ),
135
- today: "font-semibold text-primary rounded-md",
136
- outside: "day-outside text-muted-foreground opacity-40 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
137
- disabled: "text-muted-foreground opacity-50",
138
- hidden: "invisible",
139
- ...classNames
140
- },
141
- components: {
142
- MonthCaption: monthCaption,
143
- ...components
144
- },
145
- ...props
146
- }
201
+ monthGridEnabled ? () => setShowMonthGrid(true) : null
202
+ ),
203
+ [locale, numberOfMonths, monthGridEnabled]
147
204
  );
205
+ return /* @__PURE__ */ jsxs("div", { className: monthGridEnabled ? "relative" : "contents", children: [
206
+ /* @__PURE__ */ jsx(
207
+ "div",
208
+ {
209
+ className: monthGridEnabled && showMonthGrid ? "pointer-events-none" : "contents",
210
+ "aria-hidden": monthGridEnabled && showMonthGrid ? true : void 0,
211
+ children: /* @__PURE__ */ jsx(
212
+ DayPicker,
213
+ {
214
+ showOutsideDays,
215
+ fixedWeeks,
216
+ pagedNavigation,
217
+ locale,
218
+ numberOfMonths,
219
+ month: displayMonth,
220
+ onMonthChange: handleMonthChange,
221
+ className: cn("p-3", className),
222
+ classNames: {
223
+ months: "flex flex-col sm:flex-row gap-4",
224
+ month: "space-y-2",
225
+ month_caption: "",
226
+ caption_label: "sr-only",
227
+ nav: "sr-only",
228
+ month_grid: "w-full border-collapse",
229
+ weekdays: "flex",
230
+ weekday: "text-muted-foreground rounded-md w-9 font-normal text-xs",
231
+ weeks: "w-full border-collapse",
232
+ week: "flex w-full mt-1",
233
+ day: "h-9 w-9 text-center text-sm p-0 relative focus-within:relative focus-within:z-20",
234
+ day_button: cn(
235
+ "h-9 w-9 p-0 font-normal aria-selected:opacity-100",
236
+ "inline-flex items-center justify-center rounded-md text-sm",
237
+ "transition-colors focus:outline-none focus-visible:outline-none disabled:pointer-events-none",
238
+ // Focus indicator is a soft accent fill instead of a ring overlay — keyboard
239
+ // users get a visible cue, but mouse-click focus does not leave a stuck ring
240
+ // on top of the selected cell.
241
+ "hover:bg-accent hover:text-accent-foreground",
242
+ "focus-visible:bg-accent focus-visible:text-accent-foreground"
243
+ ),
244
+ // React-day-picker v9 applies `classNames.selected` / `range_*` to the day
245
+ // CELL (`<td>`) wrapper, not to the inner `<button>`. To keep the parent
246
+ // fill visible through interaction, we (a) paint the cell with the desired
247
+ // bg/text, and (b) force the inner button to render transparent — so the
248
+ // button's own hover/focus-visible bg overrides cannot cover the cell fill.
249
+ selected: cn(
250
+ "!bg-primary !text-primary-foreground rounded-md",
251
+ "[&_button]:!bg-transparent [&_button]:!text-primary-foreground",
252
+ "[&_button:hover]:!bg-transparent [&_button:hover]:!text-primary-foreground",
253
+ "[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-primary-foreground"
254
+ ),
255
+ range_start: cn(
256
+ "!bg-primary !text-primary-foreground rounded-l-md !rounded-r-none",
257
+ "[&_button]:!bg-transparent [&_button]:!text-primary-foreground",
258
+ "[&_button:hover]:!bg-transparent [&_button:hover]:!text-primary-foreground",
259
+ "[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-primary-foreground"
260
+ ),
261
+ range_end: cn(
262
+ "!bg-primary !text-primary-foreground rounded-r-md !rounded-l-none",
263
+ "[&_button]:!bg-transparent [&_button]:!text-primary-foreground",
264
+ "[&_button:hover]:!bg-transparent [&_button:hover]:!text-primary-foreground",
265
+ "[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-primary-foreground"
266
+ ),
267
+ range_middle: cn(
268
+ "!bg-accent !text-accent-foreground !rounded-none",
269
+ "[&_button]:!bg-transparent [&_button]:!text-accent-foreground",
270
+ "[&_button:hover]:!bg-transparent [&_button:hover]:!text-accent-foreground",
271
+ "[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-accent-foreground"
272
+ ),
273
+ today: "font-semibold text-primary rounded-md",
274
+ outside: "day-outside text-muted-foreground opacity-40 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
275
+ disabled: "text-muted-foreground opacity-50",
276
+ hidden: "invisible",
277
+ ...classNames
278
+ },
279
+ components: {
280
+ MonthCaption: monthCaption,
281
+ ...components
282
+ },
283
+ ...props
284
+ }
285
+ )
286
+ }
287
+ ),
288
+ monthGridEnabled && showMonthGrid ? /* @__PURE__ */ jsx(
289
+ MonthGrid,
290
+ {
291
+ initialYear: displayMonth.getFullYear(),
292
+ selectedMonth: displayMonth,
293
+ locale,
294
+ onSelectMonth: handleSelectMonth,
295
+ onClose: () => setShowMonthGrid(false)
296
+ }
297
+ ) : null
298
+ ] });
148
299
  }
149
300
  export {
150
301
  Calendar
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/primitives/calendar.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { DayPicker, useDayPicker } from 'react-day-picker'\nimport type { DayPickerProps, CalendarMonth } from 'react-day-picker'\nimport { ChevronLeft, ChevronRight } from 'lucide-react'\nimport { format } from 'date-fns/format'\nimport type { Locale } from 'date-fns/locale'\nimport { cn } from '@open-mercato/shared/lib/utils'\n\nexport type CalendarProps = DayPickerProps\n\nfunction MonthNavButton({\n direction,\n locale,\n}: {\n direction: 'prev' | 'next'\n locale?: Locale\n}) {\n const dayPicker = useDayPicker() as unknown as {\n previousMonth?: Date\n nextMonth?: Date\n goToMonth?: (month: Date) => void\n }\n const target = direction === 'prev' ? dayPicker.previousMonth : dayPicker.nextMonth\n const Icon = direction === 'prev' ? ChevronLeft : ChevronRight\n const targetLabel = format(target ?? new Date(), 'MMMM yyyy', locale ? { locale } : undefined)\n const ariaLabel = `Go to ${direction === 'prev' ? 'previous' : 'next'} month: ${targetLabel}`\n return (\n <button\n type=\"button\"\n disabled={!target}\n aria-label={ariaLabel}\n onClick={() => {\n if (target && dayPicker.goToMonth) dayPicker.goToMonth(target)\n }}\n className={cn(\n 'h-9 w-9 inline-flex items-center justify-center rounded-md shrink-0',\n 'border border-border bg-background text-muted-foreground transition-colors',\n 'hover:bg-accent hover:text-accent-foreground hover:border-input',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n 'disabled:opacity-30 disabled:pointer-events-none disabled:hover:bg-background',\n )}\n >\n <Icon className=\"h-4 w-4\" aria-hidden=\"true\" />\n </button>\n )\n}\n\nfunction buildMonthCaption(locale: Locale | undefined, totalMonths: number) {\n return function MonthCaption({\n calendarMonth,\n displayIndex,\n }: {\n calendarMonth: CalendarMonth\n displayIndex?: number\n }) {\n const label = format(calendarMonth.date, 'MMMM yyyy', locale ? { locale } : undefined)\n const index = typeof displayIndex === 'number' ? displayIndex : 0\n // For multi-month layouts (e.g. range pickers) only the leftmost month\n // exposes the previous-month chevron and only the rightmost exposes the\n // next-month chevron. Navigation is always global across all visible\n // months, so showing both arrows on every month is confusing.\n const showPrev = index === 0\n const showNext = index === totalMonths - 1\n return (\n <div className=\"flex items-center justify-between gap-2 mb-3\">\n {showPrev ? (\n <MonthNavButton direction=\"prev\" locale={locale} />\n ) : (\n <div className=\"h-9 w-9 shrink-0\" aria-hidden=\"true\" />\n )}\n <div\n className=\"flex-1 flex items-center justify-center h-9 rounded-md bg-muted px-3 text-sm font-medium\"\n aria-live=\"polite\"\n >\n {label}\n </div>\n {showNext ? (\n <MonthNavButton direction=\"next\" locale={locale} />\n ) : (\n <div className=\"h-9 w-9 shrink-0\" aria-hidden=\"true\" />\n )}\n </div>\n )\n }\n}\n\nexport function Calendar({\n className,\n classNames,\n showOutsideDays = true,\n fixedWeeks = true,\n locale,\n components,\n numberOfMonths = 1,\n pagedNavigation = true,\n ...props\n}: CalendarProps) {\n const monthCaption = React.useMemo(\n () => buildMonthCaption(locale as Locale | undefined, numberOfMonths),\n [locale, numberOfMonths],\n )\n return (\n <DayPicker\n showOutsideDays={showOutsideDays}\n fixedWeeks={fixedWeeks}\n pagedNavigation={pagedNavigation}\n locale={locale}\n numberOfMonths={numberOfMonths}\n className={cn('p-3', className)}\n classNames={{\n months: 'flex flex-col sm:flex-row gap-4',\n month: 'space-y-2',\n month_caption: '',\n caption_label: 'sr-only',\n nav: 'sr-only',\n month_grid: 'w-full border-collapse',\n weekdays: 'flex',\n weekday: 'text-muted-foreground rounded-md w-9 font-normal text-xs',\n weeks: 'w-full border-collapse',\n week: 'flex w-full mt-1',\n day: 'h-9 w-9 text-center text-sm p-0 relative focus-within:relative focus-within:z-20',\n day_button: cn(\n 'h-9 w-9 p-0 font-normal aria-selected:opacity-100',\n 'inline-flex items-center justify-center rounded-md text-sm',\n 'transition-colors focus:outline-none focus-visible:outline-none disabled:pointer-events-none',\n // Focus indicator is a soft accent fill instead of a ring overlay \u2014 keyboard\n // users get a visible cue, but mouse-click focus does not leave a stuck ring\n // on top of the selected cell.\n 'hover:bg-accent hover:text-accent-foreground',\n 'focus-visible:bg-accent focus-visible:text-accent-foreground',\n ),\n // React-day-picker v9 applies `classNames.selected` / `range_*` to the day\n // CELL (`<td>`) wrapper, not to the inner `<button>`. To keep the parent\n // fill visible through interaction, we (a) paint the cell with the desired\n // bg/text, and (b) force the inner button to render transparent \u2014 so the\n // button's own hover/focus-visible bg overrides cannot cover the cell fill.\n selected: cn(\n '!bg-primary !text-primary-foreground rounded-md',\n '[&_button]:!bg-transparent [&_button]:!text-primary-foreground',\n '[&_button:hover]:!bg-transparent [&_button:hover]:!text-primary-foreground',\n '[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-primary-foreground',\n ),\n range_start: cn(\n '!bg-primary !text-primary-foreground rounded-l-md !rounded-r-none',\n '[&_button]:!bg-transparent [&_button]:!text-primary-foreground',\n '[&_button:hover]:!bg-transparent [&_button:hover]:!text-primary-foreground',\n '[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-primary-foreground',\n ),\n range_end: cn(\n '!bg-primary !text-primary-foreground rounded-r-md !rounded-l-none',\n '[&_button]:!bg-transparent [&_button]:!text-primary-foreground',\n '[&_button:hover]:!bg-transparent [&_button:hover]:!text-primary-foreground',\n '[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-primary-foreground',\n ),\n range_middle: cn(\n '!bg-accent !text-accent-foreground !rounded-none',\n '[&_button]:!bg-transparent [&_button]:!text-accent-foreground',\n '[&_button:hover]:!bg-transparent [&_button:hover]:!text-accent-foreground',\n '[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-accent-foreground',\n ),\n today: 'font-semibold text-primary rounded-md',\n outside:\n 'day-outside text-muted-foreground opacity-40 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30',\n disabled: 'text-muted-foreground opacity-50',\n hidden: 'invisible',\n ...classNames,\n }}\n components={{\n MonthCaption: monthCaption,\n ...components,\n }}\n {...props}\n />\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { DayPicker, useDayPicker } from 'react-day-picker'\nimport type { DayPickerProps, CalendarMonth } from 'react-day-picker'\nimport { ChevronLeft, ChevronRight } from 'lucide-react'\nimport { format } from 'date-fns/format'\nimport type { Locale } from 'date-fns/locale'\nimport { cn } from '@open-mercato/shared/lib/utils'\n\nexport type CalendarProps = DayPickerProps\n\nconst navButtonClassName = cn(\n 'h-9 w-9 inline-flex items-center justify-center rounded-md shrink-0',\n 'border border-border bg-background text-muted-foreground transition-colors',\n 'hover:bg-accent hover:text-accent-foreground hover:border-input',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n 'disabled:opacity-30 disabled:pointer-events-none disabled:hover:bg-background',\n)\n\nfunction MonthNavButton({\n direction,\n locale,\n}: {\n direction: 'prev' | 'next'\n locale?: Locale\n}) {\n const dayPicker = useDayPicker() as unknown as {\n previousMonth?: Date\n nextMonth?: Date\n goToMonth?: (month: Date) => void\n }\n const target = direction === 'prev' ? dayPicker.previousMonth : dayPicker.nextMonth\n const Icon = direction === 'prev' ? ChevronLeft : ChevronRight\n const targetLabel = format(target ?? new Date(), 'MMMM yyyy', locale ? { locale } : undefined)\n const ariaLabel = `Go to ${direction === 'prev' ? 'previous' : 'next'} month: ${targetLabel}`\n return (\n <button\n type=\"button\"\n disabled={!target}\n aria-label={ariaLabel}\n onClick={() => {\n if (target && dayPicker.goToMonth) dayPicker.goToMonth(target)\n }}\n className={navButtonClassName}\n >\n <Icon className=\"h-4 w-4\" aria-hidden=\"true\" />\n </button>\n )\n}\n\nfunction buildMonthCaption(\n locale: Locale | undefined,\n totalMonths: number,\n onOpenMonthGrid: (() => void) | null,\n) {\n return function MonthCaption({\n calendarMonth,\n displayIndex,\n }: {\n calendarMonth: CalendarMonth\n displayIndex?: number\n }) {\n const label = format(calendarMonth.date, 'MMMM yyyy', locale ? { locale } : undefined)\n const index = typeof displayIndex === 'number' ? displayIndex : 0\n // For multi-month layouts (e.g. range pickers) only the leftmost month\n // exposes the previous-month chevron and only the rightmost exposes the\n // next-month chevron. Navigation is always global across all visible\n // months, so showing both arrows on every month is confusing.\n const showPrev = index === 0\n const showNext = index === totalMonths - 1\n // The month grid (fast navigation) is only offered on single-month\n // calendars; on the leftmost caption it owns the click target.\n const labelInteractive = onOpenMonthGrid !== null && index === 0\n return (\n <div className=\"flex items-center justify-between gap-2 mb-3\">\n {showPrev ? (\n <MonthNavButton direction=\"prev\" locale={locale} />\n ) : (\n <div className=\"h-9 w-9 shrink-0\" aria-hidden=\"true\" />\n )}\n {labelInteractive ? (\n <button\n type=\"button\"\n onClick={onOpenMonthGrid ?? undefined}\n aria-label={`${label} \u2013 open month and year navigation`}\n className={cn(\n 'flex-1 flex items-center justify-center h-9 rounded-md bg-muted px-3 text-sm font-medium',\n 'transition-colors hover:bg-accent hover:text-accent-foreground',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n )}\n >\n {label}\n </button>\n ) : (\n <div\n className=\"flex-1 flex items-center justify-center h-9 rounded-md bg-muted px-3 text-sm font-medium\"\n aria-live=\"polite\"\n >\n {label}\n </div>\n )}\n {showNext ? (\n <MonthNavButton direction=\"next\" locale={locale} />\n ) : (\n <div className=\"h-9 w-9 shrink-0\" aria-hidden=\"true\" />\n )}\n </div>\n )\n }\n}\n\nfunction MonthGrid({\n initialYear,\n selectedMonth,\n locale,\n onSelectMonth,\n onClose,\n}: {\n initialYear: number\n selectedMonth: Date\n locale?: Locale\n onSelectMonth: (month: Date) => void\n onClose: () => void\n}) {\n const [year, setYear] = React.useState(initialYear)\n const today = new Date()\n const monthLabels = React.useMemo(\n () =>\n Array.from({ length: 12 }, (_, monthIndex) =>\n format(new Date(year, monthIndex, 1), 'MMM', locale ? { locale } : undefined),\n ),\n [year, locale],\n )\n const yearLabel = format(new Date(year, 0, 1), 'yyyy', locale ? { locale } : undefined)\n return (\n <div\n className=\"absolute inset-0 z-10 flex flex-col rounded-md bg-popover p-3\"\n role=\"dialog\"\n aria-label=\"Select month and year\"\n >\n <div className=\"flex items-center justify-between gap-2 mb-3\">\n <button\n type=\"button\"\n aria-label={`Go to previous year: ${year - 1}`}\n onClick={() => setYear((current) => current - 1)}\n className={navButtonClassName}\n >\n <ChevronLeft className=\"h-4 w-4\" aria-hidden=\"true\" />\n </button>\n <button\n type=\"button\"\n onClick={onClose}\n aria-label={`${yearLabel} \u2013 back to day selection`}\n className={cn(\n 'flex-1 flex items-center justify-center h-9 rounded-md bg-muted px-3 text-sm font-medium',\n 'transition-colors hover:bg-accent hover:text-accent-foreground',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n )}\n >\n {yearLabel}\n </button>\n <button\n type=\"button\"\n aria-label={`Go to next year: ${year + 1}`}\n onClick={() => setYear((current) => current + 1)}\n className={navButtonClassName}\n >\n <ChevronRight className=\"h-4 w-4\" aria-hidden=\"true\" />\n </button>\n </div>\n <div className=\"grid grid-cols-3 gap-2\">\n {monthLabels.map((monthLabel, monthIndex) => {\n const isSelected =\n selectedMonth.getFullYear() === year && selectedMonth.getMonth() === monthIndex\n const isCurrentMonth =\n today.getFullYear() === year && today.getMonth() === monthIndex\n return (\n <button\n key={monthIndex}\n type=\"button\"\n aria-pressed={isSelected}\n onClick={() => onSelectMonth(new Date(year, monthIndex, 1))}\n className={cn(\n 'h-9 rounded-md text-sm font-normal transition-colors',\n 'inline-flex items-center justify-center',\n 'hover:bg-accent hover:text-accent-foreground',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n isCurrentMonth && !isSelected && 'font-semibold text-primary',\n isSelected && '!bg-primary !text-primary-foreground hover:!bg-primary',\n )}\n >\n {monthLabel}\n </button>\n )\n })}\n </div>\n </div>\n )\n}\n\nexport function Calendar({\n className,\n classNames,\n showOutsideDays = true,\n fixedWeeks = true,\n locale,\n components,\n numberOfMonths = 1,\n pagedNavigation = true,\n month,\n defaultMonth,\n onMonthChange,\n ...props\n}: CalendarProps) {\n // The month/year grid (fast navigation) is only meaningful for single-month\n // calendars; multi-month layouts (range pickers) keep the static caption.\n const monthGridEnabled = numberOfMonths === 1\n const [displayMonth, setDisplayMonth] = React.useState<Date>(\n () => month ?? defaultMonth ?? new Date(),\n )\n const [showMonthGrid, setShowMonthGrid] = React.useState(false)\n\n // Honor a controlled `month` prop when consumers drive navigation externally.\n React.useEffect(() => {\n if (month) setDisplayMonth(month)\n }, [month])\n\n const handleMonthChange = React.useCallback(\n (next: Date) => {\n setDisplayMonth(next)\n onMonthChange?.(next)\n },\n [onMonthChange],\n )\n\n const handleSelectMonth = React.useCallback(\n (next: Date) => {\n handleMonthChange(next)\n setShowMonthGrid(false)\n },\n [handleMonthChange],\n )\n\n const monthCaption = React.useMemo(\n () =>\n buildMonthCaption(\n locale as Locale | undefined,\n numberOfMonths,\n monthGridEnabled ? () => setShowMonthGrid(true) : null,\n ),\n [locale, numberOfMonths, monthGridEnabled],\n )\n\n return (\n <div className={monthGridEnabled ? 'relative' : 'contents'}>\n <div\n className={monthGridEnabled && showMonthGrid ? 'pointer-events-none' : 'contents'}\n aria-hidden={monthGridEnabled && showMonthGrid ? true : undefined}\n >\n <DayPicker\n showOutsideDays={showOutsideDays}\n fixedWeeks={fixedWeeks}\n pagedNavigation={pagedNavigation}\n locale={locale}\n numberOfMonths={numberOfMonths}\n month={displayMonth}\n onMonthChange={handleMonthChange}\n className={cn('p-3', className)}\n classNames={{\n months: 'flex flex-col sm:flex-row gap-4',\n month: 'space-y-2',\n month_caption: '',\n caption_label: 'sr-only',\n nav: 'sr-only',\n month_grid: 'w-full border-collapse',\n weekdays: 'flex',\n weekday: 'text-muted-foreground rounded-md w-9 font-normal text-xs',\n weeks: 'w-full border-collapse',\n week: 'flex w-full mt-1',\n day: 'h-9 w-9 text-center text-sm p-0 relative focus-within:relative focus-within:z-20',\n day_button: cn(\n 'h-9 w-9 p-0 font-normal aria-selected:opacity-100',\n 'inline-flex items-center justify-center rounded-md text-sm',\n 'transition-colors focus:outline-none focus-visible:outline-none disabled:pointer-events-none',\n // Focus indicator is a soft accent fill instead of a ring overlay \u2014 keyboard\n // users get a visible cue, but mouse-click focus does not leave a stuck ring\n // on top of the selected cell.\n 'hover:bg-accent hover:text-accent-foreground',\n 'focus-visible:bg-accent focus-visible:text-accent-foreground',\n ),\n // React-day-picker v9 applies `classNames.selected` / `range_*` to the day\n // CELL (`<td>`) wrapper, not to the inner `<button>`. To keep the parent\n // fill visible through interaction, we (a) paint the cell with the desired\n // bg/text, and (b) force the inner button to render transparent \u2014 so the\n // button's own hover/focus-visible bg overrides cannot cover the cell fill.\n selected: cn(\n '!bg-primary !text-primary-foreground rounded-md',\n '[&_button]:!bg-transparent [&_button]:!text-primary-foreground',\n '[&_button:hover]:!bg-transparent [&_button:hover]:!text-primary-foreground',\n '[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-primary-foreground',\n ),\n range_start: cn(\n '!bg-primary !text-primary-foreground rounded-l-md !rounded-r-none',\n '[&_button]:!bg-transparent [&_button]:!text-primary-foreground',\n '[&_button:hover]:!bg-transparent [&_button:hover]:!text-primary-foreground',\n '[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-primary-foreground',\n ),\n range_end: cn(\n '!bg-primary !text-primary-foreground rounded-r-md !rounded-l-none',\n '[&_button]:!bg-transparent [&_button]:!text-primary-foreground',\n '[&_button:hover]:!bg-transparent [&_button:hover]:!text-primary-foreground',\n '[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-primary-foreground',\n ),\n range_middle: cn(\n '!bg-accent !text-accent-foreground !rounded-none',\n '[&_button]:!bg-transparent [&_button]:!text-accent-foreground',\n '[&_button:hover]:!bg-transparent [&_button:hover]:!text-accent-foreground',\n '[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-accent-foreground',\n ),\n today: 'font-semibold text-primary rounded-md',\n outside:\n 'day-outside text-muted-foreground opacity-40 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30',\n disabled: 'text-muted-foreground opacity-50',\n hidden: 'invisible',\n ...classNames,\n }}\n components={{\n MonthCaption: monthCaption,\n ...components,\n }}\n {...props}\n />\n </div>\n {monthGridEnabled && showMonthGrid ? (\n <MonthGrid\n initialYear={displayMonth.getFullYear()}\n selectedMonth={displayMonth}\n locale={locale as Locale | undefined}\n onSelectMonth={handleSelectMonth}\n onClose={() => setShowMonthGrid(false)}\n />\n ) : null}\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/ui",
3
- "version": "0.6.6-develop.5876.1.6a45c370ac",
3
+ "version": "0.6.6-develop.5903.1.33a2be39eb",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -154,13 +154,13 @@
154
154
  "remark-gfm": "^4.0.1"
155
155
  },
156
156
  "peerDependencies": {
157
- "@open-mercato/shared": "0.6.6-develop.5876.1.6a45c370ac",
157
+ "@open-mercato/shared": "0.6.6-develop.5903.1.33a2be39eb",
158
158
  "react": ">=18.0.0",
159
159
  "react-dom": ">=18.0.0",
160
160
  "react-is": ">=18.0.0"
161
161
  },
162
162
  "devDependencies": {
163
- "@open-mercato/shared": "0.6.6-develop.5876.1.6a45c370ac",
163
+ "@open-mercato/shared": "0.6.6-develop.5903.1.33a2be39eb",
164
164
  "@testing-library/dom": "^10.4.1",
165
165
  "@testing-library/jest-dom": "^6.9.1",
166
166
  "@testing-library/react": "^16.3.1",
@@ -178,6 +178,59 @@ describe('DatePicker primitive', () => {
178
178
  }
179
179
  })
180
180
 
181
+ it('toggles the month/year grid when the caption label is clicked', async () => {
182
+ renderWithI18n(<DatePicker value={new Date(2026, 5, 9)} onChange={() => {}} footer="none" />)
183
+ await openPopover()
184
+ // Day grid is visible initially.
185
+ expect(screen.getByRole('grid')).toBeInTheDocument()
186
+ const captionLabel = screen.getByRole('button', {
187
+ name: /June 2026 – open month and year navigation/,
188
+ })
189
+ await act(async () => {
190
+ fireEvent.click(captionLabel)
191
+ })
192
+ // Month grid replaces the day grid; all 12 short month labels are shown.
193
+ expect(screen.queryByRole('grid')).not.toBeInTheDocument()
194
+ for (const label of ['Jan', 'Feb', 'Mar', 'Dec']) {
195
+ expect(screen.getByRole('button', { name: label })).toBeInTheDocument()
196
+ }
197
+ })
198
+
199
+ it('jumps to the chosen month and returns to day selection from the grid', async () => {
200
+ renderWithI18n(<DatePicker value={new Date(2026, 5, 9)} onChange={() => {}} footer="none" />)
201
+ await openPopover()
202
+ await act(async () => {
203
+ fireEvent.click(
204
+ screen.getByRole('button', { name: /June 2026 – open month and year navigation/ }),
205
+ )
206
+ })
207
+ await act(async () => {
208
+ fireEvent.click(screen.getByRole('button', { name: 'Jan' }))
209
+ })
210
+ // Back to the day view, now showing the selected month.
211
+ expect(screen.getByRole('grid')).toBeInTheDocument()
212
+ expect(
213
+ screen.getByRole('button', { name: /January 2026 – open month and year navigation/ }),
214
+ ).toBeInTheDocument()
215
+ })
216
+
217
+ it('navigates years inside the month grid without leaving it', async () => {
218
+ renderWithI18n(<DatePicker value={new Date(2026, 5, 9)} onChange={() => {}} footer="none" />)
219
+ await openPopover()
220
+ await act(async () => {
221
+ fireEvent.click(
222
+ screen.getByRole('button', { name: /June 2026 – open month and year navigation/ }),
223
+ )
224
+ })
225
+ expect(screen.getByRole('button', { name: /2026 – back to day selection/ })).toBeInTheDocument()
226
+ await act(async () => {
227
+ fireEvent.click(screen.getByRole('button', { name: 'Go to next year: 2027' }))
228
+ })
229
+ expect(screen.getByRole('button', { name: /2027 – back to day selection/ })).toBeInTheDocument()
230
+ // Still in the grid, no day grid yet.
231
+ expect(screen.queryByRole('grid')).not.toBeInTheDocument()
232
+ })
233
+
181
234
  it('opens on the month of the selected value, not the current month (regression)', async () => {
182
235
  // A value far from any plausible "today" so the assertion stays deterministic.
183
236
  // react-day-picker v10 derives the initial month from `month`/`defaultMonth`
@@ -10,6 +10,14 @@ import { cn } from '@open-mercato/shared/lib/utils'
10
10
 
11
11
  export type CalendarProps = DayPickerProps
12
12
 
13
+ const navButtonClassName = cn(
14
+ 'h-9 w-9 inline-flex items-center justify-center rounded-md shrink-0',
15
+ 'border border-border bg-background text-muted-foreground transition-colors',
16
+ 'hover:bg-accent hover:text-accent-foreground hover:border-input',
17
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
18
+ 'disabled:opacity-30 disabled:pointer-events-none disabled:hover:bg-background',
19
+ )
20
+
13
21
  function MonthNavButton({
14
22
  direction,
15
23
  locale,
@@ -34,20 +42,18 @@ function MonthNavButton({
34
42
  onClick={() => {
35
43
  if (target && dayPicker.goToMonth) dayPicker.goToMonth(target)
36
44
  }}
37
- className={cn(
38
- 'h-9 w-9 inline-flex items-center justify-center rounded-md shrink-0',
39
- 'border border-border bg-background text-muted-foreground transition-colors',
40
- 'hover:bg-accent hover:text-accent-foreground hover:border-input',
41
- 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
42
- 'disabled:opacity-30 disabled:pointer-events-none disabled:hover:bg-background',
43
- )}
45
+ className={navButtonClassName}
44
46
  >
45
47
  <Icon className="h-4 w-4" aria-hidden="true" />
46
48
  </button>
47
49
  )
48
50
  }
49
51
 
50
- function buildMonthCaption(locale: Locale | undefined, totalMonths: number) {
52
+ function buildMonthCaption(
53
+ locale: Locale | undefined,
54
+ totalMonths: number,
55
+ onOpenMonthGrid: (() => void) | null,
56
+ ) {
51
57
  return function MonthCaption({
52
58
  calendarMonth,
53
59
  displayIndex,
@@ -63,6 +69,9 @@ function buildMonthCaption(locale: Locale | undefined, totalMonths: number) {
63
69
  // months, so showing both arrows on every month is confusing.
64
70
  const showPrev = index === 0
65
71
  const showNext = index === totalMonths - 1
72
+ // The month grid (fast navigation) is only offered on single-month
73
+ // calendars; on the leftmost caption it owns the click target.
74
+ const labelInteractive = onOpenMonthGrid !== null && index === 0
66
75
  return (
67
76
  <div className="flex items-center justify-between gap-2 mb-3">
68
77
  {showPrev ? (
@@ -70,12 +79,27 @@ function buildMonthCaption(locale: Locale | undefined, totalMonths: number) {
70
79
  ) : (
71
80
  <div className="h-9 w-9 shrink-0" aria-hidden="true" />
72
81
  )}
73
- <div
74
- className="flex-1 flex items-center justify-center h-9 rounded-md bg-muted px-3 text-sm font-medium"
75
- aria-live="polite"
76
- >
77
- {label}
78
- </div>
82
+ {labelInteractive ? (
83
+ <button
84
+ type="button"
85
+ onClick={onOpenMonthGrid ?? undefined}
86
+ aria-label={`${label} – open month and year navigation`}
87
+ className={cn(
88
+ 'flex-1 flex items-center justify-center h-9 rounded-md bg-muted px-3 text-sm font-medium',
89
+ 'transition-colors hover:bg-accent hover:text-accent-foreground',
90
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
91
+ )}
92
+ >
93
+ {label}
94
+ </button>
95
+ ) : (
96
+ <div
97
+ className="flex-1 flex items-center justify-center h-9 rounded-md bg-muted px-3 text-sm font-medium"
98
+ aria-live="polite"
99
+ >
100
+ {label}
101
+ </div>
102
+ )}
79
103
  {showNext ? (
80
104
  <MonthNavButton direction="next" locale={locale} />
81
105
  ) : (
@@ -86,6 +110,95 @@ function buildMonthCaption(locale: Locale | undefined, totalMonths: number) {
86
110
  }
87
111
  }
88
112
 
113
+ function MonthGrid({
114
+ initialYear,
115
+ selectedMonth,
116
+ locale,
117
+ onSelectMonth,
118
+ onClose,
119
+ }: {
120
+ initialYear: number
121
+ selectedMonth: Date
122
+ locale?: Locale
123
+ onSelectMonth: (month: Date) => void
124
+ onClose: () => void
125
+ }) {
126
+ const [year, setYear] = React.useState(initialYear)
127
+ const today = new Date()
128
+ const monthLabels = React.useMemo(
129
+ () =>
130
+ Array.from({ length: 12 }, (_, monthIndex) =>
131
+ format(new Date(year, monthIndex, 1), 'MMM', locale ? { locale } : undefined),
132
+ ),
133
+ [year, locale],
134
+ )
135
+ const yearLabel = format(new Date(year, 0, 1), 'yyyy', locale ? { locale } : undefined)
136
+ return (
137
+ <div
138
+ className="absolute inset-0 z-10 flex flex-col rounded-md bg-popover p-3"
139
+ role="dialog"
140
+ aria-label="Select month and year"
141
+ >
142
+ <div className="flex items-center justify-between gap-2 mb-3">
143
+ <button
144
+ type="button"
145
+ aria-label={`Go to previous year: ${year - 1}`}
146
+ onClick={() => setYear((current) => current - 1)}
147
+ className={navButtonClassName}
148
+ >
149
+ <ChevronLeft className="h-4 w-4" aria-hidden="true" />
150
+ </button>
151
+ <button
152
+ type="button"
153
+ onClick={onClose}
154
+ aria-label={`${yearLabel} – back to day selection`}
155
+ className={cn(
156
+ 'flex-1 flex items-center justify-center h-9 rounded-md bg-muted px-3 text-sm font-medium',
157
+ 'transition-colors hover:bg-accent hover:text-accent-foreground',
158
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
159
+ )}
160
+ >
161
+ {yearLabel}
162
+ </button>
163
+ <button
164
+ type="button"
165
+ aria-label={`Go to next year: ${year + 1}`}
166
+ onClick={() => setYear((current) => current + 1)}
167
+ className={navButtonClassName}
168
+ >
169
+ <ChevronRight className="h-4 w-4" aria-hidden="true" />
170
+ </button>
171
+ </div>
172
+ <div className="grid grid-cols-3 gap-2">
173
+ {monthLabels.map((monthLabel, monthIndex) => {
174
+ const isSelected =
175
+ selectedMonth.getFullYear() === year && selectedMonth.getMonth() === monthIndex
176
+ const isCurrentMonth =
177
+ today.getFullYear() === year && today.getMonth() === monthIndex
178
+ return (
179
+ <button
180
+ key={monthIndex}
181
+ type="button"
182
+ aria-pressed={isSelected}
183
+ onClick={() => onSelectMonth(new Date(year, monthIndex, 1))}
184
+ className={cn(
185
+ 'h-9 rounded-md text-sm font-normal transition-colors',
186
+ 'inline-flex items-center justify-center',
187
+ 'hover:bg-accent hover:text-accent-foreground',
188
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
189
+ isCurrentMonth && !isSelected && 'font-semibold text-primary',
190
+ isSelected && '!bg-primary !text-primary-foreground hover:!bg-primary',
191
+ )}
192
+ >
193
+ {monthLabel}
194
+ </button>
195
+ )
196
+ })}
197
+ </div>
198
+ </div>
199
+ )
200
+ }
201
+
89
202
  export function Calendar({
90
203
  className,
91
204
  classNames,
@@ -95,83 +208,139 @@ export function Calendar({
95
208
  components,
96
209
  numberOfMonths = 1,
97
210
  pagedNavigation = true,
211
+ month,
212
+ defaultMonth,
213
+ onMonthChange,
98
214
  ...props
99
215
  }: CalendarProps) {
216
+ // The month/year grid (fast navigation) is only meaningful for single-month
217
+ // calendars; multi-month layouts (range pickers) keep the static caption.
218
+ const monthGridEnabled = numberOfMonths === 1
219
+ const [displayMonth, setDisplayMonth] = React.useState<Date>(
220
+ () => month ?? defaultMonth ?? new Date(),
221
+ )
222
+ const [showMonthGrid, setShowMonthGrid] = React.useState(false)
223
+
224
+ // Honor a controlled `month` prop when consumers drive navigation externally.
225
+ React.useEffect(() => {
226
+ if (month) setDisplayMonth(month)
227
+ }, [month])
228
+
229
+ const handleMonthChange = React.useCallback(
230
+ (next: Date) => {
231
+ setDisplayMonth(next)
232
+ onMonthChange?.(next)
233
+ },
234
+ [onMonthChange],
235
+ )
236
+
237
+ const handleSelectMonth = React.useCallback(
238
+ (next: Date) => {
239
+ handleMonthChange(next)
240
+ setShowMonthGrid(false)
241
+ },
242
+ [handleMonthChange],
243
+ )
244
+
100
245
  const monthCaption = React.useMemo(
101
- () => buildMonthCaption(locale as Locale | undefined, numberOfMonths),
102
- [locale, numberOfMonths],
246
+ () =>
247
+ buildMonthCaption(
248
+ locale as Locale | undefined,
249
+ numberOfMonths,
250
+ monthGridEnabled ? () => setShowMonthGrid(true) : null,
251
+ ),
252
+ [locale, numberOfMonths, monthGridEnabled],
103
253
  )
254
+
104
255
  return (
105
- <DayPicker
106
- showOutsideDays={showOutsideDays}
107
- fixedWeeks={fixedWeeks}
108
- pagedNavigation={pagedNavigation}
109
- locale={locale}
110
- numberOfMonths={numberOfMonths}
111
- className={cn('p-3', className)}
112
- classNames={{
113
- months: 'flex flex-col sm:flex-row gap-4',
114
- month: 'space-y-2',
115
- month_caption: '',
116
- caption_label: 'sr-only',
117
- nav: 'sr-only',
118
- month_grid: 'w-full border-collapse',
119
- weekdays: 'flex',
120
- weekday: 'text-muted-foreground rounded-md w-9 font-normal text-xs',
121
- weeks: 'w-full border-collapse',
122
- week: 'flex w-full mt-1',
123
- day: 'h-9 w-9 text-center text-sm p-0 relative focus-within:relative focus-within:z-20',
124
- day_button: cn(
125
- 'h-9 w-9 p-0 font-normal aria-selected:opacity-100',
126
- 'inline-flex items-center justify-center rounded-md text-sm',
127
- 'transition-colors focus:outline-none focus-visible:outline-none disabled:pointer-events-none',
128
- // Focus indicator is a soft accent fill instead of a ring overlay — keyboard
129
- // users get a visible cue, but mouse-click focus does not leave a stuck ring
130
- // on top of the selected cell.
131
- 'hover:bg-accent hover:text-accent-foreground',
132
- 'focus-visible:bg-accent focus-visible:text-accent-foreground',
133
- ),
134
- // React-day-picker v9 applies `classNames.selected` / `range_*` to the day
135
- // CELL (`<td>`) wrapper, not to the inner `<button>`. To keep the parent
136
- // fill visible through interaction, we (a) paint the cell with the desired
137
- // bg/text, and (b) force the inner button to render transparent — so the
138
- // button's own hover/focus-visible bg overrides cannot cover the cell fill.
139
- selected: cn(
140
- '!bg-primary !text-primary-foreground rounded-md',
141
- '[&_button]:!bg-transparent [&_button]:!text-primary-foreground',
142
- '[&_button:hover]:!bg-transparent [&_button:hover]:!text-primary-foreground',
143
- '[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-primary-foreground',
144
- ),
145
- range_start: cn(
146
- '!bg-primary !text-primary-foreground rounded-l-md !rounded-r-none',
147
- '[&_button]:!bg-transparent [&_button]:!text-primary-foreground',
148
- '[&_button:hover]:!bg-transparent [&_button:hover]:!text-primary-foreground',
149
- '[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-primary-foreground',
150
- ),
151
- range_end: cn(
152
- '!bg-primary !text-primary-foreground rounded-r-md !rounded-l-none',
153
- '[&_button]:!bg-transparent [&_button]:!text-primary-foreground',
154
- '[&_button:hover]:!bg-transparent [&_button:hover]:!text-primary-foreground',
155
- '[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-primary-foreground',
156
- ),
157
- range_middle: cn(
158
- '!bg-accent !text-accent-foreground !rounded-none',
159
- '[&_button]:!bg-transparent [&_button]:!text-accent-foreground',
160
- '[&_button:hover]:!bg-transparent [&_button:hover]:!text-accent-foreground',
161
- '[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-accent-foreground',
162
- ),
163
- today: 'font-semibold text-primary rounded-md',
164
- outside:
165
- 'day-outside text-muted-foreground opacity-40 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30',
166
- disabled: 'text-muted-foreground opacity-50',
167
- hidden: 'invisible',
168
- ...classNames,
169
- }}
170
- components={{
171
- MonthCaption: monthCaption,
172
- ...components,
173
- }}
174
- {...props}
175
- />
256
+ <div className={monthGridEnabled ? 'relative' : 'contents'}>
257
+ <div
258
+ className={monthGridEnabled && showMonthGrid ? 'pointer-events-none' : 'contents'}
259
+ aria-hidden={monthGridEnabled && showMonthGrid ? true : undefined}
260
+ >
261
+ <DayPicker
262
+ showOutsideDays={showOutsideDays}
263
+ fixedWeeks={fixedWeeks}
264
+ pagedNavigation={pagedNavigation}
265
+ locale={locale}
266
+ numberOfMonths={numberOfMonths}
267
+ month={displayMonth}
268
+ onMonthChange={handleMonthChange}
269
+ className={cn('p-3', className)}
270
+ classNames={{
271
+ months: 'flex flex-col sm:flex-row gap-4',
272
+ month: 'space-y-2',
273
+ month_caption: '',
274
+ caption_label: 'sr-only',
275
+ nav: 'sr-only',
276
+ month_grid: 'w-full border-collapse',
277
+ weekdays: 'flex',
278
+ weekday: 'text-muted-foreground rounded-md w-9 font-normal text-xs',
279
+ weeks: 'w-full border-collapse',
280
+ week: 'flex w-full mt-1',
281
+ day: 'h-9 w-9 text-center text-sm p-0 relative focus-within:relative focus-within:z-20',
282
+ day_button: cn(
283
+ 'h-9 w-9 p-0 font-normal aria-selected:opacity-100',
284
+ 'inline-flex items-center justify-center rounded-md text-sm',
285
+ 'transition-colors focus:outline-none focus-visible:outline-none disabled:pointer-events-none',
286
+ // Focus indicator is a soft accent fill instead of a ring overlay — keyboard
287
+ // users get a visible cue, but mouse-click focus does not leave a stuck ring
288
+ // on top of the selected cell.
289
+ 'hover:bg-accent hover:text-accent-foreground',
290
+ 'focus-visible:bg-accent focus-visible:text-accent-foreground',
291
+ ),
292
+ // React-day-picker v9 applies `classNames.selected` / `range_*` to the day
293
+ // CELL (`<td>`) wrapper, not to the inner `<button>`. To keep the parent
294
+ // fill visible through interaction, we (a) paint the cell with the desired
295
+ // bg/text, and (b) force the inner button to render transparent — so the
296
+ // button's own hover/focus-visible bg overrides cannot cover the cell fill.
297
+ selected: cn(
298
+ '!bg-primary !text-primary-foreground rounded-md',
299
+ '[&_button]:!bg-transparent [&_button]:!text-primary-foreground',
300
+ '[&_button:hover]:!bg-transparent [&_button:hover]:!text-primary-foreground',
301
+ '[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-primary-foreground',
302
+ ),
303
+ range_start: cn(
304
+ '!bg-primary !text-primary-foreground rounded-l-md !rounded-r-none',
305
+ '[&_button]:!bg-transparent [&_button]:!text-primary-foreground',
306
+ '[&_button:hover]:!bg-transparent [&_button:hover]:!text-primary-foreground',
307
+ '[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-primary-foreground',
308
+ ),
309
+ range_end: cn(
310
+ '!bg-primary !text-primary-foreground rounded-r-md !rounded-l-none',
311
+ '[&_button]:!bg-transparent [&_button]:!text-primary-foreground',
312
+ '[&_button:hover]:!bg-transparent [&_button:hover]:!text-primary-foreground',
313
+ '[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-primary-foreground',
314
+ ),
315
+ range_middle: cn(
316
+ '!bg-accent !text-accent-foreground !rounded-none',
317
+ '[&_button]:!bg-transparent [&_button]:!text-accent-foreground',
318
+ '[&_button:hover]:!bg-transparent [&_button:hover]:!text-accent-foreground',
319
+ '[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-accent-foreground',
320
+ ),
321
+ today: 'font-semibold text-primary rounded-md',
322
+ outside:
323
+ 'day-outside text-muted-foreground opacity-40 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30',
324
+ disabled: 'text-muted-foreground opacity-50',
325
+ hidden: 'invisible',
326
+ ...classNames,
327
+ }}
328
+ components={{
329
+ MonthCaption: monthCaption,
330
+ ...components,
331
+ }}
332
+ {...props}
333
+ />
334
+ </div>
335
+ {monthGridEnabled && showMonthGrid ? (
336
+ <MonthGrid
337
+ initialYear={displayMonth.getFullYear()}
338
+ selectedMonth={displayMonth}
339
+ locale={locale as Locale | undefined}
340
+ onSelectMonth={handleSelectMonth}
341
+ onClose={() => setShowMonthGrid(false)}
342
+ />
343
+ ) : null}
344
+ </div>
176
345
  )
177
346
  }