@godxjp/ui 31.6.0 → 31.8.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 (61) hide show
  1. package/agent/START-HERE.md +4 -4
  2. package/agent/components/Breadcrumb.json +1 -1
  3. package/agent/components/ColorPicker.json +16 -8
  4. package/agent/components/PageContainer.json +1 -1
  5. package/agent/components/Progress.json +12 -1
  6. package/agent/components/Select.json +22 -0
  7. package/agent/components/Text.json +1 -1
  8. package/agent/components.json +53 -12
  9. package/agent/index.json +4 -4
  10. package/agent/llms.txt +5 -5
  11. package/agent/tokens.json +30 -0
  12. package/dist/components/data-display/progress.d.ts +16 -0
  13. package/dist/components/data-display/progress.js +17 -3
  14. package/dist/components/data-entry/chat-composer.js +11 -4
  15. package/dist/components/data-entry/chat-suggestion.js +2 -0
  16. package/dist/components/data-entry/color-picker.d.ts +2 -2
  17. package/dist/components/data-entry/color-picker.js +150 -37
  18. package/dist/components/data-entry/command-palette.js +2 -1
  19. package/dist/components/data-entry/date-picker.js +2 -0
  20. package/dist/components/data-entry/index.d.ts +1 -1
  21. package/dist/components/data-entry/number-input.js +2 -1
  22. package/dist/components/data-entry/search-select.js +55 -17
  23. package/dist/components/data-entry/select.js +1 -1
  24. package/dist/components/data-entry/time-picker.js +3 -0
  25. package/dist/components/feedback/non-modal-layer.js +2 -1
  26. package/dist/components/feedback/tooltip.d.ts +21 -0
  27. package/dist/components/feedback/tooltip.js +101 -0
  28. package/dist/components/general/typography.js +35 -12
  29. package/dist/components/layout/breadcrumb-ellipsis.d.ts +16 -0
  30. package/dist/components/layout/breadcrumb-ellipsis.js +28 -0
  31. package/dist/components/layout/breadcrumb.js +16 -7
  32. package/dist/components/layout/page-container.js +29 -20
  33. package/dist/components/navigation/pagination.js +2 -1
  34. package/dist/components/ui/tag-input.js +2 -1
  35. package/dist/contracts/measurement.json +1 -1
  36. package/dist/i18n/messages/en.json +6 -3
  37. package/dist/i18n/messages/ja.json +6 -3
  38. package/dist/i18n/messages/vi.json +6 -3
  39. package/dist/lib/ime.d.ts +12 -0
  40. package/dist/lib/ime.js +7 -0
  41. package/dist/props/components/data-entry.prop.d.ts +66 -1
  42. package/dist/props/components/index.d.ts +1 -1
  43. package/dist/props/registry.d.ts +20 -0
  44. package/dist/props/registry.js +22 -0
  45. package/dist/props/vocabulary/index.d.ts +1 -1
  46. package/dist/props/vocabulary/navigation.prop.d.ts +14 -0
  47. package/dist/styles/control.css +79 -0
  48. package/dist/styles/layers.json +3 -2
  49. package/dist/styles/layout.css +22 -0
  50. package/dist/styles/table-layout.css +8 -0
  51. package/dist/tokens/components/control.css +8 -0
  52. package/dist/tokens/components/navigation.css +2 -0
  53. package/docs/data-display/progress.tsx +24 -6
  54. package/docs/data-entry/color-picker.tsx +72 -0
  55. package/docs/data-entry/select.tsx +38 -0
  56. package/docs/general/typography.tsx +84 -1
  57. package/docs/i18n/messages/en.json +47 -1
  58. package/docs/i18n/messages/ja.json +47 -1
  59. package/docs/i18n/messages/vi.json +47 -1
  60. package/docs/navigation/breadcrumb.tsx +24 -0
  61. 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();
@@ -51,4 +51,25 @@ interface TooltipContentProps extends React.ComponentPropsWithRef<"div"> {
51
51
  forceMount?: true;
52
52
  }
53
53
  export declare function TooltipContent({ className, style, children, ref, side, align, sideOffset, alignOffset, avoidCollisions, collisionPadding, sticky: _sticky, hideWhenDetached: _hideWhenDetached, forceMount: _forceMount, ...props }: TooltipContentProps): React.JSX.Element;
54
+ /**
55
+ * antd's ellipsis tooltip (`Base/Ellipsis` + `useTooltipProps`), ported for gh#1045 / gh#1046.
56
+ * Module-internal (not in the public index): `Text`/`Paragraph`/`Title` `ellipsis.tooltip` and
57
+ * `BreadcrumbItemProp.ellipsis` render it. It lives here, not in typography, so Breadcrumb does not
58
+ * pull the Typography module (and its Textarea) into its bundle and stylesheet layers.
59
+ *
60
+ * Open while the anchor is ACTUALLY clipping — measured at the moment of the pointer or the focus,
61
+ * so a box that fits never shows one — on pointer hover of the anchor, and on keyboard focus of the
62
+ * anchor itself (`Text asChild` link, a breadcrumb link) or of the nearest focusable ANCESTOR that is
63
+ * a control (`treeitem`, a sort `button`, a link). The anchor never becomes focusable, so a Text in
64
+ * a treeitem stays inside that item's single tab stop, and the accessible name stays the full text
65
+ * node — the truncation is paint only. Escape dismisses (WCAG 1.4.13); hovering the tooltip keeps
66
+ * it open.
67
+ *
68
+ * When the focus host is ALREADY a tooltip trigger (a DataTable sort button carries the sort hint on
69
+ * top), this one opens underneath so the two never cover each other.
70
+ */
71
+ export declare function EllipsisTooltip({ anchorRef, title, }: {
72
+ anchorRef: React.RefObject<HTMLElement | null>;
73
+ title: React.ReactNode;
74
+ }): React.JSX.Element;
54
75
  export {};
@@ -12,6 +12,7 @@ import {
12
12
  import { cn } from "../../lib/utils.js";
13
13
  import { Slot } from "../../lib/slot.js";
14
14
  const DEFAULT_DELAY_DURATION = 200;
15
+ const TooltipAnchorContext = React.createContext(null);
15
16
  function toPlacement(side, align) {
16
17
  if (align === "center") return side;
17
18
  if (side === "top" || side === "bottom") return `${side} ${align}`;
@@ -76,9 +77,11 @@ function TooltipContent({
76
77
  ...props
77
78
  }) {
78
79
  const overlayPortalContainer = useOverlayPortalContainer();
80
+ const anchorRef = React.useContext(TooltipAnchorContext);
79
81
  return /* @__PURE__ */ jsx(
80
82
  AriaTooltip,
81
83
  {
84
+ ...anchorRef ? { triggerRef: anchorRef } : null,
82
85
  UNSTABLE_portalContainer: overlayPortalContainer,
83
86
  placement: toPlacement(side, align),
84
87
  offset: sideOffset,
@@ -119,7 +122,105 @@ function TooltipContent({
119
122
  }
120
123
  );
121
124
  }
125
+ const FOCUS_HOST = 'a[href], button, input, select, textarea, summary, [tabindex]:is([role="treeitem"], [role="gridcell"], [role="row"], [role="option"], [role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"], [role="tab"], [role="link"], [role="button"], [role="checkbox"], [role="radio"], [role="switch"])';
126
+ function isClipping(node) {
127
+ const box = node.querySelector(':scope > [data-slot="typography-content"]') ?? node;
128
+ return box.scrollWidth > box.clientWidth || box.scrollHeight > box.clientHeight;
129
+ }
130
+ function isFocusVisible(node) {
131
+ try {
132
+ return node.matches(":focus-visible");
133
+ } catch {
134
+ return true;
135
+ }
136
+ }
137
+ function EllipsisTooltip({
138
+ anchorRef,
139
+ title
140
+ }) {
141
+ const [open, setOpen] = React.useState(false);
142
+ const [side, setSide] = React.useState("top");
143
+ const timer = React.useRef(void 0);
144
+ React.useEffect(() => {
145
+ const node = anchorRef.current;
146
+ if (!node) return void 0;
147
+ const host = node.matches(FOCUS_HOST) ? node : node.parentElement?.closest(FOCUS_HOST);
148
+ const schedule = (next, delay) => {
149
+ clearTimeout(timer.current);
150
+ timer.current = setTimeout(() => {
151
+ setOpen(next);
152
+ }, delay);
153
+ };
154
+ const show = (delay) => {
155
+ if (!isClipping(node)) return;
156
+ setSide(host && host !== node && host.hasAttribute("data-state") ? "bottom" : "top");
157
+ schedule(true, delay);
158
+ };
159
+ const hide = () => {
160
+ schedule(false, 100);
161
+ };
162
+ const onPointerEnter = () => {
163
+ show(DEFAULT_DELAY_DURATION);
164
+ };
165
+ const onFocusIn = () => {
166
+ const active = document.activeElement;
167
+ if (active && (active === host || node.contains(active)) && isFocusVisible(active)) show(0);
168
+ };
169
+ node.addEventListener("pointerenter", onPointerEnter);
170
+ node.addEventListener("pointerleave", hide);
171
+ node.addEventListener("focusin", onFocusIn);
172
+ node.addEventListener("focusout", hide);
173
+ host?.addEventListener("focusin", onFocusIn);
174
+ host?.addEventListener("focusout", hide);
175
+ return () => {
176
+ clearTimeout(timer.current);
177
+ node.removeEventListener("pointerenter", onPointerEnter);
178
+ node.removeEventListener("pointerleave", hide);
179
+ node.removeEventListener("focusin", onFocusIn);
180
+ node.removeEventListener("focusout", hide);
181
+ host?.removeEventListener("focusin", onFocusIn);
182
+ host?.removeEventListener("focusout", hide);
183
+ };
184
+ }, [anchorRef]);
185
+ React.useEffect(() => {
186
+ if (!open) return void 0;
187
+ const onKeyDown = (event) => {
188
+ if (event.key === "Escape") {
189
+ clearTimeout(timer.current);
190
+ setOpen(false);
191
+ }
192
+ };
193
+ document.addEventListener("keydown", onKeyDown);
194
+ return () => {
195
+ document.removeEventListener("keydown", onKeyDown);
196
+ };
197
+ }, [open]);
198
+ return /* @__PURE__ */ jsx(
199
+ Tooltip,
200
+ {
201
+ open,
202
+ onOpenChange: (next) => {
203
+ if (!next) setOpen(false);
204
+ },
205
+ children: /* @__PURE__ */ jsx(TooltipAnchorContext.Provider, { value: anchorRef, children: /* @__PURE__ */ jsx(
206
+ TooltipContent,
207
+ {
208
+ side,
209
+ "data-ellipsis-tooltip": "",
210
+ onPointerEnter: () => {
211
+ clearTimeout(timer.current);
212
+ },
213
+ onPointerLeave: () => {
214
+ setOpen(false);
215
+ },
216
+ children: title
217
+ }
218
+ ) })
219
+ }
220
+ );
221
+ }
122
222
  export {
223
+ EllipsisTooltip,
123
224
  Tooltip,
124
225
  TooltipContent,
125
226
  TooltipProvider,