@godxjp/ui 32.2.2 → 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 (38) hide show
  1. package/README.md +2 -2
  2. package/agent/START-HERE.md +4 -4
  3. package/agent/components/Alert.json +1 -0
  4. package/agent/components/DatePicker.json +6 -0
  5. package/agent/components/EmptyState.json +7 -0
  6. package/agent/components/Form.json +1 -0
  7. package/agent/components/FormField.json +2 -1
  8. package/agent/components/MobileShell.json +1 -1
  9. package/agent/components/PageContainer.json +1 -1
  10. package/agent/components/Toaster.json +2 -1
  11. package/agent/components.json +21 -4
  12. package/agent/index.json +4 -4
  13. package/agent/llms.txt +5 -5
  14. package/agent/tokens.json +12 -0
  15. package/dist/components/data-display/empty-state.d.ts +1 -1
  16. package/dist/components/data-display/empty-state.js +2 -1
  17. package/dist/components/data-entry/calendar.js +16 -1
  18. package/dist/components/data-entry/date-picker.js +22 -7
  19. package/dist/components/data-entry/record-picker.js +22 -16
  20. package/dist/components/feedback/sonner.js +4 -1
  21. package/dist/components/layout/mobile-shell.js +19 -2
  22. package/dist/components/layout/page-container.js +4 -1
  23. package/dist/contracts/measurement.json +1 -1
  24. package/dist/i18n/messages/en.json +3 -1
  25. package/dist/i18n/messages/ja.json +3 -1
  26. package/dist/i18n/messages/vi.json +3 -1
  27. package/dist/lib/datetime/picker-format.d.ts +8 -1
  28. package/dist/lib/datetime/picker-format.js +16 -4
  29. package/dist/lib/toast-clearance.d.ts +25 -0
  30. package/dist/lib/toast-clearance.js +27 -0
  31. package/dist/props/components/data-display.prop.d.ts +7 -0
  32. package/dist/props/components/data-entry.prop.d.ts +12 -0
  33. package/dist/styles/control.css +57 -0
  34. package/dist/styles/focus-ring.css +1 -0
  35. package/dist/styles/layers.json +1 -1
  36. package/dist/styles.css +1 -1
  37. package/dist/tokens/components/feedback.css +4 -0
  38. package/package.json +2 -2
@@ -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.2",
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.2",
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",