prime-ui-kit 0.12.0 → 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 (138) hide show
  1. package/README.md +3 -1
  2. package/dist/chunks/{chunk-VOZ7J72X.js → chunk-E6SQBSDF.js} +133 -28
  3. package/dist/chunks/{chunk-YJO3NJIJ.js → chunk-Z3WKKAS6.js} +6662 -5282
  4. package/dist/chunks/{chunk-OTOIOHVE.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 +10 -8
  22. package/dist/components/kanban/Kanban.d.ts +17 -2
  23. package/dist/components/list/List.d.ts +18 -3
  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.js +6 -2
  36. package/dist/internal/attachmentTile.d.ts +5 -1
  37. package/dist/internal/richText.d.ts +7 -2
  38. package/dist/internal/textFormatting.d.ts +52 -0
  39. package/dist/internal/textFormattingToolbar.d.ts +26 -0
  40. package/dist/skill/cheatsheet.md +12 -0
  41. package/dist/skill/choosing.md +5 -2
  42. package/dist/skill/components.md +5 -4
  43. package/dist/skill/composition.md +1 -1
  44. package/dist/skill/layouts.md +2 -0
  45. package/dist/skill/patterns/chat.module.css +0 -9
  46. package/dist/skill/patterns/chat.tsx +71 -25
  47. package/dist/skill/patterns/kanban-board.tsx +2 -8
  48. package/dist/skill/patterns/schedule.tsx +15 -3
  49. package/dist/skill/reference/components/chat/COMPONENT.md +13 -4
  50. package/dist/skill/reference/components/chat/examples/states.tsx +100 -0
  51. package/dist/skill/reference/components/chat-list/COMPONENT.md +256 -0
  52. package/dist/skill/reference/components/chat-list/examples/controlled.tsx +103 -0
  53. package/dist/skill/reference/components/chat-list/examples/examples.module.css +66 -0
  54. package/dist/skill/reference/components/chat-list/examples/live.tsx +133 -0
  55. package/dist/skill/reference/components/chat-list/examples/long-list.tsx +87 -0
  56. package/dist/skill/reference/components/chat-list/examples/narrow.tsx +88 -0
  57. package/dist/skill/reference/components/chat-list/examples/overview.tsx +194 -0
  58. package/dist/skill/reference/components/chat-list/examples/search.tsx +71 -0
  59. package/dist/skill/reference/components/chat-list/examples/sizes.tsx +61 -0
  60. package/dist/skill/reference/components/chat-list/examples/states.tsx +107 -0
  61. package/dist/skill/reference/components/chat-list/examples/structure.tsx +135 -0
  62. package/dist/skill/reference/components/command-menu/COMPONENT.md +15 -11
  63. package/dist/skill/reference/components/command-menu/examples/server-search.tsx +11 -9
  64. package/dist/skill/reference/components/composer/COMPONENT.md +8 -7
  65. package/dist/skill/reference/components/composer/examples/formatting.tsx +1 -1
  66. package/dist/skill/reference/components/confirm-dialog/COMPONENT.md +1 -0
  67. package/dist/skill/reference/components/data-table/COMPONENT.md +21 -6
  68. package/dist/skill/reference/components/data-table/examples/groups.tsx +2 -1
  69. package/dist/skill/reference/components/data-table/examples/trend.tsx +85 -0
  70. package/dist/skill/reference/components/drawer/COMPONENT.md +7 -6
  71. package/dist/skill/reference/components/drawer/examples/client-card.tsx +50 -0
  72. package/dist/skill/reference/components/fieldset/COMPONENT.md +1 -0
  73. package/dist/skill/reference/components/file-upload/COMPONENT.md +9 -7
  74. package/dist/skill/reference/components/file-upload/examples/overview.tsx +14 -3
  75. package/dist/skill/reference/components/gantt/COMPONENT.md +15 -5
  76. package/dist/skill/reference/components/gantt/examples/row-link.tsx +96 -0
  77. package/dist/skill/reference/components/kanban/COMPONENT.md +12 -4
  78. package/dist/skill/reference/components/kanban/examples/item-link.tsx +92 -0
  79. package/dist/skill/reference/components/list/COMPONENT.md +19 -4
  80. package/dist/skill/reference/components/list/examples/reorder.tsx +48 -0
  81. package/dist/skill/reference/components/message/COMPONENT.md +18 -2
  82. package/dist/skill/reference/components/message/examples/bot-actions.tsx +51 -0
  83. package/dist/skill/reference/components/modal/COMPONENT.md +6 -4
  84. package/dist/skill/reference/components/select/COMPONENT.md +38 -7
  85. package/dist/skill/reference/components/select/examples/create-action.tsx +107 -0
  86. package/dist/skill/reference/components/select/examples/saving.tsx +47 -0
  87. package/dist/skill/reference/components/sheet/COMPONENT.md +4 -4
  88. package/dist/skill/reference/components/sparkline/COMPONENT.md +16 -2
  89. package/dist/skill/reference/components/sparkline/examples/compact.tsx +41 -0
  90. package/dist/skill/reference/components/sparkline/examples/examples.module.css +12 -0
  91. package/dist/skill/reference/components/textarea/COMPONENT.md +22 -2
  92. package/dist/skill/reference/components/textarea/examples/formatting.tsx +20 -0
  93. package/package.json +2 -2
  94. package/src/components/chat/COMPONENT.md +13 -4
  95. package/src/components/chat/examples/states.tsx +100 -0
  96. package/src/components/chat-list/COMPONENT.md +256 -0
  97. package/src/components/chat-list/examples/controlled.tsx +103 -0
  98. package/src/components/chat-list/examples/examples.module.css +66 -0
  99. package/src/components/chat-list/examples/live.tsx +133 -0
  100. package/src/components/chat-list/examples/long-list.tsx +87 -0
  101. package/src/components/chat-list/examples/narrow.tsx +88 -0
  102. package/src/components/chat-list/examples/overview.tsx +194 -0
  103. package/src/components/chat-list/examples/search.tsx +71 -0
  104. package/src/components/chat-list/examples/sizes.tsx +61 -0
  105. package/src/components/chat-list/examples/states.tsx +107 -0
  106. package/src/components/chat-list/examples/structure.tsx +135 -0
  107. package/src/components/command-menu/COMPONENT.md +15 -11
  108. package/src/components/command-menu/examples/server-search.tsx +11 -9
  109. package/src/components/composer/COMPONENT.md +8 -7
  110. package/src/components/composer/examples/formatting.tsx +1 -1
  111. package/src/components/confirm-dialog/COMPONENT.md +1 -0
  112. package/src/components/data-table/COMPONENT.md +21 -6
  113. package/src/components/data-table/examples/groups.tsx +2 -1
  114. package/src/components/data-table/examples/trend.tsx +85 -0
  115. package/src/components/drawer/COMPONENT.md +7 -6
  116. package/src/components/drawer/examples/client-card.tsx +50 -0
  117. package/src/components/fieldset/COMPONENT.md +1 -0
  118. package/src/components/file-upload/COMPONENT.md +9 -7
  119. package/src/components/file-upload/examples/overview.tsx +14 -3
  120. package/src/components/gantt/COMPONENT.md +15 -5
  121. package/src/components/gantt/examples/row-link.tsx +96 -0
  122. package/src/components/kanban/COMPONENT.md +12 -4
  123. package/src/components/kanban/examples/item-link.tsx +92 -0
  124. package/src/components/list/COMPONENT.md +19 -4
  125. package/src/components/list/examples/reorder.tsx +48 -0
  126. package/src/components/message/COMPONENT.md +18 -2
  127. package/src/components/message/examples/bot-actions.tsx +51 -0
  128. package/src/components/modal/COMPONENT.md +6 -4
  129. package/src/components/select/COMPONENT.md +38 -7
  130. package/src/components/select/examples/create-action.tsx +107 -0
  131. package/src/components/select/examples/saving.tsx +47 -0
  132. package/src/components/sheet/COMPONENT.md +4 -4
  133. package/src/components/sparkline/COMPONENT.md +16 -2
  134. package/src/components/sparkline/examples/compact.tsx +41 -0
  135. package/src/components/sparkline/examples/examples.module.css +12 -0
  136. package/src/components/textarea/COMPONENT.md +22 -2
  137. package/src/components/textarea/examples/formatting.tsx +20 -0
  138. 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. 78 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
 
@@ -337,6 +338,7 @@ accessibility, examples and common mistakes.
337
338
  | `getPasswordStrength(value)` | The kit's 0–4 password estimate behind `Input.Root strength`; replace it with `getStrength`. |
338
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. |
339
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». |
340
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. |
341
343
  | `moveBefore(items, id, beforeId, getId)` | Applies a `Dnd` or DataTable `onReorder` to your array. |
342
344
  | `matchesSmartFilter`, `resolveSmartFilterValues` | Apply a SmartFilter value to your rows. |
@@ -1095,6 +1095,8 @@ var iconRegistry = {
1095
1095
  "format.link": Link2,
1096
1096
  "format.list": List,
1097
1097
  "format.quote": TextQuote,
1098
+ /** Hidden text (a spoiler) in text formatting; `field.password.hide` masks a password. */
1099
+ "format.spoiler": EyeOff,
1098
1100
  "format.strikethrough": Strikethrough,
1099
1101
  "format.underline": Underline,
1100
1102
  "object.activity": Activity,
@@ -5939,6 +5941,7 @@ var Select_default = {
5939
5941
  search: "Select_search",
5940
5942
  group: "Select_group",
5941
5943
  separator: "Select_separator",
5944
+ actions: "Select_actions",
5942
5945
  item: "Select_item",
5943
5946
  itemText: "Select_itemText",
5944
5947
  itemTitle: "Select_itemTitle",
@@ -6174,6 +6177,59 @@ function SelectItem({
6174
6177
  );
6175
6178
  }
6176
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";
6177
6233
 
6178
6234
  // src/components/select/Select.tsx
6179
6235
  import { jsx as jsx39, jsxs as jsxs26 } from "react/jsx-runtime";
@@ -6184,7 +6240,8 @@ var SELECT_LABELS = {
6184
6240
  loading: "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430\u2026",
6185
6241
  retry: "\u041F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u044C",
6186
6242
  clear: "\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C",
6187
- 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"
6188
6245
  };
6189
6246
  function SelectRoot(props) {
6190
6247
  const {
@@ -6227,11 +6284,30 @@ function SelectRoot(props) {
6227
6284
  const disabled = useInheritedDisabled(disabledProp);
6228
6285
  const labels = React51.useMemo(() => ({ ...SELECT_LABELS, ...labelsProp }), [labelsProp]);
6229
6286
  const ids = useFieldFrame(id, { hint, error, invalid });
6230
- const [value, setValue] = useControllableState({
6287
+ const [stored, setStored] = useControllableState({
6231
6288
  value: props.value,
6232
- defaultValue: props.defaultValue ?? (multiple ? [] : void 0),
6233
- onChange: props.onValueChange
6289
+ defaultValue: props.defaultValue ?? (multiple ? [] : void 0)
6234
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;
6235
6311
  const selected = React51.useMemo(
6236
6312
  () => Array.isArray(value) ? value : value ? [value] : [],
6237
6313
  [value]
@@ -6257,18 +6333,18 @@ function SelectRoot(props) {
6257
6333
  const pick = React51.useCallback(
6258
6334
  (optionValue) => {
6259
6335
  if (multiple) {
6260
- setValue((prev) => {
6261
- const list = Array.isArray(prev) ? prev : [];
6262
- return list.includes(optionValue) ? list.filter((item) => item !== optionValue) : [...list, optionValue];
6263
- });
6336
+ const list = Array.isArray(value) ? value : [];
6337
+ commitRef.current(
6338
+ list.includes(optionValue) ? list.filter((item) => item !== optionValue) : [...list, optionValue]
6339
+ );
6264
6340
  return;
6265
6341
  }
6266
- setValue(optionValue);
6342
+ commitRef.current(optionValue);
6267
6343
  setOpen(false);
6268
6344
  },
6269
- [multiple, setValue, setOpen]
6345
+ [multiple, value, setOpen]
6270
6346
  );
6271
- const clear = React51.useCallback(() => setValue(multiple ? [] : ""), [multiple, setValue]);
6347
+ const clear = React51.useCallback(() => commitRef.current(multiple ? [] : ""), [multiple]);
6272
6348
  const context = React51.useMemo(
6273
6349
  () => ({
6274
6350
  size,
@@ -6284,6 +6360,7 @@ function SelectRoot(props) {
6284
6360
  clear,
6285
6361
  clearable,
6286
6362
  loading,
6363
+ saving,
6287
6364
  loadingMore,
6288
6365
  hasMore,
6289
6366
  onLoadMore,
@@ -6317,6 +6394,7 @@ function SelectRoot(props) {
6317
6394
  clear,
6318
6395
  clearable,
6319
6396
  loading,
6397
+ saving,
6320
6398
  loadingMore,
6321
6399
  hasMore,
6322
6400
  onLoadMore,
@@ -6347,12 +6425,14 @@ function SelectRoot(props) {
6347
6425
  className,
6348
6426
  children: [
6349
6427
  /* @__PURE__ */ jsx39(SelectProvider, { value: context, children: /* @__PURE__ */ jsx39(ControlSizeProvider, { value: size, children }) }),
6350
- 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 })
6351
6430
  ]
6352
6431
  }
6353
6432
  );
6354
6433
  }
6355
6434
  SelectRoot.displayName = "Select.Root";
6435
+ var takeNext = (_current, next) => next;
6356
6436
  function SelectTrigger({
6357
6437
  className,
6358
6438
  children,
@@ -6375,13 +6455,15 @@ function SelectTrigger({
6375
6455
  describedBy,
6376
6456
  triggerRef,
6377
6457
  loading,
6458
+ saving,
6378
6459
  clearable,
6379
6460
  selected,
6380
6461
  clear,
6381
6462
  labels
6382
6463
  } = useSelectContext();
6383
6464
  const mergedRef = useMergedRefs(triggerRef, ref);
6384
- const showClear = clearable && selected.length > 0 && !disabled && !loading;
6465
+ const busy = loading || saving;
6466
+ const showClear = clearable && selected.length > 0 && !disabled && !busy;
6385
6467
  return /* @__PURE__ */ jsxs26(
6386
6468
  "button",
6387
6469
  {
@@ -6396,7 +6478,7 @@ function SelectTrigger({
6396
6478
  "aria-invalid": invalid || void 0,
6397
6479
  "aria-required": required || void 0,
6398
6480
  "aria-describedby": [ariaDescribedBy, describedBy].filter(Boolean).join(" ") || void 0,
6399
- "aria-busy": loading || void 0,
6481
+ "aria-busy": busy || void 0,
6400
6482
  disabled,
6401
6483
  className: cx(fieldTriggerClass, Select_default.trigger, className),
6402
6484
  onClick: (event) => {
@@ -6418,7 +6500,7 @@ function SelectTrigger({
6418
6500
  state: isOpen ? "open" : "closed",
6419
6501
  size,
6420
6502
  invalid: invalid || void 0,
6421
- loading: loading || void 0,
6503
+ loading: busy || void 0,
6422
6504
  disabled: disabled || void 0,
6423
6505
  "focus-ring": focusRing ? void 0 : false
6424
6506
  }),
@@ -6443,7 +6525,7 @@ function SelectTrigger({
6443
6525
  }
6444
6526
  )
6445
6527
  ) : null,
6446
- /* @__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" }) })
6447
6529
  ]
6448
6530
  }
6449
6531
  );
@@ -6516,6 +6598,7 @@ function SelectContent({ searchable = false, className, children, ...rest }) {
6516
6598
  query,
6517
6599
  setQuery,
6518
6600
  loading,
6601
+ saving,
6519
6602
  loadingMore,
6520
6603
  hasMore,
6521
6604
  onLoadMore,
@@ -6526,6 +6609,12 @@ function SelectContent({ searchable = false, className, children, ...rest }) {
6526
6609
  registry
6527
6610
  } = useSelectContext();
6528
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`;
6529
6618
  const floating = useFloatingLayer({
6530
6619
  open: isOpen,
6531
6620
  onOpenChange: setOpen,
@@ -6567,20 +6656,27 @@ function SelectContent({ searchable = false, className, children, ...rest }) {
6567
6656
  }
6568
6657
  const inSearch = event.target instanceof HTMLInputElement;
6569
6658
  if (inSearch && [" ", "Home", "End"].includes(event.key)) return;
6570
- const items = enabledOptions(listboxRef.current);
6659
+ const stops = enabledOptions(panelRef.current);
6571
6660
  const printable = event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey;
6572
6661
  if (printable && !inSearch && (event.key !== " " || typeahead.isTyping())) {
6573
6662
  event.preventDefault();
6574
- const match = typeahead.find(event.key, items, highlight.get());
6663
+ const match = typeahead.find(event.key, stops, highlight.get());
6575
6664
  if (match) {
6576
6665
  highlight.set(match.dataset.value);
6577
6666
  match.scrollIntoView?.({ block: "nearest" });
6578
6667
  }
6579
6668
  return;
6580
6669
  }
6581
- 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
+ });
6582
6678
  };
6583
- if (!floating.mounted) return /* @__PURE__ */ jsx39("div", { hidden: true, children });
6679
+ if (!floating.mounted) return /* @__PURE__ */ jsx39("div", { hidden: true, children: items });
6584
6680
  const activeDescendant = isOpen && highlighted !== void 0 ? optionDomId(listboxId, highlighted) : void 0;
6585
6681
  return /* @__PURE__ */ jsxs26(
6586
6682
  FloatingPanel,
@@ -6624,13 +6720,14 @@ function SelectContent({ searchable = false, className, children, ...rest }) {
6624
6720
  "aria-multiselectable": multiple || void 0,
6625
6721
  "aria-labelledby": triggerId,
6626
6722
  "aria-activedescendant": searchable ? void 0 : activeDescendant,
6627
- "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,
6628
6725
  tabIndex: -1,
6629
6726
  hidden: region === "error" ? true : void 0,
6630
6727
  "data-autofocus": searchable || region === "error" ? void 0 : "",
6631
6728
  className: cx(Select_default.listbox, swapClass),
6632
6729
  children: [
6633
- children,
6730
+ items,
6634
6731
  region === "loading" ? /* @__PURE__ */ jsx39(SkeletonRows, { count: LOADING_ROWS2 }) : null,
6635
6732
  region === "ready" && hasMore ? /* @__PURE__ */ jsx39("div", { ref: sentinelRef, "aria-hidden": "true", className: menu_default.loadSentinel }) : null,
6636
6733
  region === "ready" && loadingMore ? /* @__PURE__ */ jsx39(SkeletonRows, { count: LOADING_MORE_ROWS }) : null
@@ -6642,6 +6739,12 @@ function SelectContent({ searchable = false, className, children, ...rest }) {
6642
6739
  /* @__PURE__ */ jsx39(EmptyPage.Title, { as: "p", children: labels.empty }),
6643
6740
  query !== "" && labels.emptyHint ? /* @__PURE__ */ jsx39(EmptyPage.Description, { children: labels.emptyHint }) : null
6644
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,
6645
6748
  region === "error" ? /* @__PURE__ */ jsxs26(EmptyPage.Root, { layout: "compact", size, role: "alert", className: swapClass, children: [
6646
6749
  /* @__PURE__ */ jsx39(EmptyPage.Title, { as: "p", children: loadError }),
6647
6750
  onRetry ? /* @__PURE__ */ jsx39(EmptyPage.Actions, { children: /* @__PURE__ */ jsxs26(Button.Root, { variant: "soft", tone: "neutral", onClick: onRetry, children: [
@@ -6650,7 +6753,7 @@ function SelectContent({ searchable = false, className, children, ...rest }) {
6650
6753
  ] }) }) : null
6651
6754
  ] }) : null,
6652
6755
  /* @__PURE__ */ jsx39(VisuallyHidden, { "aria-live": "polite", children: loading || loadingMore ? labels.loading : "" }),
6653
- /* @__PURE__ */ jsx39(HighlightOnOpen, { listboxRef, highlight })
6756
+ /* @__PURE__ */ jsx39(HighlightOnOpen, { listboxRef, panelRef, highlight })
6654
6757
  ]
6655
6758
  }
6656
6759
  );
@@ -6658,6 +6761,7 @@ function SelectContent({ searchable = false, className, children, ...rest }) {
6658
6761
  SelectContent.displayName = "Select.Content";
6659
6762
  function HighlightOnOpen({
6660
6763
  listboxRef,
6764
+ panelRef,
6661
6765
  highlight
6662
6766
  }) {
6663
6767
  const { isOpen, query, selected, registry } = useSelectContext();
@@ -6673,15 +6777,15 @@ function HighlightOnOpen({
6673
6777
  return () => highlight.set(void 0);
6674
6778
  }, [isOpen, listboxRef, highlight]);
6675
6779
  React51.useLayoutEffect(() => {
6676
- if (query !== "") highlight.set(enabledOptions(listboxRef.current)[0]?.dataset.value);
6677
- }, [query, listboxRef, highlight]);
6780
+ if (query !== "") highlight.set(enabledOptions(panelRef.current)[0]?.dataset.value);
6781
+ }, [query, panelRef, highlight]);
6678
6782
  React51.useLayoutEffect(() => {
6679
6783
  if (query === "") return;
6680
6784
  const options = enabledOptions(listboxRef.current);
6681
6785
  if (!options.some((item) => item.dataset.value === highlight.get())) {
6682
- highlight.set(options[0]?.dataset.value);
6786
+ highlight.set((options[0] ?? enabledOptions(panelRef.current)[0])?.dataset.value);
6683
6787
  }
6684
- }, [version, query, listboxRef, highlight]);
6788
+ }, [version, query, listboxRef, panelRef, highlight]);
6685
6789
  return null;
6686
6790
  }
6687
6791
  function SelectGroup({ className, ...rest }) {
@@ -6704,7 +6808,8 @@ var Select = {
6704
6808
  ItemDescription: SelectItemDescription,
6705
6809
  ItemMeta: SelectItemMeta,
6706
6810
  Group: SelectGroup,
6707
- Separator: SelectSeparator
6811
+ Separator: SelectSeparator,
6812
+ Action: SelectAction
6708
6813
  };
6709
6814
 
6710
6815
  export {