prime-ui-kit 0.11.1 → 0.13.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 (171) hide show
  1. package/README.md +4 -1
  2. package/dist/chunks/{chunk-WDTSHOTD.js → chunk-E6SQBSDF.js} +140 -30
  3. package/dist/chunks/{chunk-CUH6DJ57.js → chunk-Z3WKKAS6.js} +6572 -5024
  4. package/dist/chunks/{chunk-GKX3QAUC.js → chunk-ZSJMNA46.js} +14 -5
  5. package/dist/color-picker.js +2 -2
  6. package/dist/components/chat/Chat.d.ts +11 -12
  7. package/dist/components/chat/chatContext.d.ts +2 -0
  8. package/dist/components/chat-list/ChatList.d.ts +191 -0
  9. package/dist/components/chat-list/formatChatTime.d.ts +6 -0
  10. package/dist/components/command-menu/CommandMenu.d.ts +10 -5
  11. package/dist/components/composer/Composer.d.ts +3 -10
  12. package/dist/components/data-table/DataTable.d.ts +16 -2
  13. package/dist/components/data-table/Row.d.ts +23 -7
  14. package/dist/components/data-table/StateRows.d.ts +5 -1
  15. package/dist/components/data-table/reorder.d.ts +3 -1
  16. package/dist/components/data-table/rows.d.ts +20 -4
  17. package/dist/components/file-upload/FileUpload.d.ts +7 -0
  18. package/dist/components/gantt/Gantt.d.ts +19 -1
  19. package/dist/components/gantt/GanttBar.d.ts +4 -1
  20. package/dist/components/gantt/GanttRow.d.ts +8 -1
  21. package/dist/components/index.d.ts +11 -7
  22. package/dist/components/kanban/Kanban.d.ts +17 -2
  23. package/dist/components/list/List.d.ts +120 -0
  24. package/dist/components/message/Message.d.ts +13 -0
  25. package/dist/components/message/MessageParts.d.ts +2 -0
  26. package/dist/components/modal/DialogParts.d.ts +5 -1
  27. package/dist/components/select/Select.d.ts +11 -5
  28. package/dist/components/select/SelectItem.d.ts +19 -0
  29. package/dist/components/select/selectContext.d.ts +4 -0
  30. package/dist/components/sparkline/Sparkline.d.ts +14 -3
  31. package/dist/components/textarea/Textarea.d.ts +11 -3
  32. package/dist/hooks/useUploadQueue.d.ts +5 -1
  33. package/dist/icons/registry.d.ts +2 -0
  34. package/dist/index.css +1 -1
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.js +10 -2
  37. package/dist/internal/VisuallyHidden.d.ts +6 -1
  38. package/dist/internal/attachmentTile.d.ts +5 -1
  39. package/dist/internal/richText.d.ts +7 -2
  40. package/dist/internal/textFormatting.d.ts +52 -0
  41. package/dist/internal/textFormattingToolbar.d.ts +26 -0
  42. package/dist/skill/api-contract.md +1 -1
  43. package/dist/skill/cheatsheet.md +13 -1
  44. package/dist/skill/choosing.md +9 -4
  45. package/dist/skill/components.md +7 -5
  46. package/dist/skill/composition.md +9 -1
  47. package/dist/skill/layouts.md +4 -1
  48. package/dist/skill/patterns/app-shell.module.css +18 -0
  49. package/dist/skill/patterns/app-shell.tsx +301 -0
  50. package/dist/skill/patterns/chat.module.css +0 -25
  51. package/dist/skill/patterns/chat.tsx +72 -29
  52. package/dist/skill/patterns/kanban-board.module.css +28 -0
  53. package/dist/skill/patterns/kanban-board.tsx +612 -0
  54. package/dist/skill/patterns/media-library.module.css +66 -0
  55. package/dist/skill/patterns/media-library.tsx +638 -0
  56. package/dist/skill/patterns/schedule.module.css +22 -0
  57. package/dist/skill/patterns/schedule.tsx +722 -0
  58. package/dist/skill/reference/components/chat/COMPONENT.md +13 -4
  59. package/dist/skill/reference/components/chat/examples/states.tsx +100 -0
  60. package/dist/skill/reference/components/chat-list/COMPONENT.md +256 -0
  61. package/dist/skill/reference/components/chat-list/examples/controlled.tsx +103 -0
  62. package/dist/skill/reference/components/chat-list/examples/examples.module.css +66 -0
  63. package/dist/skill/reference/components/chat-list/examples/live.tsx +133 -0
  64. package/dist/skill/reference/components/chat-list/examples/long-list.tsx +87 -0
  65. package/dist/skill/reference/components/chat-list/examples/narrow.tsx +88 -0
  66. package/dist/skill/reference/components/chat-list/examples/overview.tsx +194 -0
  67. package/dist/skill/reference/components/chat-list/examples/search.tsx +71 -0
  68. package/dist/skill/reference/components/chat-list/examples/sizes.tsx +61 -0
  69. package/dist/skill/reference/components/chat-list/examples/states.tsx +107 -0
  70. package/dist/skill/reference/components/chat-list/examples/structure.tsx +135 -0
  71. package/dist/skill/reference/components/command-menu/COMPONENT.md +15 -11
  72. package/dist/skill/reference/components/command-menu/examples/server-search.tsx +11 -9
  73. package/dist/skill/reference/components/composer/COMPONENT.md +8 -7
  74. package/dist/skill/reference/components/composer/examples/formatting.tsx +1 -1
  75. package/dist/skill/reference/components/confirm-dialog/COMPONENT.md +1 -0
  76. package/dist/skill/reference/components/data-table/COMPONENT.md +21 -6
  77. package/dist/skill/reference/components/data-table/examples/groups.tsx +2 -1
  78. package/dist/skill/reference/components/data-table/examples/trend.tsx +85 -0
  79. package/dist/skill/reference/components/drawer/COMPONENT.md +7 -6
  80. package/dist/skill/reference/components/drawer/examples/client-card.tsx +50 -0
  81. package/dist/skill/reference/components/fieldset/COMPONENT.md +1 -0
  82. package/dist/skill/reference/components/file-upload/COMPONENT.md +12 -8
  83. package/dist/skill/reference/components/file-upload/examples/overview.tsx +14 -3
  84. package/dist/skill/reference/components/gantt/COMPONENT.md +15 -5
  85. package/dist/skill/reference/components/gantt/examples/row-link.tsx +96 -0
  86. package/dist/skill/reference/components/kanban/COMPONENT.md +12 -4
  87. package/dist/skill/reference/components/kanban/examples/item-link.tsx +92 -0
  88. package/dist/skill/reference/components/list/COMPONENT.md +195 -0
  89. package/dist/skill/reference/components/list/examples/as-link.tsx +37 -0
  90. package/dist/skill/reference/components/list/examples/examples.module.css +18 -0
  91. package/dist/skill/reference/components/list/examples/overflow.tsx +46 -0
  92. package/dist/skill/reference/components/list/examples/overview.tsx +63 -0
  93. package/dist/skill/reference/components/list/examples/reorder.tsx +48 -0
  94. package/dist/skill/reference/components/list/examples/sizes.tsx +35 -0
  95. package/dist/skill/reference/components/list/examples/states.tsx +27 -0
  96. package/dist/skill/reference/components/list/examples/with-icon.tsx +60 -0
  97. package/dist/skill/reference/components/message/COMPONENT.md +18 -2
  98. package/dist/skill/reference/components/message/examples/bot-actions.tsx +51 -0
  99. package/dist/skill/reference/components/modal/COMPONENT.md +6 -4
  100. package/dist/skill/reference/components/page-content/COMPONENT.md +2 -2
  101. package/dist/skill/reference/components/page-content/examples/examples.module.css +5 -12
  102. package/dist/skill/reference/components/page-content/examples/list-detail.tsx +13 -18
  103. package/dist/skill/reference/components/select/COMPONENT.md +38 -7
  104. package/dist/skill/reference/components/select/examples/create-action.tsx +107 -0
  105. package/dist/skill/reference/components/select/examples/saving.tsx +47 -0
  106. package/dist/skill/reference/components/sheet/COMPONENT.md +4 -4
  107. package/dist/skill/reference/components/sparkline/COMPONENT.md +16 -2
  108. package/dist/skill/reference/components/sparkline/examples/compact.tsx +41 -0
  109. package/dist/skill/reference/components/sparkline/examples/examples.module.css +12 -0
  110. package/dist/skill/reference/components/textarea/COMPONENT.md +22 -2
  111. package/dist/skill/reference/components/textarea/examples/formatting.tsx +20 -0
  112. package/dist/skill/reference/components/visually-hidden/COMPONENT.md +6 -3
  113. package/dist/skill/reference/components/visually-hidden/examples/overview.tsx +2 -2
  114. package/package.json +2 -2
  115. package/src/components/chat/COMPONENT.md +13 -4
  116. package/src/components/chat/examples/states.tsx +100 -0
  117. package/src/components/chat-list/COMPONENT.md +256 -0
  118. package/src/components/chat-list/examples/controlled.tsx +103 -0
  119. package/src/components/chat-list/examples/examples.module.css +66 -0
  120. package/src/components/chat-list/examples/live.tsx +133 -0
  121. package/src/components/chat-list/examples/long-list.tsx +87 -0
  122. package/src/components/chat-list/examples/narrow.tsx +88 -0
  123. package/src/components/chat-list/examples/overview.tsx +194 -0
  124. package/src/components/chat-list/examples/search.tsx +71 -0
  125. package/src/components/chat-list/examples/sizes.tsx +61 -0
  126. package/src/components/chat-list/examples/states.tsx +107 -0
  127. package/src/components/chat-list/examples/structure.tsx +135 -0
  128. package/src/components/command-menu/COMPONENT.md +15 -11
  129. package/src/components/command-menu/examples/server-search.tsx +11 -9
  130. package/src/components/composer/COMPONENT.md +8 -7
  131. package/src/components/composer/examples/formatting.tsx +1 -1
  132. package/src/components/confirm-dialog/COMPONENT.md +1 -0
  133. package/src/components/data-table/COMPONENT.md +21 -6
  134. package/src/components/data-table/examples/groups.tsx +2 -1
  135. package/src/components/data-table/examples/trend.tsx +85 -0
  136. package/src/components/drawer/COMPONENT.md +7 -6
  137. package/src/components/drawer/examples/client-card.tsx +50 -0
  138. package/src/components/fieldset/COMPONENT.md +1 -0
  139. package/src/components/file-upload/COMPONENT.md +12 -8
  140. package/src/components/file-upload/examples/overview.tsx +14 -3
  141. package/src/components/gantt/COMPONENT.md +15 -5
  142. package/src/components/gantt/examples/row-link.tsx +96 -0
  143. package/src/components/kanban/COMPONENT.md +12 -4
  144. package/src/components/kanban/examples/item-link.tsx +92 -0
  145. package/src/components/list/COMPONENT.md +195 -0
  146. package/src/components/list/examples/as-link.tsx +37 -0
  147. package/src/components/list/examples/examples.module.css +18 -0
  148. package/src/components/list/examples/overflow.tsx +46 -0
  149. package/src/components/list/examples/overview.tsx +63 -0
  150. package/src/components/list/examples/reorder.tsx +48 -0
  151. package/src/components/list/examples/sizes.tsx +35 -0
  152. package/src/components/list/examples/states.tsx +27 -0
  153. package/src/components/list/examples/with-icon.tsx +60 -0
  154. package/src/components/message/COMPONENT.md +18 -2
  155. package/src/components/message/examples/bot-actions.tsx +51 -0
  156. package/src/components/modal/COMPONENT.md +6 -4
  157. package/src/components/page-content/COMPONENT.md +2 -2
  158. package/src/components/page-content/examples/examples.module.css +5 -12
  159. package/src/components/page-content/examples/list-detail.tsx +13 -18
  160. package/src/components/select/COMPONENT.md +38 -7
  161. package/src/components/select/examples/create-action.tsx +107 -0
  162. package/src/components/select/examples/saving.tsx +47 -0
  163. package/src/components/sheet/COMPONENT.md +4 -4
  164. package/src/components/sparkline/COMPONENT.md +16 -2
  165. package/src/components/sparkline/examples/compact.tsx +41 -0
  166. package/src/components/sparkline/examples/examples.module.css +12 -0
  167. package/src/components/textarea/COMPONENT.md +22 -2
  168. package/src/components/textarea/examples/formatting.tsx +20 -0
  169. package/src/components/visually-hidden/COMPONENT.md +6 -3
  170. package/src/components/visually-hidden/examples/overview.tsx +2 -2
  171. package/dist/components/composer/format.d.ts +0 -39
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  [![TypeScript](https://img.shields.io/badge/TypeScript-5-3178c6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
7
7
 
8
8
  **A calm, precise React 19 UI kit for product interfaces** — admin panels, dashboards, settings, forms
9
- and data tables. 77 components on one design contract, one API vocabulary and one set of tokens, so
9
+ and data tables. 79 components on one design contract, one API vocabulary and one set of tokens, so
10
10
  every screen you build looks like it was drawn by the same hand.
11
11
 
12
12
  - **Graphite design language.** Depth from fill, not lines — a surface ladder where every nested
@@ -249,6 +249,7 @@ accessibility, examples and common mistakes.
249
249
  | [**VoicePlayer**](https://github.com/esurkov1/prime-ui/blob/main/src/components/voice-player/COMPONENT.md) | A voice message player: a round play disk, a waveform that is the seek slider, and the time with the playback speed; one sound at a time, `getWaveform` for audio without levels. |
250
250
  | [**VideoNote**](https://github.com/esurkov1/prime-ui/blob/main/src/components/video-note/COMPONENT.md) | A round video message: a still first frame with the play disk, played with sound on a press; the same player API as VoicePlayer — transcript, transfer either way, one sound at a time. |
251
251
  | [**Composer**](https://github.com/esurkov1/prime-ui/blob/main/src/components/composer/COMPONENT.md) | The message input of a chat: a growing field, Enter to send, attachments, reply / edit bar, Markdown formatting, voice and video messages with pause, the typing signal. |
252
+ | [**ChatList**](https://github.com/esurkov1/prime-ui/blob/main/src/components/chat-list/COMPONENT.md) | The conversation list of a messenger: avatars with presence, names, times, the last message with its author or media word, unread / muted / mention counts, typing and drafts, a row menu, search, rows that glide to a new order, long lists rendered around the view. |
252
253
 
253
254
  ### Status and loading (`status`)
254
255
 
@@ -271,6 +272,7 @@ accessibility, examples and common mistakes.
271
272
  | [**Tabs**](https://github.com/esurkov1/prime-ui/blob/main/src/components/tabs/COMPONENT.md) | Tabs for navigating between content panels of one screen. |
272
273
  | [**Breadcrumb**](https://github.com/esurkov1/prime-ui/blob/main/src/components/breadcrumb/COMPONENT.md) | Breadcrumbs: the path to the current page. |
273
274
  | [**Pagination**](https://github.com/esurkov1/prime-ui/blob/main/src/components/pagination/COMPONENT.md) | Page-by-page navigation: arrows, page numbers with ellipsis and a compact «3 / 12» view. |
275
+ | [**List**](https://github.com/esurkov1/prime-ui/blob/main/src/components/list/COMPONENT.md) | A selectable list of rows — the list half of a list + detail screen: conversations, tickets, mail, records. |
274
276
  | [**Stepper**](https://github.com/esurkov1/prime-ui/blob/main/src/components/stepper/COMPONENT.md) | Steps of a multi-step process with pending, active, completed and danger statuses. |
275
277
  | [**Sidebar**](https://github.com/esurkov1/prime-ui/blob/main/src/layout/sidebar/COMPONENT.md) | App side navigation in three modes — expanded, compact, hidden — and an off-canvas panel on narrow screens. |
276
278
  | [**BottomNav**](https://github.com/esurkov1/prime-ui/blob/main/src/layout/bottom-nav/COMPONENT.md) | Phone navigation bar: 2–6 sections, flat or a floating glass capsule, with an optional round action. |
@@ -336,6 +338,7 @@ accessibility, examples and common mistakes.
336
338
  | `getPasswordStrength(value)` | The kit's 0–4 password estimate behind `Input.Root strength`; replace it with `getStrength`. |
337
339
  | `useUploadQueue({ upload, concurrency })` | The upload queue behind FileUpload `queue`: your `upload(file, { signal, onProgress })` runs a few at a time, with progress, abort on remove and retry. |
338
340
  | `groupMessages`, `formatMessageDate`, `formatMessageTime`, `hueOf` | Chat data → dated runs of one author (`Chat.Messages items` runs it for you), the date and time labels, and a palette hue stable per person. |
341
+ | `formatChatTime(date, now)` | The time of a dialog's last message as `ChatList` shows it: «14:32» today, «пн» this week, «12 мар», «12.03.2025». |
339
342
  | `getWaveform` | The 0…1 levels of a recorded voice message (a `Blob` or a URL) for `VoicePlayer waveform`, decoded with the Web Audio API. |
340
343
  | `moveBefore(items, id, beforeId, getId)` | Applies a `Dnd` or DataTable `onReorder` to your array. |
341
344
  | `matchesSmartFilter`, `resolveSmartFilterValues` | Apply a SmartFilter value to your rows. |
@@ -134,8 +134,13 @@ var VisuallyHidden_default = {
134
134
  // src/internal/VisuallyHidden.tsx
135
135
  import { jsx } from "react/jsx-runtime";
136
136
  var visuallyHiddenClass = VisuallyHidden_default.root;
137
- function VisuallyHidden({ className, ...rest }) {
138
- return /* @__PURE__ */ jsx("span", { className: cx(VisuallyHidden_default.root, className), ...rest });
137
+ function VisuallyHidden({ className, live, ...rest }) {
138
+ const region = live ? {
139
+ role: live === "assertive" ? "alert" : "status",
140
+ "aria-live": live,
141
+ "aria-atomic": true
142
+ } : null;
143
+ return /* @__PURE__ */ jsx("span", { className: cx(VisuallyHidden_default.root, className), ...region, ...rest });
139
144
  }
140
145
  VisuallyHidden.displayName = "VisuallyHidden";
141
146
 
@@ -1090,6 +1095,8 @@ var iconRegistry = {
1090
1095
  "format.link": Link2,
1091
1096
  "format.list": List,
1092
1097
  "format.quote": TextQuote,
1098
+ /** Hidden text (a spoiler) in text formatting; `field.password.hide` masks a password. */
1099
+ "format.spoiler": EyeOff,
1093
1100
  "format.strikethrough": Strikethrough,
1094
1101
  "format.underline": Underline,
1095
1102
  "object.activity": Activity,
@@ -5934,6 +5941,7 @@ var Select_default = {
5934
5941
  search: "Select_search",
5935
5942
  group: "Select_group",
5936
5943
  separator: "Select_separator",
5944
+ actions: "Select_actions",
5937
5945
  item: "Select_item",
5938
5946
  itemText: "Select_itemText",
5939
5947
  itemTitle: "Select_itemTitle",
@@ -6169,6 +6177,59 @@ function SelectItem({
6169
6177
  );
6170
6178
  }
6171
6179
  SelectItem.displayName = "Select.Item";
6180
+ var ACTION_PREFIX = "__prime_select_action";
6181
+ var isActionValue = (value) => value.startsWith(ACTION_PREFIX);
6182
+ function SelectAction({
6183
+ onSelect,
6184
+ disabled = false,
6185
+ className,
6186
+ children,
6187
+ onClick: onClick2,
6188
+ onMouseMove,
6189
+ ref,
6190
+ ...rest
6191
+ }) {
6192
+ const { highlight, listboxId, setOpen, size } = useSelectContext();
6193
+ const value = `${ACTION_PREFIX}${React50.useId()}`;
6194
+ const isHighlighted = useStoreSlice(highlight, (current) => current === value);
6195
+ const parts = splitItemChildren(children);
6196
+ return (
6197
+ // biome-ignore lint/a11y/useFocusableInteractive: focus stays on the listbox / search (aria-activedescendant)
6198
+ // biome-ignore lint/a11y/useKeyWithClickEvents: the keyboard is handled on the listbox
6199
+ /* @__PURE__ */ jsxs25(
6200
+ "div",
6201
+ {
6202
+ ...rest,
6203
+ ref,
6204
+ id: optionDomId(listboxId, value),
6205
+ role: "option",
6206
+ "aria-disabled": disabled || void 0,
6207
+ className: cx(menu_default.item, className),
6208
+ onClick: (event) => {
6209
+ onClick2?.(event);
6210
+ if (event.defaultPrevented || disabled) return;
6211
+ onSelect();
6212
+ setOpen(false);
6213
+ },
6214
+ onMouseMove: (event) => {
6215
+ onMouseMove?.(event);
6216
+ if (!disabled && !isHighlighted) highlight.set(value);
6217
+ },
6218
+ ...toDataAttributes({
6219
+ value,
6220
+ action: true,
6221
+ highlighted: isHighlighted,
6222
+ disabled
6223
+ }),
6224
+ children: [
6225
+ sizeMedia(parts.leading, size),
6226
+ /* @__PURE__ */ jsx38("span", { className: Select_default.itemText, children: parts.body })
6227
+ ]
6228
+ }
6229
+ )
6230
+ );
6231
+ }
6232
+ SelectAction.displayName = "Select.Action";
6172
6233
 
6173
6234
  // src/components/select/Select.tsx
6174
6235
  import { jsx as jsx39, jsxs as jsxs26 } from "react/jsx-runtime";
@@ -6179,7 +6240,8 @@ var SELECT_LABELS = {
6179
6240
  loading: "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430\u2026",
6180
6241
  retry: "\u041F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u044C",
6181
6242
  clear: "\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C",
6182
- optional: "\u043D\u0435\u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E"
6243
+ optional: "\u043D\u0435\u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E",
6244
+ changeFailed: "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C \u0432\u044B\u0431\u043E\u0440"
6183
6245
  };
6184
6246
  function SelectRoot(props) {
6185
6247
  const {
@@ -6222,11 +6284,30 @@ function SelectRoot(props) {
6222
6284
  const disabled = useInheritedDisabled(disabledProp);
6223
6285
  const labels = React51.useMemo(() => ({ ...SELECT_LABELS, ...labelsProp }), [labelsProp]);
6224
6286
  const ids = useFieldFrame(id, { hint, error, invalid });
6225
- const [value, setValue] = useControllableState({
6287
+ const [stored, setStored] = useControllableState({
6226
6288
  value: props.value,
6227
- defaultValue: props.defaultValue ?? (multiple ? [] : void 0),
6228
- onChange: props.onValueChange
6289
+ defaultValue: props.defaultValue ?? (multiple ? [] : void 0)
6229
6290
  });
6291
+ const changes = usePendingChanges(stored, takeNext);
6292
+ const value = changes.shown;
6293
+ const saving = changes.pending.length > 0;
6294
+ const [announcement, setAnnouncement] = React51.useState("");
6295
+ const onValueChange = props.onValueChange;
6296
+ const commit = (next) => {
6297
+ if (saving || Object.is(next, value)) return;
6298
+ const outcome = onValueChange?.(next);
6299
+ if (!isPromiseLike(outcome)) {
6300
+ setStored(next);
6301
+ return;
6302
+ }
6303
+ setAnnouncement("");
6304
+ changes.track(next, outcome, {
6305
+ onResolve: () => setStored(next),
6306
+ onReject: () => setAnnouncement(labels.changeFailed)
6307
+ });
6308
+ };
6309
+ const commitRef = React51.useRef(commit);
6310
+ commitRef.current = commit;
6230
6311
  const selected = React51.useMemo(
6231
6312
  () => Array.isArray(value) ? value : value ? [value] : [],
6232
6313
  [value]
@@ -6252,18 +6333,18 @@ function SelectRoot(props) {
6252
6333
  const pick = React51.useCallback(
6253
6334
  (optionValue) => {
6254
6335
  if (multiple) {
6255
- setValue((prev) => {
6256
- const list = Array.isArray(prev) ? prev : [];
6257
- return list.includes(optionValue) ? list.filter((item) => item !== optionValue) : [...list, optionValue];
6258
- });
6336
+ const list = Array.isArray(value) ? value : [];
6337
+ commitRef.current(
6338
+ list.includes(optionValue) ? list.filter((item) => item !== optionValue) : [...list, optionValue]
6339
+ );
6259
6340
  return;
6260
6341
  }
6261
- setValue(optionValue);
6342
+ commitRef.current(optionValue);
6262
6343
  setOpen(false);
6263
6344
  },
6264
- [multiple, setValue, setOpen]
6345
+ [multiple, value, setOpen]
6265
6346
  );
6266
- const clear = React51.useCallback(() => setValue(multiple ? [] : ""), [multiple, setValue]);
6347
+ const clear = React51.useCallback(() => commitRef.current(multiple ? [] : ""), [multiple]);
6267
6348
  const context = React51.useMemo(
6268
6349
  () => ({
6269
6350
  size,
@@ -6279,6 +6360,7 @@ function SelectRoot(props) {
6279
6360
  clear,
6280
6361
  clearable,
6281
6362
  loading,
6363
+ saving,
6282
6364
  loadingMore,
6283
6365
  hasMore,
6284
6366
  onLoadMore,
@@ -6312,6 +6394,7 @@ function SelectRoot(props) {
6312
6394
  clear,
6313
6395
  clearable,
6314
6396
  loading,
6397
+ saving,
6315
6398
  loadingMore,
6316
6399
  hasMore,
6317
6400
  onLoadMore,
@@ -6342,12 +6425,14 @@ function SelectRoot(props) {
6342
6425
  className,
6343
6426
  children: [
6344
6427
  /* @__PURE__ */ jsx39(SelectProvider, { value: context, children: /* @__PURE__ */ jsx39(ControlSizeProvider, { value: size, children }) }),
6345
- name ? (selected.length > 0 || multiple ? selected : [""]).map((item) => /* @__PURE__ */ jsx39("input", { type: "hidden", name, value: item, disabled }, item)) : null
6428
+ name ? (selected.length > 0 || multiple ? selected : [""]).map((item) => /* @__PURE__ */ jsx39("input", { type: "hidden", name, value: item, disabled }, item)) : null,
6429
+ /* @__PURE__ */ jsx39(VisuallyHidden, { role: "status", "aria-live": "polite", children: announcement })
6346
6430
  ]
6347
6431
  }
6348
6432
  );
6349
6433
  }
6350
6434
  SelectRoot.displayName = "Select.Root";
6435
+ var takeNext = (_current, next) => next;
6351
6436
  function SelectTrigger({
6352
6437
  className,
6353
6438
  children,
@@ -6370,13 +6455,15 @@ function SelectTrigger({
6370
6455
  describedBy,
6371
6456
  triggerRef,
6372
6457
  loading,
6458
+ saving,
6373
6459
  clearable,
6374
6460
  selected,
6375
6461
  clear,
6376
6462
  labels
6377
6463
  } = useSelectContext();
6378
6464
  const mergedRef = useMergedRefs(triggerRef, ref);
6379
- const showClear = clearable && selected.length > 0 && !disabled && !loading;
6465
+ const busy = loading || saving;
6466
+ const showClear = clearable && selected.length > 0 && !disabled && !busy;
6380
6467
  return /* @__PURE__ */ jsxs26(
6381
6468
  "button",
6382
6469
  {
@@ -6391,7 +6478,7 @@ function SelectTrigger({
6391
6478
  "aria-invalid": invalid || void 0,
6392
6479
  "aria-required": required || void 0,
6393
6480
  "aria-describedby": [ariaDescribedBy, describedBy].filter(Boolean).join(" ") || void 0,
6394
- "aria-busy": loading || void 0,
6481
+ "aria-busy": busy || void 0,
6395
6482
  disabled,
6396
6483
  className: cx(fieldTriggerClass, Select_default.trigger, className),
6397
6484
  onClick: (event) => {
@@ -6413,7 +6500,7 @@ function SelectTrigger({
6413
6500
  state: isOpen ? "open" : "closed",
6414
6501
  size,
6415
6502
  invalid: invalid || void 0,
6416
- loading: loading || void 0,
6503
+ loading: busy || void 0,
6417
6504
  disabled: disabled || void 0,
6418
6505
  "focus-ring": focusRing ? void 0 : false
6419
6506
  }),
@@ -6438,7 +6525,7 @@ function SelectTrigger({
6438
6525
  }
6439
6526
  )
6440
6527
  ) : null,
6441
- /* @__PURE__ */ jsx39("span", { className: Select_default.chevron, "aria-hidden": "true", children: loading ? /* @__PURE__ */ jsx39(Spinner, { "aria-hidden": "true" }) : /* @__PURE__ */ jsx39(Icon, { name: "nav.chevronDown" }) })
6528
+ /* @__PURE__ */ jsx39("span", { className: Select_default.chevron, "aria-hidden": "true", children: busy ? /* @__PURE__ */ jsx39(Spinner, { "aria-hidden": "true" }) : /* @__PURE__ */ jsx39(Icon, { name: "nav.chevronDown" }) })
6442
6529
  ]
6443
6530
  }
6444
6531
  );
@@ -6511,6 +6598,7 @@ function SelectContent({ searchable = false, className, children, ...rest }) {
6511
6598
  query,
6512
6599
  setQuery,
6513
6600
  loading,
6601
+ saving,
6514
6602
  loadingMore,
6515
6603
  hasMore,
6516
6604
  onLoadMore,
@@ -6521,6 +6609,12 @@ function SelectContent({ searchable = false, className, children, ...rest }) {
6521
6609
  registry
6522
6610
  } = useSelectContext();
6523
6611
  const listboxRef = React51.useRef(null);
6612
+ const items = [];
6613
+ const actions = [];
6614
+ for (const child of React51.Children.toArray(children)) {
6615
+ (React51.isValidElement(child) && child.type === SelectAction ? actions : items).push(child);
6616
+ }
6617
+ const actionsId = `${listboxId}-actions`;
6524
6618
  const floating = useFloatingLayer({
6525
6619
  open: isOpen,
6526
6620
  onOpenChange: setOpen,
@@ -6562,20 +6656,27 @@ function SelectContent({ searchable = false, className, children, ...rest }) {
6562
6656
  }
6563
6657
  const inSearch = event.target instanceof HTMLInputElement;
6564
6658
  if (inSearch && [" ", "Home", "End"].includes(event.key)) return;
6565
- const items = enabledOptions(listboxRef.current);
6659
+ const stops = enabledOptions(panelRef.current);
6566
6660
  const printable = event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey;
6567
6661
  if (printable && !inSearch && (event.key !== " " || typeahead.isTyping())) {
6568
6662
  event.preventDefault();
6569
- const match = typeahead.find(event.key, items, highlight.get());
6663
+ const match = typeahead.find(event.key, stops, highlight.get());
6570
6664
  if (match) {
6571
6665
  highlight.set(match.dataset.value);
6572
6666
  match.scrollIntoView?.({ block: "nearest" });
6573
6667
  }
6574
6668
  return;
6575
6669
  }
6576
- handleListboxKeyDown(event, { items, highlight, onSelect: pick });
6670
+ handleListboxKeyDown(event, {
6671
+ items: stops,
6672
+ highlight,
6673
+ onSelect: (value) => {
6674
+ if (isActionValue(value)) document.getElementById(optionDomId(listboxId, value))?.click();
6675
+ else pick(value);
6676
+ }
6677
+ });
6577
6678
  };
6578
- if (!floating.mounted) return /* @__PURE__ */ jsx39("div", { hidden: true, children });
6679
+ if (!floating.mounted) return /* @__PURE__ */ jsx39("div", { hidden: true, children: items });
6579
6680
  const activeDescendant = isOpen && highlighted !== void 0 ? optionDomId(listboxId, highlighted) : void 0;
6580
6681
  return /* @__PURE__ */ jsxs26(
6581
6682
  FloatingPanel,
@@ -6619,13 +6720,14 @@ function SelectContent({ searchable = false, className, children, ...rest }) {
6619
6720
  "aria-multiselectable": multiple || void 0,
6620
6721
  "aria-labelledby": triggerId,
6621
6722
  "aria-activedescendant": searchable ? void 0 : activeDescendant,
6622
- "aria-busy": loading || loadingMore || void 0,
6723
+ "aria-busy": loading || loadingMore || saving || void 0,
6724
+ "aria-owns": actions.length > 0 && region !== "error" ? actionsId : void 0,
6623
6725
  tabIndex: -1,
6624
6726
  hidden: region === "error" ? true : void 0,
6625
6727
  "data-autofocus": searchable || region === "error" ? void 0 : "",
6626
6728
  className: cx(Select_default.listbox, swapClass),
6627
6729
  children: [
6628
- children,
6730
+ items,
6629
6731
  region === "loading" ? /* @__PURE__ */ jsx39(SkeletonRows, { count: LOADING_ROWS2 }) : null,
6630
6732
  region === "ready" && hasMore ? /* @__PURE__ */ jsx39("div", { ref: sentinelRef, "aria-hidden": "true", className: menu_default.loadSentinel }) : null,
6631
6733
  region === "ready" && loadingMore ? /* @__PURE__ */ jsx39(SkeletonRows, { count: LOADING_MORE_ROWS }) : null
@@ -6637,6 +6739,12 @@ function SelectContent({ searchable = false, className, children, ...rest }) {
6637
6739
  /* @__PURE__ */ jsx39(EmptyPage.Title, { as: "p", children: labels.empty }),
6638
6740
  query !== "" && labels.emptyHint ? /* @__PURE__ */ jsx39(EmptyPage.Description, { children: labels.emptyHint }) : null
6639
6741
  ] }) : null,
6742
+ actions.length > 0 && region !== "error" ? (
6743
+ // Owned by the listbox (`aria-owns`): its options for assistive tech, pinned under the
6744
+ // scroller and the empty state for the eye.
6745
+ // biome-ignore lint/a11y/useSemanticElements: a group of options inside a listbox, not a form fieldset
6746
+ /* @__PURE__ */ jsx39("div", { id: actionsId, role: "group", className: Select_default.actions, children: actions })
6747
+ ) : null,
6640
6748
  region === "error" ? /* @__PURE__ */ jsxs26(EmptyPage.Root, { layout: "compact", size, role: "alert", className: swapClass, children: [
6641
6749
  /* @__PURE__ */ jsx39(EmptyPage.Title, { as: "p", children: loadError }),
6642
6750
  onRetry ? /* @__PURE__ */ jsx39(EmptyPage.Actions, { children: /* @__PURE__ */ jsxs26(Button.Root, { variant: "soft", tone: "neutral", onClick: onRetry, children: [
@@ -6645,7 +6753,7 @@ function SelectContent({ searchable = false, className, children, ...rest }) {
6645
6753
  ] }) }) : null
6646
6754
  ] }) : null,
6647
6755
  /* @__PURE__ */ jsx39(VisuallyHidden, { "aria-live": "polite", children: loading || loadingMore ? labels.loading : "" }),
6648
- /* @__PURE__ */ jsx39(HighlightOnOpen, { listboxRef, highlight })
6756
+ /* @__PURE__ */ jsx39(HighlightOnOpen, { listboxRef, panelRef, highlight })
6649
6757
  ]
6650
6758
  }
6651
6759
  );
@@ -6653,6 +6761,7 @@ function SelectContent({ searchable = false, className, children, ...rest }) {
6653
6761
  SelectContent.displayName = "Select.Content";
6654
6762
  function HighlightOnOpen({
6655
6763
  listboxRef,
6764
+ panelRef,
6656
6765
  highlight
6657
6766
  }) {
6658
6767
  const { isOpen, query, selected, registry } = useSelectContext();
@@ -6668,15 +6777,15 @@ function HighlightOnOpen({
6668
6777
  return () => highlight.set(void 0);
6669
6778
  }, [isOpen, listboxRef, highlight]);
6670
6779
  React51.useLayoutEffect(() => {
6671
- if (query !== "") highlight.set(enabledOptions(listboxRef.current)[0]?.dataset.value);
6672
- }, [query, listboxRef, highlight]);
6780
+ if (query !== "") highlight.set(enabledOptions(panelRef.current)[0]?.dataset.value);
6781
+ }, [query, panelRef, highlight]);
6673
6782
  React51.useLayoutEffect(() => {
6674
6783
  if (query === "") return;
6675
6784
  const options = enabledOptions(listboxRef.current);
6676
6785
  if (!options.some((item) => item.dataset.value === highlight.get())) {
6677
- highlight.set(options[0]?.dataset.value);
6786
+ highlight.set((options[0] ?? enabledOptions(panelRef.current)[0])?.dataset.value);
6678
6787
  }
6679
- }, [version, query, listboxRef, highlight]);
6788
+ }, [version, query, listboxRef, panelRef, highlight]);
6680
6789
  return null;
6681
6790
  }
6682
6791
  function SelectGroup({ className, ...rest }) {
@@ -6699,7 +6808,8 @@ var Select = {
6699
6808
  ItemDescription: SelectItemDescription,
6700
6809
  ItemMeta: SelectItemMeta,
6701
6810
  Group: SelectGroup,
6702
- Separator: SelectSeparator
6811
+ Separator: SelectSeparator,
6812
+ Action: SelectAction
6703
6813
  };
6704
6814
 
6705
6815
  export {