@godxjp/ui 20.0.0 → 20.2.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 (273) hide show
  1. package/README.md +8 -2
  2. package/dist/app/app-provider.js +37 -21
  3. package/dist/app/index.d.ts +2 -0
  4. package/dist/app/index.js +2 -0
  5. package/dist/app/storage.d.ts +17 -0
  6. package/dist/app/storage.js +28 -0
  7. package/dist/app/theme-axes.d.ts +2 -0
  8. package/dist/app/theme-axes.js +45 -0
  9. package/dist/components/charts/chart-cartesian.d.ts +2 -1
  10. package/dist/components/charts/chart-cartesian.js +52 -3
  11. package/dist/components/charts/chart-category-axis.d.ts +55 -0
  12. package/dist/components/charts/chart-category-axis.js +93 -0
  13. package/dist/components/charts/chart-frame.d.ts +10 -1
  14. package/dist/components/charts/chart-frame.js +19 -3
  15. package/dist/components/charts/compact-bar-trend.d.ts +1 -1
  16. package/dist/components/charts/compact-bar-trend.js +2 -0
  17. package/dist/components/charts/pie-chart.d.ts +1 -1
  18. package/dist/components/charts/pie-chart.js +12 -1
  19. package/dist/components/charts/recharts-peer.d.ts +49 -0
  20. package/dist/components/charts/recharts-peer.js +45 -0
  21. package/dist/components/data-display/card.d.ts +12 -1
  22. package/dist/components/data-display/card.js +22 -4
  23. package/dist/components/data-display/code-block.js +6 -2
  24. package/dist/components/data-display/data-table.d.ts +39 -4
  25. package/dist/components/data-display/data-table.js +802 -270
  26. package/dist/components/data-display/descriptions.d.ts +27 -6
  27. package/dist/components/data-display/descriptions.js +58 -17
  28. package/dist/components/data-display/index.d.ts +2 -0
  29. package/dist/components/data-display/index.js +2 -0
  30. package/dist/components/data-display/list-row.d.ts +10 -1
  31. package/dist/components/data-display/list-row.js +7 -1
  32. package/dist/components/data-display/popover.d.ts +9 -3
  33. package/dist/components/data-display/popover.js +65 -11
  34. package/dist/components/data-display/range-timeline.d.ts +38 -0
  35. package/dist/components/data-display/range-timeline.js +161 -0
  36. package/dist/components/data-display/scroll-area.js +13 -2
  37. package/dist/components/data-display/service-launcher-card.js +14 -12
  38. package/dist/components/data-display/table.d.ts +71 -3
  39. package/dist/components/data-display/table.js +56 -18
  40. package/dist/components/data-display/tree-list.js +4 -9
  41. package/dist/components/data-entry/calendar.d.ts +2 -2
  42. package/dist/components/data-entry/calendar.js +23 -3
  43. package/dist/components/data-entry/cascader.d.ts +1 -1
  44. package/dist/components/data-entry/cascader.js +188 -24
  45. package/dist/components/data-entry/color-picker.d.ts +1 -1
  46. package/dist/components/data-entry/color-picker.js +17 -5
  47. package/dist/components/data-entry/control-appearance.d.ts +64 -0
  48. package/dist/components/data-entry/control-appearance.js +39 -0
  49. package/dist/components/data-entry/control-surface.d.ts +61 -0
  50. package/dist/components/data-entry/control-surface.js +39 -0
  51. package/dist/components/data-entry/date-picker.d.ts +1 -1
  52. package/dist/components/data-entry/date-picker.js +344 -113
  53. package/dist/components/data-entry/date-range-picker.d.ts +1 -1
  54. package/dist/components/data-entry/date-range-picker.js +278 -140
  55. package/dist/components/data-entry/form-field.d.ts +1 -1
  56. package/dist/components/data-entry/form-field.js +39 -4
  57. package/dist/components/data-entry/form.d.ts +4 -0
  58. package/dist/components/data-entry/form.js +4 -2
  59. package/dist/components/data-entry/index.d.ts +7 -3
  60. package/dist/components/data-entry/index.js +10 -1
  61. package/dist/components/data-entry/input-otp.d.ts +1 -1
  62. package/dist/components/data-entry/input.d.ts +12 -34
  63. package/dist/components/data-entry/input.js +92 -24
  64. package/dist/components/data-entry/month-picker.d.ts +1 -1
  65. package/dist/components/data-entry/month-picker.js +47 -10
  66. package/dist/components/data-entry/month-range-picker.d.ts +1 -1
  67. package/dist/components/data-entry/month-range-picker.js +51 -11
  68. package/dist/components/data-entry/number-input.d.ts +7 -0
  69. package/dist/components/data-entry/number-input.js +147 -98
  70. package/dist/components/data-entry/password-input.d.ts +1 -1
  71. package/dist/components/data-entry/radio.d.ts +1 -1
  72. package/dist/components/data-entry/radio.js +61 -13
  73. package/dist/components/data-entry/search-input.d.ts +1 -1
  74. package/dist/components/data-entry/search-input.js +16 -2
  75. package/dist/components/data-entry/search-select.d.ts +2 -2
  76. package/dist/components/data-entry/search-select.js +209 -78
  77. package/dist/components/data-entry/select.d.ts +22 -4
  78. package/dist/components/data-entry/select.js +215 -163
  79. package/dist/components/data-entry/slider.d.ts +9 -1
  80. package/dist/components/data-entry/slider.js +87 -9
  81. package/dist/components/data-entry/switch.d.ts +3 -0
  82. package/dist/components/data-entry/switch.js +29 -3
  83. package/dist/components/data-entry/textarea.d.ts +14 -56
  84. package/dist/components/data-entry/textarea.js +80 -33
  85. package/dist/components/data-entry/time-picker.d.ts +2 -2
  86. package/dist/components/data-entry/time-picker.js +333 -109
  87. package/dist/components/data-entry/time-range-picker.d.ts +5 -0
  88. package/dist/components/data-entry/time-range-picker.js +89 -0
  89. package/dist/components/data-entry/transfer.d.ts +1 -1
  90. package/dist/components/data-entry/transfer.js +86 -28
  91. package/dist/components/data-entry/tree-select.d.ts +1 -1
  92. package/dist/components/data-entry/tree-select.js +201 -113
  93. package/dist/components/data-entry/tree-utils.js +7 -14
  94. package/dist/components/data-entry/upload-files.d.ts +2 -0
  95. package/dist/components/data-entry/upload-files.js +31 -0
  96. package/dist/components/data-entry/upload-request.d.ts +4 -0
  97. package/dist/components/data-entry/upload-request.js +53 -0
  98. package/dist/components/data-entry/upload-types.d.ts +28 -0
  99. package/dist/components/data-entry/upload-types.js +2 -0
  100. package/dist/components/data-entry/upload.d.ts +2 -2
  101. package/dist/components/data-entry/upload.js +475 -115
  102. package/dist/components/feedback/dialog.js +19 -19
  103. package/dist/components/feedback/sheet.js +3 -0
  104. package/dist/components/feedback/tooltip.js +3 -0
  105. package/dist/components/general/button.d.ts +2 -1
  106. package/dist/components/general/button.js +7 -1
  107. package/dist/components/general/index.d.ts +1 -0
  108. package/dist/components/general/index.js +2 -0
  109. package/dist/components/general/typography.d.ts +3 -0
  110. package/dist/components/general/typography.js +15 -1
  111. package/dist/components/general/visually-hidden.d.ts +3 -0
  112. package/dist/components/general/visually-hidden.js +9 -0
  113. package/dist/components/layout/app-launcher.d.ts +34 -0
  114. package/dist/components/layout/app-launcher.js +261 -0
  115. package/dist/components/layout/app-shell.d.ts +3 -1
  116. package/dist/components/layout/app-shell.js +34 -14
  117. package/dist/components/layout/auth-shell.d.ts +1 -1
  118. package/dist/components/layout/auth-shell.js +2 -0
  119. package/dist/components/layout/breadcrumb.d.ts +14 -2
  120. package/dist/components/layout/breadcrumb.js +46 -4
  121. package/dist/components/layout/flex.d.ts +1 -1
  122. package/dist/components/layout/flex.js +23 -2
  123. package/dist/components/layout/index.d.ts +3 -0
  124. package/dist/components/layout/index.js +5 -1
  125. package/dist/components/layout/org-switcher.js +20 -1
  126. package/dist/components/layout/page-container.d.ts +1 -1
  127. package/dist/components/layout/page-container.js +5 -3
  128. package/dist/components/layout/responsive-grid.d.ts +16 -2
  129. package/dist/components/layout/responsive-grid.js +29 -2
  130. package/dist/components/layout/sidebar.js +2 -1
  131. package/dist/components/layout/topbar-item.d.ts +3 -0
  132. package/dist/components/layout/topbar-item.js +18 -3
  133. package/dist/components/layout/topbar.d.ts +1 -1
  134. package/dist/components/layout/topbar.js +27 -6
  135. package/dist/components/navigation/dropdown-menu.d.ts +30 -2
  136. package/dist/components/navigation/dropdown-menu.js +73 -11
  137. package/dist/components/navigation/pagination.d.ts +2 -2
  138. package/dist/components/navigation/pagination.js +155 -83
  139. package/dist/components/navigation/steps.d.ts +2 -2
  140. package/dist/components/navigation/steps.js +29 -4
  141. package/dist/components/navigation/tabs.d.ts +5 -33
  142. package/dist/components/navigation/tabs.js +232 -64
  143. package/dist/components/ui/hover-card.js +3 -0
  144. package/dist/components/ui/input-otp.d.ts +26 -28
  145. package/dist/components/ui/input-otp.js +50 -7
  146. package/dist/components/ui/password-input.d.ts +36 -2
  147. package/dist/components/ui/password-input.js +27 -7
  148. package/dist/components/ui/rating.d.ts +25 -0
  149. package/dist/components/ui/rating.js +67 -27
  150. package/dist/components/ui/segmented.d.ts +9 -0
  151. package/dist/components/ui/segmented.js +17 -2
  152. package/dist/components/ui/separator.d.ts +4 -0
  153. package/dist/components/ui/separator.js +13 -0
  154. package/dist/components/ui/tag-input.d.ts +45 -0
  155. package/dist/components/ui/tag-input.js +109 -27
  156. package/dist/form/form-context.d.ts +29 -0
  157. package/dist/form/form-context.js +44 -4
  158. package/dist/form/form-field-array.d.ts +22 -0
  159. package/dist/form/form-field-array.js +50 -0
  160. package/dist/form/form-field-control.d.ts +2 -1
  161. package/dist/form/form-field-control.js +116 -40
  162. package/dist/form/form-root.d.ts +2 -1
  163. package/dist/form/form-root.js +142 -23
  164. package/dist/form/index.d.ts +3 -1
  165. package/dist/form/index.js +12 -1
  166. package/dist/i18n/index.d.ts +1 -1
  167. package/dist/i18n/index.js +3 -1
  168. package/dist/i18n/messages/en.json +59 -6
  169. package/dist/i18n/messages/ja.json +59 -6
  170. package/dist/i18n/messages/vi.json +59 -6
  171. package/dist/i18n/translate.d.ts +19 -0
  172. package/dist/i18n/translate.js +24 -0
  173. package/dist/inertia/index.d.ts +20 -0
  174. package/dist/inertia/index.js +60 -1
  175. package/dist/lib/control-styles.d.ts +25 -3
  176. package/dist/lib/control-styles.js +9 -3
  177. package/dist/lib/datetime/picker-format.d.ts +8 -0
  178. package/dist/lib/datetime/picker-format.js +54 -0
  179. package/dist/lib/overlay-portal.d.ts +18 -0
  180. package/dist/lib/overlay-portal.js +17 -0
  181. package/dist/props/components/app.prop.d.ts +13 -2
  182. package/dist/props/components/charts.prop.d.ts +21 -0
  183. package/dist/props/components/data-display.prop.d.ts +55 -4
  184. package/dist/props/components/data-entry.prop.d.ts +687 -43
  185. package/dist/props/components/form.prop.d.ts +142 -4
  186. package/dist/props/components/general.prop.d.ts +30 -1
  187. package/dist/props/components/index.d.ts +2 -2
  188. package/dist/props/components/layout.prop.d.ts +255 -8
  189. package/dist/props/components/navigation.prop.d.ts +113 -3
  190. package/dist/props/registry.d.ts +376 -10
  191. package/dist/props/registry.js +493 -9
  192. package/dist/props/vocabulary/data.prop.d.ts +179 -1
  193. package/dist/props/vocabulary/index.d.ts +4 -4
  194. package/dist/props/vocabulary/interaction.prop.d.ts +51 -2
  195. package/dist/props/vocabulary/navigation.prop.d.ts +40 -0
  196. package/dist/props/vocabulary/shared.prop.d.ts +33 -0
  197. package/dist/styles/badge-layout.css +2 -1
  198. package/dist/styles/card-layout.css +36 -5
  199. package/dist/styles/chart-layout.css +17 -1
  200. package/dist/styles/control.css +520 -14
  201. package/dist/styles/data-display-layout.css +248 -7
  202. package/dist/styles/data-entry-layout.css +13 -0
  203. package/dist/styles/layout.css +221 -4
  204. package/dist/styles/navigation-layout.css +230 -1
  205. package/dist/styles/shell-layout.css +690 -21
  206. package/dist/styles/table-layout.css +95 -6
  207. package/dist/styles/text-layout.css +5 -0
  208. package/dist/tokens/base.css +1 -0
  209. package/dist/tokens/components/badge.css +1 -0
  210. package/dist/tokens/components/chart.css +6 -0
  211. package/dist/tokens/components/control.css +81 -3
  212. package/dist/tokens/components/data-display.css +9 -0
  213. package/dist/tokens/components/descriptions.css +11 -0
  214. package/dist/tokens/components/flex.css +8 -0
  215. package/dist/tokens/components/navigation.css +55 -0
  216. package/dist/tokens/components/shell.css +100 -5
  217. package/dist/tokens/components/table.css +14 -0
  218. package/dist/tokens/foundation.css +2 -0
  219. package/dist/tokens/semantic/layout.css +9 -0
  220. package/docs/CONSUMER-RULES.md +13 -0
  221. package/docs/DESIGN-AUTHORITY.md +43 -6
  222. package/docs/DEVELOPMENT.md +4 -3
  223. package/docs/FORMS.md +151 -6
  224. package/docs/FRAME-COVERAGE-REPORT.md +29 -16
  225. package/docs/README.md +1 -1
  226. package/docs/STANDARDS-vocabulary-tokens.md +1 -1
  227. package/docs/TESTING.md +15 -6
  228. package/docs/charts/cjk-category-axis.tsx +81 -0
  229. package/docs/data-display/card/index.tsx +22 -0
  230. package/docs/data-display/data-table/examples/antd-parity.tsx +257 -0
  231. package/docs/data-display/data-table/index.tsx +23 -0
  232. package/docs/data-display/descriptions.tsx +41 -0
  233. package/docs/data-display/list-row.tsx +6 -6
  234. package/docs/data-display/scroll-area.tsx +31 -25
  235. package/docs/data-display/table.tsx +104 -45
  236. package/docs/data-display/timeline.tsx +41 -0
  237. package/docs/data-entry/date-picker.tsx +85 -0
  238. package/docs/data-entry/date-range-picker.tsx +26 -0
  239. package/docs/data-entry/form-dynamic-fields.tsx +199 -0
  240. package/docs/data-entry/form.tsx +74 -4
  241. package/docs/data-entry/input-otp.tsx +24 -0
  242. package/docs/data-entry/input.tsx +46 -1
  243. package/docs/data-entry/month-range-picker.tsx +1 -1
  244. package/docs/data-entry/select.tsx +27 -0
  245. package/docs/data-entry/switch.tsx +41 -0
  246. package/docs/data-entry/textarea.tsx +38 -0
  247. package/docs/data-entry/time-picker.tsx +85 -1
  248. package/docs/data-entry/time-range-picker.tsx +55 -0
  249. package/docs/data-entry/transfer.tsx +17 -0
  250. package/docs/data-entry/upload.tsx +56 -12
  251. package/docs/feedback/tooltip.tsx +1 -1
  252. package/docs/general/activity.tsx +2 -2
  253. package/docs/general/button/index.tsx +14 -1
  254. package/docs/general/typography.tsx +14 -1
  255. package/docs/layout/app-launcher.tsx +195 -0
  256. package/docs/layout/app-shell-arrangements.tsx +5 -5
  257. package/docs/layout/app-shell.tsx +11 -0
  258. package/docs/layout/flex.tsx +50 -0
  259. package/docs/layout/responsive-grid.tsx +21 -1
  260. package/docs/layout/topbar.tsx +5 -9
  261. package/docs/navigation/app-setting-picker.tsx +11 -0
  262. package/docs/navigation/breadcrumb.tsx +48 -0
  263. package/docs/navigation/dropdown-menu.tsx +21 -0
  264. package/docs/navigation/pagination.tsx +52 -0
  265. package/docs/navigation/steps.tsx +37 -0
  266. package/docs/navigation/tabs.tsx +97 -1
  267. package/docs/query/button-refetch.tsx +1 -0
  268. package/docs/showcase/settings-security-mfa.tsx +3 -3
  269. package/package.json +24 -4
  270. package/scripts/_agent-setup.mjs +76 -9
  271. package/scripts/init-guinea-pig.mjs +26 -4
  272. package/scripts/ui-audit.mjs +243 -30
  273. package/scripts/visual-audit.mjs +45 -7
@@ -5,50 +5,67 @@ 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
7
  import { cn } from "../../lib/utils.js";
8
- import { controlTriggerClass } from "../../lib/control-styles.js";
8
+ import { controlSurfaceTriggerClass } from "../../lib/control-styles.js";
9
+ import {
10
+ applyMaxTagCount,
11
+ controlSurfaceAttrs,
12
+ resolveAllowClear,
13
+ resolveAriaInvalid
14
+ } from "./control-surface.js";
9
15
  import { Popover, PopoverContent, PopoverTrigger } from "../data-display/popover.js";
10
16
  import { Command, CommandGroup } from "./command.js";
11
17
  import { Input } from "./input.js";
12
18
  const DEBOUNCE_MS = 250;
13
- function SearchSelect({
14
- value: valueProp,
15
- defaultValue,
16
- onValueChange,
17
- options: staticOptions,
18
- loadOptions,
19
- renderOption,
20
- labelRender,
21
- selectedLabel,
22
- selectedIcon,
23
- placeholder,
24
- searchPlaceholder,
25
- emptyMessage,
26
- loadingMessage,
27
- errorMessage,
28
- clearLabel,
29
- clearable = true,
30
- disabled = false,
31
- readOnly = false,
32
- size,
33
- open: openProp,
34
- onOpenChange,
35
- search: searchProp,
36
- onSearchChange,
37
- filterOption,
38
- renderError,
39
- renderLoadMore,
40
- name,
41
- id,
42
- className,
43
- "data-testid": dataTestId,
44
- "data-field": dataField,
45
- "aria-label": ariaLabel,
46
- "aria-labelledby": ariaLabelledby,
47
- "aria-describedby": ariaDescribedby,
48
- "aria-errormessage": ariaErrorMessage,
49
- "aria-invalid": ariaInvalid,
50
- "aria-required": ariaRequired
51
- }) {
19
+ function SearchSelect(props) {
20
+ const {
21
+ options: staticOptions,
22
+ loadOptions,
23
+ renderOption,
24
+ labelRender,
25
+ selectedLabel,
26
+ selectedIcon,
27
+ placeholder,
28
+ searchPlaceholder,
29
+ emptyMessage,
30
+ loadingMessage,
31
+ errorMessage,
32
+ clearLabel,
33
+ clearable = true,
34
+ disabled = false,
35
+ readOnly = false,
36
+ size,
37
+ status,
38
+ variant,
39
+ loading: loadingProp = false,
40
+ open: openProp,
41
+ defaultOpen = false,
42
+ onOpenChange,
43
+ search: searchProp,
44
+ onSearchChange,
45
+ filterOption,
46
+ filterSort,
47
+ autoClearSearchValue = true,
48
+ optionRender,
49
+ menuItemSelectedIcon,
50
+ notFoundContent,
51
+ popupMatchSelectWidth = true,
52
+ allowClear,
53
+ onClear,
54
+ renderError,
55
+ renderLoadMore,
56
+ name,
57
+ id,
58
+ className,
59
+ "data-testid": dataTestId,
60
+ "data-field": dataField,
61
+ "aria-label": ariaLabel,
62
+ "aria-labelledby": ariaLabelledby,
63
+ "aria-describedby": ariaDescribedby,
64
+ "aria-errormessage": ariaErrorMessage,
65
+ "aria-invalid": ariaInvalid,
66
+ "aria-required": ariaRequired
67
+ } = props;
68
+ const multiple = props.mode === "multiple";
52
69
  const { t } = useTranslation();
53
70
  const nameFallback = useFieldNameFallback({
54
71
  "aria-label": ariaLabel,
@@ -62,7 +79,7 @@ function SearchSelect({
62
79
  const reactId = React.useId();
63
80
  const listId = `${reactId}-listbox`;
64
81
  const optionDomId = (optionValue) => `${reactId}-opt-${optionValue}`;
65
- const [internalOpen, setInternalOpen] = React.useState(false);
82
+ const [internalOpen, setInternalOpen] = React.useState(defaultOpen);
66
83
  const isOpenControlled = openProp !== void 0;
67
84
  const open = isOpenControlled ? openProp : internalOpen;
68
85
  const setOpen = React.useCallback(
@@ -89,22 +106,34 @@ function SearchSelect({
89
106
  const [loading, setLoading] = React.useState(false);
90
107
  const [error, setError] = React.useState(false);
91
108
  const [activeIndex, setActiveIndex] = React.useState(0);
92
- const [picked, setPicked] = React.useState(null);
93
- const [internalValue, setInternalValue] = React.useState(defaultValue ?? "");
94
- const isControlled = valueProp !== void 0;
95
- const value = isControlled ? valueProp : internalValue;
109
+ const [pickedOptions, setPickedOptions] = React.useState(
110
+ {}
111
+ );
112
+ const [internalValue, setInternalValue] = React.useState(
113
+ props.defaultValue ?? (props.mode === "multiple" ? [] : "")
114
+ );
115
+ const isControlled = props.value !== void 0;
116
+ const rawValue = isControlled ? props.value : internalValue;
117
+ const values = React.useMemo(() => {
118
+ if (!multiple) return [];
119
+ return Array.isArray(rawValue) ? rawValue : rawValue ? [rawValue] : [];
120
+ }, [multiple, rawValue]);
121
+ const value = multiple ? "" : Array.isArray(rawValue) ? rawValue[0] ?? "" : rawValue;
122
+ const isSelected = (optionValue) => multiple ? values.includes(optionValue) : value === optionValue;
123
+ const hasSelection = multiple ? values.length > 0 : Boolean(value);
96
124
  const reqId = React.useRef(0);
97
125
  const resolvedLoad = React.useMemo(
98
126
  () => loadOptions ?? (async ({ query: search }) => {
99
127
  const needle = search.trim();
100
128
  const list = staticOptions ?? [];
101
129
  const matches = (option) => filterOption ? filterOption(option, needle) : option.label.toLowerCase().includes(needle.toLowerCase()) || option.value.toLowerCase().includes(needle.toLowerCase());
130
+ const kept = needle ? list.filter(matches) : list;
102
131
  return {
103
- options: needle ? list.filter(matches) : list,
132
+ options: filterSort ? [...kept].sort((a, b) => filterSort(a, b, { searchValue: needle })) : kept,
104
133
  hasMore: false
105
134
  };
106
135
  }),
107
- [loadOptions, staticOptions, filterOption]
136
+ [loadOptions, staticOptions, filterOption, filterSort]
108
137
  );
109
138
  React.useEffect(() => {
110
139
  const handle = window.setTimeout(() => setDebouncedQuery(query.trim()), DEBOUNCE_MS);
@@ -163,20 +192,59 @@ function SearchSelect({
163
192
  [grouped]
164
193
  );
165
194
  const resolvedPlaceholder = placeholder ?? t("dataEntry.searchSelect.placeholder");
166
- const selectedOption = value ? [picked, ...staticOptions ?? [], ...loaded].find((option) => option?.value === value) ?? null : null;
195
+ const optionFor = (optionValue) => pickedOptions[optionValue] ?? (staticOptions ?? []).find((option) => option.value === optionValue) ?? loaded.find((option) => option.value === optionValue) ?? null;
196
+ const selectedOption = value ? optionFor(value) : null;
167
197
  const currentLabel = value ? selectedOption?.label ?? selectedLabel ?? value : resolvedPlaceholder;
168
198
  const currentIcon = value ? selectedOption?.icon ?? selectedIcon : null;
199
+ const selectedChips = multiple ? values.map((entry) => ({
200
+ value: entry,
201
+ label: optionFor(entry)?.label ?? entry
202
+ })) : [];
203
+ const {
204
+ visible: visibleChips,
205
+ omitted: omittedChips,
206
+ overflow: chipOverflow
207
+ } = applyMaxTagCount(
208
+ selectedChips,
209
+ props.mode === "multiple" ? props.maxTagCount : void 0,
210
+ props.mode === "multiple" ? props.maxTagPlaceholder : void 0
211
+ );
212
+ const maxCount = props.mode === "multiple" ? props.maxCount : void 0;
213
+ const atMaxCount = maxCount !== void 0 && values.length >= maxCount;
214
+ const remember = (option) => setPickedOptions((prev) => ({ ...prev, [option.value]: option }));
169
215
  const select = (option) => {
170
216
  if (option.disabled) return;
171
- setPicked(option);
217
+ if (props.mode === "multiple") {
218
+ const already = values.includes(option.value);
219
+ if (!already && atMaxCount) return;
220
+ const next = already ? values.filter((entry) => entry !== option.value) : [...values, option.value];
221
+ remember(option);
222
+ if (!isControlled) setInternalValue(next);
223
+ props.onValueChange?.(
224
+ next,
225
+ next.map((entry) => entry === option.value ? option : optionFor(entry)).filter((entry) => entry !== null)
226
+ );
227
+ if (already) props.onDeselect?.(option.value, option);
228
+ else props.onSelect?.(option.value, option);
229
+ if (autoClearSearchValue) setQuery("");
230
+ return;
231
+ }
232
+ remember(option);
172
233
  if (!isControlled) setInternalValue(option.value);
173
- onValueChange?.(option.value, option);
234
+ props.onValueChange?.(option.value, option);
235
+ props.onSelect?.(option.value, option);
236
+ if (autoClearSearchValue) setQuery("");
174
237
  setOpen(false);
175
238
  };
176
239
  const clear = () => {
177
- setPicked(null);
178
- if (!isControlled) setInternalValue("");
179
- onValueChange?.("", void 0);
240
+ if (props.mode === "multiple") {
241
+ if (!isControlled) setInternalValue([]);
242
+ props.onValueChange?.([], []);
243
+ } else {
244
+ if (!isControlled) setInternalValue("");
245
+ props.onValueChange?.("", void 0);
246
+ }
247
+ onClear?.();
180
248
  setOpen(false);
181
249
  };
182
250
  const onScroll = (event) => {
@@ -226,7 +294,13 @@ function SearchSelect({
226
294
  const optionTestId = (optionValue) => dataTestId ? `${dataTestId}-option-${optionValue}` : void 0;
227
295
  const activeOption = flatOrdered[activeIndex];
228
296
  const activeOptionId = activeOption ? optionDomId(activeOption.value) : void 0;
229
- const showClear = clearable && Boolean(value) && !disabled && !readOnly;
297
+ const clearControl = resolveAllowClear(
298
+ allowClear,
299
+ clearable,
300
+ clearLabel ?? t("dataEntry.searchSelect.clear")
301
+ );
302
+ const showClear = clearControl.enabled && hasSelection && !disabled && !readOnly && !loadingProp;
303
+ const surface = controlSurfaceAttrs({ variant, status, size });
230
304
  return /* @__PURE__ */ jsxs("div", { className: cn("relative", className), children: [
231
305
  /* @__PURE__ */ jsxs(
232
306
  Popover,
@@ -235,7 +309,7 @@ function SearchSelect({
235
309
  onOpenChange: (next) => {
236
310
  if (readOnly && next) return;
237
311
  setOpen(next);
238
- if (!next) setQuery("");
312
+ if (!next && autoClearSearchValue) setQuery("");
239
313
  },
240
314
  children: [
241
315
  /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
@@ -244,22 +318,24 @@ function SearchSelect({
244
318
  id,
245
319
  type: "button",
246
320
  role: "combobox",
247
- "data-size": size === "md" ? void 0 : size,
321
+ ...surface,
248
322
  "aria-expanded": open,
249
323
  "aria-controls": open ? listId : void 0,
250
324
  "aria-label": triggerAriaLabel,
251
325
  "aria-labelledby": triggerAriaLabelledby,
252
326
  "aria-describedby": ariaDescribedby,
253
327
  "aria-errormessage": ariaErrorMessage,
254
- "aria-invalid": ariaInvalid,
328
+ "aria-invalid": resolveAriaInvalid(ariaInvalid, status),
255
329
  "aria-required": ariaRequired,
256
330
  "aria-readonly": readOnly || void 0,
331
+ "aria-busy": loadingProp || void 0,
257
332
  disabled,
258
333
  "data-testid": dataTestId,
259
334
  "data-field": resolvedField,
260
- "data-value": value || void 0,
335
+ "data-value": (multiple ? values.join(",") : value) || void 0,
336
+ "data-mode": multiple ? "multiple" : void 0,
261
337
  className: cn(
262
- controlTriggerClass,
338
+ controlSurfaceTriggerClass,
263
339
  "w-full justify-start",
264
340
  // Reserve trailing room for the single clear-or-chevron overlay rendered below.
265
341
  "ui-control-trigger-affixed"
@@ -269,9 +345,36 @@ function SearchSelect({
269
345
  {
270
346
  className: cn(
271
347
  "ui-search-select-option-body text-start",
272
- !value && "text-muted-foreground"
348
+ !hasSelection && "text-muted-foreground"
273
349
  ),
274
- children: value && labelRender ? labelRender({ value, label: currentLabel, option: selectedOption ?? void 0 }) : /* @__PURE__ */ jsxs(Fragment, { children: [
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: [
275
378
  currentIcon ? /* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-center", "aria-hidden": "true", children: currentIcon }) : null,
276
379
  /* @__PURE__ */ jsx("span", { className: "truncate", children: currentLabel })
277
380
  ] })
@@ -279,7 +382,7 @@ function SearchSelect({
279
382
  )
280
383
  }
281
384
  ) }),
282
- resolvedName ? /* @__PURE__ */ jsx("input", { type: "hidden", name: resolvedName, value, readOnly: true }) : null,
385
+ 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,
283
386
  /* @__PURE__ */ jsx(
284
387
  PopoverContent,
285
388
  {
@@ -289,6 +392,10 @@ function SearchSelect({
289
392
  sideOffset: 4,
290
393
  collisionPadding: 12,
291
394
  className: "ui-search-select-panel",
395
+ "data-popup-match": popupMatchSelectWidth === true ? void 0 : popupMatchSelectWidth === false ? "content" : "fixed",
396
+ style: typeof popupMatchSelectWidth === "number" ? {
397
+ "--search-select-panel-inline-size": `${popupMatchSelectWidth}px`
398
+ } : void 0,
292
399
  children: /* @__PURE__ */ jsxs(Command, { value, shouldFilter: false, className: "ui-search-select-command", children: [
293
400
  /* @__PURE__ */ jsx("div", { className: "ui-search-select-search", children: /* @__PURE__ */ jsx(
294
401
  Input,
@@ -310,38 +417,50 @@ function SearchSelect({
310
417
  {
311
418
  id: listId,
312
419
  role: "listbox",
420
+ "aria-multiselectable": multiple || void 0,
313
421
  "aria-busy": loading,
314
422
  className: "ui-search-select-list",
315
423
  onScroll,
316
424
  children: [
317
425
  grouped.map((group) => {
318
- const rows = group.items.map(({ option, index }) => /* @__PURE__ */ jsx(
426
+ const rows = group.items.map(({ option, index }) => /* @__PURE__ */ jsxs(
319
427
  "div",
320
428
  {
321
429
  id: optionDomId(option.value),
322
430
  role: "option",
323
431
  "data-testid": optionTestId(option.value),
324
- "aria-selected": value === option.value,
325
- "aria-disabled": option.disabled || void 0,
326
- "data-disabled": option.disabled || void 0,
432
+ "aria-selected": isSelected(option.value),
433
+ "aria-disabled": option.disabled || atMaxCount && !isSelected(option.value) || void 0,
434
+ "data-disabled": option.disabled || atMaxCount && !isSelected(option.value) || void 0,
327
435
  className: cn(
328
436
  "ui-command-item",
329
437
  // Selected = persistent bg-accent + medium weight (NO check icon — saves width),
330
438
  // matching the plain SelectItem's `data-[state=checked]` convention; active =
331
439
  // hover/keyboard accent. Same bg so selection stays coherent across both Selects.
332
- value === option.value && "bg-accent text-foreground font-medium",
440
+ isSelected(option.value) && "bg-accent text-foreground font-medium",
333
441
  activeIndex === index && "bg-accent text-accent-foreground"
334
442
  ),
335
443
  onMouseEnter: () => !option.disabled && setActiveIndex(index),
336
444
  onMouseDown: (event) => event.preventDefault(),
337
445
  onClick: () => select(option),
338
- children: renderOption ? /* @__PURE__ */ jsx("div", { className: "ui-search-select-option-slot", children: renderOption(option) }) : /* @__PURE__ */ jsxs("div", { className: "ui-search-select-option-body", children: [
339
- option.icon ? /* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-center", "aria-hidden": "true", children: option.icon }) : null,
340
- /* @__PURE__ */ jsxs("div", { className: "ui-search-select-option-text", children: [
341
- /* @__PURE__ */ jsx("span", { className: "ui-search-select-option-label", children: option.label }),
342
- option.sublabel ? /* @__PURE__ */ jsx("span", { className: "ui-search-select-option-sublabel", children: option.sublabel }) : null
343
- ] })
344
- ] })
446
+ children: [
447
+ 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: [
448
+ option.icon ? /* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-center", "aria-hidden": "true", children: option.icon }) : null,
449
+ /* @__PURE__ */ jsxs("div", { className: "ui-search-select-option-text", children: [
450
+ /* @__PURE__ */ jsx("span", { className: "ui-search-select-option-label", children: option.label }),
451
+ option.sublabel ? /* @__PURE__ */ jsx("span", { className: "ui-search-select-option-sublabel", children: option.sublabel }) : null
452
+ ] })
453
+ ] }),
454
+ menuItemSelectedIcon && isSelected(option.value) ? /* @__PURE__ */ jsx(
455
+ "span",
456
+ {
457
+ "data-slot": "search-select-selected-icon",
458
+ className: "ui-search-select-selected-icon",
459
+ "aria-hidden": "true",
460
+ children: menuItemSelectedIcon
461
+ }
462
+ ) : null
463
+ ]
345
464
  },
346
465
  option.value
347
466
  ));
@@ -372,7 +491,7 @@ function SearchSelect({
372
491
  "aria-disabled": "true",
373
492
  "aria-selected": false,
374
493
  className: "ui-search-select-placeholder",
375
- children: emptyMessage ?? t("dataEntry.searchSelect.empty")
494
+ children: notFoundContent ?? emptyMessage ?? t("dataEntry.searchSelect.empty")
376
495
  }
377
496
  ) : null
378
497
  ]
@@ -398,15 +517,27 @@ function SearchSelect({
398
517
  ]
399
518
  }
400
519
  ),
401
- /* @__PURE__ */ jsx("div", { className: "ui-control-affix", children: showClear ? /* @__PURE__ */ jsx(
520
+ /* @__PURE__ */ jsx("div", { className: "ui-control-affix", children: loadingProp ? (
521
+ // antd `loading` — the FIELD is in flight (the form is still resolving this value), which
522
+ // is a different claim from the list-level spinner inside the panel. It takes the affix
523
+ // seat so the two can never both animate in the same place.
524
+ /* @__PURE__ */ jsx(
525
+ Loader2,
526
+ {
527
+ "data-slot": "search-select-loading",
528
+ className: "ui-control-affix-icon ui-control-affix-indicator animate-spin",
529
+ "aria-hidden": "true"
530
+ }
531
+ )
532
+ ) : showClear ? /* @__PURE__ */ jsx(
402
533
  "button",
403
534
  {
404
535
  type: "button",
405
- "aria-label": clearLabel ?? t("dataEntry.searchSelect.clear"),
536
+ "aria-label": clearControl.label,
406
537
  "data-testid": optionTestId("clear"),
407
538
  className: "ui-control-affix-action",
408
539
  onClick: clear,
409
- children: /* @__PURE__ */ jsx(X, { className: "ui-control-affix-icon", "aria-hidden": "true" })
540
+ children: clearControl.clearIcon ?? /* @__PURE__ */ jsx(X, { className: "ui-control-affix-icon", "aria-hidden": "true" })
410
541
  }
411
542
  ) : /* @__PURE__ */ jsx(
412
543
  ChevronsUpDown,
@@ -1,5 +1,5 @@
1
1
  import * as React from "react";
2
- import type { ControlWidthProp } from "../../props/vocabulary/shared.prop.js";
2
+ import type { ControlStatusProp, ControlVariantProp, ControlWidthProp, SizeProp } from "../../props/vocabulary/index.js";
3
3
  import * as SelectPrimitive from "@radix-ui/react-select";
4
4
  import type { FieldA11yProps } from "../../lib/field-a11y.js";
5
5
  import type { SelectDataProp } from "../../props/components/data-entry.prop.js";
@@ -20,7 +20,12 @@ export declare function Select(props: SelectProp): React.JSX.Element;
20
20
  export declare function SelectGroup(props: React.ComponentProps<typeof SelectPrimitive.Group>): React.JSX.Element;
21
21
  export declare function SelectValue(props: React.ComponentProps<typeof SelectPrimitive.Value>): React.JSX.Element;
22
22
  export declare const SelectTrigger: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectTriggerProps & React.RefAttributes<HTMLButtonElement>, "ref"> & {
23
- size?: "sm" | "md";
23
+ /** Control height tier — the shared `--control-height` ladder (antd `size`). */
24
+ size?: SizeProp;
25
+ /** Control surface (antd `variant`). Default `outlined`. */
26
+ variant?: ControlVariantProp;
27
+ /** Validation status (antd `status`). `error` also sets `aria-invalid`. */
28
+ status?: ControlStatusProp;
24
29
  /**
25
30
  * `full` (default) fills the field column — right inside a FormField. `auto` sizes to the
26
31
  * selected label — right in a PageContainer `extra` slot, a toolbar or a footer row, where a
@@ -45,7 +50,20 @@ export declare const SelectTrigger: React.ForwardRefExoticComponent<Omit<SelectP
45
50
  } & React.RefAttributes<HTMLButtonElement>>;
46
51
  export declare const SelectScrollUpButton: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectScrollUpButtonProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
47
52
  export declare const SelectScrollDownButton: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectScrollDownButtonProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
48
- export declare const SelectContent: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectContentProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
53
+ export declare const SelectContent: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectContentProps & React.RefAttributes<HTMLDivElement>, "ref"> & {
54
+ /**
55
+ * How the popup sizes against its trigger — the DOM half of antd `popupMatchSelectWidth`.
56
+ * Absent = the trigger's width is the floor (antd's `true`); `content` releases that floor;
57
+ * `fixed` pins both edges to `--select-content-inline-size`.
58
+ */
59
+ "data-popup-match"?: "content" | "fixed";
60
+ } & React.RefAttributes<HTMLDivElement>>;
49
61
  export declare const SelectLabel: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectLabelProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
50
- export declare const SelectItem: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectItemProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
62
+ export declare const SelectItem: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectItemProps & React.RefAttributes<HTMLDivElement>, "ref"> & {
63
+ /**
64
+ * antd `menuItemSelectedIcon` — a decorative mark on the picked row. Off by default: this
65
+ * library marks the picked row with fill + weight, which costs no width.
66
+ */
67
+ selectedIcon?: React.ReactNode;
68
+ } & React.RefAttributes<HTMLDivElement>>;
51
69
  export declare const SelectSeparator: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectSeparatorProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;