@godxjp/ui 27.0.0 → 27.3.1

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 (68) hide show
  1. package/dist/components/data-display/badge.d.ts +10 -3
  2. package/dist/components/data-display/badge.js +14 -3
  3. package/dist/components/data-display/data-table.d.ts +20 -12
  4. package/dist/components/data-display/data-table.js +53 -5
  5. package/dist/components/data-display/range-timeline.d.ts +16 -0
  6. package/dist/components/data-display/range-timeline.js +115 -80
  7. package/dist/components/data-display/table.d.ts +16 -0
  8. package/dist/components/data-display/table.js +2 -0
  9. package/dist/components/data-entry/checkbox.js +23 -5
  10. package/dist/components/data-entry/command.d.ts +3 -1
  11. package/dist/components/data-entry/command.js +10 -2
  12. package/dist/components/data-entry/form-errors.d.ts +5 -0
  13. package/dist/components/data-entry/form-errors.js +6 -0
  14. package/dist/components/data-entry/form-field.js +9 -8
  15. package/dist/components/data-entry/index.d.ts +1 -0
  16. package/dist/components/data-entry/search-select.js +145 -136
  17. package/dist/components/data-entry/select.js +38 -3
  18. package/dist/components/feedback/dialog.d.ts +4 -3
  19. package/dist/components/feedback/dialog.js +46 -19
  20. package/dist/components/feedback/non-modal-layer.d.ts +16 -0
  21. package/dist/components/feedback/non-modal-layer.js +37 -0
  22. package/dist/components/feedback/sheet.d.ts +7 -4
  23. package/dist/components/feedback/sheet.js +63 -40
  24. package/dist/components/layout/org-switcher.js +1 -1
  25. package/dist/components/layout/responsive-grid.d.ts +8 -2
  26. package/dist/components/layout/responsive-grid.js +2 -0
  27. package/dist/components/query/mutation-feedback.js +2 -4
  28. package/dist/contracts/measurement.json +1 -1
  29. package/dist/form/form-field-control.d.ts +1 -1
  30. package/dist/form/form-field-control.js +32 -23
  31. package/dist/form/form-root.d.ts +1 -1
  32. package/dist/form/form-root.js +25 -6
  33. package/dist/i18n/messages/en.json +2 -1
  34. package/dist/i18n/messages/ja.json +2 -1
  35. package/dist/i18n/messages/vi.json +2 -1
  36. package/dist/props/components/data-display.prop.d.ts +12 -0
  37. package/dist/props/components/data-entry.prop.d.ts +10 -0
  38. package/dist/props/components/feedback.prop.d.ts +7 -1
  39. package/dist/props/components/form.prop.d.ts +52 -3
  40. package/dist/props/components/index.d.ts +1 -1
  41. package/dist/props/components/layout.prop.d.ts +7 -0
  42. package/dist/props/registry.d.ts +26 -3
  43. package/dist/props/registry.js +25 -2
  44. package/dist/props/vocabulary/data.prop.d.ts +27 -1
  45. package/dist/props/vocabulary/index.d.ts +1 -1
  46. package/dist/styles/control.css +44 -1
  47. package/dist/styles/data-display-layout.css +58 -1
  48. package/dist/styles/form-layout.css +21 -0
  49. package/dist/styles/layout.css +7 -0
  50. package/dist/styles/navigation-layout.css +5 -1
  51. package/dist/styles/shell-layout.css +1 -14
  52. package/dist/styles/table-layout.css +80 -8
  53. package/dist/tokens/components/control.css +5 -0
  54. package/dist/tokens/components/data-display.css +6 -0
  55. package/dist/tokens/components/navigation.css +1 -1
  56. package/dist/tokens/components/table.css +2 -0
  57. package/docs/DESIGN-AUTHORITY.md +32 -1
  58. package/docs/FORMS.md +11 -10
  59. package/docs/data-display/data-table/examples/server-paged.tsx +156 -0
  60. package/docs/data-display/data-table/examples/striped-list.tsx +173 -0
  61. package/docs/data-display/popover.tsx +43 -1
  62. package/docs/data-display/timeline.tsx +73 -9
  63. package/docs/data-entry/command.tsx +80 -1
  64. package/docs/data-entry/form-root.tsx +12 -2
  65. package/docs/feedback/dialog.tsx +82 -0
  66. package/docs/feedback/sheet.tsx +84 -0
  67. package/docs/layout/responsive-grid.tsx +100 -0
  68. package/package.json +4 -2
@@ -6,9 +6,11 @@ import { chain, mergeRefs } from "@react-aria/utils";
6
6
  import { Dialog as RacDialog, Modal, ModalOverlay } from "react-aria-components";
7
7
  import { AlertCircle, X } from "lucide-react";
8
8
  import { cn } from "../../lib/utils.js";
9
+ import { isDevelopment } from "../../lib/dev.js";
9
10
  import { Slot } from "../../lib/slot.js";
10
11
  import { overlayHeaderToneClass } from "./overlay-header-tone.js";
11
12
  import { useOverlayCloseFocus } from "./overlay-close-focus.js";
13
+ import { closeOnEscape, NonModalPortal, useNonModalPortal } from "./non-modal-layer.js";
12
14
  import { buttonVariants } from "../general/button.js";
13
15
  import { useTranslation } from "../../i18n/use-translation.js";
14
16
  import { Button } from "../general/button.js";
@@ -41,6 +43,7 @@ function useDialogOpenState(component) {
41
43
  }
42
44
  const DialogLabelContext = React.createContext(null);
43
45
  const DialogVariantContext = React.createContext("default");
46
+ const DialogModalContext = React.createContext(true);
44
47
  const OVERLAY_CLASS = "ui-dialog-overlay data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0";
45
48
  const CONTENT_CLASS = "data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 duration-200 outline-none";
46
49
  function DialogShell({
@@ -59,8 +62,47 @@ function DialogShell({
59
62
  const descriptionId = React.useId();
60
63
  const labels = React.useMemo(() => ({ titleId, descriptionId }), [titleId, descriptionId]);
61
64
  const dataState = state.isOpen ? "open" : "closed";
62
- useOverlayCloseFocus(state.isOpen, onCloseAutoFocus);
65
+ const requestedModal = React.useContext(DialogModalContext);
66
+ const modal = requestedModal || role === "alertdialog";
67
+ const ignoresNonModal = !requestedModal && role === "alertdialog";
68
+ React.useEffect(() => {
69
+ if (ignoresNonModal && isDevelopment()) {
70
+ console.warn(
71
+ '[godxjp-ui] Dialog: `modal={false}` is ignored for `variant="destructive"` \u2014 an alertdialog is always modal (WAI-ARIA APG).'
72
+ );
73
+ }
74
+ }, [ignoresNonModal]);
75
+ const { contentRef, isMounted } = useNonModalPortal(state.isOpen, !modal);
76
+ useOverlayCloseFocus(state.isOpen, onCloseAutoFocus, modal ? void 0 : contentRef);
63
77
  const overlayPortalContainer = useOverlayPortalContainer();
78
+ const dialog = /* @__PURE__ */ jsx(
79
+ RacDialog,
80
+ {
81
+ role,
82
+ "aria-labelledby": props["aria-labelledby"] ?? titleId,
83
+ "aria-describedby": props["aria-describedby"] ?? descriptionId,
84
+ "data-slot": "dialog-content",
85
+ "data-state": dataState,
86
+ className: cn(CONTENT_CLASS, className),
87
+ render: (racProps) => {
88
+ const { "data-rac": _rac, ref: racRef, ...rest } = racProps;
89
+ return modal ? /* @__PURE__ */ jsx("section", { ...rest, ...props, style, ref: mergeRefs(ref, racRef) }) : /* @__PURE__ */ jsx(
90
+ "section",
91
+ {
92
+ ...rest,
93
+ ...props,
94
+ style,
95
+ ref: mergeRefs(ref, racRef, contentRef),
96
+ onKeyDown: chain(props.onKeyDown, closeOnEscape(() => state.setOpen(false)))
97
+ }
98
+ );
99
+ },
100
+ children: /* @__PURE__ */ jsx(DialogLabelContext.Provider, { value: labels, children })
101
+ }
102
+ );
103
+ if (!modal) {
104
+ return isMounted ? /* @__PURE__ */ jsx(NonModalPortal, { container: overlayPortalContainer, children: dialog }) : null;
105
+ }
64
106
  return /* @__PURE__ */ jsx(
65
107
  ModalOverlay,
66
108
  {
@@ -71,33 +113,18 @@ function DialogShell({
71
113
  "data-slot": "dialog-overlay",
72
114
  "data-state": dataState,
73
115
  className: cn(OVERLAY_CLASS, overlayClassName),
74
- children: /* @__PURE__ */ jsx(Modal, { className: "contents", children: /* @__PURE__ */ jsx(
75
- RacDialog,
76
- {
77
- role,
78
- "aria-labelledby": props["aria-labelledby"] ?? titleId,
79
- "aria-describedby": props["aria-describedby"] ?? descriptionId,
80
- "data-slot": "dialog-content",
81
- "data-state": dataState,
82
- className: cn(CONTENT_CLASS, className),
83
- render: (racProps) => {
84
- const { "data-rac": _rac, ref: racRef, ...rest } = racProps;
85
- return /* @__PURE__ */ jsx("section", { ...rest, ...props, style, ref: mergeRefs(ref, racRef) });
86
- },
87
- children: /* @__PURE__ */ jsx(DialogLabelContext.Provider, { value: labels, children })
88
- }
89
- ) })
116
+ children: /* @__PURE__ */ jsx(Modal, { className: "contents", children: dialog })
90
117
  }
91
118
  );
92
119
  }
93
120
  function DialogRoot({
94
121
  children,
95
- modal: _modal,
122
+ modal = true,
96
123
  variant = "default",
97
124
  ...openProps
98
125
  }) {
99
126
  const state = useOverlayOpenState(openProps);
100
- return /* @__PURE__ */ jsx(DialogOpenContext.Provider, { value: state, children: /* @__PURE__ */ jsx(DialogVariantContext.Provider, { value: variant, children }) });
127
+ return /* @__PURE__ */ jsx(DialogOpenContext.Provider, { value: state, children: /* @__PURE__ */ jsx(DialogModalContext.Provider, { value: modal, children: /* @__PURE__ */ jsx(DialogVariantContext.Provider, { value: variant, children }) }) });
101
128
  }
102
129
  function DialogTrigger({ asChild, onClick, ...props }) {
103
130
  const state = useDialogOpenState("DialogTrigger");
@@ -0,0 +1,16 @@
1
+ import * as React from "react";
2
+ /**
3
+ * Trạng thái dựng của nhánh non-modal. `enabled=false` (nhánh modal) thì không làm gì: `contentRef`
4
+ * không được gắn, và `ModalOverlay` tự lo hoạt ảnh thoát.
5
+ */
6
+ export declare function useNonModalPortal(isOpen: boolean, enabled: boolean): {
7
+ contentRef: React.RefObject<HTMLElement | null>;
8
+ isMounted: boolean;
9
+ };
10
+ /** `onKeyDown` cho thẻ dialog non-modal: Esc (tiêu điểm ở trong) đóng. */
11
+ export declare function closeOnEscape(close: () => void): (event: React.KeyboardEvent<HTMLElement>) => void;
12
+ /** Portal + `FocusScope autoFocus restoreFocus` (không `contain`) quanh bề mặt non-modal. */
13
+ export declare function NonModalPortal({ container, children, }: {
14
+ container: Element | undefined;
15
+ children: React.ReactNode;
16
+ }): React.ReactPortal | null;
@@ -0,0 +1,37 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { createPortal } from "react-dom";
5
+ import { useExitAnimation } from "@react-aria/utils";
6
+ import { FocusScope } from "react-aria";
7
+ function useNonModalPortal(isOpen, enabled) {
8
+ const contentRef = React.useRef(null);
9
+ const isExiting = useExitAnimation(contentRef, enabled && isOpen);
10
+ return { contentRef, isMounted: isOpen || isExiting };
11
+ }
12
+ function closeOnEscape(close) {
13
+ return (event) => {
14
+ if (event.key === "Escape" && !event.defaultPrevented && !event.nativeEvent.isComposing) {
15
+ event.preventDefault();
16
+ event.stopPropagation();
17
+ close();
18
+ }
19
+ };
20
+ }
21
+ function NonModalPortal({
22
+ container,
23
+ children
24
+ }) {
25
+ if (typeof document === "undefined") {
26
+ return null;
27
+ }
28
+ return createPortal(
29
+ /* @__PURE__ */ jsx(FocusScope, { autoFocus: true, restoreFocus: true, children }),
30
+ container ?? document.body
31
+ );
32
+ }
33
+ export {
34
+ NonModalPortal,
35
+ closeOnEscape,
36
+ useNonModalPortal
37
+ };
@@ -19,13 +19,16 @@ export interface SheetProps {
19
19
  /** Gọi khi trạng thái mở đổi. */
20
20
  onOpenChange?: (open: boolean) => void;
21
21
  /**
22
- * Giữ tên prop của Radix. RAC không có kiểu overlay không-modal: `Modal` LUÔN khoá cuộn và ẩn
23
- * nền khỏi trình đọc màn hình, nên `modal={false}` không còn tắt được điều đó.
22
+ * `false` renders a NON-MODAL sheet (WAI-ARIA APG allows non-modal dialogs): the page behind
23
+ * stays interactive and in the accessibility tree, no scroll lock, no scrim, an outside press
24
+ * does not close it, no `aria-modal`. Focus moves in on open, Tab can leave, Escape closes while
25
+ * focus is inside, focus returns to the trigger. Side, size and tokens are unchanged. Default
26
+ * `true`. gh#701.
24
27
  */
25
28
  modal?: boolean;
26
29
  children?: React.ReactNode;
27
30
  }
28
- export declare function Sheet({ open, defaultOpen, onOpenChange, modal: _modal, children }: SheetProps): React.JSX.Element;
31
+ export declare function Sheet({ open, defaultOpen, onOpenChange, modal, children, }: SheetProps): React.JSX.Element;
29
32
  export interface SheetTriggerProps extends React.ComponentPropsWithRef<"button"> {
30
33
  /** Mượn thẻ của con thay vì dựng `<button>` riêng. */
31
34
  asChild?: boolean;
@@ -74,7 +77,7 @@ export interface SheetContentProps extends React.ComponentPropsWithRef<"section"
74
77
  */
75
78
  responsive?: SheetResponsiveProp;
76
79
  }
77
- export declare function SheetContent({ side, className, children, showCloseButton, overlayClassName, forceMount: _forceMount, width, responsive, style, ref, ...props }: SheetContentProps): React.JSX.Element;
80
+ export declare function SheetContent({ side, className, children, showCloseButton, overlayClassName, forceMount: _forceMount, width, responsive, style, ref, ...props }: SheetContentProps): React.JSX.Element | null;
78
81
  export declare namespace SheetContent {
79
82
  var displayName: string;
80
83
  }
@@ -11,6 +11,7 @@ import { cn } from "../../lib/utils.js";
11
11
  import { Slot } from "../../lib/slot.js";
12
12
  import { overlayHeaderToneClass } from "./overlay-header-tone.js";
13
13
  import { useOverlayCloseFocus } from "./overlay-close-focus.js";
14
+ import { closeOnEscape, NonModalPortal, useNonModalPortal } from "./non-modal-layer.js";
14
15
  import { useTranslation } from "../../i18n/use-translation.js";
15
16
  const toCssLength = (v) => typeof v === "number" ? `${v}px` : v;
16
17
  const SHEET_BREAKPOINT_TOKEN = "--sheet-responsive-breakpoint-width";
@@ -28,8 +29,15 @@ function useSheetOpenState(component) {
28
29
  }
29
30
  return state;
30
31
  }
32
+ const SheetModalContext = React.createContext(true);
31
33
  const SheetLabelContext = React.createContext(null);
32
- function Sheet({ open, defaultOpen, onOpenChange, modal: _modal, children }) {
34
+ function Sheet({
35
+ open,
36
+ defaultOpen,
37
+ onOpenChange,
38
+ modal = true,
39
+ children
40
+ }) {
33
41
  const [uncontrolled, setUncontrolled] = React.useState(defaultOpen ?? false);
34
42
  const isOpen = open ?? uncontrolled;
35
43
  const setOpen = React.useCallback(
@@ -45,7 +53,7 @@ function Sheet({ open, defaultOpen, onOpenChange, modal: _modal, children }) {
45
53
  [isOpen, open, onOpenChange]
46
54
  );
47
55
  const state = React.useMemo(() => ({ isOpen, setOpen }), [isOpen, setOpen]);
48
- return /* @__PURE__ */ jsx(SheetOpenContext.Provider, { value: state, children });
56
+ return /* @__PURE__ */ jsx(SheetOpenContext.Provider, { value: state, children: /* @__PURE__ */ jsx(SheetModalContext.Provider, { value: modal, children }) });
49
57
  }
50
58
  function SheetTrigger({ asChild, onClick, ...props }) {
51
59
  const state = useSheetOpenState("SheetTrigger");
@@ -132,8 +140,59 @@ function SheetContent({
132
140
  const widthSet = width != null && horizontal;
133
141
  const mergedStyle = widthSet ? { ...style, ["--sheet-width"]: toCssLength(width) } : style;
134
142
  const dataState = state.isOpen ? "open" : "closed";
135
- useOverlayCloseFocus(state.isOpen);
143
+ const modal = React.useContext(SheetModalContext);
144
+ const { contentRef, isMounted } = useNonModalPortal(state.isOpen, !modal);
145
+ useOverlayCloseFocus(state.isOpen, void 0, modal ? void 0 : contentRef);
136
146
  const overlayPortalContainer = useOverlayPortalContainer();
147
+ const dialog = /* @__PURE__ */ jsx(
148
+ RacDialog,
149
+ {
150
+ "aria-labelledby": props["aria-labelledby"] ?? titleId,
151
+ "aria-describedby": props["aria-describedby"] ?? descriptionId,
152
+ "data-slot": "sheet-content",
153
+ "data-state": dataState,
154
+ "data-responsive": responsive,
155
+ "data-side": resolvedSide,
156
+ className: cn(
157
+ sheetVariants({ side: resolvedSide }),
158
+ // `width` caps at the viewport: full-width panel on a small screen, capped on a large one.
159
+ widthSet && "w-[min(var(--sheet-width),100%)] max-w-none sm:max-w-none",
160
+ // Only the RESPONSIVE bottom presentation is capped — a plain `side="bottom"` sheet keeps
161
+ // its content-sized height so existing usage is untouched.
162
+ bottomSheet && "max-h-[var(--sheet-bottom-max-height)]",
163
+ className
164
+ ),
165
+ render: (racProps) => {
166
+ const { "data-rac": _rac, ref: racRef, ...rest } = racProps;
167
+ return modal ? /* @__PURE__ */ jsx("section", { ...rest, ...props, style: mergedStyle, ref: mergeRefs(ref, racRef) }) : /* @__PURE__ */ jsx(
168
+ "section",
169
+ {
170
+ ...rest,
171
+ ...props,
172
+ style: mergedStyle,
173
+ ref: mergeRefs(ref, racRef, contentRef),
174
+ onKeyDown: chain(props.onKeyDown, closeOnEscape(() => state.setOpen(false)))
175
+ }
176
+ );
177
+ },
178
+ children: /* @__PURE__ */ jsxs(SheetLabelContext.Provider, { value: labels, children: [
179
+ children,
180
+ showCloseButton ? /* @__PURE__ */ jsxs(
181
+ SheetClose,
182
+ {
183
+ className: "ui-sheet-close ui-focus-ring disabled:pointer-events-none",
184
+ children: [
185
+ /* @__PURE__ */ jsx(X, { className: "ui-sheet-close-icon", "aria-hidden": "true" }),
186
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: t("feedback.alert.dismiss") })
187
+ ]
188
+ }
189
+ ) : null
190
+ ] })
191
+ }
192
+ );
193
+ if (!modal) {
194
+ return isMounted ? /* @__PURE__ */ jsx(NonModalPortal, { container: overlayPortalContainer, children: dialog }) : null;
195
+ }
137
196
  return /* @__PURE__ */ jsx(
138
197
  ModalOverlay,
139
198
  {
@@ -144,43 +203,7 @@ function SheetContent({
144
203
  "data-slot": "sheet-overlay",
145
204
  "data-state": dataState,
146
205
  className: cn(SHEET_OVERLAY_CLASS, overlayClassName),
147
- children: /* @__PURE__ */ jsx(Modal, { className: "contents", children: /* @__PURE__ */ jsx(
148
- RacDialog,
149
- {
150
- "aria-labelledby": props["aria-labelledby"] ?? titleId,
151
- "aria-describedby": props["aria-describedby"] ?? descriptionId,
152
- "data-slot": "sheet-content",
153
- "data-state": dataState,
154
- "data-responsive": responsive,
155
- "data-side": resolvedSide,
156
- className: cn(
157
- sheetVariants({ side: resolvedSide }),
158
- // `width` caps at the viewport: full-width panel on a small screen, capped on a large one.
159
- widthSet && "w-[min(var(--sheet-width),100%)] max-w-none sm:max-w-none",
160
- // Only the RESPONSIVE bottom presentation is capped — a plain `side="bottom"` sheet keeps
161
- // its content-sized height so existing usage is untouched.
162
- bottomSheet && "max-h-[var(--sheet-bottom-max-height)]",
163
- className
164
- ),
165
- render: (racProps) => {
166
- const { "data-rac": _rac, ref: racRef, ...rest } = racProps;
167
- return /* @__PURE__ */ jsx("section", { ...rest, ...props, style: mergedStyle, ref: mergeRefs(ref, racRef) });
168
- },
169
- children: /* @__PURE__ */ jsxs(SheetLabelContext.Provider, { value: labels, children: [
170
- children,
171
- showCloseButton ? /* @__PURE__ */ jsxs(
172
- SheetClose,
173
- {
174
- className: "ui-sheet-close ui-focus-ring disabled:pointer-events-none",
175
- children: [
176
- /* @__PURE__ */ jsx(X, { className: "ui-sheet-close-icon", "aria-hidden": "true" }),
177
- /* @__PURE__ */ jsx("span", { className: "sr-only", children: t("feedback.alert.dismiss") })
178
- ]
179
- }
180
- ) : null
181
- ] })
182
- }
183
- ) })
206
+ children: /* @__PURE__ */ jsx(Modal, { className: "contents", children: dialog })
184
207
  }
185
208
  );
186
209
  }
@@ -105,7 +105,7 @@ function OrgSwitcherPanel({
105
105
  ] }) : null
106
106
  ] });
107
107
  }
108
- return /* @__PURE__ */ jsxs(Command, { label: labels.title, className: "ui-org-switcher-command", children: [
108
+ return /* @__PURE__ */ jsxs(Command, { label: labels.title, className: "ui-org-switcher-command", split: true, children: [
109
109
  /* @__PURE__ */ jsx(CommandInput, { placeholder: labels.search, "aria-label": labels.search }),
110
110
  /* @__PURE__ */ jsxs(CommandList, { children: [
111
111
  /* @__PURE__ */ jsx(CommandEmpty, { children: labels.empty }),
@@ -1,5 +1,5 @@
1
1
  import type { ReactNode } from "react";
2
- import type { ResponsiveGridColumnsProp, ResponsiveGridFlowProp, ResponsiveGridPresetProp } from "../../props/components/layout.prop.js";
2
+ import type { ResponsiveGridAlignProp, ResponsiveGridColumnsProp, ResponsiveGridFlowProp, ResponsiveGridPresetProp } from "../../props/components/layout.prop.js";
3
3
  import type { PadProp, PadRawProp } from "../../props/vocabulary/index.js";
4
4
  import type { GapProp } from "../../props/vocabulary/layout.prop.js";
5
5
  export type ResponsiveGridProps = {
@@ -12,6 +12,12 @@ export type ResponsiveGridProps = {
12
12
  columns?: ResponsiveGridColumnsProp;
13
13
  /** Keep collections in a horizontal sequence; pair with ScrollArea orientation="horizontal". */
14
14
  flow?: ResponsiveGridFlowProp;
15
+ /**
16
+ * Block-axis alignment of the cells (antd `Row align`). `stretch` makes every cell as tall as
17
+ * the tallest — Kanban lanes, where an empty lane must stay a full-height drop zone. Omitted,
18
+ * `flow="columns"` aligns to `start` and `flow="rows"` stretches, as before.
19
+ */
20
+ align?: ResponsiveGridAlignProp;
15
21
  /**
16
22
  * Named column geometry for a recognised collection shape — see `ResponsiveGridPresetProp`.
17
23
  * Wins over `columns` when both are set, so a caller migrating to a preset does not also need
@@ -20,7 +26,7 @@ export type ResponsiveGridProps = {
20
26
  preset?: ResponsiveGridPresetProp;
21
27
  children: ReactNode;
22
28
  };
23
- export declare function ResponsiveGrid({ columns, flow, gap, pad, padRaw, preset, className, children, }: ResponsiveGridProps): import("react").JSX.Element;
29
+ export declare function ResponsiveGrid({ columns, flow, align, gap, pad, padRaw, preset, className, children, }: ResponsiveGridProps): import("react").JSX.Element;
24
30
  export declare namespace ResponsiveGrid {
25
31
  export { ResponsiveGridItem as Item };
26
32
  }
@@ -31,6 +31,7 @@ function toStyle(resolved) {
31
31
  function ResponsiveGrid({
32
32
  columns = 4,
33
33
  flow = "rows",
34
+ align,
34
35
  gap,
35
36
  pad,
36
37
  padRaw,
@@ -45,6 +46,7 @@ function ResponsiveGrid({
45
46
  className: cn("ui-responsive-grid", className),
46
47
  "data-gap": gap,
47
48
  "data-flow": flow === "columns" ? "columns" : void 0,
49
+ "data-align": align,
48
50
  style: { ...toStyle(resolved), ...padStyle(pad, padRaw) },
49
51
  "data-pad-raw": padRaw === void 0 ? void 0 : "",
50
52
  children
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { Fragment, jsx } from "react/jsx-runtime";
3
3
  import { AlertQueryError } from "../feedback/alert.js";
4
- import { useFormErrorsRegistry } from "../data-entry/form-errors.js";
4
+ import { errorBagHasMessage, useFormErrorsRegistry } from "../data-entry/form-errors.js";
5
5
  import { classifyQueryError } from "../../lib/query-error.js";
6
6
  function AlertMutationFeedback({
7
7
  mutation,
@@ -12,9 +12,7 @@ function AlertMutationFeedback({
12
12
  className
13
13
  }) {
14
14
  const registry = useFormErrorsRegistry();
15
- const bagShowsErrors = registry !== null && Object.values(registry.errors).some(
16
- (entry) => Array.isArray(entry) ? entry.some(Boolean) : Boolean(entry)
17
- );
15
+ const bagShowsErrors = registry !== null && errorBagHasMessage(registry.errors);
18
16
  if (mutation.isPending && pending) return /* @__PURE__ */ jsx(Fragment, { children: pending });
19
17
  if (!mutation.isError || mutation.error == null) return null;
20
18
  if ((ignoreValidationErrors ?? bagShowsErrors) && classifyQueryError(mutation.error).category === "validation") {
@@ -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": "27.0.0",
3
+ "version": "27.3.1",
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,
@@ -2,4 +2,4 @@ import * as React from "react";
2
2
  import { type FieldValues } from "react-hook-form";
3
3
  import type { FormFieldControlProp } from "../props/components/form.prop.js";
4
4
  export type { FormFieldControlProp } from "../props/components/form.prop.js";
5
- export declare function FormFieldControl<TFieldValues extends FieldValues>({ name, id, field: fieldKey, labelAddon, layout, labelWidth, controlWidth, colSpan, label, required, helper, disabled, dependencies, getValueFromEvent, normalize, help, validateStatus, hasFeedback, feedback, preserve, className, children, }: FormFieldControlProp<TFieldValues>): React.JSX.Element;
5
+ export declare function FormFieldControl<TFieldValues extends FieldValues>({ name, id, field: fieldKey, labelAddon, layout, labelWidth, controlWidth, colSpan, label, required, helper, disabled, dependencies, getValueFromEvent, normalize, help, validateStatus, hasFeedback, feedback, preserve, className, valuePropName, children, }: FormFieldControlProp<TFieldValues>): React.JSX.Element;
@@ -51,6 +51,7 @@ function FormFieldControl({
51
51
  feedback,
52
52
  preserve,
53
53
  className,
54
+ valuePropName,
54
55
  children
55
56
  }) {
56
57
  const autoId = React.useId();
@@ -62,9 +63,23 @@ function FormFieldControl({
62
63
  const isDisabled = disabled ?? formDisabled;
63
64
  const disabledProp = isDisabled ? { disabled: true } : disabled === false ? { disabled: false } : void 0;
64
65
  const readValue = (args, previousValue) => {
65
- const raw = getValueFromEvent ? getValueFromEvent(...args) : extractControlValue(args[0]);
66
+ const extracted = getValueFromEvent ? getValueFromEvent(...args) : extractControlValue(args[0]);
67
+ const raw = valuePropName === "checked" ? extracted === true : extracted;
66
68
  return normalize ? normalize(raw, previousValue) : raw;
67
69
  };
70
+ const renderControl = (bag) => {
71
+ const render = children;
72
+ if (valuePropName === "checked") {
73
+ const { value, onChange, ...rest } = bag;
74
+ return render({
75
+ ...rest,
76
+ checked: value === true,
77
+ onCheckedChange: (checked) => onChange(checked),
78
+ ...disabledProp
79
+ });
80
+ }
81
+ return render(bindField({ ...bag, ...disabledProp }));
82
+ };
68
83
  if (adapter) {
69
84
  const fieldName = String(name);
70
85
  return /* @__PURE__ */ jsx(
@@ -81,17 +96,14 @@ function FormFieldControl({
81
96
  hasFeedback,
82
97
  feedback,
83
98
  className,
84
- children: children(
85
- bindField({
86
- id: resolvedId,
87
- name: fieldName,
88
- value: adapter.getValue(fieldName),
89
- onChange: (...args) => adapter.setValue(fieldName, readValue(args, adapter.getValue(fieldName))),
90
- onBlur: () => adapter.onBlur?.(fieldName),
91
- ref: noopRef,
92
- ...disabledProp
93
- })
94
- )
99
+ children: renderControl({
100
+ id: resolvedId,
101
+ name: fieldName,
102
+ value: adapter.getValue(fieldName),
103
+ onChange: (...args) => adapter.setValue(fieldName, readValue(args, adapter.getValue(fieldName))),
104
+ onBlur: () => adapter.onBlur?.(fieldName),
105
+ ref: noopRef
106
+ })
95
107
  }
96
108
  );
97
109
  }
@@ -125,17 +137,14 @@ function FormFieldControl({
125
137
  hasFeedback,
126
138
  feedback,
127
139
  className,
128
- children: children(
129
- bindField({
130
- id: resolvedId,
131
- name: field.name,
132
- value: field.value,
133
- onChange: (...args) => field.onChange(readValue(args, field.value)),
134
- onBlur: field.onBlur,
135
- ref: field.ref,
136
- ...disabledProp
137
- })
138
- )
140
+ children: renderControl({
141
+ id: resolvedId,
142
+ name: field.name,
143
+ value: field.value,
144
+ onChange: (...args) => field.onChange(readValue(args, field.value)),
145
+ onBlur: field.onBlur,
146
+ ref: field.ref
147
+ })
139
148
  }
140
149
  )
141
150
  }
@@ -7,4 +7,4 @@ export type { FormRootProp } from "../props/components/form.prop.js";
7
7
  * validation) OR `adapter` (a framework-agnostic {@link FormStateAdapter} for a server-driven form
8
8
  * library — Inertia's `useForm` via `@godxjp/ui/inertia`, formik, TanStack Form).
9
9
  */
10
- export declare function FormRoot<TFieldValues extends FieldValues>({ form, adapter, onSubmit, onSubmitFailed, onSubmitError, onReset, scrollToFirstError, disabled, layout, labelWidth, controlWidth, labelAlign, collapseBelow, density, columns, requiredMark, errors, children, className, id, }: FormRootProp<TFieldValues>): React.JSX.Element;
10
+ export declare function FormRoot<TFieldValues extends FieldValues>({ form, adapter, onSubmit, onSubmitFailed, onSubmitError, submitFailedMessage, onReset, scrollToFirstError, disabled, layout, labelWidth, controlWidth, labelAlign, collapseBelow, density, columns, requiredMark, errors, children, className, id, }: FormRootProp<TFieldValues>): React.JSX.Element;
@@ -5,8 +5,10 @@ import { FormProvider } from "react-hook-form";
5
5
  import { Alert, AlertDescription } from "../components/feedback/alert.js";
6
6
  import { ResponsiveGrid } from "../components/layout/responsive-grid.js";
7
7
  import { useTranslation } from "../i18n/use-translation.js";
8
+ import { classifyQueryError } from "../lib/query-error.js";
8
9
  import { cn } from "../lib/utils.js";
9
10
  import { Form } from "../components/data-entry/form.js";
11
+ import { errorBagHasMessage } from "../components/data-entry/form-errors.js";
10
12
  import { FormAdapterContext, FormOptionsContext } from "./form-context.js";
11
13
  function collectErrorNames(errors, prefix = "", out = []) {
12
14
  if (!errors || typeof errors !== "object") return out;
@@ -43,6 +45,7 @@ function FormRoot({
43
45
  onSubmit,
44
46
  onSubmitFailed,
45
47
  onSubmitError,
48
+ submitFailedMessage,
46
49
  onReset,
47
50
  scrollToFirstError = true,
48
51
  disabled = false,
@@ -63,21 +66,37 @@ function FormRoot({
63
66
  const { t } = useTranslation();
64
67
  const pending = React.useRef(false);
65
68
  const [submitting, setSubmitting] = React.useState(false);
66
- const [submitFailed, setSubmitFailed] = React.useState(false);
69
+ const [submitFailure, setSubmitFailure] = React.useState(null);
67
70
  const options = React.useMemo(() => ({ disabled, submitting }), [disabled, submitting]);
71
+ const fieldsShowFailure = submitFailure?.validation === true && errorBagHasMessage(errors);
72
+ React.useEffect(() => {
73
+ if (!fieldsShowFailure) return;
74
+ setSubmitFailure(
75
+ (current) => current && !current.shownByFields ? { ...current, shownByFields: true } : current
76
+ );
77
+ }, [fieldsShowFailure]);
78
+ const showSubmitFailed = submitFailure !== null && submitFailure.settled && !submitFailure.shownByFields && !fieldsShowFailure && submitFailedMessage !== false;
68
79
  const content = /* @__PURE__ */ jsxs(Fragment, { children: [
69
- submitFailed && /* @__PURE__ */ jsx(Alert, { tone: "destructive", children: /* @__PURE__ */ jsx(AlertDescription, { children: t("dataEntry.form.submitFailed") }) }),
80
+ showSubmitFailed && /* @__PURE__ */ jsx(Alert, { tone: "destructive", children: /* @__PURE__ */ jsx(AlertDescription, { children: submitFailedMessage ?? t("dataEntry.form.submitFailed") }) }),
70
81
  columns ? /* @__PURE__ */ jsx(ResponsiveGrid, { columns, children }) : children
71
82
  ] });
72
83
  const submit = async (work) => {
73
84
  if (disabled || pending.current || adapter?.isSubmitting) return;
74
85
  pending.current = true;
75
86
  setSubmitting(true);
76
- setSubmitFailed(false);
87
+ setSubmitFailure(null);
77
88
  try {
78
89
  await work();
79
90
  } catch (error) {
80
- setSubmitFailed(true);
91
+ const validation = classifyQueryError(error).category === "validation";
92
+ setSubmitFailure({ error, validation, settled: !validation, shownByFields: false });
93
+ if (validation) {
94
+ setTimeout(
95
+ () => setSubmitFailure(
96
+ (current) => current && current.error === error ? { ...current, settled: true } : current
97
+ )
98
+ );
99
+ }
81
100
  onSubmitError?.(error);
82
101
  } finally {
83
102
  pending.current = false;
@@ -117,7 +136,7 @@ function FormRoot({
117
136
  onReset: (event) => {
118
137
  event.preventDefault();
119
138
  if (pending.current) return;
120
- setSubmitFailed(false);
139
+ setSubmitFailure(null);
121
140
  adapter.reset?.();
122
141
  onReset?.();
123
142
  },
@@ -151,7 +170,7 @@ function FormRoot({
151
170
  onReset: (event) => {
152
171
  event.preventDefault();
153
172
  if (pending.current) return;
154
- setSubmitFailed(false);
173
+ setSubmitFailure(null);
155
174
  form.reset();
156
175
  onReset?.();
157
176
  },
@@ -607,7 +607,8 @@
607
607
  "trendCurrent": "current"
608
608
  },
609
609
  "rangeTimeline": {
610
- "outsideRange": "Outside the visible range",
610
+ "outsideAfter": "After the visible range",
611
+ "outsideBefore": "Before the visible range",
611
612
  "schedule": "Schedule"
612
613
  },
613
614
  "textExamples": {
@@ -594,7 +594,8 @@
594
594
  "trendCurrent": "現在"
595
595
  },
596
596
  "rangeTimeline": {
597
- "outsideRange": "表示範囲外",
597
+ "outsideAfter": "表示範囲より後",
598
+ "outsideBefore": "表示範囲より前",
598
599
  "schedule": "スケジュール"
599
600
  },
600
601
  "textExamples": {
@@ -595,7 +595,8 @@
595
595
  "trendCurrent": "hiện tại"
596
596
  },
597
597
  "rangeTimeline": {
598
- "outsideRange": "Ngoài khoảng đang xem",
598
+ "outsideAfter": "Sau khoảng đang xem",
599
+ "outsideBefore": "Trước khoảng đang xem",
599
600
  "schedule": "Lịch công việc"
600
601
  },
601
602
  "textExamples": {
@@ -294,6 +294,12 @@ export type BadgeProp = {
294
294
  * Port name `onRemove` (not `onClose`) — see {@link Badge} JSDoc and `docs/DESIGN-AUTHORITY.md`.
295
295
  */
296
296
  onRemove?: () => void;
297
+ /**
298
+ * The × button's full accessible name, verbatim (antd 5.15+ `closable={{ 'aria-label' }}`, gh#706).
299
+ * Without it the name quotes the chip's label — a string `children`, or the label's rendered text
300
+ * when `children` is a link or other node.
301
+ */
302
+ removeLabel?: string;
297
303
  };
298
304
  /** @see CredentialReveal */
299
305
  /**
@@ -403,6 +409,12 @@ export type DataTableProp<T> = {
403
409
  onRow?: OnRowProp<T>;
404
410
  /** Outer frame + vertical rules between columns (antd `bordered`). */
405
411
  bordered?: boolean;
412
+ /**
413
+ * Zebra rows by logical record (Element Plus `stripe`, Bootstrap `.table-striped`; antd has no
414
+ * prop). Omit to inherit the theme default `--table-row-striped-alpha`; `true` / `false`
415
+ * override it for this table.
416
+ */
417
+ striped?: boolean;
406
418
  /** Explain the next sort step in a tooltip on sortable headers (antd `showSorterTooltip`). */
407
419
  showSorterTooltip?: boolean;
408
420
  /** Table-wide sort cycle; a column's own `sortDirections` wins (antd `sortDirections`). */