@godxjp/ui 26.1.0 → 26.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +8 -8
- package/dist/styles/float-button-layout.css +1 -1
- package/dist/styles/focus-ring.css +1 -1
- package/dist/styles/form-layout.css +31 -0
- package/dist/styles/layout.css +8 -1
- package/dist/styles/shell-layout.css +5 -5
- package/dist/tokens/components/control.css +7 -6
- package/dist/tokens/components/float-button.css +1 -1
- package/dist/tokens/components/segmented.css +1 -1
- package/dist/tokens/components/shell.css +4 -4
- package/dist/tokens/foundation.css +4 -7
- package/docs/CUSTOMER-THEMING.md +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 +55 -10
- package/scripts/cli.mjs +63 -2
|
@@ -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.3.0",
|
|
4
4
|
"targetSize": {
|
|
5
5
|
"standard": "WCAG 2.2 SC 2.5.8 Target Size (Minimum), level AA — 24×24 CSS px",
|
|
6
6
|
"min": 24,
|
|
@@ -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
|
@@ -1119,7 +1119,7 @@
|
|
|
1119
1119
|
}
|
|
1120
1120
|
|
|
1121
1121
|
.ui-segmented-item:not([data-state="checked"]):not([data-disabled]):hover {
|
|
1122
|
-
background: hsl(var(--segmented-item-hover-background));
|
|
1122
|
+
background: hsl(var(--segmented-item-hover-background, var(--accent)));
|
|
1123
1123
|
color: hsl(var(--segmented-item-hover-color));
|
|
1124
1124
|
}
|
|
1125
1125
|
.ui-segmented-item:not([data-state="checked"]):not([data-disabled]):active {
|
|
@@ -2722,7 +2722,7 @@
|
|
|
2722
2722
|
}
|
|
2723
2723
|
|
|
2724
2724
|
.ui-control--filled:hover:not(:disabled):not([readonly]):not([data-disabled]) {
|
|
2725
|
-
background-color: hsl(var(--control-variant-filled-hover-background));
|
|
2725
|
+
background-color: hsl(var(--control-variant-filled-hover-background, var(--accent)));
|
|
2726
2726
|
}
|
|
2727
2727
|
|
|
2728
2728
|
.ui-control--borderless {
|
|
@@ -2939,7 +2939,7 @@
|
|
|
2939
2939
|
}
|
|
2940
2940
|
|
|
2941
2941
|
.ui-slider-dot[data-active="true"] {
|
|
2942
|
-
border-color: hsl(var(--slider-dot-active-border-color));
|
|
2942
|
+
border-color: hsl(var(--slider-dot-active-border-color, var(--primary)));
|
|
2943
2943
|
}
|
|
2944
2944
|
|
|
2945
2945
|
.ui-slider-tooltip {
|
|
@@ -3053,14 +3053,14 @@
|
|
|
3053
3053
|
}
|
|
3054
3054
|
|
|
3055
3055
|
.ui-radio-button[data-state="checked"] {
|
|
3056
|
-
color: hsl(var(--choice-button-selected-color));
|
|
3057
|
-
border-color: hsl(var(--choice-button-selected-border-color));
|
|
3056
|
+
color: hsl(var(--choice-button-selected-color, var(--primary)));
|
|
3057
|
+
border-color: hsl(var(--choice-button-selected-border-color, var(--primary)));
|
|
3058
3058
|
}
|
|
3059
3059
|
|
|
3060
3060
|
.ui-radio-button-bar[data-button-style="solid"] .ui-radio-button[data-state="checked"] {
|
|
3061
|
-
color: hsl(var(--choice-button-solid-color));
|
|
3062
|
-
background: hsl(var(--choice-button-solid-background));
|
|
3063
|
-
border-color: hsl(var(--choice-button-solid-background));
|
|
3061
|
+
color: hsl(var(--choice-button-solid-color, var(--primary-foreground)));
|
|
3062
|
+
background: hsl(var(--choice-button-solid-background, var(--primary)));
|
|
3063
|
+
border-color: hsl(var(--choice-button-solid-background, var(--primary)));
|
|
3064
3064
|
}
|
|
3065
3065
|
|
|
3066
3066
|
.ui-radio-button[data-disabled] {
|
|
@@ -143,7 +143,7 @@
|
|
|
143
143
|
content: "";
|
|
144
144
|
inset: 0;
|
|
145
145
|
background: conic-gradient(
|
|
146
|
-
var(--float-button-progress-color) var(--float-button-progress-offset),
|
|
146
|
+
var(--float-button-progress-color, hsl(var(--primary))) var(--float-button-progress-offset),
|
|
147
147
|
var(--float-button-progress-track-color) var(--float-button-progress-offset)
|
|
148
148
|
);
|
|
149
149
|
mask: radial-gradient(
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
.ui-otp-slot[data-active="true"],
|
|
54
54
|
.ui-otp-group[data-appearance="grouped"]:has(.ui-otp-slot[data-active="true"]) {
|
|
55
55
|
outline: var(--focus-ring-width) solid
|
|
56
|
-
hsl(var(--focus-outline-color) / var(--focus-ring-opacity, 1));
|
|
56
|
+
hsl(var(--focus-outline-color, var(--focus-ring-color, var(--ring))) / var(--focus-ring-opacity, 1));
|
|
57
57
|
outline-offset: var(--focus-ring-offset);
|
|
58
58
|
|
|
59
59
|
transition:
|
|
@@ -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"],
|
package/dist/styles/layout.css
CHANGED
|
@@ -96,7 +96,14 @@
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
.ui-brand-glow {
|
|
99
|
-
background-image: var(
|
|
99
|
+
background-image: var(
|
|
100
|
+
--brand-glow,
|
|
101
|
+
radial-gradient(
|
|
102
|
+
ellipse var(--brand-glow-size) at var(--brand-glow-position),
|
|
103
|
+
hsl(var(--brand-glow-color, var(--primary)) / var(--brand-glow-alpha)),
|
|
104
|
+
transparent 70%
|
|
105
|
+
)
|
|
106
|
+
);
|
|
100
107
|
pointer-events: none;
|
|
101
108
|
}
|
|
102
109
|
|
|
@@ -1434,8 +1434,8 @@
|
|
|
1434
1434
|
}
|
|
1435
1435
|
|
|
1436
1436
|
.ui-app-launcher-tile:hover {
|
|
1437
|
-
background: hsl(var(--app-launcher-tile-hover-background));
|
|
1438
|
-
color: hsl(var(--app-launcher-tile-hover-color));
|
|
1437
|
+
background: hsl(var(--app-launcher-tile-hover-background, var(--accent)));
|
|
1438
|
+
color: hsl(var(--app-launcher-tile-hover-color, var(--accent-foreground)));
|
|
1439
1439
|
}
|
|
1440
1440
|
|
|
1441
1441
|
.ui-app-launcher-tile[aria-current="page"] {
|
|
@@ -2040,14 +2040,14 @@
|
|
|
2040
2040
|
}
|
|
2041
2041
|
|
|
2042
2042
|
.ui-topbar-item:hover {
|
|
2043
|
-
background: hsl(var(--topbar-item-hover-background));
|
|
2044
|
-
color: hsl(var(--topbar-item-hover-color));
|
|
2043
|
+
background: hsl(var(--topbar-item-hover-background, var(--accent)));
|
|
2044
|
+
color: hsl(var(--topbar-item-hover-color, var(--accent-foreground)));
|
|
2045
2045
|
}
|
|
2046
2046
|
|
|
2047
2047
|
.ui-topbar-item:active,
|
|
2048
2048
|
.ui-topbar-item[data-state="open"] {
|
|
2049
2049
|
background: hsl(var(--topbar-item-active-background));
|
|
2050
|
-
color: hsl(var(--topbar-item-hover-color));
|
|
2050
|
+
color: hsl(var(--topbar-item-hover-color, var(--accent-foreground)));
|
|
2051
2051
|
}
|
|
2052
2052
|
|
|
2053
2053
|
.ui-topbar-item:disabled,
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
--number-input-touch-height: var(--band-height-2xl);
|
|
36
36
|
|
|
37
37
|
--control-variant-filled-background: var(--muted);
|
|
38
|
-
--control-variant-filled-hover-background:
|
|
38
|
+
--control-variant-filled-hover-background: initial;
|
|
39
39
|
|
|
40
40
|
--control-count-font-size: var(--font-size-sm);
|
|
41
41
|
--control-count-color: var(--muted-foreground);
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
--slider-dot-size: var(--slider-track-height);
|
|
59
59
|
--slider-dot-background: var(--background);
|
|
60
60
|
--slider-dot-border-color: var(--border);
|
|
61
|
-
--slider-dot-active-border-color:
|
|
61
|
+
--slider-dot-active-border-color: initial;
|
|
62
62
|
--slider-tooltip-background: var(--popover);
|
|
63
63
|
--slider-tooltip-color: var(--popover-foreground);
|
|
64
64
|
--slider-tooltip-font-size: var(--font-size-xs);
|
|
@@ -69,10 +69,11 @@
|
|
|
69
69
|
--choice-button-background: var(--background);
|
|
70
70
|
--choice-button-color: var(--foreground);
|
|
71
71
|
--choice-button-border-color: var(--input);
|
|
72
|
-
|
|
73
|
-
--choice-button-selected-
|
|
74
|
-
--choice-button-
|
|
75
|
-
--choice-button-solid-
|
|
72
|
+
|
|
73
|
+
--choice-button-selected-color: initial;
|
|
74
|
+
--choice-button-selected-border-color: initial;
|
|
75
|
+
--choice-button-solid-background: initial;
|
|
76
|
+
--choice-button-solid-color: initial;
|
|
76
77
|
|
|
77
78
|
--toggle-focus-ring-width: var(--stroke-lg);
|
|
78
79
|
--toggle-focus-ring-alpha: 1;
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
|
|
33
33
|
--float-button-progress-offset: 0turn;
|
|
34
34
|
--float-button-progress-width: var(--space-1);
|
|
35
|
-
--float-button-progress-color:
|
|
35
|
+
--float-button-progress-color: initial;
|
|
36
36
|
--float-button-progress-track-color: hsl(var(--border));
|
|
37
37
|
|
|
38
38
|
--float-button-motion-translate: var(--float-button-size);
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
--segmented-item-hover-color: var(--foreground);
|
|
18
18
|
--segmented-item-selected-color: var(--foreground);
|
|
19
19
|
|
|
20
|
-
--segmented-item-hover-background:
|
|
20
|
+
--segmented-item-hover-background: initial;
|
|
21
21
|
--segmented-item-active-background: var(--secondary);
|
|
22
22
|
|
|
23
23
|
--segmented-item-selected-background: var(--background);
|
|
@@ -110,8 +110,8 @@
|
|
|
110
110
|
--topbar-item-radius: var(--radius-sharp);
|
|
111
111
|
--topbar-item-color: var(--muted-foreground);
|
|
112
112
|
|
|
113
|
-
--topbar-item-hover-background:
|
|
114
|
-
--topbar-item-hover-color:
|
|
113
|
+
--topbar-item-hover-background: initial;
|
|
114
|
+
--topbar-item-hover-color: initial;
|
|
115
115
|
--topbar-item-active-background: var(--secondary);
|
|
116
116
|
|
|
117
117
|
--topbar-item-badge-offset-block: var(--space-1);
|
|
@@ -158,8 +158,8 @@
|
|
|
158
158
|
--app-launcher-tile-min-height: var(--control-height-comfortable);
|
|
159
159
|
--app-launcher-tile-radius: var(--radius);
|
|
160
160
|
--app-launcher-tile-color: var(--foreground);
|
|
161
|
-
--app-launcher-tile-hover-background:
|
|
162
|
-
--app-launcher-tile-hover-color:
|
|
161
|
+
--app-launcher-tile-hover-background: initial;
|
|
162
|
+
--app-launcher-tile-hover-color: initial;
|
|
163
163
|
|
|
164
164
|
--app-launcher-tile-current-background: initial;
|
|
165
165
|
--app-launcher-mark-size: 2.5rem;
|
|
@@ -100,19 +100,16 @@
|
|
|
100
100
|
|
|
101
101
|
--shadow-glow: 0 0 0 0 transparent;
|
|
102
102
|
|
|
103
|
-
--brand-glow-color:
|
|
103
|
+
--brand-glow-color: initial;
|
|
104
104
|
--brand-glow-alpha: 0.18;
|
|
105
105
|
--brand-glow-size: 60% 50%;
|
|
106
106
|
--brand-glow-position: 50% 0%;
|
|
107
|
-
--brand-glow:
|
|
108
|
-
ellipse var(--brand-glow-size) at var(--brand-glow-position),
|
|
109
|
-
hsl(var(--brand-glow-color) / var(--brand-glow-alpha)),
|
|
110
|
-
transparent 70%
|
|
111
|
-
);
|
|
107
|
+
--brand-glow: initial;
|
|
112
108
|
|
|
113
109
|
--focus-outline: 1;
|
|
114
110
|
--focus-outline-weight: var(--stroke-hairline);
|
|
115
|
-
|
|
111
|
+
|
|
112
|
+
--focus-outline-color: initial;
|
|
116
113
|
|
|
117
114
|
--control-outline-width: calc(var(--stroke-md) * var(--focus-outline));
|
|
118
115
|
--focus-outline-offset: 0px;
|
package/docs/CUSTOMER-THEMING.md
CHANGED
|
@@ -243,6 +243,9 @@ The interaction states are **derived from the `--primary` in scope, at the eleme
|
|
|
243
243
|
| `--control-outline` | **yes** — the focused field's halo (`--control-outline-alpha` stays per theme) | light `h s calc(l - 5.3)`, dark `h calc(s * 0.99) calc(l - 25.1)` |
|
|
244
244
|
| `--primary-border` | **yes** (painted by no package surface; kept for the measurement in DESIGN-AUTHORITY) | light `h calc(s * 0.68) calc(l + 16)`, dark `h calc(s * 0.467) calc(l * 0.27043)` |
|
|
245
245
|
| `--sidebar-item-active-foreground` | **yes** — defaults to the live `--primary-active` | — |
|
|
246
|
+
| `--focus-outline-color` | **yes, through `--ring`** — every keyboard-focus outline (gh#687) | `var(--focus-ring-color, var(--ring))`, read at the focused element — so it follows the `--ring` you set in the scope |
|
|
247
|
+
| Radio button bar selected (`--choice-button-*`), Slider active dot, BackTop progress, `.ui-brand-glow` | **yes** (gh#687) | `var(--primary)` / `var(--primary-foreground)` at the call site |
|
|
248
|
+
| Topbar item / AppLauncher tile / Segmented / filled-control hover | **follow `--accent`** (gh#687) | `var(--accent)` / `var(--accent-foreground)` at the call site |
|
|
246
249
|
| `--primary-foreground` | **no — set it** | the label on a filled primary; you choose it for your seed |
|
|
247
250
|
| `--ring` | **only on the element that declares `--primary`** — set it in a nested scope | `var(--primary)` on `:root` / `.dark`. `--ring` is a public role read as `hsl(var(--ring))` in consumer CSS, so it cannot become a live default; a scope below `<html>` inherits the root's |
|
|
248
251
|
| `--destructive-*`, `--control-outline-error` | no — not brand | literals |
|
|
@@ -251,6 +254,7 @@ On the package seed these produce exactly the identity kit values the tier used
|
|
|
251
254
|
|
|
252
255
|
**Rules that come with it:**
|
|
253
256
|
|
|
257
|
+
- **No token tier binds to a scoped role at `:root`.** Every knob whose default is `--primary`, `--primary-foreground`, `--ring`, `--accent` or `--accent-foreground` is `initial`, with the role as the call-site fallback — `--ring` itself is the one exception (above). `tenant-scope-freeze-687.test.ts` holds it.
|
|
254
258
|
- **Override a state only by setting its knob** (`--primary-hover: 221 90% 72%`). A set knob wins over the derived default, in its own scope and below.
|
|
255
259
|
- **Read a state through its fallback, not bare.** The four knobs are `initial` so that the default can resolve at the painting element (docs/TOKENS.md, the freeze rule). `hsl(var(--primary-hover))` on its own therefore paints nothing — use the utility (`bg-primary-hover`) or `hsl(var(--primary-hover, from hsl(var(--primary)) var(--primary-hover-channels)))`.
|
|
256
260
|
- **Label polarity.** The theme's pair steps AWAY from that theme's default label: `darken` under a light label, `lighten` under a dark one, which keeps a label that clears 4.5:1 at rest at 4.5:1 in hover and pressed for every seed (measured, `derived-seed-sweep.test.ts`). If your seed needs the OTHER polarity of label (a pale yellow with dark text in the light theme), point the pair at it too — `applyPrimaryColor()` does this automatically from the label it picks:
|
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
|
@@ -45,12 +45,32 @@ function mcpEntryMatches(a, b) {
|
|
|
45
45
|
return keysA.every((k) => envA[k] === envB[k]);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
/**
|
|
49
|
+
* True when `entry` is the shape THIS package writes (`npx @godxjp/ui-mcp@<pin>`, env at most
|
|
50
|
+
* GODX_UI_VERSION), whatever pin and version it carries. Such an entry is ours to move forward on
|
|
51
|
+
* upgrade: treating it as custom left consumers on the MCP of the release they first installed
|
|
52
|
+
* (a `@godxjp/ui-mcp@25.4.0` pin survived the upgrade to 26.x).
|
|
53
|
+
*/
|
|
54
|
+
function isPackageWrittenMcpEntry(entry) {
|
|
55
|
+
if (entry?.command !== "npx") return false;
|
|
56
|
+
const args = entry.args ?? [];
|
|
57
|
+
if (args.length !== 1 || !/^@godxjp\/ui-mcp@[^\s]+$/.test(args[0])) return false;
|
|
58
|
+
return Object.keys(entry.env ?? {}).every((key) => key === "GODX_UI_VERSION");
|
|
59
|
+
}
|
|
60
|
+
|
|
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) {
|
|
49
68
|
const ui = readConsumerUiMetadata(root);
|
|
50
69
|
const pin = expected.args?.[0] ?? "@godxjp/ui-mcp";
|
|
51
70
|
const ver = ui?.version ?? "(unknown)";
|
|
52
71
|
return (
|
|
53
|
-
`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 ` +
|
|
54
74
|
`env.GODX_UI_VERSION=${ver} from node_modules/@godxjp/ui)`
|
|
55
75
|
);
|
|
56
76
|
}
|
|
@@ -252,7 +272,18 @@ function readJsonFile(path) {
|
|
|
252
272
|
// writing the result back is the same data loss by a different door, so it is a refusal too.
|
|
253
273
|
if (json === null || typeof json !== "object" || Array.isArray(json))
|
|
254
274
|
return { state: "wrong-shape" };
|
|
255
|
-
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;
|
|
256
287
|
}
|
|
257
288
|
|
|
258
289
|
/** The sentence that goes in the refusal, so a consumer knows which of the three it hit. */
|
|
@@ -347,7 +378,11 @@ export function refreshBlock(current, next, startMarker, endMarker) {
|
|
|
347
378
|
if (!endMarker) return null;
|
|
348
379
|
const j = current.indexOf(endMarker, first);
|
|
349
380
|
if (j < 0) return null;
|
|
350
|
-
|
|
381
|
+
// `next` ends in its own newline, and the old block's newline is still the first character after
|
|
382
|
+
// the end marker — keeping both grew the file by one blank line on every refresh.
|
|
383
|
+
let tail = current.slice(j + endMarker.length);
|
|
384
|
+
if (next.endsWith("\n") && tail.startsWith("\n")) tail = tail.slice(1);
|
|
385
|
+
return current.slice(0, first) + next + tail;
|
|
351
386
|
}
|
|
352
387
|
|
|
353
388
|
export function ensureMcpJson(root) {
|
|
@@ -368,15 +403,25 @@ export function ensureMcpJson(root) {
|
|
|
368
403
|
return refuseAndSuggest(path, suggested, "`mcpServers` is not an object");
|
|
369
404
|
}
|
|
370
405
|
json.mcpServers = json.mcpServers ?? {};
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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);
|
|
374
417
|
}
|
|
375
|
-
|
|
418
|
+
json.mcpServers[key] = expected;
|
|
419
|
+
writeFileAtomic(path, JSON.stringify(json, null, indent) + "\n");
|
|
420
|
+
return "refreshed";
|
|
376
421
|
}
|
|
377
422
|
const created = read.state === "missing";
|
|
378
423
|
json.mcpServers[MCP_KEY] = expected;
|
|
379
|
-
writeFileAtomic(path, JSON.stringify(json, null,
|
|
424
|
+
writeFileAtomic(path, JSON.stringify(json, null, indent) + "\n");
|
|
380
425
|
return created ? "created" : "added";
|
|
381
426
|
}
|
|
382
427
|
|
|
@@ -430,7 +475,7 @@ export function ensureClaudeHooks(root) {
|
|
|
430
475
|
added.push("SessionStart:workflow-primer");
|
|
431
476
|
}
|
|
432
477
|
|
|
433
|
-
writeFileAtomic(path, JSON.stringify(json, null, 2) + "\n");
|
|
478
|
+
writeFileAtomic(path, JSON.stringify(json, null, read.state === "ok" ? read.indent : 2) + "\n");
|
|
434
479
|
return added;
|
|
435
480
|
}
|
|
436
481
|
|
package/scripts/cli.mjs
CHANGED
|
@@ -18,13 +18,74 @@ const MAP = {
|
|
|
18
18
|
"visual-audit": "visual-audit.mjs",
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
+
// `<command> --help` is answered HERE: the scripts take free positional arguments, so a `--help`
|
|
22
|
+
// forwarded to ui-audit used to be read as nothing and ran a full audit instead of printing help.
|
|
23
|
+
const HELP = {
|
|
24
|
+
"init-agent": `godxjp-ui init-agent
|
|
25
|
+
|
|
26
|
+
Install the agent forcing-kit into the current consumer app (run it from the app root):
|
|
27
|
+
.mcp.json the godx-ui MCP server
|
|
28
|
+
.claude/settings.json a PostToolUse hook that audits every Write/Edit of a .tsx file
|
|
29
|
+
.claude/godxjp-ui-workflow.md
|
|
30
|
+
CLAUDE.md the godxjp-ui mandate block
|
|
31
|
+
Idempotent. Refuses inside the @godxjp/ui repo itself. Restart the agent afterwards.`,
|
|
32
|
+
"sync-rules": `godxjp-ui sync-rules
|
|
33
|
+
|
|
34
|
+
Refresh the package-owned agent files for the installed @godxjp/ui version: the godx-ui entry in
|
|
35
|
+
.mcp.json, the managed CLAUDE.md block, .claude/godxjp-ui-workflow.md and .ai/rules/godxjp-ui.md.
|
|
36
|
+
It is the postinstall step, run by hand — use it when the app installs with ignore-scripts=true,
|
|
37
|
+
where postinstall never runs and the rules go stale. Silent no-op in CI and when opted out.`,
|
|
38
|
+
audit: `godxjp-ui audit [dir …] [--changed] [--format json] [--quiet] [--rules]
|
|
39
|
+
|
|
40
|
+
Static UI-standardization audit over source (regex, no browser, fast). Exits non-zero on errors.
|
|
41
|
+
dir … directories or files to scan
|
|
42
|
+
--changed scan what this branch changed vs origin/main (committed, staged, untracked);
|
|
43
|
+
fails rather than reporting clean when origin/main cannot be resolved
|
|
44
|
+
--format json machine-readable findings
|
|
45
|
+
--quiet print errors only (warnings hidden)
|
|
46
|
+
--rules print the rule catalog as JSON and exit
|
|
47
|
+
Pre-commit: npx godxjp-ui audit resources/js || exit 1`,
|
|
48
|
+
"visual-audit": `godxjp-ui visual-audit [--format json] [--strict] <baseUrl> [route …]
|
|
49
|
+
|
|
50
|
+
Runtime audit (Playwright + axe-core) against an app you are ALREADY running locally.
|
|
51
|
+
baseUrl the running app, e.g. http://localhost:5173
|
|
52
|
+
route … paths appended to baseUrl (default "/")
|
|
53
|
+
--strict exit non-zero on any finding, not only errors
|
|
54
|
+
--format json machine-readable findings
|
|
55
|
+
--rules print the visual rule catalog as JSON and exit
|
|
56
|
+
Needs the playwright peer installed.`,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const USAGE = `usage: godxjp-ui <command> [args]
|
|
60
|
+
|
|
61
|
+
commands:
|
|
62
|
+
init-agent install the agent forcing-kit (MCP + auto-audit hook + CLAUDE.md mandate)
|
|
63
|
+
sync-rules refresh package-owned agent rules (postinstall, by hand)
|
|
64
|
+
audit static UI audit over source
|
|
65
|
+
visual-audit runtime audit (Playwright + axe-core) against a running app
|
|
66
|
+
|
|
67
|
+
godxjp-ui <command> --help details and flags for one command`;
|
|
68
|
+
|
|
69
|
+
const isHelp = (arg) => arg === "--help" || arg === "-h" || arg === "help";
|
|
21
70
|
const [cmd, ...rest] = process.argv.slice(2);
|
|
71
|
+
|
|
72
|
+
if (cmd === undefined || isHelp(cmd)) {
|
|
73
|
+
const topic = isHelp(cmd) ? rest[0] : undefined;
|
|
74
|
+
console.log(topic && HELP[topic] ? HELP[topic] : USAGE);
|
|
75
|
+
process.exit(cmd === undefined ? 1 : 0);
|
|
76
|
+
}
|
|
22
77
|
const script = MAP[cmd];
|
|
23
78
|
if (!script) {
|
|
24
|
-
console.error(
|
|
79
|
+
console.error(`unknown command: ${cmd}\n\n${USAGE}`);
|
|
25
80
|
process.exit(1);
|
|
26
81
|
}
|
|
82
|
+
if (rest.includes("--help") || rest.includes("-h")) {
|
|
83
|
+
console.log(HELP[cmd]);
|
|
84
|
+
process.exit(0);
|
|
85
|
+
}
|
|
27
86
|
const env =
|
|
28
|
-
cmd === "sync-rules"
|
|
87
|
+
cmd === "sync-rules"
|
|
88
|
+
? { ...process.env, INIT_CWD: process.env.INIT_CWD ?? process.cwd() }
|
|
89
|
+
: process.env;
|
|
29
90
|
const r = spawnSync("node", [join(HERE, script), ...rest], { stdio: "inherit", env });
|
|
30
91
|
process.exit(r.status ?? 0);
|