@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.
- package/dist/components/data-display/badge.d.ts +10 -3
- package/dist/components/data-display/badge.js +14 -3
- package/dist/components/data-display/data-table.d.ts +20 -12
- package/dist/components/data-display/data-table.js +53 -5
- package/dist/components/data-display/range-timeline.d.ts +16 -0
- package/dist/components/data-display/range-timeline.js +115 -80
- package/dist/components/data-display/table.d.ts +16 -0
- package/dist/components/data-display/table.js +2 -0
- package/dist/components/data-entry/checkbox.js +23 -5
- package/dist/components/data-entry/command.d.ts +3 -1
- package/dist/components/data-entry/command.js +10 -2
- package/dist/components/data-entry/form-errors.d.ts +5 -0
- package/dist/components/data-entry/form-errors.js +6 -0
- package/dist/components/data-entry/form-field.js +9 -8
- package/dist/components/data-entry/index.d.ts +1 -0
- package/dist/components/data-entry/search-select.js +145 -136
- package/dist/components/data-entry/select.js +38 -3
- package/dist/components/feedback/dialog.d.ts +4 -3
- package/dist/components/feedback/dialog.js +46 -19
- package/dist/components/feedback/non-modal-layer.d.ts +16 -0
- package/dist/components/feedback/non-modal-layer.js +37 -0
- package/dist/components/feedback/sheet.d.ts +7 -4
- package/dist/components/feedback/sheet.js +63 -40
- package/dist/components/layout/org-switcher.js +1 -1
- package/dist/components/layout/responsive-grid.d.ts +8 -2
- package/dist/components/layout/responsive-grid.js +2 -0
- package/dist/components/query/mutation-feedback.js +2 -4
- package/dist/contracts/measurement.json +1 -1
- package/dist/form/form-field-control.d.ts +1 -1
- package/dist/form/form-field-control.js +32 -23
- package/dist/form/form-root.d.ts +1 -1
- package/dist/form/form-root.js +25 -6
- package/dist/i18n/messages/en.json +2 -1
- package/dist/i18n/messages/ja.json +2 -1
- package/dist/i18n/messages/vi.json +2 -1
- package/dist/props/components/data-display.prop.d.ts +12 -0
- package/dist/props/components/data-entry.prop.d.ts +10 -0
- package/dist/props/components/feedback.prop.d.ts +7 -1
- package/dist/props/components/form.prop.d.ts +52 -3
- package/dist/props/components/index.d.ts +1 -1
- package/dist/props/components/layout.prop.d.ts +7 -0
- package/dist/props/registry.d.ts +26 -3
- package/dist/props/registry.js +25 -2
- package/dist/props/vocabulary/data.prop.d.ts +27 -1
- package/dist/props/vocabulary/index.d.ts +1 -1
- package/dist/styles/control.css +44 -1
- package/dist/styles/data-display-layout.css +58 -1
- package/dist/styles/form-layout.css +21 -0
- package/dist/styles/layout.css +7 -0
- package/dist/styles/navigation-layout.css +5 -1
- package/dist/styles/shell-layout.css +1 -14
- package/dist/styles/table-layout.css +80 -8
- package/dist/tokens/components/control.css +5 -0
- package/dist/tokens/components/data-display.css +6 -0
- package/dist/tokens/components/navigation.css +1 -1
- package/dist/tokens/components/table.css +2 -0
- package/docs/DESIGN-AUTHORITY.md +32 -1
- package/docs/FORMS.md +11 -10
- package/docs/data-display/data-table/examples/server-paged.tsx +156 -0
- package/docs/data-display/data-table/examples/striped-list.tsx +173 -0
- package/docs/data-display/popover.tsx +43 -1
- package/docs/data-display/timeline.tsx +73 -9
- package/docs/data-entry/command.tsx +80 -1
- package/docs/data-entry/form-root.tsx +12 -2
- package/docs/feedback/dialog.tsx +82 -0
- package/docs/feedback/sheet.tsx +84 -0
- package/docs/layout/responsive-grid.tsx +100 -0
- 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
|
-
|
|
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:
|
|
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
|
|
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
|
-
*
|
|
23
|
-
*
|
|
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
|
|
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({
|
|
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
|
-
|
|
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:
|
|
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 &&
|
|
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.
|
|
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
|
|
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:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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:
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
}
|
package/dist/form/form-root.d.ts
CHANGED
|
@@ -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;
|
package/dist/form/form-root.js
CHANGED
|
@@ -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 [
|
|
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
|
-
|
|
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
|
-
|
|
87
|
+
setSubmitFailure(null);
|
|
77
88
|
try {
|
|
78
89
|
await work();
|
|
79
90
|
} catch (error) {
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
610
|
+
"outsideAfter": "After the visible range",
|
|
611
|
+
"outsideBefore": "Before the visible range",
|
|
611
612
|
"schedule": "Schedule"
|
|
612
613
|
},
|
|
613
614
|
"textExamples": {
|
|
@@ -595,7 +595,8 @@
|
|
|
595
595
|
"trendCurrent": "hiện tại"
|
|
596
596
|
},
|
|
597
597
|
"rangeTimeline": {
|
|
598
|
-
"
|
|
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`). */
|