@mirohq/design-system-calendar 2.4.42 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js CHANGED
@@ -123,6 +123,18 @@ const ClearAction = React.forwardRef(({ "aria-label": ariaLabel, label, ...restP
123
123
  /* @__PURE__ */ jsxRuntime.jsx(designSystemTooltip.Tooltip.Content, { children: label })
124
124
  ] }));
125
125
 
126
+ const PERIOD_TO_VIEW_MODE = {
127
+ quarter: "quarters",
128
+ month: "months"
129
+ };
130
+ const getNavigationYear = (value) => {
131
+ var _a;
132
+ if (value == null) {
133
+ return (/* @__PURE__ */ new Date()).getFullYear();
134
+ }
135
+ const date = "start" in value ? value.start : value;
136
+ return (_a = date == null ? void 0 : date.year) != null ? _a : (/* @__PURE__ */ new Date()).getFullYear();
137
+ };
126
138
  const CalendarContext = React.createContext({});
127
139
  const CalendarProvider = ({
128
140
  children,
@@ -136,7 +148,7 @@ const CalendarProvider = ({
136
148
  onClose,
137
149
  ...restProps
138
150
  }) => {
139
- var _a;
151
+ var _a, _b;
140
152
  const [immediateSave, setImmediateSave] = React.useState(false);
141
153
  const [internalCalendarValue, setInternalCalendarValue] = React.useState(valueProp != null ? valueProp : defaultValueProp);
142
154
  const visibleDuration = React.useMemo(
@@ -150,6 +162,13 @@ const CalendarProvider = ({
150
162
  defaultProp: defaultValueProp,
151
163
  onChange: onValueChange
152
164
  });
165
+ const defaultViewMode = (_a = restProps.period != null ? PERIOD_TO_VIEW_MODE[restProps.period] : void 0) != null ? _a : "days";
166
+ const [viewMode, setViewMode] = React.useState(defaultViewMode);
167
+ const [navigationYear, setNavigationYear] = React.useState(
168
+ () => getNavigationYear(value)
169
+ );
170
+ const valueRef = React.useRef(value);
171
+ valueRef.current = value;
153
172
  const [open = false, setOpen] = reactUseControllableState.useControllableState({
154
173
  prop: openProp,
155
174
  defaultProp: defaultOpenProp,
@@ -157,10 +176,20 @@ const CalendarProvider = ({
157
176
  if (state) {
158
177
  onOpen == null ? void 0 : onOpen();
159
178
  } else {
179
+ setViewMode(defaultViewMode);
160
180
  onClose == null ? void 0 : onClose();
161
181
  }
162
182
  }
163
183
  });
184
+ const valueYear = getNavigationYear(value);
185
+ React.useEffect(() => {
186
+ setNavigationYear(valueYear);
187
+ }, [valueYear]);
188
+ React.useEffect(() => {
189
+ if (open) {
190
+ setNavigationYear(getNavigationYear(valueRef.current));
191
+ }
192
+ }, [open]);
164
193
  const onCalendarDateClick = React.useCallback(
165
194
  (value2) => {
166
195
  if (immediateSave) {
@@ -176,11 +205,15 @@ const CalendarProvider = ({
176
205
  setValue(internalCalendarValue);
177
206
  setOpen(false);
178
207
  };
208
+ const periodImpliesNavigation = restProps.period === "quarter" || restProps.period === "month" || restProps.period === "week";
209
+ const effectivePeriodNavigation = restProps.navigation === true && visibleMonths <= 1 || periodImpliesNavigation;
210
+ const effectiveViewMode = effectivePeriodNavigation ? viewMode : "days";
179
211
  return /* @__PURE__ */ jsxRuntime.jsx(
180
212
  CalendarContext.Provider,
181
213
  {
182
214
  value: {
183
215
  ...restProps,
216
+ navigation: effectivePeriodNavigation,
184
217
  visibleDuration,
185
218
  value,
186
219
  setValue,
@@ -189,8 +222,11 @@ const CalendarProvider = ({
189
222
  onCalendarDateClick,
190
223
  setImmediateSave,
191
224
  onSaveClick,
192
- // value should not be undefined otherwise react-aria calendar will complain in console
193
- calendarValue: (_a = immediateSave ? internalCalendarValue : value) != null ? _a : null
225
+ calendarValue: (_b = immediateSave ? internalCalendarValue : value) != null ? _b : null,
226
+ viewMode: effectiveViewMode,
227
+ setViewMode,
228
+ navigationYear,
229
+ setNavigationYear
194
230
  },
195
231
  children
196
232
  }
@@ -253,6 +289,298 @@ const Trigger = React.forwardRef(
253
289
  }
254
290
  );
255
291
 
292
+ const DECADE_SIZE = 12;
293
+
294
+ const StyledValue = designSystemStitches.styled(designSystemPrimitive.Primitive.span, {});
295
+
296
+ const en = {
297
+ quarter: (q) => "Q".concat(q),
298
+ quarterWithYear: (label, year) => "".concat(label, " ").concat(year),
299
+ views: { months: "months", quarters: "quarters", years: "years" },
300
+ switchView: (title, nextView) => "".concat(title, ", switch to ").concat(nextView, " view"),
301
+ previousYear: "Previous year",
302
+ nextYear: "Next year",
303
+ previousYears: (n) => "Previous ".concat(n, " years"),
304
+ nextYears: (n) => "Next ".concat(n, " years")
305
+ };
306
+ const STRINGS = {
307
+ en,
308
+ de: {
309
+ quarter: (q) => "Q".concat(q),
310
+ quarterWithYear: (label, year) => "".concat(label, " ").concat(year),
311
+ views: { months: "Monate", quarters: "Quartale", years: "Jahre" },
312
+ switchView: (title, nextView) => "".concat(title, ", zur Ansicht ").concat(nextView, " wechseln"),
313
+ previousYear: "Vorheriges Jahr",
314
+ nextYear: "N\xE4chstes Jahr",
315
+ previousYears: (n) => "Vorherige ".concat(n, " Jahre"),
316
+ nextYears: (n) => "N\xE4chste ".concat(n, " Jahre")
317
+ },
318
+ es: {
319
+ quarter: (q) => "T".concat(q),
320
+ quarterWithYear: (label, year) => "".concat(label, " ").concat(year),
321
+ views: { months: "meses", quarters: "trimestres", years: "a\xF1os" },
322
+ switchView: (title, nextView) => "".concat(title, ", cambiar a la vista de ").concat(nextView),
323
+ previousYear: "A\xF1o anterior",
324
+ nextYear: "A\xF1o siguiente",
325
+ previousYears: (n) => "".concat(n, " a\xF1os anteriores"),
326
+ nextYears: (n) => "".concat(n, " a\xF1os siguientes")
327
+ },
328
+ fr: {
329
+ quarter: (q) => "T".concat(q),
330
+ quarterWithYear: (label, year) => "".concat(label, " ").concat(year),
331
+ views: { months: "mois", quarters: "trimestres", years: "ann\xE9es" },
332
+ switchView: (title, nextView) => "".concat(title, ", passer \xE0 la vue ").concat(nextView),
333
+ previousYear: "Ann\xE9e pr\xE9c\xE9dente",
334
+ nextYear: "Ann\xE9e suivante",
335
+ previousYears: (n) => "".concat(n, " ann\xE9es pr\xE9c\xE9dentes"),
336
+ nextYears: (n) => "".concat(n, " ann\xE9es suivantes")
337
+ },
338
+ it: {
339
+ quarter: (q) => "T".concat(q),
340
+ quarterWithYear: (label, year) => "".concat(label, " ").concat(year),
341
+ views: { months: "mesi", quarters: "trimestri", years: "anni" },
342
+ switchView: (title, nextView) => "".concat(title, ", passa alla vista ").concat(nextView),
343
+ previousYear: "Anno precedente",
344
+ nextYear: "Anno successivo",
345
+ previousYears: (n) => "".concat(n, " anni precedenti"),
346
+ nextYears: (n) => "".concat(n, " anni successivi")
347
+ },
348
+ ja: {
349
+ quarter: (q) => "\u7B2C".concat(q, "\u56DB\u534A\u671F"),
350
+ quarterWithYear: (label, year) => "".concat(year, "\u5E74").concat(label),
351
+ views: { months: "\u6708", quarters: "\u56DB\u534A\u671F", years: "\u5E74" },
352
+ switchView: (title, nextView) => "".concat(title, "\u3001").concat(nextView, "\u8868\u793A\u306B\u5207\u308A\u66FF\u3048"),
353
+ previousYear: "\u524D\u5E74",
354
+ nextYear: "\u7FCC\u5E74",
355
+ previousYears: (n) => "\u524D\u306E".concat(n, "\u5E74"),
356
+ nextYears: (n) => "\u6B21\u306E".concat(n, "\u5E74")
357
+ },
358
+ ko: {
359
+ quarter: (q) => "".concat(q, "\uBD84\uAE30"),
360
+ quarterWithYear: (label, year) => "".concat(year, "\uB144 ").concat(label),
361
+ views: { months: "\uC6D4", quarters: "\uBD84\uAE30", years: "\uB144" },
362
+ switchView: (title, nextView) => "".concat(title, ", ").concat(nextView, " \uBCF4\uAE30\uB85C \uC804\uD658"),
363
+ previousYear: "\uC774\uC804 \uD574",
364
+ nextYear: "\uB2E4\uC74C \uD574",
365
+ previousYears: (n) => "\uC774\uC804 ".concat(n, "\uB144"),
366
+ nextYears: (n) => "\uB2E4\uC74C ".concat(n, "\uB144")
367
+ },
368
+ nl: {
369
+ quarter: (q) => "K".concat(q),
370
+ quarterWithYear: (label, year) => "".concat(label, " ").concat(year),
371
+ views: { months: "maanden", quarters: "kwartalen", years: "jaren" },
372
+ switchView: (title, nextView) => "".concat(title, ", ga naar weergave ").concat(nextView),
373
+ previousYear: "Vorig jaar",
374
+ nextYear: "Volgend jaar",
375
+ previousYears: (n) => "Vorige ".concat(n, " jaar"),
376
+ nextYears: (n) => "Volgende ".concat(n, " jaar")
377
+ },
378
+ pl: {
379
+ quarter: (q) => "K".concat(q),
380
+ quarterWithYear: (label, year) => "".concat(label, " ").concat(year),
381
+ views: { months: "miesi\u0105ce", quarters: "kwarta\u0142y", years: "lata" },
382
+ switchView: (title, nextView) => "".concat(title, ", prze\u0142\u0105cz na widok: ").concat(nextView),
383
+ previousYear: "Poprzedni rok",
384
+ nextYear: "Nast\u0119pny rok",
385
+ previousYears: (n) => "Poprzednie ".concat(n, " lat"),
386
+ nextYears: (n) => "Nast\u0119pne ".concat(n, " lat")
387
+ },
388
+ pt: {
389
+ quarter: (q) => "T".concat(q),
390
+ quarterWithYear: (label, year) => "".concat(label, " ").concat(year),
391
+ views: { months: "meses", quarters: "trimestres", years: "anos" },
392
+ switchView: (title, nextView) => "".concat(title, ", mudar para a visualiza\xE7\xE3o de ").concat(nextView),
393
+ previousYear: "Ano anterior",
394
+ nextYear: "Pr\xF3ximo ano",
395
+ previousYears: (n) => "".concat(n, " anos anteriores"),
396
+ nextYears: (n) => "Pr\xF3ximos ".concat(n, " anos")
397
+ },
398
+ ru: {
399
+ quarter: (q) => "".concat(q, " \u043A\u0432."),
400
+ quarterWithYear: (label, year) => "".concat(label, " ").concat(year),
401
+ views: { months: "\u043C\u0435\u0441\u044F\u0446\u044B", quarters: "\u043A\u0432\u0430\u0440\u0442\u0430\u043B\u044B", years: "\u0433\u043E\u0434\u044B" },
402
+ switchView: (title, nextView) => "".concat(title, ", \u043F\u0435\u0440\u0435\u043A\u043B\u044E\u0447\u0438\u0442\u044C\u0441\u044F \u043D\u0430 ").concat(nextView),
403
+ previousYear: "\u041F\u0440\u0435\u0434\u044B\u0434\u0443\u0449\u0438\u0439 \u0433\u043E\u0434",
404
+ nextYear: "\u0421\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0439 \u0433\u043E\u0434",
405
+ previousYears: (n) => "\u041F\u0440\u0435\u0434\u044B\u0434\u0443\u0449\u0438\u0435 ".concat(n, " \u043B\u0435\u0442"),
406
+ nextYears: (n) => "\u0421\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0435 ".concat(n, " \u043B\u0435\u0442")
407
+ },
408
+ tr: {
409
+ quarter: (q) => "\xC7".concat(q),
410
+ quarterWithYear: (label, year) => "".concat(label, " ").concat(year),
411
+ views: { months: "aylar", quarters: "\xE7eyrekler", years: "y\u0131llar" },
412
+ switchView: (title, nextView) => "".concat(title, ", ").concat(nextView, " g\xF6r\xFCn\xFCm\xFCne ge\xE7"),
413
+ previousYear: "\xD6nceki y\u0131l",
414
+ nextYear: "Sonraki y\u0131l",
415
+ previousYears: (n) => "\xD6nceki ".concat(n, " y\u0131l"),
416
+ nextYears: (n) => "Sonraki ".concat(n, " y\u0131l")
417
+ },
418
+ uk: {
419
+ quarter: (q) => "".concat(q, " \u043A\u0432."),
420
+ quarterWithYear: (label, year) => "".concat(label, " ").concat(year),
421
+ views: { months: "\u043C\u0456\u0441\u044F\u0446\u0456", quarters: "\u043A\u0432\u0430\u0440\u0442\u0430\u043B\u0438", years: "\u0440\u043E\u043A\u0438" },
422
+ switchView: (title, nextView) => "".concat(title, ", \u043F\u0435\u0440\u0435\u0439\u0442\u0438 \u0434\u043E ").concat(nextView),
423
+ previousYear: "\u041F\u043E\u043F\u0435\u0440\u0435\u0434\u043D\u0456\u0439 \u0440\u0456\u043A",
424
+ nextYear: "\u041D\u0430\u0441\u0442\u0443\u043F\u043D\u0438\u0439 \u0440\u0456\u043A",
425
+ previousYears: (n) => "\u041F\u043E\u043F\u0435\u0440\u0435\u0434\u043D\u0456 ".concat(n, " \u0440\u043E\u043A\u0456\u0432"),
426
+ nextYears: (n) => "\u041D\u0430\u0441\u0442\u0443\u043F\u043D\u0456 ".concat(n, " \u0440\u043E\u043A\u0456\u0432")
427
+ },
428
+ zh: {
429
+ quarter: (q) => "\u7B2C".concat(q, "\u5B63\u5EA6"),
430
+ quarterWithYear: (label, year) => "".concat(year, "\u5E74").concat(label),
431
+ views: { months: "\u6708\u4EFD", quarters: "\u5B63\u5EA6", years: "\u5E74\u4EFD" },
432
+ switchView: (title, nextView) => "".concat(title, "\uFF0C\u5207\u6362\u5230").concat(nextView, "\u89C6\u56FE"),
433
+ previousYear: "\u4E0A\u4E00\u5E74",
434
+ nextYear: "\u4E0B\u4E00\u5E74",
435
+ previousYears: (n) => "\u524D".concat(n, "\u5E74"),
436
+ nextYears: (n) => "\u540E".concat(n, "\u5E74")
437
+ }
438
+ };
439
+ const getCalendarStrings = (locale) => {
440
+ var _a;
441
+ return (_a = STRINGS[locale.split(/[-_]/)[0].toLowerCase()]) != null ? _a : en;
442
+ };
443
+ const useCalendarStrings = () => {
444
+ const { locale } = reactAria.useLocale();
445
+ return React.useMemo(() => getCalendarStrings(locale), [locale]);
446
+ };
447
+
448
+ const DEFAULT_FORMAT = {
449
+ day: "2-digit",
450
+ month: "short",
451
+ year: "numeric"
452
+ };
453
+ const PERIOD_DAY_FORMAT = {
454
+ day: "numeric",
455
+ month: "short"
456
+ };
457
+ const PERIOD_WEEK_FORMAT = PERIOD_DAY_FORMAT;
458
+ const PERIOD_MONTH_FORMAT = {
459
+ month: "short",
460
+ year: "2-digit"
461
+ };
462
+ function formatDate(date, formatter, timeZone) {
463
+ return formatter.format(date.toDate(timeZone));
464
+ }
465
+ function formatValue(value, formatter, timeZone) {
466
+ if (value == null || value === void 0) {
467
+ return void 0;
468
+ }
469
+ if ("start" in value) {
470
+ if (value.start == null || value.end == null) {
471
+ return void 0;
472
+ }
473
+ return "".concat(formatDate(value.start, formatter, timeZone), " - ").concat(formatDate(
474
+ value.end,
475
+ formatter,
476
+ timeZone
477
+ ));
478
+ }
479
+ return formatDate(value, formatter, timeZone);
480
+ }
481
+ function isCurrentPeriod(value, period, locale) {
482
+ const todayDate = date.today(date.getLocalTimeZone());
483
+ switch (period) {
484
+ case "day":
485
+ case void 0:
486
+ return date.isSameDay(value, todayDate);
487
+ case "week": {
488
+ const currentWeekStart = date.startOfWeek(todayDate, locale);
489
+ const selectedWeekStart = date.startOfWeek(value, locale);
490
+ return date.isSameDay(currentWeekStart, selectedWeekStart);
491
+ }
492
+ case "month":
493
+ return value.year === todayDate.year && value.month === todayDate.month;
494
+ case "quarter": {
495
+ const currentQuarter = Math.ceil(todayDate.month / 3);
496
+ const selectedQuarter = Math.ceil(value.month / 3);
497
+ return value.year === todayDate.year && selectedQuarter === currentQuarter;
498
+ }
499
+ default:
500
+ return false;
501
+ }
502
+ }
503
+ const Value = React.forwardRef(({ format, currentLabel, ...restProps }, forwardRef) => {
504
+ const { value, period, formatQuarterLabel } = useCalendarContext();
505
+ const { locale } = reactAria.useLocale();
506
+ const strings = useCalendarStrings();
507
+ const yearFormatter = React.useMemo(
508
+ () => new Intl.NumberFormat(locale, { useGrouping: false }),
509
+ [locale]
510
+ );
511
+ const timeZone = React.useMemo(date.getLocalTimeZone, []);
512
+ const effectivePeriod = period != null ? period : "day";
513
+ const periodFormat = React.useMemo(() => {
514
+ if (format != null) return format;
515
+ if (period == null) return DEFAULT_FORMAT;
516
+ switch (period) {
517
+ case "day":
518
+ return PERIOD_DAY_FORMAT;
519
+ case "week":
520
+ return PERIOD_WEEK_FORMAT;
521
+ case "month":
522
+ return PERIOD_MONTH_FORMAT;
523
+ default:
524
+ return DEFAULT_FORMAT;
525
+ }
526
+ }, [format, period]);
527
+ const formatWithTimeZone = React.useMemo(
528
+ () => ({ ...periodFormat, timeZone }),
529
+ [periodFormat, timeZone]
530
+ );
531
+ const dateFormatter = reactAria.useDateFormatter(formatWithTimeZone);
532
+ const dayOnlyFormat = React.useMemo(
533
+ () => ({ day: "numeric", timeZone }),
534
+ [timeZone]
535
+ );
536
+ const dayOnlyFormatter = reactAria.useDateFormatter(dayOnlyFormat);
537
+ let valueToDisplay;
538
+ if (value == null) {
539
+ valueToDisplay = void 0;
540
+ } else if ("start" in value) {
541
+ valueToDisplay = formatValue(value, dateFormatter, timeZone);
542
+ } else {
543
+ switch (effectivePeriod) {
544
+ case "week": {
545
+ const calendarDate = value;
546
+ const weekStart = date.startOfWeek(calendarDate, locale);
547
+ const weekEnd = date.endOfWeek(calendarDate, locale);
548
+ const startStr = formatDate(weekStart, dateFormatter, timeZone);
549
+ if (weekStart.month === weekEnd.month) {
550
+ const endDay = dayOnlyFormatter.format(weekEnd.toDate(timeZone));
551
+ valueToDisplay = "".concat(startStr, " - ").concat(endDay);
552
+ } else {
553
+ const endStr = formatDate(weekEnd, dateFormatter, timeZone);
554
+ if (startStr != null && endStr != null) {
555
+ valueToDisplay = "".concat(startStr, " - ").concat(endStr);
556
+ }
557
+ }
558
+ break;
559
+ }
560
+ case "quarter": {
561
+ const quarter = Math.ceil(value.month / 3);
562
+ valueToDisplay = strings.quarterWithYear(
563
+ (formatQuarterLabel != null ? formatQuarterLabel : strings.quarter)(quarter),
564
+ yearFormatter.format(value.year)
565
+ );
566
+ break;
567
+ }
568
+ case "month": {
569
+ valueToDisplay = formatDate(value, dateFormatter, timeZone);
570
+ break;
571
+ }
572
+ default: {
573
+ valueToDisplay = formatDate(value, dateFormatter, timeZone);
574
+ break;
575
+ }
576
+ }
577
+ if (valueToDisplay != null && currentLabel != null && !("start" in value) && isCurrentPeriod(value, period, locale)) {
578
+ valueToDisplay = "".concat(valueToDisplay, " (").concat(currentLabel, ")");
579
+ }
580
+ }
581
+ return /* @__PURE__ */ jsxRuntime.jsx(StyledValue, { ref: forwardRef, ...restProps, children: valueToDisplay });
582
+ });
583
+
256
584
  const StyledContent = designSystemStitches.styled(RadixPopover.Content, {
257
585
  ...designSystemStyles.floating.css(),
258
586
  zIndex: "$calendar",
@@ -274,12 +602,54 @@ const StyledClearContent = designSystemStitches.styled(designSystemPrimitive.Pri
274
602
  padding: "$200",
275
603
  borderTop: "1px solid $border-default"
276
604
  });
605
+ const StyledGoToCurrent = designSystemStitches.styled(designSystemBaseButton.BaseButton, {
606
+ display: "flex",
607
+ alignItems: "center",
608
+ gap: "$100",
609
+ marginTop: "$150",
610
+ padding: "$100 $150",
611
+ borderRadius: "$calendar",
612
+ color: "$text-secondary",
613
+ fontSize: "$body-xs",
614
+ lineHeight: "$body-xs",
615
+ fontWeight: "$body-xs",
616
+ "& svg": {
617
+ width: "16px",
618
+ height: "16px",
619
+ color: "$icon-secondary"
620
+ },
621
+ _hover: {
622
+ backgroundColor: "$background-interactive-surface-secondary-hover"
623
+ },
624
+ ...designSystemStyles.focus.css({
625
+ boxShadow: "$focus",
626
+ outline: "1px solid transparent"
627
+ })
628
+ });
277
629
 
278
630
  const StyledHeaderTitle = designSystemStitches.styled(designSystemTypography.Heading, {
279
631
  margin: 0,
280
632
  flex: "2",
281
633
  textAlign: "center"
282
634
  });
635
+ const StyledHeaderTitleButton = designSystemStitches.styled(designSystemBaseButton.BaseButton, {
636
+ display: "inline-block",
637
+ textAlign: "center",
638
+ fontSize: "$250",
639
+ fontWeight: "$bold",
640
+ lineHeight: "$250",
641
+ color: "$text-interactive-primary",
642
+ borderRadius: "$button",
643
+ padding: "$50 $100",
644
+ ...designSystemStyles.focus.css({
645
+ boxShadow: "$focus",
646
+ outline: "1px solid transparent"
647
+ }),
648
+ _hover: {
649
+ color: "$text-interactive-primary-hover",
650
+ backgroundColor: "$background-interactive-surface-secondary-hover"
651
+ }
652
+ });
283
653
  const StyledHeaderPlaceholder = designSystemStitches.styled("div", {
284
654
  height: "$8",
285
655
  // to compensate icon buttons' width and space between 2 calendars
@@ -307,25 +677,88 @@ const Header = ({
307
677
  state,
308
678
  prevButtonProps,
309
679
  nextButtonProps,
310
- visibleMonths
680
+ visibleMonths,
681
+ yearFormatter,
682
+ onPrevOverride,
683
+ onNextOverride,
684
+ onZoomOut,
685
+ isPrevDisabled,
686
+ isNextDisabled
311
687
  }) => {
688
+ const { navigation, viewMode, navigationYear, formatSwitchViewLabel } = useCalendarContext();
689
+ const strings = useCalendarStrings();
312
690
  const monthDateFormatter = reactAria.useDateFormatter({
313
691
  month: "long",
314
692
  year: "numeric",
315
693
  timeZone: state.timeZone
316
694
  });
695
+ const getTitleText = () => {
696
+ switch (viewMode) {
697
+ case "months":
698
+ case "quarters":
699
+ return yearFormatter.format(navigationYear);
700
+ case "years": {
701
+ const decadeStart = Math.floor(navigationYear / DECADE_SIZE) * DECADE_SIZE;
702
+ return "".concat(yearFormatter.format(decadeStart), "\u2013").concat(yearFormatter.format(
703
+ decadeStart + DECADE_SIZE - 1
704
+ ));
705
+ }
706
+ default:
707
+ return monthDateFormatter.format(
708
+ state.visibleRange.start.toDate(state.timeZone)
709
+ );
710
+ }
711
+ };
712
+ const getNextViewLabel = () => {
713
+ if (viewMode === "days") return strings.views.months;
714
+ if (viewMode === "months" || viewMode === "quarters") {
715
+ return strings.views.years;
716
+ }
717
+ return "";
718
+ };
719
+ const isTopLevel = viewMode === "years";
720
+ const showClickableTitle = navigation === true && !isTopLevel;
721
+ const isDayView = viewMode === "days";
722
+ const stepLabels = viewMode === "years" ? {
723
+ prev: strings.previousYears(DECADE_SIZE),
724
+ next: strings.nextYears(DECADE_SIZE)
725
+ } : { prev: strings.previousYear, next: strings.nextYear };
317
726
  return /* @__PURE__ */ jsxRuntime.jsxs(StyledHeader, { children: [
318
- /* @__PURE__ */ jsxRuntime.jsx(HeaderButton, { ...prevButtonProps, children: /* @__PURE__ */ jsxRuntime.jsx(designSystemIcons.IconChevronLeft, {}) }),
319
- /* @__PURE__ */ jsxRuntime.jsx(StyledHeaderTitle, { level: 2, styledAs: "h3", children: monthDateFormatter.format(
320
- state.visibleRange.start.toDate(state.timeZone)
321
- ) }),
322
- visibleMonths === 2 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
727
+ isDayView ? /* @__PURE__ */ jsxRuntime.jsx(HeaderButton, { ...prevButtonProps, children: /* @__PURE__ */ jsxRuntime.jsx(designSystemIcons.IconChevronLeft, {}) }) : /* @__PURE__ */ jsxRuntime.jsx(
728
+ HeaderButton,
729
+ {
730
+ "aria-label": stepLabels.prev,
731
+ isDisabled: isPrevDisabled,
732
+ onClick: onPrevOverride,
733
+ children: /* @__PURE__ */ jsxRuntime.jsx(designSystemIcons.IconChevronLeft, {})
734
+ }
735
+ ),
736
+ /* @__PURE__ */ jsxRuntime.jsx(StyledHeaderTitle, { level: 2, styledAs: "h3", children: showClickableTitle ? /* @__PURE__ */ jsxRuntime.jsx(
737
+ StyledHeaderTitleButton,
738
+ {
739
+ onClick: onZoomOut,
740
+ "aria-label": (formatSwitchViewLabel != null ? formatSwitchViewLabel : strings.switchView)(
741
+ getTitleText(),
742
+ getNextViewLabel()
743
+ ),
744
+ children: getTitleText()
745
+ }
746
+ ) : getTitleText() }),
747
+ visibleMonths === 2 && isDayView && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
323
748
  /* @__PURE__ */ jsxRuntime.jsx(StyledHeaderPlaceholder, {}),
324
749
  /* @__PURE__ */ jsxRuntime.jsx(StyledHeaderTitle, { level: 2, styledAs: "h3", children: monthDateFormatter.format(
325
750
  state.visibleRange.start.add({ months: 1 }).toDate(state.timeZone)
326
751
  ) })
327
752
  ] }),
328
- /* @__PURE__ */ jsxRuntime.jsx(HeaderButton, { ...nextButtonProps, children: /* @__PURE__ */ jsxRuntime.jsx(designSystemIcons.IconChevronRight, {}) })
753
+ isDayView ? /* @__PURE__ */ jsxRuntime.jsx(HeaderButton, { ...nextButtonProps, children: /* @__PURE__ */ jsxRuntime.jsx(designSystemIcons.IconChevronRight, {}) }) : /* @__PURE__ */ jsxRuntime.jsx(
754
+ HeaderButton,
755
+ {
756
+ "aria-label": stepLabels.next,
757
+ isDisabled: isNextDisabled,
758
+ onClick: onNextOverride,
759
+ children: /* @__PURE__ */ jsxRuntime.jsx(designSystemIcons.IconChevronRight, {})
760
+ }
761
+ )
329
762
  ] });
330
763
  };
331
764
 
@@ -405,13 +838,30 @@ const StyledCellContent = designSystemStitches.styled(designSystemPrimitive.Prim
405
838
  true: {
406
839
  display: "none"
407
840
  }
841
+ },
842
+ outsideMonth: {
843
+ true: {
844
+ color: "$text-secondary",
845
+ '&[aria-disabled="true"]': {
846
+ textDecoration: "none"
847
+ }
848
+ }
408
849
  }
850
+ },
851
+ // Week period: the row is the week, so hovering or focusing anywhere in it
852
+ // highlights the whole week. Cells in the selected week carry no
853
+ // data-week-hover, so the selected styling is not overridden.
854
+ "tr:hover > td > &[data-week-hover], tr:focus-within > td > &[data-week-hover]": {
855
+ backgroundColor: "$background-interactive-surface-secondary-hover",
856
+ borderRadius: 0
409
857
  }
410
858
  });
411
859
 
412
860
  const Cell = (props) => {
413
861
  let { state, date: date$1, currentMonth } = props;
414
862
  const ref = React.useRef(null);
863
+ const { period, minDate, maxDate, onCalendarDateClick } = useCalendarContext();
864
+ const { locale } = reactAria.useLocale();
415
865
  const { cellProps, buttonProps, isSelected, formattedDate } = reactAria.useCalendarCell(
416
866
  { date: date$1 },
417
867
  state,
@@ -424,15 +874,65 @@ const Cell = (props) => {
424
874
  const { focusProps } = reactAria.useFocusRing();
425
875
  const isRange = isSelected && (!isSelectionStart || !isSelectionEnd);
426
876
  const rangeVariant = isRange ? isSelectionStart ? "start" : isSelectionEnd ? "end" : "middle" : void 0;
427
- return /* @__PURE__ */ jsxRuntime.jsx(StyledCell, { ...cellProps, children: /* @__PURE__ */ jsxRuntime.jsx(
877
+ let effectiveSelected = isSelected && (isSelectionStart || isSelectionEnd);
878
+ let effectiveRange = rangeVariant;
879
+ const isWeekPeriod = period === "week";
880
+ if (isWeekPeriod) {
881
+ const selectedValue = props.state.value;
882
+ if (selectedValue != null) {
883
+ const weekStart2 = date.startOfWeek(selectedValue, locale);
884
+ const weekEnd2 = date.endOfWeek(selectedValue, locale);
885
+ const inWeek = date$1.compare(weekStart2) >= 0 && date$1.compare(weekEnd2) <= 0;
886
+ if (inWeek) {
887
+ const isStart = date.isSameDay(date$1, weekStart2);
888
+ const isEnd = date.isSameDay(date$1, weekEnd2);
889
+ effectiveSelected = isStart || isEnd;
890
+ effectiveRange = isStart ? "start" : isEnd ? "end" : "middle";
891
+ } else {
892
+ effectiveSelected = false;
893
+ effectiveRange = void 0;
894
+ }
895
+ }
896
+ }
897
+ const weekStart = isWeekPeriod ? date.startOfWeek(date$1, locale) : null;
898
+ const weekEnd = isWeekPeriod ? date.endOfWeek(date$1, locale) : null;
899
+ const isWeekOutOfBounds = minDate != null && weekStart != null && weekStart.compare(minDate) < 0 || maxDate != null && weekEnd != null && weekEnd.compare(maxDate) > 0;
900
+ const enableOutsideDay = isWeekPeriod && isOutsideMonth && !isWeekOutOfBounds;
901
+ const selectWeek = () => {
902
+ if (weekStart != null) {
903
+ onCalendarDateClick(weekStart);
904
+ }
905
+ };
906
+ let weekDayProps = {};
907
+ if (isWeekPeriod && isWeekOutOfBounds) {
908
+ weekDayProps = { "aria-disabled": true, tabIndex: -1 };
909
+ } else if (enableOutsideDay) {
910
+ weekDayProps = {
911
+ "aria-disabled": void 0,
912
+ tabIndex: effectiveRange === "start" ? 0 : -1,
913
+ onClick: selectWeek,
914
+ onKeyDown: (event) => {
915
+ if (event.key === "Enter" || event.key === " ") {
916
+ event.preventDefault();
917
+ event.stopPropagation();
918
+ selectWeek();
919
+ }
920
+ }
921
+ };
922
+ }
923
+ const gridCellProps = isWeekPeriod && (enableOutsideDay || isWeekOutOfBounds) ? { ...cellProps, "aria-disabled": isWeekOutOfBounds ? true : void 0 } : cellProps;
924
+ return /* @__PURE__ */ jsxRuntime.jsx(StyledCell, { ...gridCellProps, children: /* @__PURE__ */ jsxRuntime.jsx(
428
925
  StyledCellContent,
429
926
  {
430
927
  ...reactAria.mergeProps(buttonProps, focusProps),
928
+ ...weekDayProps,
431
929
  ref,
432
- selected: isSelected && (isSelectionStart || isSelectionEnd),
930
+ selected: effectiveSelected,
433
931
  today: date.isToday(date$1, date.getLocalTimeZone()),
434
- range: rangeVariant,
435
- hidden: isOutsideMonth,
932
+ range: effectiveRange,
933
+ hidden: isOutsideMonth && !isWeekPeriod,
934
+ outsideMonth: isOutsideMonth && isWeekPeriod,
935
+ "data-week-hover": isWeekPeriod && effectiveRange == null ? "" : void 0,
436
936
  children: formattedDate
437
937
  }
438
938
  ) });
@@ -476,6 +976,507 @@ const Grid = React.forwardRef(
476
976
  }
477
977
  );
478
978
 
979
+ const StyledPeriodGrid = designSystemStitches.styled(designSystemPrimitive.Primitive.div, {
980
+ display: "grid",
981
+ gap: "$50",
982
+ padding: "$50 0",
983
+ // Match the day grid width: 7 cells × $10 (40px) = 280px
984
+ width: "calc($10 * 7)",
985
+ variants: {
986
+ columns: {
987
+ 2: { gridTemplateColumns: "repeat(2, 1fr)" },
988
+ 3: { gridTemplateColumns: "repeat(3, 1fr)" },
989
+ 4: { gridTemplateColumns: "repeat(4, 1fr)" }
990
+ }
991
+ },
992
+ defaultVariants: {
993
+ columns: 4
994
+ }
995
+ });
996
+ const StyledPeriodRow = designSystemStitches.styled(designSystemPrimitive.Primitive.div, {
997
+ display: "contents"
998
+ });
999
+ const StyledPeriodGridCell = designSystemStitches.styled(designSystemPrimitive.Primitive.div, {
1000
+ display: "contents"
1001
+ });
1002
+ const StyledPeriodCell = designSystemStitches.styled(designSystemBaseButton.BaseButton, {
1003
+ justifyContent: "center",
1004
+ height: "$10",
1005
+ color: "$text-default",
1006
+ fontSize: "$200",
1007
+ ...designSystemStyles.focus.css({
1008
+ position: "relative",
1009
+ zIndex: "1",
1010
+ boxShadow: "$focus",
1011
+ outline: "1px solid transparent"
1012
+ }),
1013
+ '&:not([aria-disabled="true"])': {
1014
+ _hover: {
1015
+ backgroundColor: "$background-interactive-surface-secondary-hover"
1016
+ }
1017
+ },
1018
+ '&[aria-disabled="true"]': {
1019
+ color: "$text-disabled"
1020
+ },
1021
+ variants: {
1022
+ current: {
1023
+ true: {
1024
+ backgroundColor: "$background-interactive-surface-secondary-hover",
1025
+ color: "$text-default"
1026
+ }
1027
+ },
1028
+ selected: {
1029
+ true: {
1030
+ backgroundColor: "$background-interactive-selected",
1031
+ color: "$text-on-interactive-primary",
1032
+ "& span": {
1033
+ color: "inherit"
1034
+ },
1035
+ '&:not([aria-disabled="true"])': {
1036
+ _hover: {
1037
+ backgroundColor: "$background-interactive-primary-hover",
1038
+ color: "$text-on-interactive-primary"
1039
+ }
1040
+ }
1041
+ }
1042
+ },
1043
+ tall: {
1044
+ true: {
1045
+ height: "$14",
1046
+ flexDirection: "column",
1047
+ gap: "$25"
1048
+ }
1049
+ }
1050
+ }
1051
+ });
1052
+ const StyledPeriodCellLabel = designSystemStitches.styled(designSystemPrimitive.Primitive.span, {
1053
+ fontSize: "$100",
1054
+ color: "$text-secondary"
1055
+ });
1056
+
1057
+ const useGridNavigation = ({
1058
+ rows,
1059
+ columns,
1060
+ selectedIndex,
1061
+ currentIndex,
1062
+ resetKey,
1063
+ focusOnMount
1064
+ }) => {
1065
+ const cellRefs = React.useRef(/* @__PURE__ */ new Map());
1066
+ const [focusedRow, setFocusedRow] = React.useState(0);
1067
+ const [focusedCol, setFocusedCol] = React.useState(0);
1068
+ const hasFocusedRef = React.useRef(false);
1069
+ const key = (r, c) => "".concat(r, "-").concat(c);
1070
+ const isEnabled = React.useCallback((r, c) => {
1071
+ const el = cellRefs.current.get(key(r, c));
1072
+ return el != null && el.getAttribute("aria-disabled") !== "true";
1073
+ }, []);
1074
+ const findFirstEnabled = React.useCallback(() => {
1075
+ for (let r = 0; r < rows; r++) {
1076
+ for (let c = 0; c < columns; c++) {
1077
+ if (isEnabled(r, c)) {
1078
+ return [r, c];
1079
+ }
1080
+ }
1081
+ }
1082
+ return null;
1083
+ }, [rows, columns, isEnabled]);
1084
+ const focusCell = React.useCallback(
1085
+ (row, col) => {
1086
+ var _a;
1087
+ if (isEnabled(row, col)) {
1088
+ (_a = cellRefs.current.get(key(row, col))) == null ? void 0 : _a.focus();
1089
+ }
1090
+ },
1091
+ [isEnabled]
1092
+ );
1093
+ const resolveFocusTarget = React.useCallback(() => {
1094
+ for (const index of [selectedIndex, currentIndex]) {
1095
+ if (index == null || index < 0 || index >= rows * columns) {
1096
+ continue;
1097
+ }
1098
+ const candidate = [
1099
+ Math.floor(index / columns),
1100
+ index % columns
1101
+ ];
1102
+ if (isEnabled(...candidate)) {
1103
+ return candidate;
1104
+ }
1105
+ }
1106
+ return findFirstEnabled();
1107
+ }, [selectedIndex, currentIndex, rows, columns, isEnabled, findFirstEnabled]);
1108
+ React.useLayoutEffect(() => {
1109
+ const target = resolveFocusTarget();
1110
+ if (target == null) {
1111
+ return;
1112
+ }
1113
+ const [row, col] = target;
1114
+ setFocusedRow(row);
1115
+ setFocusedCol(col);
1116
+ if (focusOnMount === true && !hasFocusedRef.current) {
1117
+ hasFocusedRef.current = true;
1118
+ focusCell(row, col);
1119
+ }
1120
+ }, [resolveFocusTarget, focusCell, focusOnMount, resetKey]);
1121
+ const findNextEnabled = React.useCallback(
1122
+ (row, col, dr, dc) => {
1123
+ let r = row + dr;
1124
+ let c = col + dc;
1125
+ while (r >= 0 && r < rows && c >= 0 && c < columns) {
1126
+ if (isEnabled(r, c)) {
1127
+ return [r, c];
1128
+ }
1129
+ r += dr;
1130
+ c += dc;
1131
+ }
1132
+ return [row, col];
1133
+ },
1134
+ [rows, columns, isEnabled]
1135
+ );
1136
+ const getCellProps = React.useCallback(
1137
+ (row, col) => ({
1138
+ tabIndex: row === focusedRow && col === focusedCol ? 0 : -1,
1139
+ ref: (el) => {
1140
+ const k = key(row, col);
1141
+ if (el != null) {
1142
+ cellRefs.current.set(k, el);
1143
+ } else {
1144
+ cellRefs.current.delete(k);
1145
+ }
1146
+ },
1147
+ onFocus: () => {
1148
+ setFocusedRow(row);
1149
+ setFocusedCol(col);
1150
+ },
1151
+ onKeyDown: (e) => {
1152
+ let nextRow = row;
1153
+ let nextCol = col;
1154
+ switch (e.key) {
1155
+ case "ArrowRight":
1156
+ if (col < columns - 1) {
1157
+ [nextRow, nextCol] = findNextEnabled(row, col, 0, 1);
1158
+ }
1159
+ break;
1160
+ case "ArrowLeft":
1161
+ if (col > 0) {
1162
+ [nextRow, nextCol] = findNextEnabled(row, col, 0, -1);
1163
+ }
1164
+ break;
1165
+ case "ArrowDown":
1166
+ if (row < rows - 1) {
1167
+ [nextRow, nextCol] = findNextEnabled(row, col, 1, 0);
1168
+ }
1169
+ break;
1170
+ case "ArrowUp":
1171
+ if (row > 0) {
1172
+ [nextRow, nextCol] = findNextEnabled(row, col, -1, 0);
1173
+ }
1174
+ break;
1175
+ case "Home": {
1176
+ for (let r = 0; r < rows; r++) {
1177
+ for (let c = 0; c < columns; c++) {
1178
+ if (isEnabled(r, c)) {
1179
+ [nextRow, nextCol] = [r, c];
1180
+ r = rows;
1181
+ break;
1182
+ }
1183
+ }
1184
+ }
1185
+ break;
1186
+ }
1187
+ case "End": {
1188
+ for (let r = rows - 1; r >= 0; r--) {
1189
+ for (let c = columns - 1; c >= 0; c--) {
1190
+ if (isEnabled(r, c)) {
1191
+ [nextRow, nextCol] = [r, c];
1192
+ r = -1;
1193
+ break;
1194
+ }
1195
+ }
1196
+ }
1197
+ break;
1198
+ }
1199
+ default:
1200
+ return;
1201
+ }
1202
+ if (nextRow !== row || nextCol !== col) {
1203
+ e.preventDefault();
1204
+ focusCell(nextRow, nextCol);
1205
+ }
1206
+ }
1207
+ }),
1208
+ [
1209
+ focusedRow,
1210
+ focusedCol,
1211
+ rows,
1212
+ columns,
1213
+ focusCell,
1214
+ findNextEnabled,
1215
+ isEnabled
1216
+ ]
1217
+ );
1218
+ return { getCellProps };
1219
+ };
1220
+
1221
+ const ROWS$2 = 3;
1222
+ const COLUMNS$2 = 4;
1223
+ const MonthGrid = ({
1224
+ navigationYear,
1225
+ timeZone,
1226
+ onMonthSelect,
1227
+ currentMonth,
1228
+ currentYear,
1229
+ selectedMonth,
1230
+ selectedYear,
1231
+ minDate,
1232
+ maxDate,
1233
+ yearFormatter,
1234
+ focusOnMount
1235
+ }) => {
1236
+ const monthFormatter = reactAria.useDateFormatter({
1237
+ month: "short",
1238
+ timeZone
1239
+ });
1240
+ const { getCellProps } = useGridNavigation({
1241
+ rows: ROWS$2,
1242
+ columns: COLUMNS$2,
1243
+ selectedIndex: selectedYear === navigationYear && selectedMonth != null ? selectedMonth - 1 : -1,
1244
+ currentIndex: currentYear === navigationYear && currentMonth != null ? currentMonth - 1 : -1,
1245
+ resetKey: navigationYear,
1246
+ focusOnMount
1247
+ });
1248
+ const isMonthDisabled = (month) => {
1249
+ if (minDate != null) {
1250
+ const minYear = minDate.year;
1251
+ const minMonth = minDate.month;
1252
+ if (navigationYear < minYear || navigationYear === minYear && month < minMonth) {
1253
+ return true;
1254
+ }
1255
+ }
1256
+ if (maxDate != null) {
1257
+ const maxYear = maxDate.year;
1258
+ const maxMonth = maxDate.month;
1259
+ if (navigationYear > maxYear || navigationYear === maxYear && month > maxMonth) {
1260
+ return true;
1261
+ }
1262
+ }
1263
+ return false;
1264
+ };
1265
+ return /* @__PURE__ */ jsxRuntime.jsx(
1266
+ StyledPeriodGrid,
1267
+ {
1268
+ columns: 4,
1269
+ role: "grid",
1270
+ "aria-label": yearFormatter.format(navigationYear),
1271
+ children: Array.from({ length: ROWS$2 }, (_, row) => /* @__PURE__ */ jsxRuntime.jsx(StyledPeriodRow, { role: "row", children: Array.from({ length: COLUMNS$2 }, (_2, col) => {
1272
+ const month = row * COLUMNS$2 + col + 1;
1273
+ const date$1 = new date.CalendarDate(navigationYear, month, 1).toDate(
1274
+ timeZone
1275
+ );
1276
+ const label = monthFormatter.format(date$1);
1277
+ const disabled = isMonthDisabled(month);
1278
+ const isCurrent = navigationYear === currentYear && month === currentMonth;
1279
+ const isSelected = navigationYear === selectedYear && month === selectedMonth;
1280
+ const cellProps = getCellProps(row, col);
1281
+ return /* @__PURE__ */ jsxRuntime.jsx(
1282
+ StyledPeriodGridCell,
1283
+ {
1284
+ role: "gridcell",
1285
+ "aria-selected": isSelected || void 0,
1286
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1287
+ StyledPeriodCell,
1288
+ {
1289
+ ...cellProps,
1290
+ current: isCurrent,
1291
+ selected: isSelected,
1292
+ "aria-current": isCurrent ? "date" : void 0,
1293
+ "aria-disabled": disabled || void 0,
1294
+ "aria-label": "".concat(label, " ").concat(yearFormatter.format(
1295
+ navigationYear
1296
+ )),
1297
+ onPress: () => onMonthSelect(month),
1298
+ children: label
1299
+ }
1300
+ )
1301
+ },
1302
+ month
1303
+ );
1304
+ }) }, row))
1305
+ }
1306
+ );
1307
+ };
1308
+
1309
+ const ROWS$1 = 2;
1310
+ const COLUMNS$1 = 2;
1311
+ const QUARTER_MONTHS = [
1312
+ [1, 3],
1313
+ [4, 6],
1314
+ [7, 9],
1315
+ [10, 12]
1316
+ ];
1317
+ const QuarterGrid = ({
1318
+ navigationYear,
1319
+ timeZone,
1320
+ onQuarterSelect,
1321
+ currentQuarter,
1322
+ currentYear,
1323
+ selectedQuarter,
1324
+ selectedYear,
1325
+ minDate,
1326
+ maxDate,
1327
+ formatQuarterLabel,
1328
+ yearFormatter,
1329
+ focusOnMount
1330
+ }) => {
1331
+ const strings = useCalendarStrings();
1332
+ const monthFormatter = reactAria.useDateFormatter({
1333
+ month: "short",
1334
+ timeZone
1335
+ });
1336
+ const { getCellProps } = useGridNavigation({
1337
+ rows: ROWS$1,
1338
+ columns: COLUMNS$1,
1339
+ selectedIndex: selectedYear === navigationYear && selectedQuarter != null ? selectedQuarter - 1 : -1,
1340
+ currentIndex: currentYear === navigationYear && currentQuarter != null ? currentQuarter - 1 : -1,
1341
+ resetKey: navigationYear,
1342
+ focusOnMount
1343
+ });
1344
+ const isQuarterDisabled = (quarter) => {
1345
+ const [startMonth, endMonth] = QUARTER_MONTHS[quarter - 1];
1346
+ if (minDate != null) {
1347
+ if (navigationYear < minDate.year || navigationYear === minDate.year && endMonth < minDate.month) {
1348
+ return true;
1349
+ }
1350
+ }
1351
+ if (maxDate != null) {
1352
+ if (navigationYear > maxDate.year || navigationYear === maxDate.year && startMonth > maxDate.month) {
1353
+ return true;
1354
+ }
1355
+ }
1356
+ return false;
1357
+ };
1358
+ const formatRange = (quarter) => {
1359
+ const [startMonth, endMonth] = QUARTER_MONTHS[quarter - 1];
1360
+ const start = monthFormatter.format(
1361
+ new date.CalendarDate(navigationYear, startMonth, 1).toDate(timeZone)
1362
+ );
1363
+ const end = monthFormatter.format(
1364
+ new date.CalendarDate(navigationYear, endMonth, 1).toDate(timeZone)
1365
+ );
1366
+ return "".concat(start, " \u2013 ").concat(end);
1367
+ };
1368
+ return /* @__PURE__ */ jsxRuntime.jsx(
1369
+ StyledPeriodGrid,
1370
+ {
1371
+ columns: 2,
1372
+ role: "grid",
1373
+ "aria-label": yearFormatter.format(navigationYear),
1374
+ children: Array.from({ length: ROWS$1 }, (_, row) => /* @__PURE__ */ jsxRuntime.jsx(StyledPeriodRow, { role: "row", children: Array.from({ length: COLUMNS$1 }, (_2, col) => {
1375
+ const quarter = row * COLUMNS$1 + col + 1;
1376
+ const disabled = isQuarterDisabled(quarter);
1377
+ const isCurrent = navigationYear === currentYear && quarter === currentQuarter;
1378
+ const isSelected = navigationYear === selectedYear && quarter === selectedQuarter;
1379
+ const cellProps = getCellProps(row, col);
1380
+ const rangeLabel = formatRange(quarter);
1381
+ const label = (formatQuarterLabel != null ? formatQuarterLabel : strings.quarter)(quarter);
1382
+ return /* @__PURE__ */ jsxRuntime.jsx(
1383
+ StyledPeriodGridCell,
1384
+ {
1385
+ role: "gridcell",
1386
+ "aria-selected": isSelected || void 0,
1387
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
1388
+ StyledPeriodCell,
1389
+ {
1390
+ ...cellProps,
1391
+ current: isCurrent,
1392
+ selected: isSelected,
1393
+ tall: true,
1394
+ "aria-current": isCurrent ? "date" : void 0,
1395
+ "aria-disabled": disabled || void 0,
1396
+ "aria-label": "".concat(label, " ").concat(yearFormatter.format(
1397
+ navigationYear
1398
+ ), ", ").concat(rangeLabel),
1399
+ onPress: () => onQuarterSelect(quarter),
1400
+ children: [
1401
+ label,
1402
+ /* @__PURE__ */ jsxRuntime.jsx(StyledPeriodCellLabel, { children: rangeLabel })
1403
+ ]
1404
+ }
1405
+ )
1406
+ },
1407
+ quarter
1408
+ );
1409
+ }) }, row))
1410
+ }
1411
+ );
1412
+ };
1413
+
1414
+ const ROWS = 3;
1415
+ const COLUMNS = 4;
1416
+ const TOTAL_CELLS = ROWS * COLUMNS;
1417
+ const YearGrid = ({
1418
+ decadeStart,
1419
+ onYearSelect,
1420
+ currentYear,
1421
+ selectedYear,
1422
+ minDate,
1423
+ maxDate,
1424
+ yearFormatter,
1425
+ focusOnMount
1426
+ }) => {
1427
+ const { getCellProps } = useGridNavigation({
1428
+ rows: ROWS,
1429
+ columns: COLUMNS,
1430
+ selectedIndex: selectedYear != null ? selectedYear - decadeStart : -1,
1431
+ currentIndex: currentYear != null ? currentYear - decadeStart : -1,
1432
+ resetKey: decadeStart,
1433
+ focusOnMount
1434
+ });
1435
+ const isYearDisabled = (year) => {
1436
+ if (minDate != null && year < minDate.year) return true;
1437
+ if (maxDate != null && year > maxDate.year) return true;
1438
+ return false;
1439
+ };
1440
+ return /* @__PURE__ */ jsxRuntime.jsx(
1441
+ StyledPeriodGrid,
1442
+ {
1443
+ columns: 4,
1444
+ role: "grid",
1445
+ "aria-label": "".concat(yearFormatter.format(decadeStart), "\u2013").concat(yearFormatter.format(
1446
+ decadeStart + TOTAL_CELLS - 1
1447
+ )),
1448
+ children: Array.from({ length: ROWS }, (_, row) => /* @__PURE__ */ jsxRuntime.jsx(StyledPeriodRow, { role: "row", children: Array.from({ length: COLUMNS }, (_2, col) => {
1449
+ const year = decadeStart + row * COLUMNS + col;
1450
+ const disabled = isYearDisabled(year);
1451
+ const isCurrent = year === currentYear;
1452
+ const isSelected = year === selectedYear;
1453
+ const cellProps = getCellProps(row, col);
1454
+ return /* @__PURE__ */ jsxRuntime.jsx(
1455
+ StyledPeriodGridCell,
1456
+ {
1457
+ role: "gridcell",
1458
+ "aria-selected": isSelected || void 0,
1459
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1460
+ StyledPeriodCell,
1461
+ {
1462
+ ...cellProps,
1463
+ current: isCurrent,
1464
+ selected: isSelected,
1465
+ "aria-current": isCurrent ? "date" : void 0,
1466
+ "aria-disabled": disabled || void 0,
1467
+ "aria-label": yearFormatter.format(year),
1468
+ onPress: () => onYearSelect(year),
1469
+ children: yearFormatter.format(year)
1470
+ }
1471
+ )
1472
+ },
1473
+ year
1474
+ );
1475
+ }) }, row))
1476
+ }
1477
+ );
1478
+ };
1479
+
479
1480
  const StyledPredefinedRanges = designSystemStitches.styled(designSystemPrimitive.Primitive.div, {
480
1481
  display: "flex",
481
1482
  flexDirection: "column",
@@ -520,9 +1521,31 @@ const PredefinedRanges = () => {
520
1521
  };
521
1522
 
522
1523
  const CONTENT_OFFSET = parseInt(designSystemStitches.theme.space[50]);
1524
+ const VIEW_MODE_UP = {
1525
+ days: "months",
1526
+ months: "years",
1527
+ quarters: "years",
1528
+ years: "years"
1529
+ };
523
1530
  const SharedContent = React.forwardRef(
524
1531
  ({ calendarProps, prevButtonProps, nextButtonProps, state }, forwardRef) => {
525
- const { visibleDuration } = useCalendarContext();
1532
+ const {
1533
+ visibleDuration,
1534
+ viewMode,
1535
+ setViewMode,
1536
+ navigationYear,
1537
+ setNavigationYear,
1538
+ navigation,
1539
+ period,
1540
+ picker,
1541
+ minDate,
1542
+ maxDate,
1543
+ onCalendarDateClick,
1544
+ formatQuarterLabel,
1545
+ goToCurrentLabel,
1546
+ value: selectedValue
1547
+ } = useCalendarContext();
1548
+ const { locale } = reactAria.useLocale();
526
1549
  const monthDateFormatter = reactAria.useDateFormatter({
527
1550
  month: "long",
528
1551
  year: "numeric",
@@ -534,8 +1557,185 @@ const SharedContent = React.forwardRef(
534
1557
  const nextAriaLabel = "".concat(nextButtonProps["aria-label"], ", ").concat(monthDateFormatter.format(
535
1558
  state.visibleRange.start.add({ months: visibleDuration.months === 2 ? 2 : 1 }).toDate(state.timeZone)
536
1559
  ));
1560
+ const todayDate = React.useMemo(() => date.today(date.getLocalTimeZone()), []);
1561
+ const currentYear = todayDate.year;
1562
+ const currentMonth = todayDate.month;
1563
+ const yearFormatter = React.useMemo(
1564
+ () => new Intl.NumberFormat(locale, { useGrouping: false }),
1565
+ [locale]
1566
+ );
1567
+ const handleZoomOut = React.useCallback(() => {
1568
+ if (viewMode === "days") {
1569
+ setNavigationYear(state.visibleRange.start.year);
1570
+ }
1571
+ setViewMode(VIEW_MODE_UP[viewMode]);
1572
+ }, [
1573
+ viewMode,
1574
+ setViewMode,
1575
+ setNavigationYear,
1576
+ state.visibleRange.start.year
1577
+ ]);
1578
+ const handleMonthSelect = React.useCallback(
1579
+ (month) => {
1580
+ if (period === "month") {
1581
+ const date$1 = new date.CalendarDate(navigationYear, month, 1);
1582
+ onCalendarDateClick(date$1);
1583
+ } else {
1584
+ state.setFocusedDate(new date.CalendarDate(navigationYear, month, 1));
1585
+ state.setFocused(true);
1586
+ setViewMode("days");
1587
+ }
1588
+ },
1589
+ [navigationYear, state, setViewMode, period, onCalendarDateClick]
1590
+ );
1591
+ const handleQuarterSelect = React.useCallback(
1592
+ (quarter) => {
1593
+ const firstMonth = (quarter - 1) * 3 + 1;
1594
+ const date$1 = new date.CalendarDate(navigationYear, firstMonth, 1);
1595
+ onCalendarDateClick(date$1);
1596
+ },
1597
+ [navigationYear, onCalendarDateClick]
1598
+ );
1599
+ const handleYearSelect = React.useCallback(
1600
+ (year) => {
1601
+ setNavigationYear(year);
1602
+ setViewMode(period === "quarter" ? "quarters" : "months");
1603
+ },
1604
+ [setNavigationYear, setViewMode, period]
1605
+ );
1606
+ const handlePrevOverride = React.useCallback(() => {
1607
+ if (viewMode === "years") {
1608
+ setNavigationYear((y) => y - DECADE_SIZE);
1609
+ } else {
1610
+ setNavigationYear((y) => y - 1);
1611
+ }
1612
+ }, [viewMode, setNavigationYear]);
1613
+ const handleNextOverride = React.useCallback(() => {
1614
+ if (viewMode === "years") {
1615
+ setNavigationYear((y) => y + DECADE_SIZE);
1616
+ } else {
1617
+ setNavigationYear((y) => y + 1);
1618
+ }
1619
+ }, [viewMode, setNavigationYear]);
1620
+ const decadeStart = Math.floor(navigationYear / DECADE_SIZE) * DECADE_SIZE;
1621
+ const [isPrevDisabled, isNextDisabled] = React.useMemo(() => {
1622
+ if (viewMode === "days") {
1623
+ return [void 0, void 0];
1624
+ }
1625
+ if (viewMode === "years") {
1626
+ return [
1627
+ minDate != null && decadeStart - 1 < minDate.year,
1628
+ maxDate != null && decadeStart + DECADE_SIZE > maxDate.year
1629
+ ];
1630
+ }
1631
+ return [
1632
+ minDate != null && navigationYear - 1 < minDate.year,
1633
+ maxDate != null && navigationYear + 1 > maxDate.year
1634
+ ];
1635
+ }, [viewMode, decadeStart, navigationYear, minDate, maxDate]);
1636
+ const currentQuarter = Math.ceil(currentMonth / 3);
1637
+ const handleGoToCurrent = React.useCallback(() => {
1638
+ const now = date.today(date.getLocalTimeZone());
1639
+ let date$1;
1640
+ switch (period) {
1641
+ case "week":
1642
+ date$1 = date.startOfWeek(now, locale);
1643
+ break;
1644
+ case "month":
1645
+ date$1 = new date.CalendarDate(currentYear, currentMonth, 1);
1646
+ break;
1647
+ case "quarter": {
1648
+ const firstMonth = (currentQuarter - 1) * 3 + 1;
1649
+ date$1 = new date.CalendarDate(currentYear, firstMonth, 1);
1650
+ break;
1651
+ }
1652
+ default:
1653
+ date$1 = now;
1654
+ break;
1655
+ }
1656
+ onCalendarDateClick(date$1);
1657
+ }, [
1658
+ period,
1659
+ locale,
1660
+ currentYear,
1661
+ currentMonth,
1662
+ currentQuarter,
1663
+ onCalendarDateClick
1664
+ ]);
1665
+ const prevViewModeRef = React.useRef(viewMode);
1666
+ const shouldAutoFocus = prevViewModeRef.current !== viewMode && viewMode !== "days";
1667
+ React.useEffect(() => {
1668
+ prevViewModeRef.current = viewMode;
1669
+ }, [viewMode]);
1670
+ const selectedDate = selectedValue != null && !("start" in selectedValue) ? selectedValue : null;
1671
+ const selectedYear = selectedDate == null ? void 0 : selectedDate.year;
1672
+ const selectedMonth = selectedDate == null ? void 0 : selectedDate.month;
1673
+ const selectedQuarter = selectedMonth != null ? Math.ceil(selectedMonth / 3) : void 0;
1674
+ const isSelectionInCurrentPeriod = React.useMemo(
1675
+ () => selectedDate != null && isCurrentPeriod(selectedDate, period, locale),
1676
+ [selectedDate, period, locale]
1677
+ );
1678
+ const showGoToCurrent = goToCurrentLabel != null && picker === "single" && !isSelectionInCurrentPeriod;
1679
+ const renderGrid = () => {
1680
+ switch (viewMode) {
1681
+ case "months":
1682
+ return /* @__PURE__ */ jsxRuntime.jsx(
1683
+ MonthGrid,
1684
+ {
1685
+ navigationYear,
1686
+ timeZone: state.timeZone,
1687
+ onMonthSelect: handleMonthSelect,
1688
+ currentMonth,
1689
+ currentYear,
1690
+ selectedMonth,
1691
+ selectedYear,
1692
+ minDate,
1693
+ maxDate,
1694
+ yearFormatter,
1695
+ focusOnMount: shouldAutoFocus
1696
+ }
1697
+ );
1698
+ case "quarters":
1699
+ return /* @__PURE__ */ jsxRuntime.jsx(
1700
+ QuarterGrid,
1701
+ {
1702
+ navigationYear,
1703
+ timeZone: state.timeZone,
1704
+ onQuarterSelect: handleQuarterSelect,
1705
+ currentQuarter,
1706
+ currentYear,
1707
+ selectedQuarter,
1708
+ selectedYear,
1709
+ minDate,
1710
+ maxDate,
1711
+ formatQuarterLabel,
1712
+ yearFormatter,
1713
+ focusOnMount: shouldAutoFocus
1714
+ }
1715
+ );
1716
+ case "years":
1717
+ return /* @__PURE__ */ jsxRuntime.jsx(
1718
+ YearGrid,
1719
+ {
1720
+ decadeStart,
1721
+ onYearSelect: handleYearSelect,
1722
+ currentYear,
1723
+ selectedYear,
1724
+ minDate,
1725
+ maxDate,
1726
+ yearFormatter,
1727
+ focusOnMount: shouldAutoFocus
1728
+ }
1729
+ );
1730
+ default:
1731
+ return /* @__PURE__ */ jsxRuntime.jsxs(StyledGridContent, { children: [
1732
+ /* @__PURE__ */ jsxRuntime.jsx(StyledBodyContentLeft, { children: /* @__PURE__ */ jsxRuntime.jsx(Grid, { state }) }),
1733
+ visibleDuration.months === 2 && /* @__PURE__ */ jsxRuntime.jsx(StyledBodyContentRight, { children: /* @__PURE__ */ jsxRuntime.jsx(Grid, { state, offset: { months: 1 } }) })
1734
+ ] });
1735
+ }
1736
+ };
537
1737
  return /* @__PURE__ */ jsxRuntime.jsxs(StyledCalendarBody, { ...calendarProps, ref: forwardRef, children: [
538
- /* @__PURE__ */ jsxRuntime.jsx(PredefinedRanges, {}),
1738
+ viewMode === "days" && /* @__PURE__ */ jsxRuntime.jsx(PredefinedRanges, {}),
539
1739
  /* @__PURE__ */ jsxRuntime.jsxs(designSystemFlex.Flex, { direction: "column", children: [
540
1740
  /* @__PURE__ */ jsxRuntime.jsx(
541
1741
  Header,
@@ -549,12 +1749,19 @@ const SharedContent = React.forwardRef(
549
1749
  ...nextButtonProps,
550
1750
  "aria-label": nextAriaLabel
551
1751
  },
552
- visibleMonths: visibleDuration.months
1752
+ visibleMonths: visibleDuration.months,
1753
+ yearFormatter,
1754
+ onPrevOverride: handlePrevOverride,
1755
+ onNextOverride: handleNextOverride,
1756
+ isPrevDisabled,
1757
+ isNextDisabled,
1758
+ onZoomOut: navigation === true ? handleZoomOut : void 0
553
1759
  }
554
1760
  ),
555
- /* @__PURE__ */ jsxRuntime.jsxs(StyledGridContent, { children: [
556
- /* @__PURE__ */ jsxRuntime.jsx(StyledBodyContentLeft, { children: /* @__PURE__ */ jsxRuntime.jsx(Grid, { state }) }),
557
- visibleDuration.months === 2 && /* @__PURE__ */ jsxRuntime.jsx(StyledBodyContentRight, { children: /* @__PURE__ */ jsxRuntime.jsx(Grid, { state, offset: { months: 1 } }) })
1761
+ renderGrid(),
1762
+ showGoToCurrent && /* @__PURE__ */ jsxRuntime.jsxs(StyledGoToCurrent, { onPress: handleGoToCurrent, children: [
1763
+ /* @__PURE__ */ jsxRuntime.jsx(designSystemIcons.IconClock, { "aria-hidden": "true" }),
1764
+ goToCurrentLabel
558
1765
  ] })
559
1766
  ] })
560
1767
  ] });
@@ -567,15 +1774,31 @@ const DatePickerContent = () => {
567
1774
  visibleDuration,
568
1775
  minDate,
569
1776
  maxDate,
570
- onCalendarDateClick
1777
+ onCalendarDateClick,
1778
+ period
571
1779
  } = useCalendarContext();
1780
+ const handleChange = React.useCallback(
1781
+ (date$1) => {
1782
+ if (period === "week") {
1783
+ const weekStart = date.startOfWeek(date$1, locale);
1784
+ const weekEnd = weekStart.add({ days: 6 });
1785
+ if (minDate != null && weekStart.compare(minDate) < 0 || maxDate != null && weekEnd.compare(maxDate) > 0) {
1786
+ return;
1787
+ }
1788
+ onCalendarDateClick(weekStart);
1789
+ } else {
1790
+ onCalendarDateClick(date$1);
1791
+ }
1792
+ },
1793
+ [period, locale, minDate, maxDate, onCalendarDateClick]
1794
+ );
572
1795
  const state = reactStately.useCalendarState({
573
1796
  value: calendarValue,
574
1797
  visibleDuration,
575
1798
  minValue: minDate,
576
1799
  maxValue: maxDate,
577
1800
  autoFocus: true,
578
- onChange: onCalendarDateClick,
1801
+ onChange: handleChange,
579
1802
  locale,
580
1803
  createCalendar: date.createCalendar
581
1804
  });
@@ -672,44 +1895,6 @@ const Content = React.forwardRef(
672
1895
  }
673
1896
  );
674
1897
 
675
- const StyledValue = designSystemStitches.styled(designSystemPrimitive.Primitive.span, {});
676
-
677
- const DEFAULT_FORMAT = {
678
- day: "2-digit",
679
- month: "short",
680
- year: "numeric"
681
- };
682
- function formatDate(date, formatter, timeZone) {
683
- return formatter.format(date.toDate(timeZone));
684
- }
685
- function formatValue(value, formatter, timeZone) {
686
- if (value == null || value === void 0) {
687
- return void 0;
688
- }
689
- if ("start" in value) {
690
- if (value.start == null || value.end == null) {
691
- return void 0;
692
- }
693
- return "".concat(formatDate(value.start, formatter, timeZone), " - ").concat(formatDate(
694
- value.end,
695
- formatter,
696
- timeZone
697
- ));
698
- }
699
- return formatDate(value, formatter, timeZone);
700
- }
701
- const Value = React.forwardRef(({ format = DEFAULT_FORMAT, ...restProps }, forwardRef) => {
702
- const { value } = useCalendarContext();
703
- const timeZone = React.useMemo(date.getLocalTimeZone, [format]);
704
- const formatWithTimeZone = React.useMemo(
705
- () => ({ ...format, timeZone }),
706
- [format, timeZone]
707
- );
708
- const dateFormatter = reactAria.useDateFormatter(formatWithTimeZone);
709
- const valueToDisplay = formatValue(value, dateFormatter, timeZone);
710
- return /* @__PURE__ */ jsxRuntime.jsx(StyledValue, { ref: forwardRef, ...restProps, children: valueToDisplay });
711
- });
712
-
713
1898
  const ClearButton = React.forwardRef(({ onClear, children, ...restProps }, forwardRef) => {
714
1899
  const { value, setValue } = useCalendarContext();
715
1900
  const onClearHandler = () => {
@@ -800,6 +1985,11 @@ const Calendar = React.forwardRef(
800
1985
  "aria-disabled": ariaDisabled,
801
1986
  children,
802
1987
  predefinedValues,
1988
+ navigation,
1989
+ period,
1990
+ formatQuarterLabel,
1991
+ formatSwitchViewLabel,
1992
+ goToCurrentLabel,
803
1993
  ...props
804
1994
  }, forwardRef) => {
805
1995
  const providerProps = {
@@ -816,7 +2006,12 @@ const Calendar = React.forwardRef(
816
2006
  onOpen,
817
2007
  disabled,
818
2008
  ariaDisabled,
819
- predefinedValues
2009
+ predefinedValues,
2010
+ navigation,
2011
+ period,
2012
+ formatQuarterLabel,
2013
+ formatSwitchViewLabel,
2014
+ goToCurrentLabel
820
2015
  };
821
2016
  const validatedLocale = getValidLocale(locale);
822
2017
  return /* @__PURE__ */ jsxRuntime.jsx(reactAria.I18nProvider, { locale: validatedLocale, children: /* @__PURE__ */ jsxRuntime.jsx(CalendarProvider, { ...providerProps, children: /* @__PURE__ */ jsxRuntime.jsx(Root, { ...props, ref: forwardRef, children }) }) });
@@ -831,7 +2026,12 @@ const CalendarGrid = ({
831
2026
  visibleMonths,
832
2027
  minDate,
833
2028
  maxDate,
834
- predefinedValues
2029
+ predefinedValues,
2030
+ navigation,
2031
+ period,
2032
+ formatQuarterLabel,
2033
+ formatSwitchViewLabel,
2034
+ goToCurrentLabel
835
2035
  }) => {
836
2036
  const providerProps = {
837
2037
  picker,
@@ -841,7 +2041,12 @@ const CalendarGrid = ({
841
2041
  visibleMonths,
842
2042
  minDate,
843
2043
  maxDate,
844
- predefinedValues
2044
+ predefinedValues,
2045
+ navigation,
2046
+ period,
2047
+ formatQuarterLabel,
2048
+ formatSwitchViewLabel,
2049
+ goToCurrentLabel
845
2050
  };
846
2051
  const validatedLocale = getValidLocale(locale);
847
2052
  return /* @__PURE__ */ jsxRuntime.jsx(reactAria.I18nProvider, { locale: validatedLocale, children: /* @__PURE__ */ jsxRuntime.jsx(CalendarProvider, { ...providerProps, children: /* @__PURE__ */ jsxRuntime.jsx(CalendarGridContent, {}) }) });