@godxjp/ui 21.0.0 → 23.0.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 (154) hide show
  1. package/README.md +4 -4
  2. package/dist/app/app-provider.js +2 -1
  3. package/dist/components/data-display/badge.d.ts +28 -1
  4. package/dist/components/data-display/badge.js +2 -0
  5. package/dist/components/data-display/data-table.d.ts +18 -2
  6. package/dist/components/data-display/data-table.js +4 -0
  7. package/dist/components/data-display/index.d.ts +1 -1
  8. package/dist/components/data-display/popover.d.ts +19 -1
  9. package/dist/components/data-display/popover.js +9 -1
  10. package/dist/components/data-display/progress.d.ts +37 -0
  11. package/dist/components/data-display/progress.js +57 -5
  12. package/dist/components/data-display/scroll-area.d.ts +24 -4
  13. package/dist/components/data-display/scroll-area.js +34 -72
  14. package/dist/components/data-entry/cascader.js +8 -6
  15. package/dist/components/data-entry/checkbox.js +2 -0
  16. package/dist/components/data-entry/choice-hit-target.d.ts +24 -0
  17. package/dist/components/data-entry/choice-hit-target.js +12 -0
  18. package/dist/components/data-entry/control-surface.d.ts +8 -0
  19. package/dist/components/data-entry/control-surface.js +10 -1
  20. package/dist/components/data-entry/date-picker.d.ts +10 -3
  21. package/dist/components/data-entry/date-picker.js +503 -238
  22. package/dist/components/data-entry/form-field.d.ts +1 -1
  23. package/dist/components/data-entry/form-field.js +7 -3
  24. package/dist/components/data-entry/form.js +10 -1
  25. package/dist/components/data-entry/index.d.ts +0 -6
  26. package/dist/components/data-entry/index.js +0 -6
  27. package/dist/components/data-entry/label.d.ts +14 -1
  28. package/dist/components/data-entry/label.js +6 -1
  29. package/dist/components/data-entry/radio.d.ts +27 -6
  30. package/dist/components/data-entry/radio.js +89 -43
  31. package/dist/components/data-entry/search-select.js +226 -89
  32. package/dist/components/data-entry/select.d.ts +76 -14
  33. package/dist/components/data-entry/select.js +497 -163
  34. package/dist/components/data-entry/slider.d.ts +3 -11
  35. package/dist/components/data-entry/slider.js +582 -93
  36. package/dist/components/data-entry/switch.d.ts +6 -3
  37. package/dist/components/data-entry/switch.js +36 -11
  38. package/dist/components/layout/mobile-shell.d.ts +1 -1
  39. package/dist/components/layout/mobile-shell.js +2 -0
  40. package/dist/components/navigation/dropdown-menu.d.ts +11 -3
  41. package/dist/components/navigation/dropdown-menu.js +190 -15
  42. package/dist/components/navigation/index.d.ts +0 -3
  43. package/dist/components/navigation/index.js +0 -78
  44. package/dist/components/navigation/steps.d.ts +1 -1
  45. package/dist/components/navigation/steps.js +3 -1
  46. package/dist/components/navigation/tabs-scroll.d.ts +27 -0
  47. package/dist/components/navigation/tabs-scroll.js +52 -1
  48. package/dist/components/navigation/tabs.d.ts +2 -2
  49. package/dist/components/navigation/tabs.js +88 -23
  50. package/dist/components/ui/index.d.ts +0 -4
  51. package/dist/components/ui/index.js +0 -4
  52. package/dist/components/ui/input-otp.d.ts +1 -1
  53. package/dist/components/ui/segmented.d.ts +15 -1
  54. package/dist/components/ui/segmented.js +15 -6
  55. package/dist/components/ui/tag-input.js +3 -5
  56. package/dist/i18n/messages/en.json +3 -2
  57. package/dist/i18n/messages/ja.json +3 -2
  58. package/dist/i18n/messages/vi.json +3 -2
  59. package/dist/lib/datetime/picker-format.d.ts +18 -0
  60. package/dist/lib/datetime/picker-format.js +35 -1
  61. package/dist/lib/field-a11y.d.ts +12 -0
  62. package/dist/lib/field-a11y.js +14 -1
  63. package/dist/lib/select-options.d.ts +21 -0
  64. package/dist/lib/select-options.js +46 -0
  65. package/dist/props/components/data-display.prop.d.ts +16 -6
  66. package/dist/props/components/data-entry.prop.d.ts +503 -129
  67. package/dist/props/components/index.d.ts +2 -2
  68. package/dist/props/components/layout.prop.d.ts +12 -1
  69. package/dist/props/components/navigation.prop.d.ts +93 -1
  70. package/dist/props/registry.d.ts +79 -16
  71. package/dist/props/registry.js +85 -15
  72. package/dist/props/vocabulary/data.prop.d.ts +12 -0
  73. package/dist/props/vocabulary/index.d.ts +2 -2
  74. package/dist/props/vocabulary/layout.prop.d.ts +11 -0
  75. package/dist/styles/badge-layout.css +4 -0
  76. package/dist/styles/card-layout.css +6 -6
  77. package/dist/styles/control.css +220 -29
  78. package/dist/styles/data-display-layout.css +94 -23
  79. package/dist/styles/focus-ring.css +6 -4
  80. package/dist/styles/fonts.css +88 -4
  81. package/dist/styles/navigation-layout.css +36 -236
  82. package/dist/styles/shell-layout.css +149 -139
  83. package/dist/styles/table-layout.css +41 -3
  84. package/dist/tokens/components/control.css +4 -0
  85. package/dist/tokens/components/data-display.css +9 -5
  86. package/dist/tokens/components/navigation.css +9 -5
  87. package/dist/tokens/components/scroll-area.css +7 -0
  88. package/dist/tokens/components/segmented.css +0 -2
  89. package/dist/tokens/components/shell.css +2 -0
  90. package/dist/tokens/components/table.css +3 -0
  91. package/dist/tokens/foundation.css +7 -0
  92. package/docs/COMPONENTS.md +14 -14
  93. package/docs/CONSUMER-RULES.md +24 -1
  94. package/docs/CUSTOMER-THEMING.md +7 -1
  95. package/docs/FRAME-COVERAGE-REPORT.md +5 -11
  96. package/docs/TOKENS.md +65 -0
  97. package/docs/data-display/badge.tsx +37 -0
  98. package/docs/data-display/data-table/index.tsx +45 -1
  99. package/docs/data-display/popover.tsx +43 -5
  100. package/docs/data-display/progress.tsx +28 -0
  101. package/docs/data-display/scroll-area.tsx +51 -45
  102. package/docs/data-entry/calendar.tsx +2 -2
  103. package/docs/data-entry/date-picker.tsx +144 -5
  104. package/docs/data-entry/form/examples/invoice-form.tsx +2 -2
  105. package/docs/data-entry/form-field/examples/a11y-contract.tsx +3 -5
  106. package/docs/data-entry/form.tsx +4 -4
  107. package/docs/data-entry/input-otp.tsx +5 -2
  108. package/docs/data-entry/number-input.tsx +31 -0
  109. package/docs/data-entry/segmented.tsx +143 -2
  110. package/docs/data-entry/select-matrix.tsx +1 -2
  111. package/docs/data-entry/select.tsx +108 -3
  112. package/docs/data-entry/slider.tsx +125 -47
  113. package/docs/feedback/dialog.tsx +1 -2
  114. package/docs/feedback/sheet.tsx +1 -2
  115. package/docs/layout/app-shell-states.tsx +143 -0
  116. package/docs/layout/mobile-shell.tsx +5 -0
  117. package/docs/navigation/dropdown-menu.tsx +75 -4
  118. package/docs/navigation/tabs.tsx +149 -0
  119. package/docs/roadmap/antd-parity.md +35 -34
  120. package/docs/roadmap/parity-audit-data-entry.md +196 -172
  121. package/docs/roadmap/parity-audit-layout-navigation-general.md +304 -310
  122. package/docs/roadmap/parity-backlog.md +8 -10
  123. package/docs/showcase/acme-portal.tsx +7 -0
  124. package/package.json +28 -64
  125. package/scripts/ui-audit.mjs +18 -5
  126. package/scripts/visual-audit-rules.mjs +0 -7
  127. package/scripts/visual-audit.mjs +6 -27
  128. package/dist/components/data-entry/date-range-picker.d.ts +0 -11
  129. package/dist/components/data-entry/date-range-picker.js +0 -349
  130. package/dist/components/data-entry/month-picker.d.ts +0 -10
  131. package/dist/components/data-entry/month-picker.js +0 -241
  132. package/dist/components/data-entry/month-range-picker.d.ts +0 -10
  133. package/dist/components/data-entry/month-range-picker.js +0 -301
  134. package/dist/components/navigation/context-menu.d.ts +0 -21
  135. package/dist/components/navigation/context-menu.js +0 -149
  136. package/dist/components/navigation/menubar.d.ts +0 -21
  137. package/dist/components/navigation/menubar.js +0 -136
  138. package/dist/components/navigation/navigation-menu.d.ts +0 -10
  139. package/dist/components/navigation/navigation-menu.js +0 -91
  140. package/dist/components/ui/context-menu.d.ts +0 -1
  141. package/dist/components/ui/context-menu.js +0 -2
  142. package/dist/components/ui/date-range-picker.d.ts +0 -1
  143. package/dist/components/ui/date-range-picker.js +0 -2
  144. package/dist/components/ui/menubar.d.ts +0 -1
  145. package/dist/components/ui/menubar.js +0 -2
  146. package/dist/components/ui/navigation-menu.d.ts +0 -1
  147. package/dist/components/ui/navigation-menu.js +0 -2
  148. package/docs/FRAME-A11Y-CI.md +0 -349
  149. package/docs/data-entry/date-range-picker.tsx +0 -175
  150. package/docs/data-entry/month-picker.tsx +0 -101
  151. package/docs/data-entry/month-range-picker.tsx +0 -115
  152. package/docs/navigation/context-menu.tsx +0 -128
  153. package/docs/navigation/menubar.tsx +0 -141
  154. package/docs/navigation/navigation-menu.tsx +0 -158
@@ -10,15 +10,24 @@ import {
10
10
  applyMaxTagCount,
11
11
  controlSurfaceAttrs,
12
12
  resolveAllowClear,
13
- resolveAriaInvalid
13
+ resolveAriaInvalid,
14
+ splitByTokenSeparators
14
15
  } from "./control-surface.js";
16
+ import { normalizeSelectOptions } from "../../lib/select-options.js";
15
17
  import { Popover, PopoverContent, PopoverTrigger } from "../data-display/popover.js";
16
18
  import { Command, CommandGroup } from "./command.js";
17
19
  import { Input } from "./input.js";
18
20
  const DEBOUNCE_MS = 250;
21
+ function PopupRender({
22
+ render,
23
+ children
24
+ }) {
25
+ return /* @__PURE__ */ jsx(Fragment, { children: render ? render(children) : children });
26
+ }
19
27
  function SearchSelect(props) {
20
28
  const {
21
- options: staticOptions,
29
+ options: rawOptions,
30
+ fieldNames,
22
31
  loadOptions,
23
32
  renderOption,
24
33
  labelRender,
@@ -36,6 +45,7 @@ function SearchSelect(props) {
36
45
  size,
37
46
  status,
38
47
  variant,
48
+ width = "full",
39
49
  loading: loadingProp = false,
40
50
  open: openProp,
41
51
  defaultOpen = false,
@@ -43,11 +53,18 @@ function SearchSelect(props) {
43
53
  search: searchProp,
44
54
  onSearchChange,
45
55
  filterOption,
56
+ optionFilterProp,
46
57
  filterSort,
47
58
  autoClearSearchValue = true,
48
59
  optionRender,
49
60
  menuItemSelectedIcon,
50
61
  notFoundContent,
62
+ prefix,
63
+ suffixIcon,
64
+ placement,
65
+ popupRender,
66
+ listHeight,
67
+ onPopupScroll,
51
68
  popupMatchSelectWidth = true,
52
69
  allowClear,
53
70
  onClear,
@@ -65,7 +82,15 @@ function SearchSelect(props) {
65
82
  "aria-invalid": ariaInvalid,
66
83
  "aria-required": ariaRequired
67
84
  } = props;
68
- const multiple = props.mode === "multiple";
85
+ const staticOptions = React.useMemo(
86
+ () => normalizeSelectOptions(rawOptions, fieldNames),
87
+ [rawOptions, fieldNames]
88
+ );
89
+ const multiple = props.mode === "multiple" || props.mode === "tags";
90
+ const tagsMode = props.mode === "tags";
91
+ const maxTagTextLength = props.mode !== void 0 ? props.maxTagTextLength : void 0;
92
+ const tokenSeparators = props.mode !== void 0 ? props.tokenSeparators : void 0;
93
+ const tagRender = props.mode !== void 0 ? props.tagRender : void 0;
69
94
  const { t } = useTranslation();
70
95
  const nameFallback = useFieldNameFallback({
71
96
  "aria-label": ariaLabel,
@@ -110,7 +135,7 @@ function SearchSelect(props) {
110
135
  {}
111
136
  );
112
137
  const [internalValue, setInternalValue] = React.useState(
113
- props.defaultValue ?? (props.mode === "multiple" ? [] : "")
138
+ props.defaultValue ?? (props.mode === "multiple" || props.mode === "tags" ? [] : "")
114
139
  );
115
140
  const isControlled = props.value !== void 0;
116
141
  const rawValue = isControlled ? props.value : internalValue;
@@ -126,14 +151,20 @@ function SearchSelect(props) {
126
151
  () => loadOptions ?? (async ({ query: search }) => {
127
152
  const needle = search.trim();
128
153
  const list = staticOptions ?? [];
129
- const matches = (option) => filterOption ? filterOption(option, needle) : option.label.toLowerCase().includes(needle.toLowerCase()) || option.value.toLowerCase().includes(needle.toLowerCase());
154
+ const matches = (option) => {
155
+ if (filterOption) return filterOption(option, needle);
156
+ const haystack = optionFilterProp ? [String(option[optionFilterProp] ?? "")] : [option.label, option.value];
157
+ return haystack.some(
158
+ (field) => String(field).toLowerCase().includes(needle.toLowerCase())
159
+ );
160
+ };
130
161
  const kept = needle ? list.filter(matches) : list;
131
162
  return {
132
163
  options: filterSort ? [...kept].sort((a, b) => filterSort(a, b, { searchValue: needle })) : kept,
133
164
  hasMore: false
134
165
  };
135
166
  }),
136
- [loadOptions, staticOptions, filterOption, filterSort]
167
+ [loadOptions, staticOptions, filterOption, optionFilterProp, filterSort]
137
168
  );
138
169
  React.useEffect(() => {
139
170
  const handle = window.setTimeout(() => setDebouncedQuery(query.trim()), DEBOUNCE_MS);
@@ -170,10 +201,24 @@ function SearchSelect(props) {
170
201
  setActiveIndex(0);
171
202
  void fetchPage(1, debouncedQuery, false);
172
203
  }, [open, debouncedQuery, fetchPage]);
204
+ const displayOptions = React.useMemo(() => {
205
+ if (!tagsMode) return loaded;
206
+ const known = new Set(loaded.map((option) => option.value));
207
+ const extras = [];
208
+ for (const entry of values) {
209
+ if (known.has(entry)) continue;
210
+ known.add(entry);
211
+ extras.push(pickedOptions[entry] ?? { value: entry, label: entry });
212
+ }
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]);
173
218
  const grouped = React.useMemo(() => {
174
219
  const order = [];
175
220
  const buckets = /* @__PURE__ */ new Map();
176
- for (const option of loaded) {
221
+ for (const option of displayOptions) {
177
222
  const key = option.group ?? "";
178
223
  if (!buckets.has(key)) {
179
224
  buckets.set(key, []);
@@ -186,7 +231,7 @@ function SearchSelect(props) {
186
231
  heading: key || void 0,
187
232
  items: (buckets.get(key) ?? []).map((option) => ({ option, index: flatIndex++ }))
188
233
  }));
189
- }, [loaded]);
234
+ }, [displayOptions]);
190
235
  const flatOrdered = React.useMemo(
191
236
  () => grouped.flatMap((group) => group.items.map((entry) => entry.option)),
192
237
  [grouped]
@@ -196,9 +241,10 @@ function SearchSelect(props) {
196
241
  const selectedOption = value ? optionFor(value) : null;
197
242
  const currentLabel = value ? selectedOption?.label ?? selectedLabel ?? value : resolvedPlaceholder;
198
243
  const currentIcon = value ? selectedOption?.icon ?? selectedIcon : null;
244
+ const chipLabel = (label) => maxTagTextLength !== void 0 && maxTagTextLength >= 0 && label.length > maxTagTextLength ? `${label.slice(0, maxTagTextLength)}\u2026` : label;
199
245
  const selectedChips = multiple ? values.map((entry) => ({
200
246
  value: entry,
201
- label: optionFor(entry)?.label ?? entry
247
+ label: chipLabel(optionFor(entry)?.label ?? entry)
202
248
  })) : [];
203
249
  const {
204
250
  visible: visibleChips,
@@ -206,15 +252,15 @@ function SearchSelect(props) {
206
252
  overflow: chipOverflow
207
253
  } = applyMaxTagCount(
208
254
  selectedChips,
209
- props.mode === "multiple" ? props.maxTagCount : void 0,
210
- props.mode === "multiple" ? props.maxTagPlaceholder : void 0
255
+ props.mode !== void 0 ? props.maxTagCount : void 0,
256
+ props.mode !== void 0 ? props.maxTagPlaceholder : void 0
211
257
  );
212
- const maxCount = props.mode === "multiple" ? props.maxCount : void 0;
258
+ const maxCount = props.mode !== void 0 ? props.maxCount : void 0;
213
259
  const atMaxCount = maxCount !== void 0 && values.length >= maxCount;
214
260
  const remember = (option) => setPickedOptions((prev) => ({ ...prev, [option.value]: option }));
215
261
  const select = (option) => {
216
262
  if (option.disabled) return;
217
- if (props.mode === "multiple") {
263
+ if (props.mode !== void 0) {
218
264
  const already = values.includes(option.value);
219
265
  if (!already && atMaxCount) return;
220
266
  const next = already ? values.filter((entry) => entry !== option.value) : [...values, option.value];
@@ -236,8 +282,35 @@ function SearchSelect(props) {
236
282
  if (autoClearSearchValue) setQuery("");
237
283
  setOpen(false);
238
284
  };
285
+ const selectTokens = (text) => {
286
+ if (props.mode === void 0) return;
287
+ const next = [...values];
288
+ const picked = [];
289
+ for (const raw of splitByTokenSeparators(text, tokenSeparators ?? [])) {
290
+ const token = raw.trim();
291
+ 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);
294
+ if (!option || option.disabled) continue;
295
+ if (next.includes(option.value)) continue;
296
+ if (maxCount !== void 0 && next.length >= maxCount) break;
297
+ next.push(option.value);
298
+ picked.push(option);
299
+ }
300
+ if (picked.length === 0) return;
301
+ for (const option of picked) remember(option);
302
+ if (!isControlled) setInternalValue(next);
303
+ props.onValueChange?.(
304
+ next,
305
+ next.map((entry) => picked.find((option) => option.value === entry) ?? optionFor(entry)).filter((entry) => entry != null)
306
+ );
307
+ for (const option of picked) props.onSelect?.(option.value, option);
308
+ };
309
+ const deselect = (chipValue) => {
310
+ select(optionFor(chipValue) ?? { value: chipValue, label: chipValue });
311
+ };
239
312
  const clear = () => {
240
- if (props.mode === "multiple") {
313
+ if (props.mode !== void 0) {
241
314
  if (!isControlled) setInternalValue([]);
242
315
  props.onValueChange?.([], []);
243
316
  } else {
@@ -301,94 +374,136 @@ function SearchSelect(props) {
301
374
  );
302
375
  const showClear = clearControl.enabled && hasSelection && !disabled && !readOnly && !loadingProp;
303
376
  const surface = controlSurfaceAttrs({ variant, status, size });
377
+ const triggerProps = {
378
+ id,
379
+ role: "combobox",
380
+ ...surface,
381
+ "aria-expanded": open,
382
+ "aria-controls": open ? listId : void 0,
383
+ "aria-label": triggerAriaLabel,
384
+ "aria-labelledby": triggerAriaLabelledby,
385
+ "aria-describedby": ariaDescribedby,
386
+ "aria-errormessage": ariaErrorMessage,
387
+ "aria-invalid": resolveAriaInvalid(ariaInvalid, status),
388
+ "aria-required": ariaRequired,
389
+ "aria-readonly": readOnly || void 0,
390
+ "aria-busy": loadingProp || void 0,
391
+ "data-testid": dataTestId,
392
+ "data-field": resolvedField,
393
+ // label. `""` (nothing selected) is omitted rather than rendered as an empty attribute.
394
+ "data-value": (multiple ? values.join(",") : value) || void 0,
395
+ "data-mode": multiple ? "multiple" : void 0,
396
+ "data-width": width,
397
+ className: cn(
398
+ controlSurfaceTriggerClass,
399
+ "justify-start",
400
+ // `bounded` deliberately emits NO width utility — its width is owned by the
401
+ // `[data-width="bounded"]` rule in control.css, and a utility here would win the layer
402
+ // order and make that token dead. Same reasoning as SelectTrigger.
403
+ width === "auto" && "w-auto",
404
+ width === "full" && "w-full",
405
+ // Reserve trailing room for the single clear-or-chevron overlay rendered below.
406
+ "ui-control-trigger-affixed"
407
+ )
408
+ };
409
+ const chipsRemovable = !disabled && !readOnly;
410
+ const triggerContent = /* @__PURE__ */ jsxs(Fragment, { children: [
411
+ prefix === void 0 || prefix === null ? null : /* @__PURE__ */ jsx("span", { "data-slot": "select-prefix", className: "ui-select-prefix", children: prefix }),
412
+ /* @__PURE__ */ jsx(
413
+ "span",
414
+ {
415
+ className: cn(
416
+ "ui-search-select-option-body text-start",
417
+ !hasSelection && "text-muted-foreground"
418
+ ),
419
+ children: multiple ? hasSelection ? /* @__PURE__ */ jsxs("span", { className: "ui-search-select-values", "data-slot": "search-select-values", children: [
420
+ visibleChips.map((chip, index) => {
421
+ const close = () => deselect(chip.value);
422
+ return /* @__PURE__ */ jsx(
423
+ "span",
424
+ {
425
+ className: tagRender ? void 0 : "ui-search-select-value",
426
+ "data-slot": "search-select-value",
427
+ children: tagRender ? tagRender({
428
+ value: chip.value,
429
+ label: chip.label,
430
+ index,
431
+ disabled: Boolean(disabled) || Boolean(readOnly),
432
+ onClose: close
433
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [
434
+ labelRender ? labelRender({
435
+ value: chip.value,
436
+ label: chip.label,
437
+ option: optionFor(chip.value) ?? void 0
438
+ }) : chip.label,
439
+ chipsRemovable ? /* @__PURE__ */ jsx(
440
+ "button",
441
+ {
442
+ type: "button",
443
+ className: "ui-search-select-value-remove",
444
+ "data-slot": "search-select-value-remove",
445
+ "aria-label": t("ui.tagInput.removeTag", { tag: chip.label }),
446
+ onClick: (event) => {
447
+ event.stopPropagation();
448
+ close();
449
+ },
450
+ children: /* @__PURE__ */ jsx(X, { "aria-hidden": "true" })
451
+ }
452
+ ) : null
453
+ ] })
454
+ },
455
+ chip.value
456
+ );
457
+ }),
458
+ omittedChips.length > 0 ? /* @__PURE__ */ jsx("span", { className: "ui-search-select-value", "data-slot": "search-select-value-overflow", children: chipOverflow ?? t("dataEntry.selection.overflow", { count: omittedChips.length }) }) : null
459
+ ] }) : /* @__PURE__ */ jsx("span", { className: "truncate", children: resolvedPlaceholder }) : value && labelRender ? labelRender({ value, label: currentLabel, option: selectedOption ?? void 0 }) : /* @__PURE__ */ jsxs(Fragment, { children: [
460
+ currentIcon ? /* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-center", "aria-hidden": "true", children: currentIcon }) : null,
461
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: currentLabel })
462
+ ] })
463
+ }
464
+ )
465
+ ] });
304
466
  return /* @__PURE__ */ jsxs("div", { className: cn("relative", className), children: [
305
467
  /* @__PURE__ */ jsxs(
306
468
  Popover,
307
469
  {
308
470
  open,
309
471
  onOpenChange: (next) => {
310
- if (readOnly && next) return;
472
+ if ((readOnly || disabled) && next) return;
311
473
  setOpen(next);
312
474
  if (!next && autoClearSearchValue) setQuery("");
313
475
  },
314
476
  children: [
315
- /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
316
- "button",
477
+ /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: multiple ? /* @__PURE__ */ jsx(
478
+ "div",
317
479
  {
318
- id,
319
- type: "button",
320
- role: "combobox",
321
- ...surface,
322
- "aria-expanded": open,
323
- "aria-controls": open ? listId : void 0,
324
- "aria-label": triggerAriaLabel,
325
- "aria-labelledby": triggerAriaLabelledby,
326
- "aria-describedby": ariaDescribedby,
327
- "aria-errormessage": ariaErrorMessage,
328
- "aria-invalid": resolveAriaInvalid(ariaInvalid, status),
329
- "aria-required": ariaRequired,
330
- "aria-readonly": readOnly || void 0,
331
- "aria-busy": loadingProp || void 0,
332
- disabled,
333
- "data-testid": dataTestId,
334
- "data-field": resolvedField,
335
- "data-value": (multiple ? values.join(",") : value) || void 0,
336
- "data-mode": multiple ? "multiple" : void 0,
337
- className: cn(
338
- controlSurfaceTriggerClass,
339
- "w-full justify-start",
340
- // Reserve trailing room for the single clear-or-chevron overlay rendered below.
341
- "ui-control-trigger-affixed"
342
- ),
343
- children: /* @__PURE__ */ jsx(
344
- "span",
345
- {
346
- className: cn(
347
- "ui-search-select-option-body text-start",
348
- !hasSelection && "text-muted-foreground"
349
- ),
350
- children: multiple ? hasSelection ? (
351
- // Labels, not removable chips: the trigger IS a <button>, and a per-label remove
352
- // button inside it would be a button nested in a button (invalid HTML →
353
- // hydration error). Removal is a second click on the row, or the clear ✕.
354
- /* @__PURE__ */ jsxs("span", { className: "ui-search-select-values", "data-slot": "search-select-values", children: [
355
- visibleChips.map((chip) => /* @__PURE__ */ jsx(
356
- "span",
357
- {
358
- className: "ui-search-select-value",
359
- "data-slot": "search-select-value",
360
- children: labelRender ? labelRender({
361
- value: chip.value,
362
- label: chip.label,
363
- option: optionFor(chip.value) ?? void 0
364
- }) : chip.label
365
- },
366
- chip.value
367
- )),
368
- omittedChips.length > 0 ? /* @__PURE__ */ jsx(
369
- "span",
370
- {
371
- className: "ui-search-select-value",
372
- "data-slot": "search-select-value-overflow",
373
- children: chipOverflow ?? t("dataEntry.selection.overflow", { count: omittedChips.length })
374
- }
375
- ) : null
376
- ] })
377
- ) : /* @__PURE__ */ jsx("span", { className: "truncate", children: resolvedPlaceholder }) : value && labelRender ? labelRender({ value, label: currentLabel, option: selectedOption ?? void 0 }) : /* @__PURE__ */ jsxs(Fragment, { children: [
378
- currentIcon ? /* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-center", "aria-hidden": "true", children: currentIcon }) : null,
379
- /* @__PURE__ */ jsx("span", { className: "truncate", children: currentLabel })
380
- ] })
480
+ ...triggerProps,
481
+ tabIndex: disabled ? -1 : 0,
482
+ "aria-disabled": disabled || void 0,
483
+ "data-disabled": disabled ? "" : void 0,
484
+ onKeyDown: (event) => {
485
+ if (disabled || readOnly) return;
486
+ if (event.key === "Enter" || event.key === " " || event.key === "ArrowDown") {
487
+ event.preventDefault();
488
+ setOpen(true);
489
+ return;
381
490
  }
382
- )
491
+ if (event.key === "Backspace" && values.length > 0) {
492
+ event.preventDefault();
493
+ deselect(values[values.length - 1]);
494
+ }
495
+ },
496
+ children: triggerContent
383
497
  }
384
- ) }),
498
+ ) : /* @__PURE__ */ jsx("button", { ...triggerProps, type: "button", disabled, children: triggerContent }) }),
385
499
  resolvedName ? multiple ? values.map((entry) => /* @__PURE__ */ jsx("input", { type: "hidden", name: resolvedName, value: entry, readOnly: true }, entry)) : /* @__PURE__ */ jsx("input", { type: "hidden", name: resolvedName, value, readOnly: true }) : null,
386
500
  /* @__PURE__ */ jsx(
387
501
  PopoverContent,
388
502
  {
389
503
  "aria-label": triggerAriaLabelledby ? void 0 : triggerAriaLabel ?? resolvedPlaceholder,
390
504
  "aria-labelledby": triggerAriaLabelledby,
391
- align: "start",
505
+ side: placement?.startsWith("top") ? "top" : "bottom",
506
+ align: placement?.endsWith("End") ? "end" : "start",
392
507
  sideOffset: 4,
393
508
  collisionPadding: 12,
394
509
  className: "ui-search-select-panel",
@@ -396,7 +511,7 @@ function SearchSelect(props) {
396
511
  style: typeof popupMatchSelectWidth === "number" ? {
397
512
  "--search-select-panel-inline-size": `${popupMatchSelectWidth}px`
398
513
  } : void 0,
399
- children: /* @__PURE__ */ jsxs(Command, { value, shouldFilter: false, className: "ui-search-select-command", children: [
514
+ children: /* @__PURE__ */ jsx(PopupRender, { render: popupRender, children: /* @__PURE__ */ jsxs(Command, { value, shouldFilter: false, className: "ui-search-select-command", children: [
400
515
  /* @__PURE__ */ jsx("div", { className: "ui-search-select-search", children: /* @__PURE__ */ jsx(
401
516
  Input,
402
517
  {
@@ -406,7 +521,23 @@ function SearchSelect(props) {
406
521
  "aria-activedescendant": activeOptionId,
407
522
  "aria-label": searchPlaceholder ?? t("dataEntry.searchSelect.search"),
408
523
  value: query,
409
- onChange: (event) => setQuery(event.target.value),
524
+ onChange: (event) => {
525
+ const next = event.target.value;
526
+ if (multiple && tokenSeparators?.length && tokenSeparators.some((separator) => next.includes(separator))) {
527
+ selectTokens(next);
528
+ setQuery("");
529
+ return;
530
+ }
531
+ setQuery(next);
532
+ },
533
+ onPaste: (event) => {
534
+ if (!multiple || !tokenSeparators?.length) return;
535
+ const text = event.clipboardData.getData("text");
536
+ if (!tokenSeparators.some((separator) => text.includes(separator))) return;
537
+ event.preventDefault();
538
+ selectTokens(text);
539
+ setQuery("");
540
+ },
410
541
  onKeyDown,
411
542
  placeholder: searchPlaceholder ?? t("dataEntry.searchSelect.search"),
412
543
  className: "ui-search-select-search-input"
@@ -420,7 +551,13 @@ function SearchSelect(props) {
420
551
  "aria-multiselectable": multiple || void 0,
421
552
  "aria-busy": loading,
422
553
  className: "ui-search-select-list",
423
- onScroll,
554
+ style: listHeight === void 0 ? void 0 : {
555
+ "--search-select-list-max-height": `${listHeight}px`
556
+ },
557
+ onScroll: (event) => {
558
+ onPopupScroll?.(event);
559
+ onScroll(event);
560
+ },
424
561
  children: [
425
562
  grouped.map((group) => {
426
563
  const rows = group.items.map(({ option, index }) => /* @__PURE__ */ jsxs(
@@ -484,7 +621,7 @@ function SearchSelect(props) {
484
621
  "data-tone": "destructive",
485
622
  children: errorMessage ?? t("dataEntry.searchSelect.error")
486
623
  }
487
- ) : loaded.length === 0 ? /* @__PURE__ */ jsx(
624
+ ) : displayOptions.length === 0 ? /* @__PURE__ */ jsx(
488
625
  "div",
489
626
  {
490
627
  role: "option",
@@ -511,7 +648,7 @@ function SearchSelect(props) {
511
648
  })
512
649
  }
513
650
  ) : null
514
- ] })
651
+ ] }) })
515
652
  }
516
653
  )
517
654
  ]
@@ -539,7 +676,7 @@ function SearchSelect(props) {
539
676
  onClick: clear,
540
677
  children: clearControl.clearIcon ?? /* @__PURE__ */ jsx(X, { className: "ui-control-affix-icon", "aria-hidden": "true" })
541
678
  }
542
- ) : /* @__PURE__ */ jsx(
679
+ ) : suffixIcon === null ? null : suffixIcon !== void 0 ? /* @__PURE__ */ jsx("span", { "data-slot": "select-suffix", className: "ui-select-suffix", "aria-hidden": "true", children: suffixIcon }) : /* @__PURE__ */ jsx(
543
680
  ChevronsUpDown,
544
681
  {
545
682
  className: "ui-control-affix-icon ui-control-affix-indicator",
@@ -1,12 +1,30 @@
1
1
  import * as React from "react";
2
2
  import type { ControlStatusProp, ControlVariantProp, ControlWidthProp, SizeProp } from "../../props/vocabulary/index.js";
3
- import * as SelectPrimitive from "@radix-ui/react-select";
4
3
  import type { FieldA11yProps } from "../../lib/field-a11y.js";
5
4
  import type { SelectDataProp } from "../../props/components/data-entry.prop.js";
6
- /** Compound-API props: the Radix root's own props PLUS the FormField field-a11y contract, which
5
+ /** Compound-API props the shape the Radix root had, plus the FormField field-a11y contract that
7
6
  * this component re-routes to the trigger (see {@link SelectFieldA11yContext}). */
8
- export type SelectCompoundProp = React.ComponentProps<typeof SelectPrimitive.Root> & FieldA11yProps & {
7
+ export type SelectCompoundProp = FieldA11yProps & {
9
8
  id?: string;
9
+ value?: string;
10
+ defaultValue?: string;
11
+ onValueChange?: (value: string) => void;
12
+ open?: boolean;
13
+ defaultOpen?: boolean;
14
+ onOpenChange?: (open: boolean) => void;
15
+ /**
16
+ * @deprecated Accepted so Radix-era call sites keep compiling. Direction now comes from the
17
+ * ambient locale (`AppProvider` / `I18nProvider`), which react-aria reads itself.
18
+ */
19
+ dir?: "ltr" | "rtl";
20
+ /** Form field name — a hidden native `<select>` carries the value on submit. */
21
+ name?: string;
22
+ autoComplete?: string;
23
+ disabled?: boolean;
24
+ /** Native `required`: an empty select blocks a native form submit. */
25
+ required?: boolean;
26
+ form?: string;
27
+ children?: React.ReactNode;
10
28
  };
11
29
  export type SelectProp = SelectDataProp | SelectCompoundProp;
12
30
  /**
@@ -17,9 +35,16 @@ export type SelectProp = SelectDataProp | SelectCompoundProp;
17
35
  * optgroup grouping, and `renderOption`.
18
36
  */
19
37
  export declare function Select(props: SelectProp): React.JSX.Element;
20
- export declare function SelectGroup(props: React.ComponentProps<typeof SelectPrimitive.Group>): React.JSX.Element;
21
- export declare function SelectValue(props: React.ComponentProps<typeof SelectPrimitive.Value>): React.JSX.Element;
22
- export declare const SelectTrigger: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectTriggerProps & React.RefAttributes<HTMLButtonElement>, "ref"> & {
38
+ export declare function SelectGroup({ className, children, ...props }: React.HTMLAttributes<HTMLElement>): React.JSX.Element;
39
+ export declare const SelectValue: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLSpanElement>, "children"> & {
40
+ /** Shown while nothing is selected. */
41
+ placeholder?: React.ReactNode;
42
+ /** Replaces the selected option's label on the trigger while a value is selected. */
43
+ children?: React.ReactNode;
44
+ /** @deprecated Radix-only; has no effect. */
45
+ asChild?: boolean;
46
+ } & React.RefAttributes<HTMLSpanElement>>;
47
+ export declare const SelectTrigger: React.ForwardRefExoticComponent<React.ButtonHTMLAttributes<HTMLButtonElement> & {
23
48
  /** Control height tier — the shared `--control-height` ladder (antd `size`). */
24
49
  size?: SizeProp;
25
50
  /** Control surface (antd `variant`). Default `outlined`. */
@@ -43,27 +68,64 @@ export declare const SelectTrigger: React.ForwardRefExoticComponent<Omit<SelectP
43
68
  "data-field"?: string;
44
69
  /**
45
70
  * Normally supplied by `Select` itself — the trigger shows the option's LABEL, and this is the
46
- * only place the underlying value is readable from the DOM without reaching into Radix's
47
- * aria-hidden native `<select>`.
71
+ * only place the underlying value is readable from the DOM without reaching into the aria-hidden
72
+ * native `<select>`.
48
73
  */
49
74
  "data-value"?: string;
75
+ /** @deprecated Radix-only; has no effect — the trigger is always the library's own button. */
76
+ asChild?: boolean;
50
77
  } & React.RefAttributes<HTMLButtonElement>>;
51
- export declare const SelectScrollUpButton: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectScrollUpButtonProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
52
- export declare const SelectScrollDownButton: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectScrollDownButtonProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
53
- export declare const SelectContent: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectContentProps & React.RefAttributes<HTMLDivElement>, "ref"> & {
78
+ type SelectScrollButtonProp = React.HTMLAttributes<HTMLDivElement>;
79
+ /**
80
+ * @deprecated Radix-only. react-aria's listbox is a native scroll container, so there is nothing
81
+ * to render; kept (as a no-op) so existing imports keep compiling until the next major.
82
+ */
83
+ export declare const SelectScrollUpButton: React.ForwardRefExoticComponent<SelectScrollButtonProp & React.RefAttributes<HTMLDivElement>>;
84
+ /** @deprecated Radix-only — see {@link SelectScrollUpButton}. */
85
+ export declare const SelectScrollDownButton: React.ForwardRefExoticComponent<SelectScrollButtonProp & React.RefAttributes<HTMLDivElement>>;
86
+ type RadixSide = "top" | "right" | "bottom" | "left";
87
+ export declare const SelectContent: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLDivElement>, "dir"> & {
54
88
  /**
55
89
  * How the popup sizes against its trigger — the DOM half of antd `popupMatchSelectWidth`.
56
90
  * Absent = the trigger's width is the floor (antd's `true`); `content` releases that floor;
57
91
  * `fixed` pins both edges to `--select-content-inline-size`.
58
92
  */
59
93
  "data-popup-match"?: "content" | "fixed";
94
+ side?: RadixSide;
95
+ sideOffset?: number;
96
+ align?: "start" | "center" | "end";
97
+ alignOffset?: number;
98
+ /** `false` keeps the popup on `side` even when it would overflow. */
99
+ avoidCollisions?: boolean;
100
+ collisionPadding?: number;
101
+ /** @deprecated Radix-only. The popup always positions against the trigger (`popper`). */
102
+ position?: "popper" | "item-aligned";
103
+ /** @deprecated Radix-only; has no effect. */
104
+ sticky?: "partial" | "always";
105
+ /** @deprecated Radix-only; has no effect. */
106
+ hideWhenDetached?: boolean;
107
+ /** @deprecated Radix-only; has no effect. */
108
+ onCloseAutoFocus?: (event: Event) => void;
109
+ /** @deprecated Radix-only; has no effect — Escape always closes. */
110
+ onEscapeKeyDown?: (event: KeyboardEvent) => void;
111
+ /** @deprecated Radix-only; has no effect — an outside press always closes. */
112
+ onPointerDownOutside?: (event: Event) => void;
113
+ /** @deprecated Radix-only; has no effect. */
114
+ forceMount?: true;
60
115
  } & React.RefAttributes<HTMLDivElement>>;
61
- export declare const SelectLabel: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectLabelProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
62
- export declare const SelectItem: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectItemProps & React.RefAttributes<HTMLDivElement>, "ref"> & {
116
+ export declare const SelectLabel: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLElement> & React.RefAttributes<HTMLElement>>;
117
+ export declare const SelectItem: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLDivElement>, "onSelect"> & {
118
+ value: string;
119
+ disabled?: boolean;
120
+ /** Text for type-to-select and the hidden native `<option>`; derived from `children` if omitted. */
121
+ textValue?: string;
63
122
  /**
64
123
  * antd `menuItemSelectedIcon` — a decorative mark on the picked row. Off by default: this
65
124
  * library marks the picked row with fill + weight, which costs no width.
66
125
  */
67
126
  selectedIcon?: React.ReactNode;
127
+ /** @deprecated Radix-only; has no effect. */
128
+ asChild?: boolean;
68
129
  } & React.RefAttributes<HTMLDivElement>>;
69
- export declare const SelectSeparator: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectSeparatorProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
130
+ export declare const SelectSeparator: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLElement> & React.RefAttributes<HTMLElement>>;
131
+ export {};