@godxjp/ui 26.2.0 → 26.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/data-entry/chat-composer.d.ts +1 -0
- package/dist/components/data-entry/chat-composer.js +12 -5
- package/dist/components/query/mutation-feedback.d.ts +3 -1
- package/dist/components/query/mutation-feedback.js +10 -0
- package/dist/contracts/measurement.json +1 -1
- 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/lib/platform.d.ts +10 -0
- package/dist/lib/platform.js +9 -0
- package/dist/lib/utils.d.ts +1 -0
- package/dist/lib/utils.js +3 -1
- package/dist/props/components/data-entry.prop.d.ts +30 -11
- package/dist/props/components/query.prop.d.ts +7 -0
- package/dist/props/registry.d.ts +4 -0
- package/dist/props/registry.js +5 -0
- package/dist/styles/control.css +3 -3
- package/dist/styles/form-layout.css +31 -0
- package/dist/tokens/components/control.css +4 -0
- package/docs/DESIGN-AUTHORITY.md +9 -0
- package/docs/data-entry/chat-composer.tsx +101 -1
- package/docs/query/mutation-feedback.tsx +59 -1
- package/package.json +2 -2
- package/scripts/_agent-setup.mjs +36 -11
|
@@ -31,6 +31,7 @@ export declare const ChatComposer: React.ForwardRefExoticComponent<Omit<React.HT
|
|
|
31
31
|
onCancel?: () => void;
|
|
32
32
|
loading?: import("../../props/index.js").PendingProp;
|
|
33
33
|
submitType?: import("./chat-composer.js").ChatComposerSubmitTypeProp;
|
|
34
|
+
allowEmptySubmit?: boolean;
|
|
34
35
|
placeholder?: import("../../props/index.js").PlaceholderProp;
|
|
35
36
|
disabled?: import("../../props/index.js").DisabledProp;
|
|
36
37
|
readOnly?: boolean;
|
|
@@ -4,6 +4,7 @@ import * as React from "react";
|
|
|
4
4
|
import { SendHorizontal, Square } from "lucide-react";
|
|
5
5
|
import { useTranslation } from "../../i18n/use-translation.js";
|
|
6
6
|
import { cn } from "../../lib/utils.js";
|
|
7
|
+
import { isApplePlatform } from "../../lib/platform.js";
|
|
7
8
|
import { omitFieldA11y, pickFieldA11y, useFieldIdentity } from "../../lib/field-a11y.js";
|
|
8
9
|
import { Button } from "../general/button.js";
|
|
9
10
|
import { Textarea } from "./textarea.js";
|
|
@@ -23,6 +24,7 @@ const ChatComposer = React.forwardRef(
|
|
|
23
24
|
onCancel,
|
|
24
25
|
loading = false,
|
|
25
26
|
submitType = "enter",
|
|
27
|
+
allowEmptySubmit = false,
|
|
26
28
|
placeholder,
|
|
27
29
|
disabled = false,
|
|
28
30
|
readOnly = false,
|
|
@@ -57,12 +59,13 @@ const ChatComposer = React.forwardRef(
|
|
|
57
59
|
const rest = omitFieldA11y(props);
|
|
58
60
|
const identity = useFieldIdentity({ id, name, "data-field": props["data-field"] });
|
|
59
61
|
const surface = controlSurfaceAttrs({ status, size });
|
|
60
|
-
const canSubmit = isSendable(draft) && !disabled && !readOnly && !loading;
|
|
62
|
+
const canSubmit = (allowEmptySubmit || isSendable(draft)) && !disabled && !readOnly && !loading;
|
|
61
63
|
const submit = React.useCallback(() => {
|
|
64
|
+
if (disabled || readOnly || loading) return;
|
|
62
65
|
const text = innerRef.current?.value ?? draft;
|
|
63
|
-
if (
|
|
64
|
-
onSubmit?.(
|
|
65
|
-
}, [draft, disabled, readOnly, loading, onSubmit]);
|
|
66
|
+
if (isSendable(text)) onSubmit?.(text);
|
|
67
|
+
else if (allowEmptySubmit) onSubmit?.("");
|
|
68
|
+
}, [draft, disabled, readOnly, loading, allowEmptySubmit, onSubmit]);
|
|
66
69
|
const handleKeyDown = (event) => {
|
|
67
70
|
onKeyDown?.(event);
|
|
68
71
|
if (event.defaultPrevented) return;
|
|
@@ -70,7 +73,11 @@ const ChatComposer = React.forwardRef(
|
|
|
70
73
|
if (composing.current || event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229) {
|
|
71
74
|
return;
|
|
72
75
|
}
|
|
73
|
-
const wantsSend = submitType === "
|
|
76
|
+
const wantsSend = submitType === "modEnter" ? (
|
|
77
|
+
// ⌘ on Apple platforms, Ctrl everywhere else — the other one is left alone, because
|
|
78
|
+
// Ctrl+Enter on a Mac is not the convention a Mac user reaches for.
|
|
79
|
+
isApplePlatform() ? event.metaKey : event.ctrlKey
|
|
80
|
+
) : submitType === "enter" ? !event.shiftKey : event.shiftKey;
|
|
74
81
|
if (!wantsSend) return;
|
|
75
82
|
event.preventDefault();
|
|
76
83
|
submit();
|
|
@@ -3,5 +3,7 @@ export type { AlertMutationFeedbackProp, AlertMutationFeedbackProp as AlertMutat
|
|
|
3
3
|
/**
|
|
4
4
|
* Inline mutation error — renders nothing when idle/success.
|
|
5
5
|
* Prefer toast for transient saves; use this for blocking form sections (SimulatorPage).
|
|
6
|
+
* Inside a form that received a server error bag (`FormRoot errors` / `Form errors`), a validation
|
|
7
|
+
* error is the fields' to show, so it is skipped by default (gh#690).
|
|
6
8
|
*/
|
|
7
|
-
export declare function AlertMutationFeedback({ mutation, onRetry, showRetry, pending, className, }: AlertMutationFeedbackProp): import("react").JSX.Element | null;
|
|
9
|
+
export declare function AlertMutationFeedback({ mutation, onRetry, showRetry, pending, ignoreValidationErrors, className, }: AlertMutationFeedbackProp): import("react").JSX.Element | null;
|
|
@@ -1,15 +1,25 @@
|
|
|
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";
|
|
5
|
+
import { classifyQueryError } from "../../lib/query-error.js";
|
|
4
6
|
function AlertMutationFeedback({
|
|
5
7
|
mutation,
|
|
6
8
|
onRetry,
|
|
7
9
|
showRetry = true,
|
|
8
10
|
pending,
|
|
11
|
+
ignoreValidationErrors,
|
|
9
12
|
className
|
|
10
13
|
}) {
|
|
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
|
+
);
|
|
11
18
|
if (mutation.isPending && pending) return /* @__PURE__ */ jsx(Fragment, { children: pending });
|
|
12
19
|
if (!mutation.isError || mutation.error == null) return null;
|
|
20
|
+
if ((ignoreValidationErrors ?? bagShowsErrors) && classifyQueryError(mutation.error).category === "validation") {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
13
23
|
return /* @__PURE__ */ jsx(
|
|
14
24
|
AlertQueryError,
|
|
15
25
|
{
|
|
@@ -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": "26.
|
|
3
|
+
"version": "26.4.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,
|
|
@@ -169,7 +169,8 @@
|
|
|
169
169
|
"send": "Send message",
|
|
170
170
|
"cancel": "Stop generating",
|
|
171
171
|
"hintEnter": "Enter to send · Shift + Enter for a new line",
|
|
172
|
-
"hintShiftEnter": "Shift + Enter to send · Enter for a new line"
|
|
172
|
+
"hintShiftEnter": "Shift + Enter to send · Enter for a new line",
|
|
173
|
+
"hintModEnter": "{modifier} + Enter to send · Enter for a new line"
|
|
173
174
|
},
|
|
174
175
|
"chatSuggestion": {
|
|
175
176
|
"label": "Suggestions",
|
|
@@ -166,7 +166,8 @@
|
|
|
166
166
|
"send": "メッセージを送信",
|
|
167
167
|
"cancel": "生成を停止",
|
|
168
168
|
"hintEnter": "Enter で送信 · Shift + Enter で改行",
|
|
169
|
-
"hintShiftEnter": "Shift + Enter で送信 · Enter で改行"
|
|
169
|
+
"hintShiftEnter": "Shift + Enter で送信 · Enter で改行",
|
|
170
|
+
"hintModEnter": "{modifier} + Enter で送信 · Enter で改行"
|
|
170
171
|
},
|
|
171
172
|
"chatSuggestion": {
|
|
172
173
|
"label": "候補",
|
|
@@ -166,7 +166,8 @@
|
|
|
166
166
|
"send": "Gửi tin nhắn",
|
|
167
167
|
"cancel": "Dừng tạo câu trả lời",
|
|
168
168
|
"hintEnter": "Enter để gửi · Shift + Enter để xuống dòng",
|
|
169
|
-
"hintShiftEnter": "Shift + Enter để gửi · Enter để xuống dòng"
|
|
169
|
+
"hintShiftEnter": "Shift + Enter để gửi · Enter để xuống dòng",
|
|
170
|
+
"hintModEnter": "{modifier} + Enter để gửi · Enter để xuống dòng"
|
|
170
171
|
},
|
|
171
172
|
"chatSuggestion": {
|
|
172
173
|
"label": "Gợi ý",
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the user agent runs on an Apple platform (macOS, iOS, iPadOS), where the primary
|
|
3
|
+
* shortcut modifier is ⌘ (`metaKey`) rather than Ctrl (`ctrlKey`).
|
|
4
|
+
*
|
|
5
|
+
* `navigator.userAgentData.platform` is the standards answer (Chromium); `navigator.platform` is
|
|
6
|
+
* the deprecated but universally present fallback (Safari and Firefox still report `MacIntel` /
|
|
7
|
+
* `iPhone` / `iPad` there). Outside a browser there is no platform, so the answer is `false` — a
|
|
8
|
+
* server render therefore describes the Ctrl shortcut.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isApplePlatform(): boolean;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
function isApplePlatform() {
|
|
2
|
+
if (typeof navigator === "undefined") return false;
|
|
3
|
+
const data = navigator.userAgentData;
|
|
4
|
+
const platform = data?.platform || navigator.platform || "";
|
|
5
|
+
return /mac|iphone|ipad|ipod/i.test(platform);
|
|
6
|
+
}
|
|
7
|
+
export {
|
|
8
|
+
isApplePlatform
|
|
9
|
+
};
|
package/dist/lib/utils.d.ts
CHANGED
package/dist/lib/utils.js
CHANGED
|
@@ -294,9 +294,9 @@ export type FormFieldProp = {
|
|
|
294
294
|
*
|
|
295
295
|
* `before` is for a helper the reader needs BEFORE they answer rather than after: the
|
|
296
296
|
* secondary language of a bilingual form, a unit or format note, a pick-one-of-these
|
|
297
|
-
* preamble. `labelAddon` cannot carry that — it
|
|
298
|
-
*
|
|
299
|
-
*
|
|
297
|
+
* preamble. `labelAddon` cannot carry that — it belongs to the label row, sized for a chip, a
|
|
298
|
+
* help button or a short text action; in a horizontal/inline field it wraps under the label
|
|
299
|
+
* inside the label column, so a full sentence stacks there instead of above the input. Putting the second line inside `label` does work, but costs the
|
|
300
300
|
* string-label fallbacks (`aria-label`, `FieldNameContext`), which fire only when `label` is
|
|
301
301
|
* a plain string.
|
|
302
302
|
*
|
|
@@ -308,7 +308,11 @@ export type FormFieldProp = {
|
|
|
308
308
|
validateStatus?: "success" | "warning" | "error" | "validating";
|
|
309
309
|
hasFeedback?: boolean;
|
|
310
310
|
feedback?: React.ReactNode;
|
|
311
|
-
/**
|
|
311
|
+
/**
|
|
312
|
+
* Optional control rendered after the label (e.g. a help button, a short text action). In a
|
|
313
|
+
* horizontal/inline field the label row wraps: an addon that does not fit beside the label
|
|
314
|
+
* drops to its own line under it, capped to the label column, never into the control column.
|
|
315
|
+
*/
|
|
312
316
|
labelAddon?: React.ReactNode;
|
|
313
317
|
/** Override the Form's layout for this field only. */
|
|
314
318
|
layout?: FormLayoutProp;
|
|
@@ -341,9 +345,9 @@ export type FormFieldProp = {
|
|
|
341
345
|
*
|
|
342
346
|
* `before` is for a helper the reader needs BEFORE they answer rather than after: the
|
|
343
347
|
* secondary language of a bilingual form, a unit or format note, a pick-one-of-these
|
|
344
|
-
* preamble. `labelAddon` cannot carry that — it
|
|
345
|
-
*
|
|
346
|
-
*
|
|
348
|
+
* preamble. `labelAddon` cannot carry that — it belongs to the label row, sized for a chip, a
|
|
349
|
+
* help button or a short text action; in a horizontal/inline field it wraps under the label
|
|
350
|
+
* inside the label column, so a full sentence stacks there instead of above the input. Putting the second line inside `label` does work, but costs the
|
|
347
351
|
* string-label fallbacks (`aria-label`, `FieldNameContext`), which fire only when `label` is
|
|
348
352
|
* a plain string.
|
|
349
353
|
*
|
|
@@ -355,7 +359,11 @@ export type FormFieldProp = {
|
|
|
355
359
|
validateStatus?: "success" | "warning" | "error" | "validating";
|
|
356
360
|
hasFeedback?: boolean;
|
|
357
361
|
feedback?: React.ReactNode;
|
|
358
|
-
/**
|
|
362
|
+
/**
|
|
363
|
+
* Optional control rendered after the label (e.g. a help button, a short text action). In a
|
|
364
|
+
* horizontal/inline field the label row wraps: an addon that does not fit beside the label
|
|
365
|
+
* drops to its own line under it, capped to the label column, never into the control column.
|
|
366
|
+
*/
|
|
359
367
|
labelAddon?: React.ReactNode;
|
|
360
368
|
/** Override the Form's layout for this field only. */
|
|
361
369
|
layout?: FormLayoutProp;
|
|
@@ -1774,10 +1782,13 @@ export type BranchScopePickerProp = FieldA11yProps & {
|
|
|
1774
1782
|
*
|
|
1775
1783
|
* `enter` (default) is the chat convention: `Enter` sends, `Shift+Enter` inserts a newline.
|
|
1776
1784
|
* `shiftEnter` is the inverse, for composers that hold long, deliberately multi-line drafts.
|
|
1777
|
-
*
|
|
1785
|
+
* `modEnter` is this library's extension (antd X has only the first two; see
|
|
1786
|
+
* docs/DESIGN-AUTHORITY.md): `⌘+Enter` on Apple platforms, `Ctrl+Enter` elsewhere sends, while
|
|
1787
|
+
* `Enter` and `Shift+Enter` both insert a newline — the record-comment convention.
|
|
1788
|
+
* None of them ever fires while an IME conversion is in flight.
|
|
1778
1789
|
* @see ChatComposer
|
|
1779
1790
|
*/
|
|
1780
|
-
export type ChatComposerSubmitTypeProp = "enter" | "shiftEnter";
|
|
1791
|
+
export type ChatComposerSubmitTypeProp = "enter" | "shiftEnter" | "modEnter";
|
|
1781
1792
|
/**
|
|
1782
1793
|
* @see ChatComposer — the message input of a conversation (Ant Design X `Sender`; the industry
|
|
1783
1794
|
* calls the control a *composer*, so that is what it is named).
|
|
@@ -1797,7 +1808,8 @@ export type ChatComposerProp = Omit<React.HTMLAttributes<HTMLDivElement>, "onSub
|
|
|
1797
1808
|
onValueChange?: OnValueChangeProp<string>;
|
|
1798
1809
|
/**
|
|
1799
1810
|
* Send the draft. Receives the text as typed; never fires for an empty or whitespace-only
|
|
1800
|
-
* draft, and never while `loading`,
|
|
1811
|
+
* draft (unless `allowEmptySubmit`, which then passes `""`), and never while `loading`,
|
|
1812
|
+
* `disabled` or `readOnly`.
|
|
1801
1813
|
*/
|
|
1802
1814
|
onSubmit?: (value: string) => void;
|
|
1803
1815
|
/** Stop the in-flight response. Only reachable while `loading`. */
|
|
@@ -1809,6 +1821,13 @@ export type ChatComposerProp = Omit<React.HTMLAttributes<HTMLDivElement>, "onSub
|
|
|
1809
1821
|
loading?: PendingProp;
|
|
1810
1822
|
/** Which keystroke sends and which breaks the line. Default `enter`. */
|
|
1811
1823
|
submitType?: ChatComposerSubmitTypeProp;
|
|
1824
|
+
/**
|
|
1825
|
+
* Let an empty or whitespace-only draft be sent — for a composer whose `header`/`footer` carry
|
|
1826
|
+
* payload of their own (a status change on a record). The send button stays enabled and both
|
|
1827
|
+
* the button and the keyboard submit call `onSubmit("")`. Still blocked while `loading`,
|
|
1828
|
+
* `disabled` or `readOnly`. Default `false`.
|
|
1829
|
+
*/
|
|
1830
|
+
allowEmptySubmit?: boolean;
|
|
1812
1831
|
/** Empty-state text of the draft box; pass it through `t()` at the call site. */
|
|
1813
1832
|
placeholder?: PlaceholderProp;
|
|
1814
1833
|
/** Disable the whole composer (draft box and every action). */
|
|
@@ -31,6 +31,13 @@ export type AlertMutationFeedbackProp = {
|
|
|
31
31
|
showRetry?: boolean;
|
|
32
32
|
/** Optional inline pending slot while `mutation.isPending`. */
|
|
33
33
|
pending?: React.ReactNode;
|
|
34
|
+
/**
|
|
35
|
+
* Skip rendering when the error classifies as a validation error (`classifyQueryError` category
|
|
36
|
+
* `"validation"`: 400/422). `true` skips every such error. When omitted, the alert is skipped only
|
|
37
|
+
* inside a `FormRoot`/`Form` whose `errors` bag holds at least one message (the fields show it;
|
|
38
|
+
* `FormErrors` shows unclaimed keys); an empty/absent bag still renders the alert.
|
|
39
|
+
*/
|
|
40
|
+
ignoreValidationErrors?: boolean;
|
|
34
41
|
className?: ClassNameProp;
|
|
35
42
|
};
|
|
36
43
|
type QueryRefetchLike = Pick<UseQueryResult<unknown>, "isFetching" | "refetch">;
|
package/dist/props/registry.d.ts
CHANGED
|
@@ -2048,6 +2048,10 @@ export declare const COMPONENT_PROP_REGISTRY: {
|
|
|
2048
2048
|
readonly field: "pending";
|
|
2049
2049
|
readonly local: true;
|
|
2050
2050
|
readonly reason: "Inline pending ReactNode slot, not boolean PendingProp state.";
|
|
2051
|
+
}, {
|
|
2052
|
+
readonly field: "ignoreValidationErrors";
|
|
2053
|
+
readonly local: true;
|
|
2054
|
+
readonly reason: "Query-error category filter; defaults on inside a Form whose error bag holds messages.";
|
|
2051
2055
|
}, "ClassNameProp"];
|
|
2052
2056
|
};
|
|
2053
2057
|
readonly ButtonRefetchProp: {
|
package/dist/props/registry.js
CHANGED
|
@@ -2401,6 +2401,11 @@ const COMPONENT_PROP_REGISTRY = {
|
|
|
2401
2401
|
local: true,
|
|
2402
2402
|
reason: "Inline pending ReactNode slot, not boolean PendingProp state."
|
|
2403
2403
|
},
|
|
2404
|
+
{
|
|
2405
|
+
field: "ignoreValidationErrors",
|
|
2406
|
+
local: true,
|
|
2407
|
+
reason: "Query-error category filter; defaults on inside a Form whose error bag holds messages."
|
|
2408
|
+
},
|
|
2404
2409
|
"ClassNameProp"
|
|
2405
2410
|
]
|
|
2406
2411
|
},
|
package/dist/styles/control.css
CHANGED
|
@@ -1060,7 +1060,7 @@
|
|
|
1060
1060
|
border: 0;
|
|
1061
1061
|
padding: 0;
|
|
1062
1062
|
cursor: pointer;
|
|
1063
|
-
color: hsl(var(--muted-foreground) /
|
|
1063
|
+
color: hsl(var(--rating-star-empty-color, var(--muted-foreground)) / var(--rating-star-empty-alpha));
|
|
1064
1064
|
line-height: 0;
|
|
1065
1065
|
}
|
|
1066
1066
|
.ui-rating-star:disabled {
|
|
@@ -1072,7 +1072,7 @@
|
|
|
1072
1072
|
}
|
|
1073
1073
|
|
|
1074
1074
|
.ui-rating-star-filled {
|
|
1075
|
-
color: hsl(var(--warning));
|
|
1075
|
+
color: hsl(var(--rating-star-filled-color, var(--warning)));
|
|
1076
1076
|
}
|
|
1077
1077
|
.ui-rating-star-filled svg {
|
|
1078
1078
|
fill: currentColor;
|
|
@@ -3017,7 +3017,7 @@
|
|
|
3017
3017
|
}
|
|
3018
3018
|
|
|
3019
3019
|
.ui-rating-star-half-filled {
|
|
3020
|
-
color: hsl(var(--warning));
|
|
3020
|
+
color: hsl(var(--rating-star-filled-color, var(--warning)));
|
|
3021
3021
|
}
|
|
3022
3022
|
|
|
3023
3023
|
.ui-radio-button-bar {
|
|
@@ -99,9 +99,16 @@
|
|
|
99
99
|
column-gap: var(--form-label-gap);
|
|
100
100
|
row-gap: var(--space-stack-xs);
|
|
101
101
|
}
|
|
102
|
+
|
|
102
103
|
.ui-form-field[data-collapse-below="false"]:is([data-layout="horizontal"], [data-layout="inline"])
|
|
103
104
|
> .ui-form-field-label {
|
|
104
105
|
min-block-size: var(--control-height);
|
|
106
|
+
flex-wrap: wrap;
|
|
107
|
+
}
|
|
108
|
+
.ui-form-field[data-collapse-below="false"]:is([data-layout="horizontal"], [data-layout="inline"])
|
|
109
|
+
> .ui-form-field-label
|
|
110
|
+
> * {
|
|
111
|
+
max-inline-size: 100%;
|
|
105
112
|
}
|
|
106
113
|
.ui-form-field[data-collapse-below="false"][data-label-align="end"]:is(
|
|
107
114
|
[data-layout="horizontal"],
|
|
@@ -125,6 +132,12 @@
|
|
|
125
132
|
.ui-form-field[data-collapse-below="sm"]:is([data-layout="horizontal"], [data-layout="inline"])
|
|
126
133
|
> .ui-form-field-label {
|
|
127
134
|
min-block-size: var(--control-height);
|
|
135
|
+
flex-wrap: wrap;
|
|
136
|
+
}
|
|
137
|
+
.ui-form-field[data-collapse-below="sm"]:is([data-layout="horizontal"], [data-layout="inline"])
|
|
138
|
+
> .ui-form-field-label
|
|
139
|
+
> * {
|
|
140
|
+
max-inline-size: 100%;
|
|
128
141
|
}
|
|
129
142
|
.ui-form-field[data-collapse-below="sm"][data-label-align="end"]:is(
|
|
130
143
|
[data-layout="horizontal"],
|
|
@@ -149,6 +162,12 @@
|
|
|
149
162
|
.ui-form-field[data-collapse-below="md"]:is([data-layout="horizontal"], [data-layout="inline"])
|
|
150
163
|
> .ui-form-field-label {
|
|
151
164
|
min-block-size: var(--control-height);
|
|
165
|
+
flex-wrap: wrap;
|
|
166
|
+
}
|
|
167
|
+
.ui-form-field[data-collapse-below="md"]:is([data-layout="horizontal"], [data-layout="inline"])
|
|
168
|
+
> .ui-form-field-label
|
|
169
|
+
> * {
|
|
170
|
+
max-inline-size: 100%;
|
|
152
171
|
}
|
|
153
172
|
.ui-form-field[data-collapse-below="md"][data-label-align="end"]:is(
|
|
154
173
|
[data-layout="horizontal"],
|
|
@@ -173,6 +192,12 @@
|
|
|
173
192
|
.ui-form-field[data-collapse-below="lg"]:is([data-layout="horizontal"], [data-layout="inline"])
|
|
174
193
|
> .ui-form-field-label {
|
|
175
194
|
min-block-size: var(--control-height);
|
|
195
|
+
flex-wrap: wrap;
|
|
196
|
+
}
|
|
197
|
+
.ui-form-field[data-collapse-below="lg"]:is([data-layout="horizontal"], [data-layout="inline"])
|
|
198
|
+
> .ui-form-field-label
|
|
199
|
+
> * {
|
|
200
|
+
max-inline-size: 100%;
|
|
176
201
|
}
|
|
177
202
|
.ui-form-field[data-collapse-below="lg"][data-label-align="end"]:is(
|
|
178
203
|
[data-layout="horizontal"],
|
|
@@ -197,6 +222,12 @@
|
|
|
197
222
|
.ui-form-field[data-collapse-below="xl"]:is([data-layout="horizontal"], [data-layout="inline"])
|
|
198
223
|
> .ui-form-field-label {
|
|
199
224
|
min-block-size: var(--control-height);
|
|
225
|
+
flex-wrap: wrap;
|
|
226
|
+
}
|
|
227
|
+
.ui-form-field[data-collapse-below="xl"]:is([data-layout="horizontal"], [data-layout="inline"])
|
|
228
|
+
> .ui-form-field-label
|
|
229
|
+
> * {
|
|
230
|
+
max-inline-size: 100%;
|
|
200
231
|
}
|
|
201
232
|
.ui-form-field[data-collapse-below="xl"][data-label-align="end"]:is(
|
|
202
233
|
[data-layout="horizontal"],
|
|
@@ -82,6 +82,10 @@
|
|
|
82
82
|
|
|
83
83
|
--rating-focus-ring-offset: 2px;
|
|
84
84
|
|
|
85
|
+
--rating-star-filled-color: initial;
|
|
86
|
+
--rating-star-empty-color: initial;
|
|
87
|
+
--rating-star-empty-alpha: 0.45;
|
|
88
|
+
|
|
85
89
|
--checkbox-border-width: var(--stroke-hairline);
|
|
86
90
|
--checkbox-size: calc(1rem * var(--scaling));
|
|
87
91
|
--checkbox-size-compact: 0.875rem;
|
package/docs/DESIGN-AUTHORITY.md
CHANGED
|
@@ -257,6 +257,15 @@ in page CSS.
|
|
|
257
257
|
`onClose`, because `onClose` already means overlay dismiss across Dialog/Drawer and would read
|
|
258
258
|
as closing a surface, not removing one applied filter chip. Implemented on `Badge` — the DS chip
|
|
259
259
|
primitive — rather than adding a separate `Tag` export beside `Badge`.
|
|
260
|
+
- **`ChatComposer.submitType="modEnter"` and `ChatComposer.allowEmptySubmit`.** Ant Design X
|
|
261
|
+
`Sender.submitType` is only `"enter" | "shiftEnter"`, and `Sender` never submits an empty draft.
|
|
262
|
+
A comment composer on a record needs the convention GitHub, Jira, Linear and GitLab share —
|
|
263
|
+
`Enter` breaks the line, `⌘+Enter` (Apple platforms, `metaKey`) / `Ctrl+Enter` (elsewhere,
|
|
264
|
+
`ctrlKey`) sends — and needs to post a status change with no text (gh#693). `"modEnter"` extends
|
|
265
|
+
antd's union rather than renaming it, so both antd values keep their meaning; its hint is
|
|
266
|
+
`dataEntry.chatComposer.hintModEnter` with `{modifier}` from `isApplePlatform()`
|
|
267
|
+
(`@godxjp/ui/lib/utils`). `allowEmptySubmit` (default `false`, antd's behaviour) sends `""`,
|
|
268
|
+
still never while `loading` / `disabled` / `readOnly`.
|
|
260
269
|
|
|
261
270
|
**A knob that only a fork could reach is not parity either.** antd's `components`,
|
|
262
271
|
`filterDropdown`, `classNames`/`styles` semantic maps and `prefixCls` all exist to let a consumer
|
|
@@ -14,9 +14,12 @@ import {
|
|
|
14
14
|
ChatComposer,
|
|
15
15
|
ChatSuggestion,
|
|
16
16
|
FormField,
|
|
17
|
+
Select,
|
|
17
18
|
type ChatSuggestionItemProp,
|
|
18
19
|
} from "@godxjp/ui/data-entry";
|
|
19
20
|
import { Button, Text } from "@godxjp/ui/general";
|
|
21
|
+
import { useTranslation } from "@godxjp/ui/i18n";
|
|
22
|
+
import { isApplePlatform } from "@godxjp/ui/lib/utils";
|
|
20
23
|
import {
|
|
21
24
|
AppShell,
|
|
22
25
|
Flex,
|
|
@@ -34,7 +37,8 @@ import { Bot, MessageSquare, Paperclip, Settings, Smile, Sparkles, Users } from
|
|
|
34
37
|
*
|
|
35
38
|
* The first card is the real screen: a live assistant transcript whose composer sends, streams and
|
|
36
39
|
* cancels, with `/` slash commands and `@` mentions wired to the same draft. Every card after it
|
|
37
|
-
* exists so the whole API is visible AT REST —
|
|
40
|
+
* exists so the whole API is visible AT REST — every `submitType` value (including the record-
|
|
41
|
+
* comment bar: `modEnter` + `allowEmptySubmit`), all four `size` steps,
|
|
38
42
|
* every slot, and each non-default state — without having to click anything.
|
|
39
43
|
*
|
|
40
44
|
* Composed only from real @godxjp/ui components.
|
|
@@ -91,6 +95,20 @@ const OPENING: Message[] = [
|
|
|
91
95
|
},
|
|
92
96
|
];
|
|
93
97
|
|
|
98
|
+
/** Record statuses for the comment bar — a status change is postable without any text. */
|
|
99
|
+
const STATUSES = [
|
|
100
|
+
{ value: "open", label: "未対応" },
|
|
101
|
+
{ value: "inProgress", label: "処理中" },
|
|
102
|
+
{ value: "resolved", label: "処理済み" },
|
|
103
|
+
{ value: "closed", label: "完了" },
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
interface Comment {
|
|
107
|
+
id: number;
|
|
108
|
+
body: string;
|
|
109
|
+
status: string | null;
|
|
110
|
+
}
|
|
111
|
+
|
|
94
112
|
/** One transcript line. The message FEED is ChatBubbleList's job; here it only sets the scene. */
|
|
95
113
|
function Line({ message }: { message: Message }) {
|
|
96
114
|
const mine = message.author === "you";
|
|
@@ -110,6 +128,8 @@ function Line({ message }: { message: Message }) {
|
|
|
110
128
|
}
|
|
111
129
|
|
|
112
130
|
export default function Demo() {
|
|
131
|
+
const { t } = useTranslation();
|
|
132
|
+
|
|
113
133
|
// ── Card 1: the live screen ───────────────────────────────────────────────────────────────
|
|
114
134
|
const [messages, setMessages] = useState<Message[]>(OPENING);
|
|
115
135
|
const [draft, setDraft] = useState("");
|
|
@@ -142,6 +162,24 @@ export default function Demo() {
|
|
|
142
162
|
const [shiftDraft, setShiftDraft] = useState("Enter は改行、Shift + Enter で送信");
|
|
143
163
|
const [lastSent, setLastSent] = useState<string>("—");
|
|
144
164
|
|
|
165
|
+
// ── Card 2b: the record-comment bar (modEnter + allowEmptySubmit) ──────────────────────────
|
|
166
|
+
const [commentDraft, setCommentDraft] = useState("");
|
|
167
|
+
const [status, setStatus] = useState("open");
|
|
168
|
+
const [savedStatus, setSavedStatus] = useState("open");
|
|
169
|
+
const [comments, setComments] = useState<Comment[]>([]);
|
|
170
|
+
|
|
171
|
+
function postComment(text: string) {
|
|
172
|
+
const changed = status !== savedStatus;
|
|
173
|
+
// An empty draft with no status change carries nothing — the consumer decides, not the box.
|
|
174
|
+
if (!text && !changed) return;
|
|
175
|
+
setComments((current) => [
|
|
176
|
+
...current,
|
|
177
|
+
{ id: current.length + 1, body: text, status: changed ? status : null },
|
|
178
|
+
]);
|
|
179
|
+
setSavedStatus(status);
|
|
180
|
+
setCommentDraft("");
|
|
181
|
+
}
|
|
182
|
+
|
|
145
183
|
// ── Card 3: the four size steps ───────────────────────────────────────────────────────────
|
|
146
184
|
const [sizeDrafts, setSizeDrafts] = useState<Record<string, string>>({
|
|
147
185
|
xs: "xs",
|
|
@@ -286,6 +324,68 @@ export default function Demo() {
|
|
|
286
324
|
</CardContent>
|
|
287
325
|
</Card>
|
|
288
326
|
|
|
327
|
+
{/* ── 2b. Record comment bar ───────────────────────────────────────────────────── */}
|
|
328
|
+
<Card>
|
|
329
|
+
<CardHeader>
|
|
330
|
+
<CardTitle level={2}>
|
|
331
|
+
submitType="modEnter" · allowEmptySubmit · 課題へのコメント
|
|
332
|
+
</CardTitle>
|
|
333
|
+
<CardDescription>
|
|
334
|
+
Enter と Shift + Enter は改行、⌘ + Enter(Mac)/ Ctrl + Enter(Windows・Linux)で
|
|
335
|
+
投稿します。allowEmptySubmit により本文が空でも送信でき、ステータスだけを
|
|
336
|
+
変更できます(onSubmit には空文字が渡ります)。
|
|
337
|
+
</CardDescription>
|
|
338
|
+
</CardHeader>
|
|
339
|
+
<CardContent>
|
|
340
|
+
<Flex direction="col" gap="md">
|
|
341
|
+
{comments.length === 0 ? (
|
|
342
|
+
<Text size="sm" tone="muted">
|
|
343
|
+
まだコメントはありません
|
|
344
|
+
</Text>
|
|
345
|
+
) : (
|
|
346
|
+
comments.map((comment) => (
|
|
347
|
+
<Flex key={comment.id} direction="col" gap="xs">
|
|
348
|
+
{comment.status ? (
|
|
349
|
+
<Badge tone="info">
|
|
350
|
+
ステータス:
|
|
351
|
+
{STATUSES.find((option) => option.value === comment.status)?.label}
|
|
352
|
+
</Badge>
|
|
353
|
+
) : null}
|
|
354
|
+
{comment.body ? <Text>{comment.body}</Text> : null}
|
|
355
|
+
</Flex>
|
|
356
|
+
))
|
|
357
|
+
)}
|
|
358
|
+
<ChatComposer
|
|
359
|
+
aria-label="課題へのコメント"
|
|
360
|
+
submitType="modEnter"
|
|
361
|
+
allowEmptySubmit
|
|
362
|
+
value={commentDraft}
|
|
363
|
+
onValueChange={setCommentDraft}
|
|
364
|
+
onSubmit={postComment}
|
|
365
|
+
placeholder="コメントを入力"
|
|
366
|
+
submitLabel="コメントを投稿"
|
|
367
|
+
footer={
|
|
368
|
+
<Flex direction="row" gap="sm" align="center" justify="between" wrap>
|
|
369
|
+
<Select
|
|
370
|
+
size="sm"
|
|
371
|
+
aria-label="ステータス"
|
|
372
|
+
value={status}
|
|
373
|
+
onValueChange={setStatus}
|
|
374
|
+
clearable={false}
|
|
375
|
+
options={STATUSES}
|
|
376
|
+
/>
|
|
377
|
+
<Text size="xs" tone="muted">
|
|
378
|
+
{t("dataEntry.chatComposer.hintModEnter", {
|
|
379
|
+
modifier: isApplePlatform() ? "⌘" : "Ctrl",
|
|
380
|
+
})}
|
|
381
|
+
</Text>
|
|
382
|
+
</Flex>
|
|
383
|
+
}
|
|
384
|
+
/>
|
|
385
|
+
</Flex>
|
|
386
|
+
</CardContent>
|
|
387
|
+
</Card>
|
|
388
|
+
|
|
289
389
|
{/* ── 3. Every size step ───────────────────────────────────────────────────────── */}
|
|
290
390
|
<Card>
|
|
291
391
|
<CardHeader>
|
|
@@ -4,15 +4,19 @@ import { QueryClient, QueryClientProvider, useMutation } from "@tanstack/react-q
|
|
|
4
4
|
|
|
5
5
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@godxjp/ui/data-display";
|
|
6
6
|
import { FormField, Input } from "@godxjp/ui/data-entry";
|
|
7
|
+
import { FormFieldControl, FormRoot, useZodForm } from "@godxjp/ui/form";
|
|
7
8
|
import { Button } from "@godxjp/ui/general";
|
|
8
9
|
import { Flex, PageContainer } from "@godxjp/ui/layout";
|
|
9
10
|
import { AlertMutationFeedback } from "@godxjp/ui/query";
|
|
11
|
+
import { z } from "zod";
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* AlertMutationFeedback · inline mutation error below a form's submit. Renders
|
|
13
15
|
* NOTHING while idle/successful; surfaces the error + a retry when useMutation
|
|
14
16
|
* fails. Composed from real @godxjp/ui + @tanstack/react-query. (This demo fires
|
|
15
|
-
* the mutation once on mount so the error state is visible.)
|
|
17
|
+
* the mutation once on mount so the error state is visible.) Inside a FormRoot
|
|
18
|
+
* that received `errors`, a 400/422 validation error is skipped by default —
|
|
19
|
+
* the fields already show the bag — while a 5xx still renders the alert.
|
|
16
20
|
*/
|
|
17
21
|
const queryClient = new QueryClient();
|
|
18
22
|
|
|
@@ -54,6 +58,59 @@ function Block() {
|
|
|
54
58
|
);
|
|
55
59
|
}
|
|
56
60
|
|
|
61
|
+
type SaveError = Error & { status: number; errors?: Record<string, string[]> };
|
|
62
|
+
|
|
63
|
+
function saveError(status: number, message: string, errors?: Record<string, string[]>) {
|
|
64
|
+
return Object.assign(new Error(message), { status, errors }) as SaveError;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const schema = z.object({ code: z.string() });
|
|
68
|
+
|
|
69
|
+
function InFormBlock() {
|
|
70
|
+
const form = useZodForm(schema, { defaultValues: { code: "BTY-0012" } });
|
|
71
|
+
const mutation = useMutation<void, SaveError, number>({
|
|
72
|
+
mutationFn: async (status) => {
|
|
73
|
+
throw status === 422
|
|
74
|
+
? saveError(422, "The given data was invalid.", {
|
|
75
|
+
code: ["取引先コードが重複しています"],
|
|
76
|
+
})
|
|
77
|
+
: saveError(500, "サーバーエラーが発生しました (500)");
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
mutation.mutate(422);
|
|
83
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
84
|
+
}, []);
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<Card>
|
|
88
|
+
<CardHeader>
|
|
89
|
+
<CardTitle level={2}>取引先の登録 · FormRoot errors</CardTitle>
|
|
90
|
+
<CardDescription>
|
|
91
|
+
A 422 shows on the field only; a 500 still renders the alert.
|
|
92
|
+
</CardDescription>
|
|
93
|
+
</CardHeader>
|
|
94
|
+
<CardContent>
|
|
95
|
+
<FormRoot form={form} errors={mutation.error?.errors} onSubmit={() => mutation.mutate(422)}>
|
|
96
|
+
<FormFieldControl name="code" label="取引先コード">
|
|
97
|
+
{(field) => <Input {...field} value={String(field.value ?? "")} />}
|
|
98
|
+
</FormFieldControl>
|
|
99
|
+
<AlertMutationFeedback mutation={mutation} onRetry={() => mutation.mutate(500)} />
|
|
100
|
+
<Flex direction="row" wrap justify="end" gap="sm">
|
|
101
|
+
<Button variant="outline" type="button" onClick={() => mutation.mutate(500)}>
|
|
102
|
+
500 を再現
|
|
103
|
+
</Button>
|
|
104
|
+
<Button type="submit" loading={mutation.isPending}>
|
|
105
|
+
保存
|
|
106
|
+
</Button>
|
|
107
|
+
</Flex>
|
|
108
|
+
</FormRoot>
|
|
109
|
+
</CardContent>
|
|
110
|
+
</Card>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
57
114
|
export default function Demo() {
|
|
58
115
|
return (
|
|
59
116
|
<QueryClientProvider client={queryClient}>
|
|
@@ -63,6 +120,7 @@ export default function Demo() {
|
|
|
63
120
|
>
|
|
64
121
|
<Flex direction="col" gap="lg">
|
|
65
122
|
<Block />
|
|
123
|
+
<InFormBlock />
|
|
66
124
|
</Flex>
|
|
67
125
|
</PageContainer>
|
|
68
126
|
</QueryClientProvider>
|
package/package.json
CHANGED
package/scripts/_agent-setup.mjs
CHANGED
|
@@ -58,12 +58,19 @@ function isPackageWrittenMcpEntry(entry) {
|
|
|
58
58
|
return Object.keys(entry.env ?? {}).every((key) => key === "GODX_UI_VERSION");
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
/** True when an `mcpServers` entry launches the catalog server, under any key or launcher flags. */
|
|
62
|
+
function runsUiMcp(entry) {
|
|
63
|
+
const parts = [entry?.command, ...(Array.isArray(entry?.args) ? entry.args : [])];
|
|
64
|
+
return parts.some((part) => typeof part === "string" && /(^|\/)@godxjp\/ui-mcp(@|$)/.test(part));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function mcpConfigMismatchMessage(root, existing, expected, key = MCP_KEY) {
|
|
62
68
|
const ui = readConsumerUiMetadata(root);
|
|
63
69
|
const pin = expected.args?.[0] ?? "@godxjp/ui-mcp";
|
|
64
70
|
const ver = ui?.version ?? "(unknown)";
|
|
65
71
|
return (
|
|
66
|
-
`present (custom godx-ui MCP entry
|
|
72
|
+
`present (custom godx-ui MCP entry${key === MCP_KEY ? "" : ` under key "${key}"`} — not overwritten, ` +
|
|
73
|
+
`no duplicate added; expected ${pin} with ` +
|
|
67
74
|
`env.GODX_UI_VERSION=${ver} from node_modules/@godxjp/ui)`
|
|
68
75
|
);
|
|
69
76
|
}
|
|
@@ -265,7 +272,18 @@ function readJsonFile(path) {
|
|
|
265
272
|
// writing the result back is the same data loss by a different door, so it is a refusal too.
|
|
266
273
|
if (json === null || typeof json !== "object" || Array.isArray(json))
|
|
267
274
|
return { state: "wrong-shape" };
|
|
268
|
-
return { state: "ok", json };
|
|
275
|
+
return { state: "ok", json, indent: indentOf(raw) };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* The indentation the file already uses — a tab, or the width of the first indented line — so a
|
|
280
|
+
* rewrite changes only the keys it touches. Re-serialising at a fixed 2 turned a one-entry edit
|
|
281
|
+
* into a whole-file diff in a repo formatted at 4 (gh#692).
|
|
282
|
+
*/
|
|
283
|
+
function indentOf(raw) {
|
|
284
|
+
const first = raw.match(/\n([ \t]+)\S/);
|
|
285
|
+
if (!first) return 2;
|
|
286
|
+
return first[1].startsWith("\t") ? "\t" : first[1].length;
|
|
269
287
|
}
|
|
270
288
|
|
|
271
289
|
/** The sentence that goes in the refusal, so a consumer knows which of the three it hit. */
|
|
@@ -385,18 +403,25 @@ export function ensureMcpJson(root) {
|
|
|
385
403
|
return refuseAndSuggest(path, suggested, "`mcpServers` is not an object");
|
|
386
404
|
}
|
|
387
405
|
json.mcpServers = json.mcpServers ?? {};
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
406
|
+
const indent = read.state === "ok" ? read.indent : 2;
|
|
407
|
+
// The catalog server may already be registered under ANOTHER key — `godxjp-ui`, `ui`, whatever
|
|
408
|
+
// the repo chose. Looking only at MCP_KEY added a second copy of the same server beside it, at
|
|
409
|
+
// a different version (gh#692). Whatever key runs @godxjp/ui-mcp is the entry.
|
|
410
|
+
const key = json.mcpServers[MCP_KEY]
|
|
411
|
+
? MCP_KEY
|
|
412
|
+
: Object.keys(json.mcpServers).find((name) => runsUiMcp(json.mcpServers[name]));
|
|
413
|
+
if (key) {
|
|
414
|
+
if (mcpEntryMatches(json.mcpServers[key], expected)) return "present";
|
|
415
|
+
if (!isPackageWrittenMcpEntry(json.mcpServers[key])) {
|
|
416
|
+
return mcpConfigMismatchMessage(root, json.mcpServers[key], expected, key);
|
|
392
417
|
}
|
|
393
|
-
json.mcpServers[
|
|
394
|
-
writeFileAtomic(path, JSON.stringify(json, null,
|
|
418
|
+
json.mcpServers[key] = expected;
|
|
419
|
+
writeFileAtomic(path, JSON.stringify(json, null, indent) + "\n");
|
|
395
420
|
return "refreshed";
|
|
396
421
|
}
|
|
397
422
|
const created = read.state === "missing";
|
|
398
423
|
json.mcpServers[MCP_KEY] = expected;
|
|
399
|
-
writeFileAtomic(path, JSON.stringify(json, null,
|
|
424
|
+
writeFileAtomic(path, JSON.stringify(json, null, indent) + "\n");
|
|
400
425
|
return created ? "created" : "added";
|
|
401
426
|
}
|
|
402
427
|
|
|
@@ -450,7 +475,7 @@ export function ensureClaudeHooks(root) {
|
|
|
450
475
|
added.push("SessionStart:workflow-primer");
|
|
451
476
|
}
|
|
452
477
|
|
|
453
|
-
writeFileAtomic(path, JSON.stringify(json, null, 2) + "\n");
|
|
478
|
+
writeFileAtomic(path, JSON.stringify(json, null, read.state === "ok" ? read.indent : 2) + "\n");
|
|
454
479
|
return added;
|
|
455
480
|
}
|
|
456
481
|
|