@godxjp/ui 32.2.1 → 32.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +3 -3
  2. package/agent/START-HERE.md +5 -5
  3. package/agent/components/Alert.json +1 -0
  4. package/agent/components/DataTable.json +1 -1
  5. package/agent/components/DatePicker.json +6 -0
  6. package/agent/components/EmptyState.json +7 -0
  7. package/agent/components/Form.json +1 -0
  8. package/agent/components/FormField.json +2 -1
  9. package/agent/components/MobileShell.json +1 -1
  10. package/agent/components/PageContainer.json +1 -1
  11. package/agent/components/Toaster.json +2 -1
  12. package/agent/components.json +22 -5
  13. package/agent/index.json +5 -5
  14. package/agent/llms.txt +6 -6
  15. package/agent/tokens.json +12 -0
  16. package/dist/components/data-display/data-table.js +13 -3
  17. package/dist/components/data-display/empty-state.d.ts +1 -1
  18. package/dist/components/data-display/empty-state.js +2 -1
  19. package/dist/components/data-entry/calendar.js +16 -1
  20. package/dist/components/data-entry/date-picker.js +22 -7
  21. package/dist/components/data-entry/record-picker.js +22 -16
  22. package/dist/components/feedback/sonner.js +4 -1
  23. package/dist/components/layout/mobile-shell.js +19 -2
  24. package/dist/components/layout/page-container.js +4 -1
  25. package/dist/contracts/measurement.json +1 -1
  26. package/dist/i18n/messages/en.json +3 -1
  27. package/dist/i18n/messages/ja.json +3 -1
  28. package/dist/i18n/messages/vi.json +3 -1
  29. package/dist/lib/datetime/picker-format.d.ts +8 -1
  30. package/dist/lib/datetime/picker-format.js +16 -4
  31. package/dist/lib/toast-clearance.d.ts +25 -0
  32. package/dist/lib/toast-clearance.js +27 -0
  33. package/dist/props/components/data-display.prop.d.ts +7 -0
  34. package/dist/props/components/data-entry.prop.d.ts +12 -0
  35. package/dist/styles/control.css +57 -0
  36. package/dist/styles/focus-ring.css +1 -0
  37. package/dist/styles/layers.json +1 -1
  38. package/dist/styles/table-layout.css +7 -0
  39. package/dist/styles.css +1 -1
  40. package/dist/tokens/components/feedback.css +4 -0
  41. package/package.json +2 -2
@@ -77,6 +77,7 @@ function DatePicker(props) {
77
77
  placement = "bottom-start",
78
78
  renderExtraFooter,
79
79
  showSummary = true,
80
+ captionLayout = "label",
80
81
  size,
81
82
  status,
82
83
  variant,
@@ -114,7 +115,7 @@ function DatePicker(props) {
114
115
  const allowed = (date) => pickerDateAllowed(date, minimum, maximum, disabledDate);
115
116
  const display = (date) => format || showTime ? formatPickerDate(date, format, locale, Boolean(showTime)) : toIsoPeriod(date, picker);
116
117
  const displayAll = (date) => Array.isArray(date) ? date.map((day) => display(day)).join(", ") : display(date);
117
- const parse = (text2) => parsePickerDate(text2, format, parseFormat, Boolean(showTime)) ?? parseIsoPeriod(text2, picker);
118
+ const parse = (text2, strict = false) => parsePickerDate(text2, format, parseFormat, Boolean(showTime), strict) ?? parseIsoPeriod(text2, picker);
118
119
  const periodDate = (date) => pickerPeriodStart(date, picker, dayPickerLocale);
119
120
  const fieldA11y = range ? pickGroupFieldA11y(ariaProps) : pickFieldA11y(ariaProps);
120
121
  const identity = useFieldIdentity({ id, name, "data-field": fieldA11y["data-field"] });
@@ -241,13 +242,13 @@ function DatePicker(props) {
241
242
  choose(void 0);
242
243
  return;
243
244
  }
244
- const parsed = parse(raw);
245
+ const parsed = parse(raw, true);
245
246
  if (parsed && allowed(parsed)) choose(parsed);
246
247
  };
247
- const commitEdge = (edge, raw) => {
248
+ const commitEdge = (edge, raw, strict = false) => {
248
249
  if (edgeDisabled(edge)) return;
249
250
  const trimmed = raw.trim();
250
- const parsed = parse(trimmed);
251
+ const parsed = parse(trimmed, strict);
251
252
  if (trimmed && (!parsed || !allowed(parsed))) return;
252
253
  if (!trimmed && !allowEmpty[edge === "from" ? 0 : 1]) return;
253
254
  const base = working;
@@ -274,11 +275,20 @@ function DatePicker(props) {
274
275
  const next = periodDate(parsed);
275
276
  emit(next);
276
277
  setText(display(parsed));
278
+ } else if (!preserveInvalidOnBlur) {
279
+ setText(displayAll(committedSingle));
277
280
  }
278
281
  setOpen(false);
279
282
  };
280
283
  const enterEdge = (edge) => () => {
281
- commitEdge(edge, edge === "from" ? fromText : toText);
284
+ const raw = edge === "from" ? fromText : toText;
285
+ const parsed = parse(raw);
286
+ commitEdge(edge, raw);
287
+ if (raw.trim() && (!parsed || !allowed(parsed)) && !preserveInvalidOnBlur) {
288
+ const fallback = display(edge === "from" ? committedRange?.from : committedRange?.to);
289
+ if (edge === "from") setFromText(fallback);
290
+ else setToText(fallback);
291
+ }
282
292
  setOpen(false);
283
293
  };
284
294
  const displayIsCanonical = !format && !showTime && !multiple && !needConfirm && !preserveInvalidOnBlur && picker === "date";
@@ -426,6 +436,7 @@ function DatePicker(props) {
426
436
  endMonth: maximum,
427
437
  showToday,
428
438
  showClose,
439
+ captionLayout,
429
440
  onClose: () => setOpen(false)
430
441
  };
431
442
  const renderDayPanel = () => range ? /* @__PURE__ */ jsx(
@@ -608,13 +619,15 @@ function DatePicker(props) {
608
619
  onChange: (event) => {
609
620
  if (edge === "from") setFromText(event.target.value);
610
621
  else setToText(event.target.value);
611
- commitEdge(edge, event.target.value);
622
+ commitEdge(edge, event.target.value, true);
612
623
  },
613
624
  onBlur: (event) => {
614
- if (preserveInvalidOnBlur) return;
615
625
  const parsed = parse(event.target.value);
616
626
  const accepted = parsed && allowed(parsed) ? parsed : void 0;
617
627
  const fallback = edge === "from" ? committedRange?.from : committedRange?.to;
628
+ if (accepted && +periodDate(accepted) !== +(fallback ?? NaN))
629
+ commitEdge(edge, event.target.value);
630
+ if (preserveInvalidOnBlur) return;
618
631
  const next = accepted ? display(accepted) : display(fallback);
619
632
  if (edge === "from") setFromText(next);
620
633
  else setToText(next);
@@ -704,6 +717,8 @@ function DatePicker(props) {
704
717
  onBlur: (event) => {
705
718
  const parsed = parse(event.target.value);
706
719
  const accepted = parsed && allowed(parsed) && timeAllowed(parsed) ? parsed : void 0;
720
+ if (accepted && !multiple && !needConfirm && +periodDate(accepted) !== +(committedSingle ?? NaN))
721
+ emit(periodDate(accepted));
707
722
  if (!preserveInvalidOnBlur)
708
723
  setText(accepted ? display(accepted) : displayAll(committedSingle));
709
724
  }
@@ -486,25 +486,31 @@ const DialogPicker = React.forwardRef(function DialogPicker2({
486
486
  ))
487
487
  }
488
488
  ) : null,
489
- /* @__PURE__ */ jsx(Command, { shouldFilter: false, split: isMultiple, className: "ui-record-picker-list", children: status === "error" ? /* @__PURE__ */ jsx(
490
- EmptyState,
491
- {
492
- variant: "compact",
493
- tone: "destructive",
494
- title: t("dataEntry.recordPicker.error"),
495
- action: /* @__PURE__ */ jsx(
496
- Button,
497
- {
498
- variant: "outline",
499
- size: "sm",
500
- onClick: () => void load(query, filterValues),
501
- children: t("common.retry")
502
- }
503
- )
504
- }
489
+ /* @__PURE__ */ jsx(Command, { shouldFilter: false, split: isMultiple, className: "ui-record-picker-list", children: status === "error" ? (
490
+ // `live` on both outcomes below: each answers the user's own query (gh#1271 —
491
+ // EmptyState is no longer a live region by default).
492
+ /* @__PURE__ */ jsx(
493
+ EmptyState,
494
+ {
495
+ live: true,
496
+ variant: "compact",
497
+ tone: "destructive",
498
+ title: t("dataEntry.recordPicker.error"),
499
+ action: /* @__PURE__ */ jsx(
500
+ Button,
501
+ {
502
+ variant: "outline",
503
+ size: "sm",
504
+ onClick: () => void load(query, filterValues),
505
+ children: t("common.retry")
506
+ }
507
+ )
508
+ }
509
+ )
505
510
  ) : status === "loading" ? /* @__PURE__ */ jsx("div", { role: "status", className: "ui-record-picker-status", children: t("dataEntry.recordPicker.loading") }) : visible.length === 0 ? /* @__PURE__ */ jsx(
506
511
  EmptyState,
507
512
  {
513
+ live: true,
508
514
  variant: "compact",
509
515
  title: notFoundContent ?? t("dataEntry.recordPicker.empty")
510
516
  }
@@ -99,8 +99,11 @@ function Toaster({ style, richColors = true, toastOptions, ...props }) {
99
99
  ...style
100
100
  },
101
101
  position: "bottom-right",
102
+ offset: {
103
+ bottom: "calc(24px + var(--toast-offset-bottom) + var(--toast-sticky-bar-height))"
104
+ },
102
105
  mobileOffset: {
103
- bottom: "var(--toast-mobile-offset)",
106
+ bottom: "calc(var(--toast-mobile-offset) + var(--toast-offset-bottom) + var(--toast-sticky-bar-height))",
104
107
  right: "var(--toast-mobile-offset)"
105
108
  },
106
109
  duration: tokenDuration,
@@ -1,6 +1,8 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useRef } from "react";
3
4
  import { useTranslation } from "../../i18n/use-translation.js";
5
+ import { useToastStickyBarClearance } from "../../lib/toast-clearance.js";
4
6
  import { cn } from "../../lib/utils.js";
5
7
  function MobileShell({
6
8
  statusBar,
@@ -13,6 +15,13 @@ function MobileShell({
13
15
  className
14
16
  }) {
15
17
  const { t } = useTranslation();
18
+ const actionsRef = useRef(null);
19
+ const tabBarRef = useRef(null);
20
+ const bottomBands = [
21
+ ...actions !== void 0 ? [actionsRef] : [],
22
+ ...tabBar !== void 0 ? [tabBarRef] : []
23
+ ];
24
+ useToastStickyBarClearance(bottomBands, height === "viewport");
16
25
  return /* @__PURE__ */ jsxs(
17
26
  "div",
18
27
  {
@@ -34,8 +43,16 @@ function MobileShell({
34
43
  children
35
44
  }
36
45
  ),
37
- actions !== void 0 && /* @__PURE__ */ jsx("footer", { className: "ui-mobile-shell-actions", children: actions }),
38
- tabBar !== void 0 && /* @__PURE__ */ jsx("nav", { className: "ui-mobile-shell-tabbar", "aria-label": t("layout.sidebar.ariaLabel"), children: tabBar })
46
+ actions !== void 0 && /* @__PURE__ */ jsx("footer", { ref: actionsRef, className: "ui-mobile-shell-actions", children: actions }),
47
+ tabBar !== void 0 && /* @__PURE__ */ jsx(
48
+ "nav",
49
+ {
50
+ ref: tabBarRef,
51
+ className: "ui-mobile-shell-tabbar",
52
+ "aria-label": t("layout.sidebar.ariaLabel"),
53
+ children: tabBar
54
+ }
55
+ )
39
56
  ]
40
57
  }
41
58
  );
@@ -4,6 +4,7 @@ import { isValidElement, useEffect, useRef, useState } from "react";
4
4
  import { ChevronRight } from "lucide-react";
5
5
  import { useTranslation } from "../../i18n/use-translation.js";
6
6
  import { scrollParent, useInView } from "../../lib/hooks.js";
7
+ import { useToastStickyBarClearance } from "../../lib/toast-clearance.js";
7
8
  import { cn } from "../../lib/utils.js";
8
9
  import { BreadcrumbCrumbText, trailEllipsis } from "./breadcrumb-ellipsis.js";
9
10
  import { densityClass, pageContainerVariantClass, padStyle } from "../../lib/variants.js";
@@ -62,6 +63,8 @@ function PageContainerRoot({
62
63
  }) {
63
64
  const reveal = stickyFooter && footer != null && footerReveal === "onScroll";
64
65
  const { headerRef, revealed } = useFooterReveal(reveal);
66
+ const footerRef = useRef(null);
67
+ useToastStickyBarClearance([footerRef], stickyFooter && !!footer && (!reveal || revealed));
65
68
  const { t } = useTranslation();
66
69
  const { start: extraStart, end: extraEnd } = resolvePageExtra(extra);
67
70
  const titleFocus = titleRef === void 0 ? void 0 : { ref: titleRef, tabIndex: -1 };
@@ -176,7 +179,7 @@ function PageContainerRoot({
176
179
  ),
177
180
  toolbar != null && /* @__PURE__ */ jsx("div", { className: "ui-page-toolbar", style: padStyle(toolbarPad, void 0), children: toolbar }),
178
181
  children != null && /* @__PURE__ */ jsx("div", { className: "ui-page-body", children }),
179
- footer && /* @__PURE__ */ jsx("footer", { className: "ui-page-footer", style: footerPadStyle(footerPad), children: footer })
182
+ footer && /* @__PURE__ */ jsx("footer", { ref: footerRef, className: "ui-page-footer", style: footerPadStyle(footerPad), children: footer })
180
183
  ]
181
184
  }
182
185
  );
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$comment": "AUTO-GENERATED by scripts/gen-measurement-contract.mjs — do not edit. Read this instead of guessing: docs/MEASUREMENT-CONTRACT.md.",
3
- "version": "32.2.1",
3
+ "version": "32.3.0",
4
4
  "targetSize": {
5
5
  "standard": "WCAG 2.2 SC 2.5.8 Target Size (Minimum), level AA — 24×24 CSS px",
6
6
  "min": 24,
@@ -22,7 +22,9 @@
22
22
  "today": "Today",
23
23
  "close": "Close",
24
24
  "nav": "{label} navigation",
25
- "name": "Calendar"
25
+ "name": "Calendar",
26
+ "monthDropdown": "Month",
27
+ "yearDropdown": "Year"
26
28
  },
27
29
  "datePicker": {
28
30
  "placeholder": "Select date",
@@ -22,7 +22,9 @@
22
22
  "today": "今日",
23
23
  "close": "閉じる",
24
24
  "nav": "{label}のナビゲーション",
25
- "name": "カレンダー"
25
+ "name": "カレンダー",
26
+ "monthDropdown": "月",
27
+ "yearDropdown": "年"
26
28
  },
27
29
  "datePicker": {
28
30
  "placeholder": "日付を選択",
@@ -22,7 +22,9 @@
22
22
  "today": "Hôm nay",
23
23
  "close": "Đóng",
24
24
  "nav": "Điều hướng {label}",
25
- "name": "Lịch"
25
+ "name": "Lịch",
26
+ "monthDropdown": "Tháng",
27
+ "yearDropdown": "Năm"
26
28
  },
27
29
  "datePicker": {
28
30
  "placeholder": "Chọn ngày",
@@ -4,7 +4,14 @@ import type { DatePickerBaseProp, PickerDateFormatProp } from "../../props/compo
4
4
  export type DatePickerPicker = NonNullable<DatePickerBaseProp["picker"]>;
5
5
  /** Display is independent from the canonical value submitted by the field. */
6
6
  export declare function formatPickerDate(date: Date | undefined, format: PickerDateFormatProp | undefined, locale: string, withTime?: boolean): string;
7
- export declare function parsePickerDate(text: string, format: PickerDateFormatProp | undefined, parser?: (text: string) => Date | undefined, withTime?: boolean): Date | undefined;
7
+ /**
8
+ * Read typed text back into a date. Tries, in order: the caller's `parser`, the explicit `format`
9
+ * pattern, then canonical ISO `yyyy-MM-dd` — and, unless `strict`, any unambiguous year-first
10
+ * spelling (see `YEAR_FIRST`). `strict` exists for the KEYSTROKE path: `2026/10/1` is a complete
11
+ * year-first date on its way to `2026/10/15`, so committing it mid-typing would rewrite the text
12
+ * under the user's cursor. The field commits loose spellings on Enter and blur instead.
13
+ */
14
+ export declare function parsePickerDate(text: string, format: PickerDateFormatProp | undefined, parser?: (text: string) => Date | undefined, withTime?: boolean, strict?: boolean): Date | undefined;
8
15
  export declare function pickerDateAllowed(date: Date, min: Date | undefined, max: Date | undefined, disabled?: (date: Date) => boolean): boolean;
9
16
  /** Normalize a reporting period with the active calendar's week convention. */
10
17
  export declare function pickerPeriodStart(date: Date, picker: DatePickerBaseProp["picker"], locale: Pick<Locale, "options">): Date;
@@ -8,7 +8,7 @@ import {
8
8
  startOfQuarter,
9
9
  startOfYear
10
10
  } from "date-fns";
11
- import { parseDateInput, toIsoDate } from "./parse.js";
11
+ import { toIsoDate } from "./parse.js";
12
12
  import { dateTimeFormat } from "../intl-cache.js";
13
13
  function formatPickerDate(date, format, locale, withTime = false) {
14
14
  if (!date || !isValid(date)) return "";
@@ -17,7 +17,18 @@ function formatPickerDate(date, format, locale, withTime = false) {
17
17
  if (typeof format === "string") return formatDate(date, format);
18
18
  return withTime ? formatDate(date, "yyyy-MM-dd HH:mm") : toIsoDate(date);
19
19
  }
20
- function parsePickerDate(text, format, parser, withTime = false) {
20
+ const YEAR_FIRST = /^(\d{4})([-/.])(\d{1,2})\2(\d{1,2})$/;
21
+ const YEAR_FIRST_COMPACT = /^(\d{4})(\d{2})(\d{2})$/;
22
+ function parseYearFirst(raw) {
23
+ const m = YEAR_FIRST.exec(raw);
24
+ const parts = m ? [m[1], m[3], m[4]] : YEAR_FIRST_COMPACT.exec(raw)?.slice(1, 4);
25
+ if (!parts) return void 0;
26
+ const [year, month, day] = parts.map(Number);
27
+ const date = new Date(year, month - 1, day);
28
+ date.setFullYear(year, month - 1, day);
29
+ return date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day ? date : void 0;
30
+ }
31
+ function parsePickerDate(text, format, parser, withTime = false, strict = false) {
21
32
  const raw = text.trim();
22
33
  if (!raw) return void 0;
23
34
  const custom = parser?.(raw);
@@ -27,8 +38,9 @@ function parsePickerDate(text, format, parser, withTime = false) {
27
38
  const parsed = parse(raw, pattern, new Date(2e3, 0, 1));
28
39
  if (isValid(parsed) && formatDate(parsed, pattern) === raw) return parsed;
29
40
  }
30
- if (withTime || !/^\d{4}-\d{2}-\d{2}$/.test(raw)) return void 0;
31
- return parseDateInput(raw) ?? void 0;
41
+ if (withTime) return void 0;
42
+ if (strict && !/^\d{4}-\d{2}-\d{2}$/.test(raw)) return void 0;
43
+ return parseYearFirst(raw);
32
44
  }
33
45
  function pickerDateAllowed(date, min, max, disabled) {
34
46
  return isValid(date) && (!min || startOfDay(date) >= startOfDay(min)) && (!max || startOfDay(date) <= startOfDay(max)) && !disabled?.(date);
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Toast clearance for sticky action bars (gh#1271).
3
+ *
4
+ * The Toaster stacks bottom-right, and a page's sticky action bar (`PageContainer stickyFooter`,
5
+ * `MobileShell actions` + `tabBar`) is pinned to the same edge — so without this a stack of three
6
+ * success toasts sits on top of the submit button that raised them. The bar publishes its own
7
+ * block size on `<html>` as `--toast-sticky-bar-height`; the Toaster adds that length to its bottom
8
+ * offset (`sonner.tsx`). The tier file declares the property `0px`, so with no bar mounted — or
9
+ * the bar hidden — the toaster's offset is exactly what it was before.
10
+ *
11
+ * Internal: reachable from no published subpath.
12
+ */
13
+ import { type RefObject } from "react";
14
+ /** The custom property the Toaster reads. Declared `0px` in `src/tokens/components/feedback.css`. */
15
+ export declare const TOAST_STICKY_BAR_PROPERTY = "--toast-sticky-bar-height";
16
+ /**
17
+ * While `enabled`, publish the summed border-box block size of `refs` on `<html>` and keep it
18
+ * current with a `ResizeObserver`; remove it when disabled or unmounted.
19
+ *
20
+ * `offsetHeight`, not `getBoundingClientRect()`: the reveal footer slides in with a `transform`,
21
+ * and a transform must not leak into the clearance mid-transition. Everything runs in an effect,
22
+ * so SSR never touches `document`. One page-level bar at a time is the assumed shape: a second
23
+ * mounted bar overwrites the first's value, and either unmount clears it.
24
+ */
25
+ export declare function useToastStickyBarClearance(refs: ReadonlyArray<RefObject<HTMLElement | null>>, enabled: boolean): void;
@@ -0,0 +1,27 @@
1
+ "use client";
2
+ import { useEffect } from "react";
3
+ const TOAST_STICKY_BAR_PROPERTY = "--toast-sticky-bar-height";
4
+ function useToastStickyBarClearance(refs, enabled) {
5
+ useEffect(() => {
6
+ const elements = refs.map((ref) => ref.current).filter((el) => el != null);
7
+ if (!enabled || elements.length === 0 || typeof ResizeObserver === "undefined") {
8
+ return void 0;
9
+ }
10
+ const html = document.documentElement;
11
+ const publish = () => {
12
+ const height = elements.reduce((sum, el) => sum + el.offsetHeight, 0);
13
+ html.style.setProperty(TOAST_STICKY_BAR_PROPERTY, `${height}px`);
14
+ };
15
+ publish();
16
+ const observer = new ResizeObserver(publish);
17
+ for (const el of elements) observer.observe(el);
18
+ return () => {
19
+ observer.disconnect();
20
+ html.style.removeProperty(TOAST_STICKY_BAR_PROPERTY);
21
+ };
22
+ }, [enabled, refs.length]);
23
+ }
24
+ export {
25
+ TOAST_STICKY_BAR_PROPERTY,
26
+ useToastStickyBarClearance
27
+ };
@@ -180,6 +180,13 @@ export type EmptyStateProp = {
180
180
  * zero-state message is not announced as a heading and cannot skip an outline level.
181
181
  */
182
182
  titleAs?: "h1" | "h2" | "h3" | "h4" | "p" | "div";
183
+ /**
184
+ * Announce the empty state as a status message (`role="status"`). Default `false`: a static
185
+ * empty state ("no records yet") is content, not an update, and is not a live region. Set it
186
+ * when the empty state APPEARS as the result of a user action — a search or filter that matched
187
+ * nothing — so a screen-reader user hears the outcome (WCAG 4.1.3).
188
+ */
189
+ live?: boolean;
183
190
  className?: ClassNameProp;
184
191
  };
185
192
  /**
@@ -917,6 +917,18 @@ export type DatePickerBaseProp = FieldA11yProps & PickerChromeProp & {
917
917
  minDate?: Date;
918
918
  maxDate?: Date;
919
919
  showWeek?: boolean;
920
+ /**
921
+ * How the day panel's month caption is drawn — forwarded to `Calendar`'s react-day-picker
922
+ * `captionLayout`. `"label"` (default) is the plain month name between the ‹ › arrows;
923
+ * `"dropdown"` turns the month and the year into selects, `"dropdown-months"` /
924
+ * `"dropdown-years"` only one of them.
925
+ *
926
+ * Use `"dropdown"` for a BIRTH DATE or any date far from today (gh#1271): with the arrows alone
927
+ * reaching 1999 is ~320 clicks, with the year select it is one. Without `minDate`/`maxDate` the
928
+ * year list spans 100 years back to the current year — set `maxDate` for a date that can lie in
929
+ * the future (an expiry).
930
+ */
931
+ captionLayout?: DayPickerProps["captionLayout"];
920
932
  needConfirm?: boolean;
921
933
  /**
922
934
  * Which period the PANEL opens on, independently of the value (antd `defaultPickerValue`, and
@@ -2790,6 +2790,60 @@
2790
2790
  font-weight: var(--font-weight-medium);
2791
2791
  }
2792
2792
 
2793
+ .ui-calendar-dropdowns {
2794
+ display: inline-flex;
2795
+ min-inline-size: 0;
2796
+ align-items: center;
2797
+ gap: var(--space-1);
2798
+ }
2799
+
2800
+ .ui-calendar-dropdown-root {
2801
+ position: relative;
2802
+ display: inline-flex;
2803
+ min-inline-size: 0;
2804
+ align-items: center;
2805
+ block-size: var(--control-height-sm);
2806
+ padding-inline: var(--space-2) var(--space-1);
2807
+ border: var(--stroke-hairline) solid hsl(var(--border));
2808
+ border-radius: var(--control-radius);
2809
+ background-color: hsl(var(--background));
2810
+ }
2811
+
2812
+ .ui-calendar-dropdown-root:hover {
2813
+ background-color: hsl(var(--accent));
2814
+ }
2815
+
2816
+ .ui-calendar-dropdown-root[data-disabled="true"] {
2817
+ opacity: var(--calendar-nav-rest-alpha);
2818
+ }
2819
+
2820
+ .ui-calendar-dropdown {
2821
+ position: absolute;
2822
+ inset: 0;
2823
+ z-index: 1;
2824
+ inline-size: 100%;
2825
+ margin: 0;
2826
+ padding: 0;
2827
+ border: 0;
2828
+ opacity: 0;
2829
+ appearance: none;
2830
+ font: inherit;
2831
+ cursor: pointer;
2832
+ }
2833
+
2834
+ .ui-calendar-dropdown-root > .ui-calendar-caption-label {
2835
+ display: inline-flex;
2836
+ align-items: center;
2837
+ gap: var(--space-1);
2838
+ white-space: nowrap;
2839
+ }
2840
+
2841
+ .ui-calendar-dropdown-root .ui-calendar-chevron {
2842
+ inline-size: var(--icon-size-sm);
2843
+ block-size: var(--icon-size-sm);
2844
+ opacity: var(--calendar-nav-rest-alpha);
2845
+ }
2846
+
2793
2847
  .ui-calendar-nav {
2794
2848
  position: absolute;
2795
2849
  inset-inline: 0;
@@ -2800,9 +2854,12 @@
2800
2854
  align-items: center;
2801
2855
  justify-content: space-between;
2802
2856
  padding-inline: var(--calendar-nav-space-inline);
2857
+
2858
+ pointer-events: none;
2803
2859
  }
2804
2860
 
2805
2861
  .ui-calendar-nav-button {
2862
+ pointer-events: auto;
2806
2863
  background: transparent;
2807
2864
  padding: 0;
2808
2865
  opacity: var(--calendar-nav-rest-alpha);
@@ -50,6 +50,7 @@
50
50
  .tb-icon-btn
51
51
  ):focus-visible,
52
52
 
53
+ .ui-calendar-dropdown-root:has(.ui-calendar-dropdown:focus-visible),
53
54
  .ui-checkbox[data-focus-visible],
54
55
  .ui-radio[data-focus-visible],
55
56
  .ui-switch[data-focus-visible],
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "32.2.1",
2
+ "version": "32.3.0",
3
3
  "generatedBy": "scripts/gen-style-layers.mjs — do not edit by hand; run `pnpm gen:style-layers`",
4
4
  "base": "base.css",
5
5
  "fonts": "fonts.css",
@@ -541,6 +541,13 @@
541
541
  overflow-wrap: anywhere;
542
542
  }
543
543
 
544
+ .ui-data-table-ellipsis[data-truncated]:focus-visible [data-truncate],
545
+ .ui-data-table-ellipsis[data-truncated]:has(:focus-visible) [data-truncate] {
546
+ overflow: visible;
547
+ white-space: normal;
548
+ overflow-wrap: anywhere;
549
+ }
550
+
544
551
  .ui-data-table-surface[data-table-layout="fixed"] [data-slot="table"] {
545
552
  table-layout: fixed;
546
553
  }