@godxjp/ui 31.7.0 → 31.9.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/agent/START-HERE.md +4 -4
- package/agent/components/ColorPicker.json +16 -8
- package/agent/components/Select.json +22 -0
- package/agent/components/SplitPane.json +8 -1
- package/agent/components.json +46 -9
- package/agent/index.json +4 -4
- package/agent/llms.txt +5 -5
- package/agent/tokens.json +24 -0
- package/dist/components/data-entry/chat-composer.js +11 -4
- package/dist/components/data-entry/chat-suggestion.js +2 -0
- package/dist/components/data-entry/color-picker.d.ts +2 -2
- package/dist/components/data-entry/color-picker.js +150 -37
- package/dist/components/data-entry/command-palette.js +2 -1
- package/dist/components/data-entry/date-picker.js +2 -0
- package/dist/components/data-entry/index.d.ts +1 -1
- package/dist/components/data-entry/number-input.js +2 -1
- package/dist/components/data-entry/search-select.js +55 -17
- package/dist/components/data-entry/select.js +1 -1
- package/dist/components/data-entry/time-picker.js +3 -0
- package/dist/components/feedback/non-modal-layer.js +2 -1
- package/dist/components/general/typography.js +5 -1
- package/dist/components/layout/split-pane.d.ts +16 -1
- package/dist/components/layout/split-pane.js +7 -2
- package/dist/components/navigation/pagination.js +2 -1
- package/dist/components/ui/tag-input.js +2 -1
- package/dist/contracts/measurement.json +1 -1
- package/dist/i18n/messages/en.json +4 -2
- package/dist/i18n/messages/ja.json +4 -2
- package/dist/i18n/messages/vi.json +4 -2
- package/dist/lib/ime.d.ts +12 -0
- package/dist/lib/ime.js +7 -0
- package/dist/props/components/data-entry.prop.d.ts +66 -1
- package/dist/props/components/index.d.ts +1 -1
- package/dist/props/registry.d.ts +15 -0
- package/dist/props/registry.js +17 -0
- package/dist/styles/control.css +79 -0
- package/dist/styles/layers.json +1 -1
- package/dist/styles/layout.css +45 -0
- package/dist/tokens/components/control.css +8 -0
- package/docs/data-entry/color-picker.tsx +72 -0
- package/docs/data-entry/select.tsx +38 -0
- package/docs/i18n/messages/en.json +28 -0
- package/docs/i18n/messages/ja.json +28 -0
- package/docs/i18n/messages/vi.json +28 -0
- package/docs/layout/split-pane.tsx +65 -0
- package/package.json +2 -2
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
|
+
import { ChevronDown } from "lucide-react";
|
|
5
|
+
import { Radio as AriaRadio, RadioGroup as AriaRadioGroup } from "react-aria-components";
|
|
4
6
|
import { useTranslation } from "../../i18n/use-translation.js";
|
|
5
7
|
import { cn } from "../../lib/utils.js";
|
|
8
|
+
import { isImeComposing } from "../../lib/ime.js";
|
|
6
9
|
import { resolveFieldA11y, useFieldIdentity } from "../../lib/field-a11y.js";
|
|
7
10
|
import { controlIconClass } from "../../lib/control-styles.js";
|
|
11
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../data-display/collapsible.js";
|
|
8
12
|
import { Input } from "./input.js";
|
|
9
13
|
const HEX_PATTERN = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
10
14
|
const NATIVE_COLOR_FALLBACK = "#000000";
|
|
@@ -12,6 +16,68 @@ function normalizeHex(value) {
|
|
|
12
16
|
if (!value.startsWith("#")) return `#${value}`;
|
|
13
17
|
return value;
|
|
14
18
|
}
|
|
19
|
+
function colorKey(value) {
|
|
20
|
+
const hex = normalizeHex(value.trim()).toLowerCase();
|
|
21
|
+
if (!HEX_PATTERN.test(hex)) return hex;
|
|
22
|
+
if (hex.length === 4) return `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}`;
|
|
23
|
+
return hex;
|
|
24
|
+
}
|
|
25
|
+
const ColorPickerPartsContext = React.createContext(null);
|
|
26
|
+
function ColorPickerPickerPart() {
|
|
27
|
+
return /* @__PURE__ */ jsx(Fragment, { children: React.useContext(ColorPickerPartsContext)?.picker });
|
|
28
|
+
}
|
|
29
|
+
function ColorPickerPresetsPart() {
|
|
30
|
+
return /* @__PURE__ */ jsx(Fragment, { children: React.useContext(ColorPickerPartsContext)?.presets });
|
|
31
|
+
}
|
|
32
|
+
const PANEL_COMPONENTS = { Picker: ColorPickerPickerPart, Presets: ColorPickerPresetsPart };
|
|
33
|
+
function ColorPickerPresetGroup({
|
|
34
|
+
preset,
|
|
35
|
+
value,
|
|
36
|
+
disabled,
|
|
37
|
+
onSelect
|
|
38
|
+
}) {
|
|
39
|
+
const { t } = useTranslation();
|
|
40
|
+
const labelId = React.useId();
|
|
41
|
+
const currentKey = colorKey(value);
|
|
42
|
+
const checked = preset.colors.find((color) => colorKey(color) === currentKey) ?? null;
|
|
43
|
+
return /* @__PURE__ */ jsxs(
|
|
44
|
+
Collapsible,
|
|
45
|
+
{
|
|
46
|
+
defaultOpen: preset.defaultOpen ?? true,
|
|
47
|
+
className: "ui-color-picker-presets-group",
|
|
48
|
+
"data-slot": "color-picker-presets-group",
|
|
49
|
+
children: [
|
|
50
|
+
/* @__PURE__ */ jsxs(CollapsibleTrigger, { className: "ui-color-picker-presets-trigger ui-focus-ring", children: [
|
|
51
|
+
/* @__PURE__ */ jsx(ChevronDown, { className: "ui-color-picker-presets-chevron", "aria-hidden": "true" }),
|
|
52
|
+
/* @__PURE__ */ jsx("span", { id: labelId, className: "ui-color-picker-presets-label", children: preset.label })
|
|
53
|
+
] }),
|
|
54
|
+
/* @__PURE__ */ jsx(CollapsibleContent, { className: "ui-color-picker-presets-content", children: preset.colors.length > 0 ? /* @__PURE__ */ jsx(
|
|
55
|
+
AriaRadioGroup,
|
|
56
|
+
{
|
|
57
|
+
"aria-labelledby": labelId,
|
|
58
|
+
orientation: "horizontal",
|
|
59
|
+
value: checked,
|
|
60
|
+
onChange: onSelect,
|
|
61
|
+
isDisabled: disabled,
|
|
62
|
+
className: "ui-color-picker-presets-items",
|
|
63
|
+
"data-slot": "color-picker-presets-items",
|
|
64
|
+
children: preset.colors.map((color, index) => /* @__PURE__ */ jsx(
|
|
65
|
+
AriaRadio,
|
|
66
|
+
{
|
|
67
|
+
value: color,
|
|
68
|
+
"aria-label": color,
|
|
69
|
+
className: "ui-color-picker-presets-color ui-focus-ring",
|
|
70
|
+
"data-slot": "color-picker-preset",
|
|
71
|
+
style: { ["--color-picker-preset-color"]: color }
|
|
72
|
+
},
|
|
73
|
+
`${index}-${color}`
|
|
74
|
+
))
|
|
75
|
+
}
|
|
76
|
+
) : /* @__PURE__ */ jsx("span", { className: "ui-color-picker-presets-empty", children: t("dataEntry.colorPicker.presetEmpty") }) })
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
}
|
|
15
81
|
function ColorPicker({
|
|
16
82
|
value: valueProp,
|
|
17
83
|
defaultValue,
|
|
@@ -21,6 +87,8 @@ function ColorPicker({
|
|
|
21
87
|
className,
|
|
22
88
|
id,
|
|
23
89
|
showHexInput = true,
|
|
90
|
+
presets,
|
|
91
|
+
panelRender,
|
|
24
92
|
...ariaProps
|
|
25
93
|
}) {
|
|
26
94
|
const { t } = useTranslation();
|
|
@@ -43,48 +111,93 @@ function ColorPicker({
|
|
|
43
111
|
if (!isControlled) setInternalValue(normalized);
|
|
44
112
|
onValueChange?.(normalized);
|
|
45
113
|
};
|
|
46
|
-
|
|
47
|
-
/* @__PURE__ */
|
|
114
|
+
const swatch = /* @__PURE__ */ jsxs("div", { className: cn("ui-color-picker-swatch", controlIconClass), children: [
|
|
115
|
+
/* @__PURE__ */ jsx(
|
|
116
|
+
"div",
|
|
117
|
+
{
|
|
118
|
+
className: "ui-color-picker-preview",
|
|
119
|
+
style: { backgroundColor: swatchValue },
|
|
120
|
+
"aria-hidden": "true"
|
|
121
|
+
}
|
|
122
|
+
),
|
|
123
|
+
/* @__PURE__ */ jsx(
|
|
124
|
+
"input",
|
|
125
|
+
{
|
|
126
|
+
id,
|
|
127
|
+
type: "color",
|
|
128
|
+
value: swatchValue,
|
|
129
|
+
disabled,
|
|
130
|
+
onChange: (event) => commit(event.target.value),
|
|
131
|
+
className: "ui-color-picker-input",
|
|
132
|
+
"data-field": ariaProps["data-field"] ?? identity["data-field"],
|
|
133
|
+
...swatchA11y
|
|
134
|
+
}
|
|
135
|
+
)
|
|
136
|
+
] });
|
|
137
|
+
const hexInput = showHexInput ? /* @__PURE__ */ jsx(
|
|
138
|
+
Input,
|
|
139
|
+
{
|
|
140
|
+
id: id ? `${id}-hex` : void 0,
|
|
141
|
+
value: display,
|
|
142
|
+
disabled,
|
|
143
|
+
"aria-label": t("dataEntry.colorPicker.hexLabel"),
|
|
144
|
+
onChange: (event) => setDraft(event.target.value),
|
|
145
|
+
onBlur: () => commit(display),
|
|
146
|
+
onKeyDown: (event) => {
|
|
147
|
+
if (event.key === "Enter" && !isImeComposing(event)) commit(display);
|
|
148
|
+
},
|
|
149
|
+
className: "ui-color-picker-hex",
|
|
150
|
+
spellCheck: false
|
|
151
|
+
}
|
|
152
|
+
) : null;
|
|
153
|
+
const hiddenInput = resolvedName ? /* @__PURE__ */ jsx("input", { type: "hidden", name: resolvedName, value, readOnly: true }) : null;
|
|
154
|
+
if (!presets && !panelRender) {
|
|
155
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("ui-color-picker", className), children: [
|
|
156
|
+
swatch,
|
|
157
|
+
hexInput,
|
|
158
|
+
hiddenInput
|
|
159
|
+
] });
|
|
160
|
+
}
|
|
161
|
+
const presetsNamed = Boolean(ariaProps["aria-labelledby"] ?? ariaProps["aria-label"]);
|
|
162
|
+
const parts = {
|
|
163
|
+
picker: /* @__PURE__ */ jsxs("div", { className: "ui-color-picker-picker", "data-slot": "color-picker-picker", children: [
|
|
164
|
+
swatch,
|
|
165
|
+
hexInput
|
|
166
|
+
] }),
|
|
167
|
+
presets: presets?.length ? (
|
|
168
|
+
// The FormField name/description also reach the palette as a GROUP, so presets-only mode
|
|
169
|
+
// (no native swatch to carry them) still announces which field these swatches fill.
|
|
48
170
|
/* @__PURE__ */ jsx(
|
|
49
171
|
"div",
|
|
50
172
|
{
|
|
51
|
-
className: "ui-color-picker-
|
|
52
|
-
|
|
53
|
-
"
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
173
|
+
className: "ui-color-picker-presets",
|
|
174
|
+
"data-slot": "color-picker-presets",
|
|
175
|
+
role: presetsNamed ? "group" : void 0,
|
|
176
|
+
"aria-labelledby": ariaProps["aria-labelledby"],
|
|
177
|
+
"aria-label": ariaProps["aria-labelledby"] ? void 0 : ariaProps["aria-label"],
|
|
178
|
+
"aria-describedby": ariaProps["aria-describedby"],
|
|
179
|
+
children: presets.map((preset, index) => /* @__PURE__ */ jsx(
|
|
180
|
+
ColorPickerPresetGroup,
|
|
181
|
+
{
|
|
182
|
+
preset,
|
|
183
|
+
value,
|
|
184
|
+
disabled,
|
|
185
|
+
onSelect: commit
|
|
186
|
+
},
|
|
187
|
+
preset.key ?? index
|
|
188
|
+
))
|
|
67
189
|
}
|
|
68
190
|
)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
value: display,
|
|
75
|
-
disabled,
|
|
76
|
-
"aria-label": t("dataEntry.colorPicker.hexLabel"),
|
|
77
|
-
onChange: (event) => setDraft(event.target.value),
|
|
78
|
-
onBlur: () => commit(display),
|
|
79
|
-
onKeyDown: (event) => {
|
|
80
|
-
if (event.key === "Enter") commit(display);
|
|
81
|
-
},
|
|
82
|
-
className: "ui-color-picker-hex",
|
|
83
|
-
spellCheck: false
|
|
84
|
-
}
|
|
85
|
-
),
|
|
86
|
-
resolvedName ? /* @__PURE__ */ jsx("input", { type: "hidden", name: resolvedName, value, readOnly: true }) : null
|
|
191
|
+
) : null
|
|
192
|
+
};
|
|
193
|
+
const panel = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
194
|
+
parts.picker,
|
|
195
|
+
parts.presets
|
|
87
196
|
] });
|
|
197
|
+
return /* @__PURE__ */ jsx(ColorPickerPartsContext.Provider, { value: parts, children: /* @__PURE__ */ jsxs("div", { className: cn("ui-color-picker", className), "data-panel": "", children: [
|
|
198
|
+
panelRender ? panelRender(panel, { components: PANEL_COMPONENTS }) : panel,
|
|
199
|
+
hiddenInput
|
|
200
|
+
] }) });
|
|
88
201
|
}
|
|
89
202
|
export {
|
|
90
203
|
ColorPicker
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
CommandItem,
|
|
13
13
|
CommandList
|
|
14
14
|
} from "./command.js";
|
|
15
|
+
import { isImeComposing } from "../../lib/ime.js";
|
|
15
16
|
function CommandPalette({
|
|
16
17
|
groups,
|
|
17
18
|
labels,
|
|
@@ -61,7 +62,7 @@ function CommandPalette({
|
|
|
61
62
|
React.useEffect(() => {
|
|
62
63
|
if (!shortcut) return;
|
|
63
64
|
const handleKeyDown = (event) => {
|
|
64
|
-
if (!event
|
|
65
|
+
if (!isImeComposing(event) && event.key.toLowerCase() === "k" && (event.metaKey || event.ctrlKey)) {
|
|
65
66
|
event.preventDefault();
|
|
66
67
|
if (!open && document.activeElement instanceof HTMLElement) {
|
|
67
68
|
shortcutRestoreFocusRef.current = document.activeElement;
|
|
@@ -23,6 +23,7 @@ import { Input } from "./input.js";
|
|
|
23
23
|
import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "../data-display/popover.js";
|
|
24
24
|
import { Calendar } from "./calendar.js";
|
|
25
25
|
import { dateTimeFormat, numberFormat } from "../../lib/intl-cache.js";
|
|
26
|
+
import { isImeComposing } from "../../lib/ime.js";
|
|
26
27
|
const ISO_HINT = "yyyy-mm-dd";
|
|
27
28
|
const PERIOD_PICKERS = /* @__PURE__ */ new Set(["month", "quarter", "year"]);
|
|
28
29
|
const PERIOD_COLUMNS = 3;
|
|
@@ -236,6 +237,7 @@ function DatePicker(props) {
|
|
|
236
237
|
};
|
|
237
238
|
const keyHandlers = (onEnter) => ({
|
|
238
239
|
onKeyDown: (event) => {
|
|
240
|
+
if (isImeComposing(event)) return;
|
|
239
241
|
if (event.key === "ArrowDown") {
|
|
240
242
|
event.preventDefault();
|
|
241
243
|
setOpen(true);
|
|
@@ -39,7 +39,7 @@ export type { DatePickerProps } from "./date-picker.js";
|
|
|
39
39
|
export { TimePicker } from "./time-picker.js";
|
|
40
40
|
export type { TimePickerProps } from "./time-picker.js";
|
|
41
41
|
export { ColorPicker } from "./color-picker.js";
|
|
42
|
-
export type { ColorPickerProps } from "./color-picker.js";
|
|
42
|
+
export type { ColorPickerProps, ColorPickerPresetProps } from "./color-picker.js";
|
|
43
43
|
export type { SelectProp, SelectProp as SelectProps } from "./select.js";
|
|
44
44
|
export type { SearchSelectOptionProp as SelectOption, SearchSelectLoadParamsProp as SelectLoadParams, SearchSelectLoadResultProp as SelectLoadResult, } from "./search-select.js";
|
|
45
45
|
export { UPLOAD_LIST_IGNORE, Upload, collectUploadCommitActions, createUploadItem, useUploadDraft, } from "./upload.js";
|
|
@@ -8,6 +8,7 @@ import { pickFieldA11y } from "../../lib/field-a11y.js";
|
|
|
8
8
|
import { Button } from "../general/button.js";
|
|
9
9
|
import { Input } from "./input.js";
|
|
10
10
|
import { numberFormat } from "../../lib/intl-cache.js";
|
|
11
|
+
import { isImeComposing } from "../../lib/ime.js";
|
|
11
12
|
function decimalsOf(n) {
|
|
12
13
|
if (!Number.isFinite(n)) return 0;
|
|
13
14
|
const s = String(n);
|
|
@@ -165,7 +166,7 @@ const NumberInput = React.forwardRef(
|
|
|
165
166
|
};
|
|
166
167
|
const handleKeyDown = (event) => {
|
|
167
168
|
if (!interactive) return;
|
|
168
|
-
if (composing.current || event
|
|
169
|
+
if (composing.current || isImeComposing(event)) return;
|
|
169
170
|
if (keyboard && event.key === "ArrowUp") {
|
|
170
171
|
event.preventDefault();
|
|
171
172
|
stepBy(1, event.shiftKey ? 10 : 1);
|
|
@@ -4,6 +4,7 @@ import * as React from "react";
|
|
|
4
4
|
import { ChevronsUpDown, Loader2, X } from "lucide-react";
|
|
5
5
|
import { useTranslation } from "../../i18n/use-translation.js";
|
|
6
6
|
import { useFieldIdentity, useFieldNameFallback } from "../../lib/field-a11y.js";
|
|
7
|
+
import { isImeComposing } from "../../lib/ime.js";
|
|
7
8
|
import { cn } from "../../lib/utils.js";
|
|
8
9
|
import { controlSurfaceTriggerClass } from "../../lib/control-styles.js";
|
|
9
10
|
import {
|
|
@@ -54,6 +55,7 @@ function SearchSelect(props) {
|
|
|
54
55
|
onSearchChange,
|
|
55
56
|
filterOption,
|
|
56
57
|
optionFilterProp,
|
|
58
|
+
caseSensitive = false,
|
|
57
59
|
filterSort,
|
|
58
60
|
autoClearSearchValue = true,
|
|
59
61
|
optionRender,
|
|
@@ -91,6 +93,18 @@ function SearchSelect(props) {
|
|
|
91
93
|
const maxTagTextLength = props.mode !== void 0 ? props.maxTagTextLength : void 0;
|
|
92
94
|
const tokenSeparators = props.mode !== void 0 ? props.tokenSeparators : void 0;
|
|
93
95
|
const tagRender = props.mode !== void 0 ? props.tagRender : void 0;
|
|
96
|
+
const allowCreate = props.mode !== void 0 ? props.allowCreate : void 0;
|
|
97
|
+
const createLabel = props.mode !== void 0 ? props.createLabel : void 0;
|
|
98
|
+
const onCreate = props.mode !== void 0 ? props.onCreate : void 0;
|
|
99
|
+
const fold = React.useCallback(
|
|
100
|
+
(text) => caseSensitive ? text : text.toLowerCase(),
|
|
101
|
+
[caseSensitive]
|
|
102
|
+
);
|
|
103
|
+
const canCreate = React.useCallback(
|
|
104
|
+
(text) => tagsMode && (typeof allowCreate === "function" ? allowCreate(text) : allowCreate !== false),
|
|
105
|
+
[tagsMode, allowCreate]
|
|
106
|
+
);
|
|
107
|
+
const composingRef = React.useRef(false);
|
|
94
108
|
const { t } = useTranslation();
|
|
95
109
|
const nameFallback = useFieldNameFallback({
|
|
96
110
|
"aria-label": ariaLabel,
|
|
@@ -154,9 +168,7 @@ function SearchSelect(props) {
|
|
|
154
168
|
const matches = (option) => {
|
|
155
169
|
if (filterOption) return filterOption(option, needle);
|
|
156
170
|
const haystack = optionFilterProp ? [String(option[optionFilterProp] ?? "")] : [option.label, option.value];
|
|
157
|
-
return haystack.some(
|
|
158
|
-
(field) => String(field).toLowerCase().includes(needle.toLowerCase())
|
|
159
|
-
);
|
|
171
|
+
return haystack.some((field) => fold(String(field)).includes(fold(needle)));
|
|
160
172
|
};
|
|
161
173
|
const kept = needle ? list.filter(matches) : list;
|
|
162
174
|
return {
|
|
@@ -164,7 +176,7 @@ function SearchSelect(props) {
|
|
|
164
176
|
hasMore: false
|
|
165
177
|
};
|
|
166
178
|
}),
|
|
167
|
-
[loadOptions, staticOptions, filterOption, optionFilterProp, filterSort]
|
|
179
|
+
[loadOptions, staticOptions, filterOption, optionFilterProp, filterSort, fold]
|
|
168
180
|
);
|
|
169
181
|
React.useEffect(() => {
|
|
170
182
|
const handle = window.setTimeout(() => setDebouncedQuery(query.trim()), DEBOUNCE_MS);
|
|
@@ -201,8 +213,9 @@ function SearchSelect(props) {
|
|
|
201
213
|
setActiveIndex(0);
|
|
202
214
|
void fetchPage(1, debouncedQuery, false);
|
|
203
215
|
}, [open, debouncedQuery, fetchPage]);
|
|
204
|
-
const
|
|
205
|
-
|
|
216
|
+
const typedText = query.trim();
|
|
217
|
+
const { displayOptions, createOption } = React.useMemo(() => {
|
|
218
|
+
if (!tagsMode) return { displayOptions: loaded, createOption: void 0 };
|
|
206
219
|
const known = new Set(loaded.map((option) => option.value));
|
|
207
220
|
const extras = [];
|
|
208
221
|
for (const entry of values) {
|
|
@@ -210,11 +223,16 @@ function SearchSelect(props) {
|
|
|
210
223
|
known.add(entry);
|
|
211
224
|
extras.push(pickedOptions[entry] ?? { value: entry, label: entry });
|
|
212
225
|
}
|
|
213
|
-
const
|
|
214
|
-
const exists = known.
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
226
|
+
const typed = fold(typedText);
|
|
227
|
+
const exists = [...known].some((entry) => fold(entry) === typed) || [...staticOptions ?? [], ...loaded].some(
|
|
228
|
+
(option) => fold(option.value) === typed || fold(option.label) === typed
|
|
229
|
+
);
|
|
230
|
+
const create = typedText && !exists && canCreate(typedText) ? { value: typedText, label: typedText } : void 0;
|
|
231
|
+
return {
|
|
232
|
+
displayOptions: create || extras.length ? [...create ? [create] : [], ...extras, ...loaded] : loaded,
|
|
233
|
+
createOption: create
|
|
234
|
+
};
|
|
235
|
+
}, [tagsMode, loaded, values, typedText, pickedOptions, staticOptions, fold, canCreate]);
|
|
218
236
|
const grouped = React.useMemo(() => {
|
|
219
237
|
const order = [];
|
|
220
238
|
const buckets = /* @__PURE__ */ new Map();
|
|
@@ -258,6 +276,7 @@ function SearchSelect(props) {
|
|
|
258
276
|
const maxCount = props.mode !== void 0 ? props.maxCount : void 0;
|
|
259
277
|
const atMaxCount = maxCount !== void 0 && values.length >= maxCount;
|
|
260
278
|
const remember = (option) => setPickedOptions((prev) => ({ ...prev, [option.value]: option }));
|
|
279
|
+
const isInvented = (entry) => tagsMode && !(staticOptions ?? []).some((option) => option.value === entry) && !loaded.some((option) => option.value === entry);
|
|
261
280
|
const select = (option) => {
|
|
262
281
|
if (option.disabled) return;
|
|
263
282
|
if (props.mode !== void 0) {
|
|
@@ -271,7 +290,10 @@ function SearchSelect(props) {
|
|
|
271
290
|
next.map((entry) => entry === option.value ? option : optionFor(entry)).filter((entry) => entry !== null)
|
|
272
291
|
);
|
|
273
292
|
if (already) props.onDeselect?.(option.value, option);
|
|
274
|
-
else
|
|
293
|
+
else {
|
|
294
|
+
props.onSelect?.(option.value, option);
|
|
295
|
+
if (isInvented(option.value)) onCreate?.(option.value);
|
|
296
|
+
}
|
|
275
297
|
if (autoClearSearchValue) setQuery("");
|
|
276
298
|
return;
|
|
277
299
|
}
|
|
@@ -289,8 +311,10 @@ function SearchSelect(props) {
|
|
|
289
311
|
for (const raw of splitByTokenSeparators(text, tokenSeparators ?? [])) {
|
|
290
312
|
const token = raw.trim();
|
|
291
313
|
if (!token) continue;
|
|
292
|
-
const match = flatOrdered.find((option2) => option2.value === token || option2.label === token)
|
|
293
|
-
|
|
314
|
+
const match = flatOrdered.find((option2) => option2.value === token || option2.label === token) ?? flatOrdered.find(
|
|
315
|
+
(option2) => option2 !== createOption && (fold(option2.value) === fold(token) || fold(option2.label) === fold(token))
|
|
316
|
+
);
|
|
317
|
+
const option = match ?? (canCreate(token) ? { value: token, label: token } : void 0);
|
|
294
318
|
if (!option || option.disabled) continue;
|
|
295
319
|
if (next.includes(option.value)) continue;
|
|
296
320
|
if (maxCount !== void 0 && next.length >= maxCount) break;
|
|
@@ -304,7 +328,10 @@ function SearchSelect(props) {
|
|
|
304
328
|
next,
|
|
305
329
|
next.map((entry) => picked.find((option) => option.value === entry) ?? optionFor(entry)).filter((entry) => entry != null)
|
|
306
330
|
);
|
|
307
|
-
for (const option of picked)
|
|
331
|
+
for (const option of picked) {
|
|
332
|
+
props.onSelect?.(option.value, option);
|
|
333
|
+
if (isInvented(option.value)) onCreate?.(option.value);
|
|
334
|
+
}
|
|
308
335
|
};
|
|
309
336
|
const deselect = (chipValue) => {
|
|
310
337
|
select(optionFor(chipValue) ?? { value: chipValue, label: chipValue });
|
|
@@ -344,6 +371,7 @@ function SearchSelect(props) {
|
|
|
344
371
|
if (next !== void 0) setActiveIndex(next);
|
|
345
372
|
};
|
|
346
373
|
const onKeyDown = (event) => {
|
|
374
|
+
if (isImeComposing(event)) return;
|
|
347
375
|
if (event.key === "ArrowDown") {
|
|
348
376
|
event.preventDefault();
|
|
349
377
|
moveActive(1);
|
|
@@ -530,7 +558,7 @@ function SearchSelect(props) {
|
|
|
530
558
|
value: query,
|
|
531
559
|
onChange: (event) => {
|
|
532
560
|
const next = event.target.value;
|
|
533
|
-
if (multiple && tokenSeparators?.length && tokenSeparators.some((separator) => next.includes(separator))) {
|
|
561
|
+
if (multiple && !composingRef.current && tokenSeparators?.length && tokenSeparators.some((separator) => next.includes(separator))) {
|
|
534
562
|
selectTokens(next);
|
|
535
563
|
setQuery("");
|
|
536
564
|
return;
|
|
@@ -546,6 +574,12 @@ function SearchSelect(props) {
|
|
|
546
574
|
setQuery("");
|
|
547
575
|
},
|
|
548
576
|
onKeyDown,
|
|
577
|
+
onCompositionStart: () => {
|
|
578
|
+
composingRef.current = true;
|
|
579
|
+
},
|
|
580
|
+
onCompositionEnd: () => {
|
|
581
|
+
composingRef.current = false;
|
|
582
|
+
},
|
|
549
583
|
placeholder: searchPlaceholder ?? t("dataEntry.searchSelect.search"),
|
|
550
584
|
className: "ui-search-select-search-input"
|
|
551
585
|
}
|
|
@@ -588,7 +622,11 @@ function SearchSelect(props) {
|
|
|
588
622
|
onMouseDown: (event) => event.preventDefault(),
|
|
589
623
|
onClick: () => select(option),
|
|
590
624
|
children: [
|
|
591
|
-
|
|
625
|
+
option === createOption ? (
|
|
626
|
+
// gh#1052: the create row SAYS it creates — antd shows the bare text, which
|
|
627
|
+
// reads like an existing option.
|
|
628
|
+
/* @__PURE__ */ jsx("div", { className: "ui-search-select-option-body", "data-slot": "select-create", children: /* @__PURE__ */ jsx("span", { className: "ui-search-select-option-label", children: createLabel ? createLabel(option.value) : t("dataEntry.searchSelect.create", { value: option.value }) }) })
|
|
629
|
+
) : optionRender ? /* @__PURE__ */ jsx("div", { className: "ui-search-select-option-slot", children: optionRender(option, { index }) }) : renderOption ? /* @__PURE__ */ jsx("div", { className: "ui-search-select-option-slot", children: renderOption(option) }) : /* @__PURE__ */ jsxs("div", { className: "ui-search-select-option-body", children: [
|
|
592
630
|
option.icon ? /* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-center", "aria-hidden": "true", children: option.icon }) : null,
|
|
593
631
|
/* @__PURE__ */ jsxs("div", { className: "ui-search-select-option-text", children: [
|
|
594
632
|
/* @__PURE__ */ jsx("span", { className: "ui-search-select-option-label", children: option.label }),
|
|
@@ -646,7 +646,7 @@ function DataSelect(props) {
|
|
|
646
646
|
props.onSearchChange?.(query);
|
|
647
647
|
} : void 0,
|
|
648
648
|
autoClearSearchValue: showSearchConfig?.autoClearSearchValue ?? props.autoClearSearchValue,
|
|
649
|
-
disabled: props.disabled || !props.loadOptions && !hasOptions,
|
|
649
|
+
disabled: props.disabled || !props.loadOptions && !hasOptions && props.mode !== "tags",
|
|
650
650
|
name: resolvedName,
|
|
651
651
|
"data-field": resolvedField
|
|
652
652
|
}
|
|
@@ -11,6 +11,7 @@ import { resolveAllowClear } from "./control-surface.js";
|
|
|
11
11
|
import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "../data-display/popover.js";
|
|
12
12
|
import { Button } from "../general/button.js";
|
|
13
13
|
import { Input } from "./input.js";
|
|
14
|
+
import { isImeComposing } from "../../lib/ime.js";
|
|
14
15
|
function pad2(n) {
|
|
15
16
|
return String(n).padStart(2, "0");
|
|
16
17
|
}
|
|
@@ -326,6 +327,7 @@ function TimePickerPanel({
|
|
|
326
327
|
"aria-label": t("dataEntry.timePicker.typeLabel"),
|
|
327
328
|
className: "text-center tabular-nums",
|
|
328
329
|
onKeyDown: (e) => {
|
|
330
|
+
if (isImeComposing(e)) return;
|
|
329
331
|
if (e.key === "Enter") {
|
|
330
332
|
e.preventDefault();
|
|
331
333
|
commitDraft();
|
|
@@ -482,6 +484,7 @@ function TimePicker({
|
|
|
482
484
|
if (!disabled) setOpen(true);
|
|
483
485
|
},
|
|
484
486
|
onKeyDown: (event) => {
|
|
487
|
+
if (isImeComposing(event)) return;
|
|
485
488
|
if (event.key === "ArrowDown") {
|
|
486
489
|
event.preventDefault();
|
|
487
490
|
setOpen(true);
|
|
@@ -4,6 +4,7 @@ import * as React from "react";
|
|
|
4
4
|
import { createPortal } from "react-dom";
|
|
5
5
|
import { useExitAnimation } from "@react-aria/utils";
|
|
6
6
|
import { FocusScope } from "react-aria";
|
|
7
|
+
import { isImeComposing } from "../../lib/ime.js";
|
|
7
8
|
function useNonModalPortal(isOpen, enabled) {
|
|
8
9
|
const contentRef = React.useRef(null);
|
|
9
10
|
const isExiting = useExitAnimation(contentRef, enabled && isOpen);
|
|
@@ -11,7 +12,7 @@ function useNonModalPortal(isOpen, enabled) {
|
|
|
11
12
|
}
|
|
12
13
|
function closeOnEscape(close) {
|
|
13
14
|
return (event) => {
|
|
14
|
-
if (event.key === "Escape" && !event.defaultPrevented && !event
|
|
15
|
+
if (event.key === "Escape" && !event.defaultPrevented && !isImeComposing(event)) {
|
|
15
16
|
event.preventDefault();
|
|
16
17
|
event.stopPropagation();
|
|
17
18
|
close();
|
|
@@ -7,6 +7,7 @@ import { useTranslation } from "../../i18n/use-translation.js";
|
|
|
7
7
|
import { EllipsisTooltip, Tooltip, TooltipContent, TooltipTrigger } from "../feedback/tooltip.js";
|
|
8
8
|
import { Textarea } from "../data-entry/textarea.js";
|
|
9
9
|
import { isDevelopment } from "../../lib/dev.js";
|
|
10
|
+
import { isImeComposing } from "../../lib/ime.js";
|
|
10
11
|
const COPIED_RESET_MS = 3e3;
|
|
11
12
|
const DECORATION_ELEMENTS = [
|
|
12
13
|
["strong", "strong"],
|
|
@@ -133,7 +134,10 @@ function TypographyEditor({
|
|
|
133
134
|
inComposition.current = false;
|
|
134
135
|
},
|
|
135
136
|
onKeyDown: (event) => {
|
|
136
|
-
if (inComposition.current)
|
|
137
|
+
if (inComposition.current || isImeComposing(event)) {
|
|
138
|
+
lastKey.current = null;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
137
141
|
lastKey.current = event.key;
|
|
138
142
|
},
|
|
139
143
|
onKeyUp: (event) => {
|
|
@@ -28,5 +28,20 @@ export type SplitPaneProps = {
|
|
|
28
28
|
* not invent it. Inside a flex column give the wrapper `flex: 1; min-height: 0`.
|
|
29
29
|
*/
|
|
30
30
|
fill?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Which pane comes first when the pane is too narrow to split and the columns STACK.
|
|
33
|
+
*
|
|
34
|
+
* `main-first` (default) keeps today's order: main on top, aside below. `aside-first` puts the
|
|
35
|
+
* aside on top — for a detail page whose properties / agent card must not fall below a long
|
|
36
|
+
* comment thread on a phone.
|
|
37
|
+
*
|
|
38
|
+
* `aside-first` moves the `<aside>` FIRST IN THE DOM, not just visually, so in the stacked
|
|
39
|
+
* layout the reading and focus order match what is on screen (WCAG 1.3.2 / 2.4.3 — a CSS
|
|
40
|
+
* `order` swap would leave Tab going through the whole main column before reaching the aside
|
|
41
|
+
* drawn above it). Once the pane is wide enough to split, grid placement puts main back in the
|
|
42
|
+
* leading column and the aside in the trailing one (inline-end, so it flips under RTL); there
|
|
43
|
+
* the DOM order is aside → main, which reads as two independent regions, not a broken sequence.
|
|
44
|
+
*/
|
|
45
|
+
stackOrder?: "main-first" | "aside-first";
|
|
31
46
|
};
|
|
32
|
-
export declare function SplitPane({ children, aside, asideWidth, asideLabel, fill, }: SplitPaneProps): import("react").JSX.Element;
|
|
47
|
+
export declare function SplitPane({ children, aside, asideWidth, asideLabel, fill, stackOrder, }: SplitPaneProps): import("react").JSX.Element;
|
|
@@ -4,9 +4,12 @@ function SplitPane({
|
|
|
4
4
|
aside,
|
|
5
5
|
asideWidth = "md",
|
|
6
6
|
asideLabel,
|
|
7
|
-
fill = false
|
|
7
|
+
fill = false,
|
|
8
|
+
stackOrder = "main-first"
|
|
8
9
|
}) {
|
|
9
10
|
const closed = aside === null || aside === void 0;
|
|
11
|
+
const asideFirst = stackOrder === "aside-first";
|
|
12
|
+
const asideElement = closed ? null : /* @__PURE__ */ jsx("aside", { className: "ui-split-pane-aside", "aria-label": asideLabel, children: aside });
|
|
10
13
|
return (
|
|
11
14
|
// `data-fill` is published on BOTH wrappers, not just the grid: the scope element is the one
|
|
12
15
|
// the consumer's own box actually contains, so without it the height stops at the scope and
|
|
@@ -19,9 +22,11 @@ function SplitPane({
|
|
|
19
22
|
"data-aside-width": asideWidth,
|
|
20
23
|
"data-aside": closed ? "closed" : void 0,
|
|
21
24
|
"data-fill": fill ? "true" : void 0,
|
|
25
|
+
"data-stack-order": asideFirst ? "aside-first" : void 0,
|
|
22
26
|
children: [
|
|
27
|
+
asideFirst ? asideElement : null,
|
|
23
28
|
/* @__PURE__ */ jsx("div", { className: "ui-split-pane-main", children }),
|
|
24
|
-
|
|
29
|
+
asideFirst ? null : asideElement
|
|
25
30
|
]
|
|
26
31
|
}
|
|
27
32
|
) })
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
SelectValue
|
|
18
18
|
} from "../data-entry/select.js";
|
|
19
19
|
import { buildPageRange } from "./pagination-utils.js";
|
|
20
|
+
import { isImeComposing } from "../../lib/ime.js";
|
|
20
21
|
const PaginationContent = React.forwardRef(({ className, ...props }, ref) => {
|
|
21
22
|
const [scrollRegion, setScrollRegion] = React.useState(null);
|
|
22
23
|
const setListElement = React.useCallback(
|
|
@@ -171,7 +172,7 @@ function Pagination({
|
|
|
171
172
|
value: jumperDraft,
|
|
172
173
|
onChange: (event) => setJumperDraft(event.target.value),
|
|
173
174
|
onKeyDown: (event) => {
|
|
174
|
-
if (event.key !== "Enter") return;
|
|
175
|
+
if (event.key !== "Enter" || isImeComposing(event)) return;
|
|
175
176
|
event.preventDefault();
|
|
176
177
|
commitJump();
|
|
177
178
|
}
|
|
@@ -3,6 +3,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { X } from "lucide-react";
|
|
5
5
|
import { useTranslation } from "../../i18n/use-translation.js";
|
|
6
|
+
import { isImeComposing } from "../../lib/ime.js";
|
|
6
7
|
import { cn } from "../../lib/utils.js";
|
|
7
8
|
import {
|
|
8
9
|
applyMaxTagCount,
|
|
@@ -62,7 +63,7 @@ const TagInput = React.forwardRef(
|
|
|
62
63
|
const removeAt = (i) => commit(tags.filter((_, idx) => idx !== i));
|
|
63
64
|
const onKeyDown = (e) => {
|
|
64
65
|
if (readOnly) return;
|
|
65
|
-
if (e
|
|
66
|
+
if (isImeComposing(e)) return;
|
|
66
67
|
if (e.key === "Enter" || tokenSeparators.includes(e.key)) {
|
|
67
68
|
e.preventDefault();
|
|
68
69
|
add(draft);
|
|
@@ -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": "31.
|
|
3
|
+
"version": "31.9.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,
|
|
@@ -56,7 +56,8 @@
|
|
|
56
56
|
},
|
|
57
57
|
"colorPicker": {
|
|
58
58
|
"ariaLabel": "Pick color",
|
|
59
|
-
"hexLabel": "Hex color value"
|
|
59
|
+
"hexLabel": "Hex color value",
|
|
60
|
+
"presetEmpty": "Empty"
|
|
60
61
|
},
|
|
61
62
|
"upload": {
|
|
62
63
|
"dropzoneLabel": "Upload files",
|
|
@@ -123,7 +124,8 @@
|
|
|
123
124
|
"clear": "Clear selection",
|
|
124
125
|
"loading": "Loading…",
|
|
125
126
|
"empty": "No results",
|
|
126
|
-
"error": "Couldn’t load options"
|
|
127
|
+
"error": "Couldn’t load options",
|
|
128
|
+
"create": "Create “{value}”"
|
|
127
129
|
},
|
|
128
130
|
"selection": {
|
|
129
131
|
"overflow": "+{count}"
|