@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.
Files changed (46) hide show
  1. package/agent/START-HERE.md +4 -4
  2. package/agent/components/ColorPicker.json +16 -8
  3. package/agent/components/Select.json +22 -0
  4. package/agent/components/SplitPane.json +8 -1
  5. package/agent/components.json +46 -9
  6. package/agent/index.json +4 -4
  7. package/agent/llms.txt +5 -5
  8. package/agent/tokens.json +24 -0
  9. package/dist/components/data-entry/chat-composer.js +11 -4
  10. package/dist/components/data-entry/chat-suggestion.js +2 -0
  11. package/dist/components/data-entry/color-picker.d.ts +2 -2
  12. package/dist/components/data-entry/color-picker.js +150 -37
  13. package/dist/components/data-entry/command-palette.js +2 -1
  14. package/dist/components/data-entry/date-picker.js +2 -0
  15. package/dist/components/data-entry/index.d.ts +1 -1
  16. package/dist/components/data-entry/number-input.js +2 -1
  17. package/dist/components/data-entry/search-select.js +55 -17
  18. package/dist/components/data-entry/select.js +1 -1
  19. package/dist/components/data-entry/time-picker.js +3 -0
  20. package/dist/components/feedback/non-modal-layer.js +2 -1
  21. package/dist/components/general/typography.js +5 -1
  22. package/dist/components/layout/split-pane.d.ts +16 -1
  23. package/dist/components/layout/split-pane.js +7 -2
  24. package/dist/components/navigation/pagination.js +2 -1
  25. package/dist/components/ui/tag-input.js +2 -1
  26. package/dist/contracts/measurement.json +1 -1
  27. package/dist/i18n/messages/en.json +4 -2
  28. package/dist/i18n/messages/ja.json +4 -2
  29. package/dist/i18n/messages/vi.json +4 -2
  30. package/dist/lib/ime.d.ts +12 -0
  31. package/dist/lib/ime.js +7 -0
  32. package/dist/props/components/data-entry.prop.d.ts +66 -1
  33. package/dist/props/components/index.d.ts +1 -1
  34. package/dist/props/registry.d.ts +15 -0
  35. package/dist/props/registry.js +17 -0
  36. package/dist/styles/control.css +79 -0
  37. package/dist/styles/layers.json +1 -1
  38. package/dist/styles/layout.css +45 -0
  39. package/dist/tokens/components/control.css +8 -0
  40. package/docs/data-entry/color-picker.tsx +72 -0
  41. package/docs/data-entry/select.tsx +38 -0
  42. package/docs/i18n/messages/en.json +28 -0
  43. package/docs/i18n/messages/ja.json +28 -0
  44. package/docs/i18n/messages/vi.json +28 -0
  45. package/docs/layout/split-pane.tsx +65 -0
  46. 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
- return /* @__PURE__ */ jsxs("div", { className: cn("ui-color-picker", className), children: [
47
- /* @__PURE__ */ jsxs("div", { className: cn("ui-color-picker-swatch", controlIconClass), children: [
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-preview",
52
- style: { backgroundColor: swatchValue },
53
- "aria-hidden": "true"
54
- }
55
- ),
56
- /* @__PURE__ */ jsx(
57
- "input",
58
- {
59
- id,
60
- type: "color",
61
- value: swatchValue,
62
- disabled,
63
- onChange: (event) => commit(event.target.value),
64
- className: "ui-color-picker-input",
65
- "data-field": ariaProps["data-field"] ?? identity["data-field"],
66
- ...swatchA11y
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
- showHexInput && /* @__PURE__ */ jsx(
71
- Input,
72
- {
73
- id: id ? `${id}-hex` : void 0,
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.isComposing && event.key.toLowerCase() === "k" && (event.metaKey || event.ctrlKey)) {
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.nativeEvent.isComposing) return;
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 displayOptions = React.useMemo(() => {
205
- if (!tagsMode) return loaded;
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 needle = query.trim();
214
- const exists = known.has(needle) || loaded.some((option) => option.label === needle);
215
- const create = needle && !exists ? [{ value: needle, label: needle }] : [];
216
- return create.length || extras.length ? [...create, ...extras, ...loaded] : loaded;
217
- }, [tagsMode, loaded, values, query, pickedOptions]);
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 props.onSelect?.(option.value, option);
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
- const option = match ?? (tagsMode ? { value: token, label: token } : void 0);
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) props.onSelect?.(option.value, option);
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
- 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: [
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.nativeEvent.isComposing) {
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) return;
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
- closed ? null : /* @__PURE__ */ jsx("aside", { className: "ui-split-pane-aside", "aria-label": asideLabel, children: aside })
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.nativeEvent.isComposing) return;
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.7.0",
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}"