@godxjp/ui 22.0.0 → 23.1.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 (115) hide show
  1. package/README.md +4 -4
  2. package/dist/app/date-format-labels.d.ts +14 -2
  3. package/dist/app/date-format-labels.js +1 -1
  4. package/dist/app/date-formats.d.ts +14 -2
  5. package/dist/app/date-formats.js +9 -2
  6. package/dist/components/data-display/data-table.d.ts +8 -1
  7. package/dist/components/data-display/data-table.js +9 -2
  8. package/dist/components/data-display/scroll-area.d.ts +24 -4
  9. package/dist/components/data-display/scroll-area.js +34 -72
  10. package/dist/components/data-entry/cascader.js +8 -6
  11. package/dist/components/data-entry/checkbox.js +3 -1
  12. package/dist/components/data-entry/choice-hit-target.d.ts +24 -0
  13. package/dist/components/data-entry/choice-hit-target.js +12 -0
  14. package/dist/components/data-entry/control-surface.d.ts +8 -0
  15. package/dist/components/data-entry/control-surface.js +10 -1
  16. package/dist/components/data-entry/form-field.js +5 -4
  17. package/dist/components/data-entry/input.js +1 -1
  18. package/dist/components/data-entry/number-input.js +27 -6
  19. package/dist/components/data-entry/radio.js +4 -3
  20. package/dist/components/data-entry/search-select.js +225 -95
  21. package/dist/components/data-entry/select.d.ts +76 -14
  22. package/dist/components/data-entry/select.js +495 -163
  23. package/dist/components/data-entry/slider.d.ts +3 -11
  24. package/dist/components/data-entry/slider.js +582 -93
  25. package/dist/components/data-entry/switch.js +3 -1
  26. package/dist/components/feedback/dialog.js +2 -2
  27. package/dist/components/feedback/sheet.js +2 -22
  28. package/dist/components/navigation/dropdown-menu.d.ts +11 -3
  29. package/dist/components/navigation/dropdown-menu.js +190 -15
  30. package/dist/components/navigation/filter-bar.js +2 -1
  31. package/dist/components/navigation/index.d.ts +0 -3
  32. package/dist/components/navigation/index.js +0 -78
  33. package/dist/components/navigation/tabs.js +16 -4
  34. package/dist/components/ui/index.d.ts +0 -3
  35. package/dist/components/ui/index.js +0 -3
  36. package/dist/components/ui/input-otp.d.ts +1 -1
  37. package/dist/components/ui/segmented.js +3 -1
  38. package/dist/components/ui/tag-input.js +3 -5
  39. package/dist/i18n/messages/en.json +5 -1
  40. package/dist/i18n/messages/ja.json +5 -1
  41. package/dist/i18n/messages/vi.json +5 -1
  42. package/dist/lib/breakpoint-token.d.ts +8 -0
  43. package/dist/lib/breakpoint-token.js +29 -0
  44. package/dist/lib/select-options.d.ts +21 -0
  45. package/dist/lib/select-options.js +46 -0
  46. package/dist/props/components/data-display.prop.d.ts +17 -7
  47. package/dist/props/components/data-entry.prop.d.ts +323 -30
  48. package/dist/props/components/index.d.ts +1 -1
  49. package/dist/props/components/navigation.prop.d.ts +25 -1
  50. package/dist/props/registry.d.ts +69 -1
  51. package/dist/props/registry.js +75 -0
  52. package/dist/props/vocabulary/data.prop.d.ts +5 -0
  53. package/dist/props/vocabulary/index.d.ts +1 -1
  54. package/dist/styles/control.css +246 -28
  55. package/dist/styles/data-display-layout.css +18 -10
  56. package/dist/styles/focus-ring.css +28 -0
  57. package/dist/styles/fonts.css +88 -4
  58. package/dist/styles/form-layout.css +4 -0
  59. package/dist/styles/layout.css +2 -1
  60. package/dist/styles/navigation-layout.css +24 -238
  61. package/dist/styles/shell-layout.css +148 -139
  62. package/dist/styles/table-layout.css +12 -5
  63. package/dist/tokens/components/control.css +12 -3
  64. package/dist/tokens/components/data-display.css +0 -5
  65. package/dist/tokens/components/navigation.css +5 -5
  66. package/dist/tokens/components/scroll-area.css +7 -0
  67. package/docs/CONSUMER-RULES.md +7 -2
  68. package/docs/CUSTOMER-THEMING.md +25 -9
  69. package/docs/DATETIME.md +11 -2
  70. package/docs/FRAME-COVERAGE-REPORT.md +4 -7
  71. package/docs/SPACING.md +35 -5
  72. package/docs/TOKENS.md +1 -1
  73. package/docs/data-display/data-table/index.tsx +44 -3
  74. package/docs/data-display/popover.tsx +4 -6
  75. package/docs/data-display/scroll-area.tsx +51 -45
  76. package/docs/data-display/table.tsx +1 -1
  77. package/docs/data-entry/date-picker.tsx +2 -3
  78. package/docs/data-entry/input-otp.tsx +5 -2
  79. package/docs/data-entry/segmented-in-filter-row.tsx +77 -0
  80. package/docs/data-entry/segmented.tsx +40 -1
  81. package/docs/data-entry/select-matrix.tsx +1 -2
  82. package/docs/data-entry/select.tsx +108 -3
  83. package/docs/data-entry/slider.tsx +125 -47
  84. package/docs/feedback/dialog.tsx +1 -2
  85. package/docs/feedback/sheet.tsx +1 -2
  86. package/docs/layout/app-shell-states.tsx +143 -0
  87. package/docs/navigation/dropdown-menu.tsx +77 -6
  88. package/docs/navigation/tabs.tsx +72 -0
  89. package/docs/roadmap/ai-chat-components.md +1 -2
  90. package/docs/roadmap/parity-audit-data-entry.md +44 -33
  91. package/docs/roadmap/parity-audit-layout-navigation-general.md +304 -310
  92. package/docs/roadmap/parity-backlog.md +2 -3
  93. package/docs/roadmap/tree-components.md +0 -1
  94. package/package.json +25 -55
  95. package/scripts/_agent-setup.mjs +18 -0
  96. package/scripts/guinea-pig-skill.md +12 -6
  97. package/scripts/ui-audit.mjs +185 -7
  98. package/scripts/visual-audit-rules.mjs +0 -7
  99. package/scripts/visual-audit.mjs +6 -27
  100. package/dist/components/navigation/context-menu.d.ts +0 -21
  101. package/dist/components/navigation/context-menu.js +0 -149
  102. package/dist/components/navigation/menubar.d.ts +0 -28
  103. package/dist/components/navigation/menubar.js +0 -136
  104. package/dist/components/navigation/navigation-menu.d.ts +0 -10
  105. package/dist/components/navigation/navigation-menu.js +0 -91
  106. package/dist/components/ui/context-menu.d.ts +0 -1
  107. package/dist/components/ui/context-menu.js +0 -2
  108. package/dist/components/ui/menubar.d.ts +0 -1
  109. package/dist/components/ui/menubar.js +0 -2
  110. package/dist/components/ui/navigation-menu.d.ts +0 -1
  111. package/dist/components/ui/navigation-menu.js +0 -2
  112. package/docs/FRAME-A11Y-CI.md +0 -349
  113. package/docs/navigation/context-menu.tsx +0 -128
  114. package/docs/navigation/menubar.tsx +0 -141
  115. package/docs/navigation/navigation-menu.tsx +0 -158
@@ -7,6 +7,7 @@ import { cn } from "../../lib/utils.js";
7
7
  import { pickFieldA11y, useFieldIdentity, useMirroredInputAttributes } from "../../lib/field-a11y.js";
8
8
  import { Field } from "./field.js";
9
9
  import { choiceGroupClassName } from "./choice-option.js";
10
+ import { withOwnHitTarget } from "./choice-hit-target.js";
10
11
  function checkedState(isSelected) {
11
12
  return isSelected ? "checked" : "unchecked";
12
13
  }
@@ -61,11 +62,11 @@ const RadioItem = React.forwardRef(
61
62
  // records: react-aria's root is a `<label>`, which is `display:inline`, so without it the
62
63
  // 16px dot collapses to nothing. `data-[invalid]` replaces `aria-invalid:` because
63
64
  // react-aria puts `aria-invalid` on the `<input>` and `data-invalid` on this box.
64
- "ui-radio data-[invalid]:border-destructive inline-flex shrink-0 items-center justify-center shadow-xs transition-shadow outline-none",
65
+ "ui-radio data-[invalid]:border-destructive inline-flex shrink-0 items-center justify-center shadow-xs transition-shadow",
65
66
  className
66
67
  ),
67
68
  ...props,
68
- render: (domProps, state) => /* @__PURE__ */ jsx("label", { ...domProps, "data-state": checkedState(state.isSelected) }),
69
+ render: (domProps, state) => /* @__PURE__ */ jsx("label", { ...domProps, "data-state": checkedState(state.isSelected), children: withOwnHitTarget(domProps.children) }),
69
70
  children: /* @__PURE__ */ jsx("span", { "data-slot": "radio-group-indicator", className: "ui-choice-indicator", children: /* @__PURE__ */ jsx(Circle, { className: "ui-radio-icon", "aria-hidden": "true" }) })
70
71
  }
71
72
  );
@@ -87,7 +88,7 @@ function RadioBarButton({
87
88
  isDisabled: option.disabled,
88
89
  "data-slot": "radio-button",
89
90
  className: "ui-radio-button ui-focus-ring",
90
- render: (domProps, state) => /* @__PURE__ */ jsx("label", { ...domProps, "data-state": checkedState(state.isSelected) }),
91
+ render: (domProps, state) => /* @__PURE__ */ jsx("label", { ...domProps, "data-state": checkedState(state.isSelected), children: withOwnHitTarget(domProps.children) }),
91
92
  children: /* @__PURE__ */ jsx("span", { className: "ui-radio-button-label", children: option.label })
92
93
  }
93
94
  );
@@ -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,
@@ -44,11 +53,18 @@ function SearchSelect(props) {
44
53
  search: searchProp,
45
54
  onSearchChange,
46
55
  filterOption,
56
+ optionFilterProp,
47
57
  filterSort,
48
58
  autoClearSearchValue = true,
49
59
  optionRender,
50
60
  menuItemSelectedIcon,
51
61
  notFoundContent,
62
+ prefix,
63
+ suffixIcon,
64
+ placement,
65
+ popupRender,
66
+ listHeight,
67
+ onPopupScroll,
52
68
  popupMatchSelectWidth = true,
53
69
  allowClear,
54
70
  onClear,
@@ -66,7 +82,15 @@ function SearchSelect(props) {
66
82
  "aria-invalid": ariaInvalid,
67
83
  "aria-required": ariaRequired
68
84
  } = props;
69
- 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;
70
94
  const { t } = useTranslation();
71
95
  const nameFallback = useFieldNameFallback({
72
96
  "aria-label": ariaLabel,
@@ -111,7 +135,7 @@ function SearchSelect(props) {
111
135
  {}
112
136
  );
113
137
  const [internalValue, setInternalValue] = React.useState(
114
- props.defaultValue ?? (props.mode === "multiple" ? [] : "")
138
+ props.defaultValue ?? (props.mode === "multiple" || props.mode === "tags" ? [] : "")
115
139
  );
116
140
  const isControlled = props.value !== void 0;
117
141
  const rawValue = isControlled ? props.value : internalValue;
@@ -127,14 +151,20 @@ function SearchSelect(props) {
127
151
  () => loadOptions ?? (async ({ query: search }) => {
128
152
  const needle = search.trim();
129
153
  const list = staticOptions ?? [];
130
- 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
+ };
131
161
  const kept = needle ? list.filter(matches) : list;
132
162
  return {
133
163
  options: filterSort ? [...kept].sort((a, b) => filterSort(a, b, { searchValue: needle })) : kept,
134
164
  hasMore: false
135
165
  };
136
166
  }),
137
- [loadOptions, staticOptions, filterOption, filterSort]
167
+ [loadOptions, staticOptions, filterOption, optionFilterProp, filterSort]
138
168
  );
139
169
  React.useEffect(() => {
140
170
  const handle = window.setTimeout(() => setDebouncedQuery(query.trim()), DEBOUNCE_MS);
@@ -171,10 +201,24 @@ function SearchSelect(props) {
171
201
  setActiveIndex(0);
172
202
  void fetchPage(1, debouncedQuery, false);
173
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]);
174
218
  const grouped = React.useMemo(() => {
175
219
  const order = [];
176
220
  const buckets = /* @__PURE__ */ new Map();
177
- for (const option of loaded) {
221
+ for (const option of displayOptions) {
178
222
  const key = option.group ?? "";
179
223
  if (!buckets.has(key)) {
180
224
  buckets.set(key, []);
@@ -187,7 +231,7 @@ function SearchSelect(props) {
187
231
  heading: key || void 0,
188
232
  items: (buckets.get(key) ?? []).map((option) => ({ option, index: flatIndex++ }))
189
233
  }));
190
- }, [loaded]);
234
+ }, [displayOptions]);
191
235
  const flatOrdered = React.useMemo(
192
236
  () => grouped.flatMap((group) => group.items.map((entry) => entry.option)),
193
237
  [grouped]
@@ -197,9 +241,10 @@ function SearchSelect(props) {
197
241
  const selectedOption = value ? optionFor(value) : null;
198
242
  const currentLabel = value ? selectedOption?.label ?? selectedLabel ?? value : resolvedPlaceholder;
199
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;
200
245
  const selectedChips = multiple ? values.map((entry) => ({
201
246
  value: entry,
202
- label: optionFor(entry)?.label ?? entry
247
+ label: chipLabel(optionFor(entry)?.label ?? entry)
203
248
  })) : [];
204
249
  const {
205
250
  visible: visibleChips,
@@ -207,15 +252,15 @@ function SearchSelect(props) {
207
252
  overflow: chipOverflow
208
253
  } = applyMaxTagCount(
209
254
  selectedChips,
210
- props.mode === "multiple" ? props.maxTagCount : void 0,
211
- 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
212
257
  );
213
- const maxCount = props.mode === "multiple" ? props.maxCount : void 0;
258
+ const maxCount = props.mode !== void 0 ? props.maxCount : void 0;
214
259
  const atMaxCount = maxCount !== void 0 && values.length >= maxCount;
215
260
  const remember = (option) => setPickedOptions((prev) => ({ ...prev, [option.value]: option }));
216
261
  const select = (option) => {
217
262
  if (option.disabled) return;
218
- if (props.mode === "multiple") {
263
+ if (props.mode !== void 0) {
219
264
  const already = values.includes(option.value);
220
265
  if (!already && atMaxCount) return;
221
266
  const next = already ? values.filter((entry) => entry !== option.value) : [...values, option.value];
@@ -237,8 +282,35 @@ function SearchSelect(props) {
237
282
  if (autoClearSearchValue) setQuery("");
238
283
  setOpen(false);
239
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
+ };
240
312
  const clear = () => {
241
- if (props.mode === "multiple") {
313
+ if (props.mode !== void 0) {
242
314
  if (!isControlled) setInternalValue([]);
243
315
  props.onValueChange?.([], []);
244
316
  } else {
@@ -302,100 +374,136 @@ function SearchSelect(props) {
302
374
  );
303
375
  const showClear = clearControl.enabled && hasSelection && !disabled && !readOnly && !loadingProp;
304
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
+ ] });
305
466
  return /* @__PURE__ */ jsxs("div", { className: cn("relative", className), children: [
306
467
  /* @__PURE__ */ jsxs(
307
468
  Popover,
308
469
  {
309
470
  open,
310
471
  onOpenChange: (next) => {
311
- if (readOnly && next) return;
472
+ if ((readOnly || disabled) && next) return;
312
473
  setOpen(next);
313
474
  if (!next && autoClearSearchValue) setQuery("");
314
475
  },
315
476
  children: [
316
- /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
317
- "button",
477
+ /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: multiple ? /* @__PURE__ */ jsx(
478
+ "div",
318
479
  {
319
- id,
320
- type: "button",
321
- role: "combobox",
322
- ...surface,
323
- "aria-expanded": open,
324
- "aria-controls": open ? listId : void 0,
325
- "aria-label": triggerAriaLabel,
326
- "aria-labelledby": triggerAriaLabelledby,
327
- "aria-describedby": ariaDescribedby,
328
- "aria-errormessage": ariaErrorMessage,
329
- "aria-invalid": resolveAriaInvalid(ariaInvalid, status),
330
- "aria-required": ariaRequired,
331
- "aria-readonly": readOnly || void 0,
332
- "aria-busy": loadingProp || void 0,
333
- disabled,
334
- "data-testid": dataTestId,
335
- "data-field": resolvedField,
336
- "data-value": (multiple ? values.join(",") : value) || void 0,
337
- "data-mode": multiple ? "multiple" : void 0,
338
- "data-width": width,
339
- className: cn(
340
- controlSurfaceTriggerClass,
341
- "justify-start",
342
- // `bounded` deliberately emits NO width utility — its width is owned by the
343
- // `[data-width="bounded"]` rule in control.css, and a utility here would win the
344
- // layer order and make that token dead. Same reasoning as SelectTrigger.
345
- width === "auto" && "w-auto",
346
- width === "full" && "w-full",
347
- // Reserve trailing room for the single clear-or-chevron overlay rendered below.
348
- "ui-control-trigger-affixed"
349
- ),
350
- children: /* @__PURE__ */ jsx(
351
- "span",
352
- {
353
- className: cn(
354
- "ui-search-select-option-body text-start",
355
- !hasSelection && "text-muted-foreground"
356
- ),
357
- children: multiple ? hasSelection ? (
358
- // Labels, not removable chips: the trigger IS a <button>, and a per-label remove
359
- // button inside it would be a button nested in a button (invalid HTML →
360
- // hydration error). Removal is a second click on the row, or the clear ✕.
361
- /* @__PURE__ */ jsxs("span", { className: "ui-search-select-values", "data-slot": "search-select-values", children: [
362
- visibleChips.map((chip) => /* @__PURE__ */ jsx(
363
- "span",
364
- {
365
- className: "ui-search-select-value",
366
- "data-slot": "search-select-value",
367
- children: labelRender ? labelRender({
368
- value: chip.value,
369
- label: chip.label,
370
- option: optionFor(chip.value) ?? void 0
371
- }) : chip.label
372
- },
373
- chip.value
374
- )),
375
- omittedChips.length > 0 ? /* @__PURE__ */ jsx(
376
- "span",
377
- {
378
- className: "ui-search-select-value",
379
- "data-slot": "search-select-value-overflow",
380
- children: chipOverflow ?? t("dataEntry.selection.overflow", { count: omittedChips.length })
381
- }
382
- ) : null
383
- ] })
384
- ) : /* @__PURE__ */ jsx("span", { className: "truncate", children: resolvedPlaceholder }) : value && labelRender ? labelRender({ value, label: currentLabel, option: selectedOption ?? void 0 }) : /* @__PURE__ */ jsxs(Fragment, { children: [
385
- currentIcon ? /* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-center", "aria-hidden": "true", children: currentIcon }) : null,
386
- /* @__PURE__ */ jsx("span", { className: "truncate", children: currentLabel })
387
- ] })
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;
388
490
  }
389
- )
491
+ if (event.key === "Backspace" && values.length > 0) {
492
+ event.preventDefault();
493
+ deselect(values[values.length - 1]);
494
+ }
495
+ },
496
+ children: triggerContent
390
497
  }
391
- ) }),
498
+ ) : /* @__PURE__ */ jsx("button", { ...triggerProps, type: "button", disabled, children: triggerContent }) }),
392
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,
393
500
  /* @__PURE__ */ jsx(
394
501
  PopoverContent,
395
502
  {
396
503
  "aria-label": triggerAriaLabelledby ? void 0 : triggerAriaLabel ?? resolvedPlaceholder,
397
504
  "aria-labelledby": triggerAriaLabelledby,
398
- align: "start",
505
+ side: placement?.startsWith("top") ? "top" : "bottom",
506
+ align: placement?.endsWith("End") ? "end" : "start",
399
507
  sideOffset: 4,
400
508
  collisionPadding: 12,
401
509
  className: "ui-search-select-panel",
@@ -403,7 +511,7 @@ function SearchSelect(props) {
403
511
  style: typeof popupMatchSelectWidth === "number" ? {
404
512
  "--search-select-panel-inline-size": `${popupMatchSelectWidth}px`
405
513
  } : void 0,
406
- 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: [
407
515
  /* @__PURE__ */ jsx("div", { className: "ui-search-select-search", children: /* @__PURE__ */ jsx(
408
516
  Input,
409
517
  {
@@ -413,7 +521,23 @@ function SearchSelect(props) {
413
521
  "aria-activedescendant": activeOptionId,
414
522
  "aria-label": searchPlaceholder ?? t("dataEntry.searchSelect.search"),
415
523
  value: query,
416
- 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
+ },
417
541
  onKeyDown,
418
542
  placeholder: searchPlaceholder ?? t("dataEntry.searchSelect.search"),
419
543
  className: "ui-search-select-search-input"
@@ -427,7 +551,13 @@ function SearchSelect(props) {
427
551
  "aria-multiselectable": multiple || void 0,
428
552
  "aria-busy": loading,
429
553
  className: "ui-search-select-list",
430
- 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
+ },
431
561
  children: [
432
562
  grouped.map((group) => {
433
563
  const rows = group.items.map(({ option, index }) => /* @__PURE__ */ jsxs(
@@ -491,7 +621,7 @@ function SearchSelect(props) {
491
621
  "data-tone": "destructive",
492
622
  children: errorMessage ?? t("dataEntry.searchSelect.error")
493
623
  }
494
- ) : loaded.length === 0 ? /* @__PURE__ */ jsx(
624
+ ) : displayOptions.length === 0 ? /* @__PURE__ */ jsx(
495
625
  "div",
496
626
  {
497
627
  role: "option",
@@ -518,7 +648,7 @@ function SearchSelect(props) {
518
648
  })
519
649
  }
520
650
  ) : null
521
- ] })
651
+ ] }) })
522
652
  }
523
653
  )
524
654
  ]
@@ -546,7 +676,7 @@ function SearchSelect(props) {
546
676
  onClick: clear,
547
677
  children: clearControl.clearIcon ?? /* @__PURE__ */ jsx(X, { className: "ui-control-affix-icon", "aria-hidden": "true" })
548
678
  }
549
- ) : /* @__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(
550
680
  ChevronsUpDown,
551
681
  {
552
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 {};