@open-mercato/ui 0.6.6-develop.5834.1.dc9eb0615e → 0.6.6-develop.5900.1.b3fb52925e
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:
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
175
|
+
const monthGridEnabled = numberOfMonths === 1;
|
|
176
|
+
const [displayMonth, setDisplayMonth] = React.useState(
|
|
177
|
+
() => month ?? defaultMonth ?? /* @__PURE__ */ new Date()
|
|
74
178
|
);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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": ";
|
|
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.
|
|
3
|
+
"version": "0.6.6-develop.5900.1.b3fb52925e",
|
|
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.
|
|
157
|
+
"@open-mercato/shared": "0.6.6-develop.5900.1.b3fb52925e",
|
|
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.
|
|
163
|
+
"@open-mercato/shared": "0.6.6-develop.5900.1.b3fb52925e",
|
|
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={
|
|
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(
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
() =>
|
|
102
|
-
|
|
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
|
-
<
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
}
|