@botiverse/raft-ui 0.83.0 → 0.85.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 (97) hide show
  1. package/CHANGELOG.md +125 -0
  2. package/dist/cn.d.mts +2 -0
  3. package/dist/cn.mjs +21 -1
  4. package/dist/components/alert-dialog/alert-dialog.mjs +2 -2
  5. package/dist/components/app-link-card/app-link-card.mjs +19 -37
  6. package/dist/components/app-shell/app-shell.mjs +2 -2
  7. package/dist/components/avatar/avatar.mjs +1 -1
  8. package/dist/components/banner/banner.mjs +15 -2
  9. package/dist/components/button/button-split.mjs +2 -2
  10. package/dist/components/button/button.mjs +2 -2
  11. package/dist/components/choice-list/choice-list.recipe.mjs +1 -1
  12. package/dist/components/code-block/code-block.recipe.mjs +2 -2
  13. package/dist/components/combobox/combobox.mjs +1 -1
  14. package/dist/components/combobox-select/combobox-select.css +1 -1
  15. package/dist/components/comment/comment.recipe.mjs +2 -2
  16. package/dist/components/composer/composer-attachment.recipe.mjs +5 -5
  17. package/dist/components/composer/composer-drop-overlay.mjs +2 -2
  18. package/dist/components/composer/composer-drop-overlay.recipe.mjs +1 -1
  19. package/dist/components/composer/sortable-composer-attachments.mjs +1 -1
  20. package/dist/components/context-menu/context-menu.mjs +1 -1
  21. package/dist/components/conversation-panel/conversation-panel-empty-state.mjs +1 -1
  22. package/dist/components/dialog/dialog.mjs +4 -4
  23. package/dist/components/drag/drag-lift.d.mts +3 -2
  24. package/dist/components/drag/drag-lift.mjs +3 -2
  25. package/dist/components/drag/drag.recipe.mjs +1 -1
  26. package/dist/components/drawer/drawer.recipe.mjs +4 -4
  27. package/dist/components/dropdown-menu/dropdown-menu.mjs +1 -1
  28. package/dist/components/file-preview/file-preview.recipe.mjs +1 -1
  29. package/dist/components/fit-toolbar/fit-toolbar.d.mts +63 -8
  30. package/dist/components/fit-toolbar/fit-toolbar.mjs +310 -43
  31. package/dist/components/fit-toolbar/fit-toolbar.recipe.mjs +21 -7
  32. package/dist/components/fit-toolbar/index.d.mts +2 -2
  33. package/dist/components/inbox-item/inbox-item.recipe.mjs +7 -7
  34. package/dist/components/lightbox/index.d.mts +2 -2
  35. package/dist/components/lightbox/lightbox.d.mts +70 -4
  36. package/dist/components/lightbox/lightbox.mjs +285 -91
  37. package/dist/components/list-row/list-row.d.mts +14 -1
  38. package/dist/components/list-row/list-row.mjs +4 -2
  39. package/dist/components/list-row/list-row.recipe.d.mts +12 -0
  40. package/dist/components/list-row/list-row.recipe.mjs +19 -7
  41. package/dist/components/message-action-card/message-action-card-content.recipe.mjs +3 -3
  42. package/dist/components/message-action-card/message-action-card.mjs +7 -3
  43. package/dist/components/message-action-card/message-action-card.recipe.mjs +2 -6
  44. package/dist/components/message-attachment/message-image-gallery.recipe.mjs +1 -1
  45. package/dist/components/message-embed/index.d.mts +2 -1
  46. package/dist/components/message-embed/message-embed.d.mts +8 -1
  47. package/dist/components/message-embed/message-embed.mjs +2 -2
  48. package/dist/components/message-embed/message-embed.recipe.d.mts +160 -0
  49. package/dist/components/message-embed/message-embed.recipe.mjs +49 -10
  50. package/dist/components/message-forwarded-bundle/message-forwarded-bundle.mjs +1 -0
  51. package/dist/components/message-forwarded-bundle/message-forwarded-bundle.recipe.mjs +1 -1
  52. package/dist/components/message-item/message-item.recipe.mjs +2 -2
  53. package/dist/components/message-list/message-list.recipe.mjs +2 -2
  54. package/dist/components/message-quoted-preview/message-quoted-preview.mjs +2 -0
  55. package/dist/components/message-replies/message-replies.mjs +1 -1
  56. package/dist/components/message-replies/message-replies.recipe.mjs +1 -1
  57. package/dist/components/mobile-nav/mobile-nav.mjs +2 -2
  58. package/dist/components/notification-center/notification-center.mjs +1 -1
  59. package/dist/components/panel/panel-header.recipe.mjs +1 -1
  60. package/dist/components/panel/panel-split-action.recipe.mjs +2 -2
  61. package/dist/components/popover/popover.d.mts +11 -2
  62. package/dist/components/popover/popover.mjs +51 -38
  63. package/dist/components/preview-card/preview-card.d.mts +5 -1
  64. package/dist/components/preview-card/preview-card.mjs +25 -28
  65. package/dist/components/pull-to-refresh/pull-to-refresh-indicator.mjs +1 -1
  66. package/dist/components/resizable/resizable.recipe.mjs +1 -1
  67. package/dist/components/save-status/save-status.d.mts +16 -9
  68. package/dist/components/save-status/save-status.mjs +19 -30
  69. package/dist/components/scroll-area/scroll-area.css +1 -0
  70. package/dist/components/segmented-control/segmented-control.mjs +45 -8
  71. package/dist/components/select/select.mjs +2 -2
  72. package/dist/components/selection-toolbar/selection-toolbar.recipe.mjs +1 -1
  73. package/dist/components/sidebar/sidebar.recipe.mjs +1 -1
  74. package/dist/components/tabs/tabs.mjs +4 -4
  75. package/dist/components/task-board/task-board.d.mts +5 -0
  76. package/dist/components/task-board/task-board.mjs +8 -1
  77. package/dist/components/task-dnd/task-dnd.d.mts +10 -1
  78. package/dist/components/task-dnd/task-dnd.mjs +2 -1
  79. package/dist/components/task-dnd/task-dnd.recipe.mjs +2 -2
  80. package/dist/components/thread-panel/thread-panel.recipe.mjs +1 -1
  81. package/dist/components/toast/toast.mjs +5 -5
  82. package/dist/components/tooltip/tooltip.mjs +2 -2
  83. package/dist/index.d.mts +2 -1
  84. package/dist/index.mjs +2 -1
  85. package/dist/lib/laid-out-children.mjs +32 -0
  86. package/dist/lib/list-cell.mjs +1 -1
  87. package/dist/lib/stacked-actions.mjs +7 -4
  88. package/dist/lint/no-raw-z-index.mjs +142 -0
  89. package/dist/lint/prefer-scroll-area.mjs +1 -1
  90. package/dist/lint/recommended.json +2 -1
  91. package/dist/lint.d.mts +8 -2
  92. package/dist/lint.mjs +16 -3
  93. package/dist/styles.css +28 -0
  94. package/dist/wip.d.mts +4 -2
  95. package/dist/wip.mjs +3 -2
  96. package/package.json +1 -1
  97. package/skills/raft-ui-guide/SKILL.md +1 -0
@@ -1,9 +1,12 @@
1
+ import { laidOutChildren } from "../../lib/laid-out-children.mjs";
1
2
  import { Button } from "../button/button.mjs";
2
3
  import { useMergedRef } from "../../lib/use-merged-ref.mjs";
3
4
  import { ScrollArea } from "../scroll-area/scroll-area.mjs";
4
5
  import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip/tooltip.mjs";
6
+ import { PickerTriggerButton } from "../picker-trigger-button/picker-trigger-button.mjs";
5
7
  import { fitToolbar } from "./fit-toolbar.recipe.mjs";
6
- import { Fragment, createContext, useCallback, useContext, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
8
+ import { ChevronDown, Plus } from "lucide-react";
9
+ import { Fragment, cloneElement, createContext, useCallback, useContext, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
7
10
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
8
11
  import { Toolbar } from "@base-ui/react/toolbar";
9
12
  //#region src/components/fit-toolbar/fit-toolbar.tsx
@@ -16,7 +19,8 @@ const STEPS = [
16
19
  "label",
17
20
  "more",
18
21
  "collapse",
19
- "wrap"
22
+ "wrap",
23
+ "plus"
20
24
  ];
21
25
  const ID = "data-fit-id";
22
26
  const DECLARED = "data-fit-steps";
@@ -27,39 +31,46 @@ const ROW = "data-fit-row";
27
31
  const NO_WRAP = "data-fit-first-line";
28
32
  const WRAP_LINE = "data-fit-wrap-line";
29
33
  const BREAK = "data-fit-break";
34
+ const NEEDS_ICON = "data-fit-needs-icon";
30
35
  const stepAttribute = (step) => `data-fit-${step}`;
36
+ const hasIcon = (item) => item.querySelector("svg, img") !== null;
37
+ /** The group has a `wrap` step it may take: the toolbar wraps and the page lets it. */
38
+ function canWrap(group, allowWrap) {
39
+ return allowWrap && !group.hasAttribute(NO_WRAP) && / wrap:/.test(` ${group.getAttribute(DECLARED) ?? ""}`);
40
+ }
31
41
  function declare(steps) {
32
42
  return STEPS.filter((step) => steps[step] !== void 0).map((step) => `${step}:${steps[step]}`).join(" ");
33
43
  }
34
- /** What a row holds, looking through `display: contents` wrappers. */
35
- function boxes(element) {
36
- return Array.from(element.children).flatMap((child) => getComputedStyle(child).display === "contents" ? boxes(child) : [child]);
37
- }
38
44
  /**
39
45
  * Nothing pokes out of the toolbar or out of any of its rows, and no line has
40
46
  * broken by itself: while groups wrap, each line (one `order`) stays one line.
41
47
  */
42
- function fits(root) {
48
+ function fits(root, allowWrap) {
43
49
  if (![root, ...root.querySelectorAll(`[${ROW}]`)].every((row) => {
44
50
  if (row.getClientRects().length === 0) return true;
45
51
  const style = getComputedStyle(row);
46
52
  const end = row.getBoundingClientRect().right - parseFloat(style.borderRightWidth) - parseFloat(style.paddingRight);
47
- return boxes(row).every((box) => box.getClientRects().length === 0 || box.getBoundingClientRect().right <= end + .5);
53
+ return laidOutChildren(row).every((box) => box.getBoundingClientRect().right <= end + .5);
48
54
  })) return false;
49
- for (const scroller of root.querySelectorAll("[data-fit-scroll]")) {
50
- if (scroller.getClientRects().length === 0) continue;
51
- const first = boxes(scroller.querySelector("[data-slot=scroll-area-content]") ?? scroller).find((box) => box.getClientRects().length > 0);
52
- if (first && scroller.clientWidth + .5 < first.getBoundingClientRect().width) return false;
55
+ for (const group of root.querySelectorAll("[data-fit-scroll]")) {
56
+ if (group.getClientRects().length === 0) continue;
57
+ const content = group.querySelector("[data-slot=scroll-area-content]") ?? group;
58
+ const [first] = laidOutChildren(content);
59
+ if (first && group.clientWidth + .5 < first.getBoundingClientRect().width) return false;
60
+ if (canWrap(group, allowWrap) && !group.hasAttribute(stepAttribute("wrap"))) {
61
+ const scroller = group.querySelector("[data-fit-scroller]") ?? group;
62
+ if (scroller.scrollWidth > scroller.clientWidth + .5) return false;
63
+ }
53
64
  }
54
65
  if (!root.hasAttribute(WRAPPING)) return true;
55
- const tops = /* @__PURE__ */ new Map();
56
- for (const child of Array.from(root.children)) {
57
- if (child.hasAttribute(BREAK) || child.getClientRects().length === 0) continue;
66
+ const lines = /* @__PURE__ */ new Map();
67
+ for (const child of laidOutChildren(root)) {
68
+ if (child.hasAttribute(BREAK)) continue;
58
69
  const line = getComputedStyle(child).order;
59
- const top = child.getBoundingClientRect().top;
60
- const first = tops.get(line);
61
- if (first === void 0) tops.set(line, top);
62
- else if (Math.abs(first - top) > .5) return false;
70
+ const rect = child.getBoundingClientRect();
71
+ const first = lines.get(line);
72
+ if (first === void 0) lines.set(line, rect);
73
+ else if (rect.top >= first.bottom - .5 || rect.bottom <= first.top + .5) return false;
63
74
  }
64
75
  return true;
65
76
  }
@@ -71,6 +82,7 @@ function fits(root) {
71
82
  */
72
83
  function measure(root, allowWrap) {
73
84
  const elements = Array.from(root.querySelectorAll(`[${ID}]`));
85
+ const scrolled = Array.from(root.querySelectorAll("[data-fit-scroller]"), (scroller) => [scroller, scroller.scrollLeft]);
74
86
  const triggers = Array.from(root.querySelectorAll(`[${MORE_TRIGGER}]`));
75
87
  for (const element of elements) {
76
88
  for (const step of STEPS) element.removeAttribute(stepAttribute(step));
@@ -90,10 +102,28 @@ function measure(root, allowWrap) {
90
102
  };
91
103
  }).filter(({ step, element: item }) => {
92
104
  if (step === "more") return triggers.length > 0;
93
- if (step === "wrap") return allowWrap && !item.hasAttribute(NO_WRAP);
105
+ if (step === "wrap") return canWrap(item, allowWrap);
106
+ if (step === "label") return !item.hasAttribute(NEEDS_ICON) || hasIcon(item);
94
107
  return true;
95
108
  }));
96
109
  queue.sort((a, b) => a.order - b.order || b.index - a.index);
110
+ const batches = [];
111
+ const byKind = /* @__PURE__ */ new Map();
112
+ for (const { element, step, order } of queue) {
113
+ const group = step === "label" || step === "plus" ? element.parentElement?.closest("[data-slot=fit-toolbar-group]") : null;
114
+ const kind = group ? `${group.getAttribute(ID)}|${step}|${order}` : void 0;
115
+ const existing = kind === void 0 ? void 0 : byKind.get(kind);
116
+ if (existing) {
117
+ existing.members.push(element);
118
+ continue;
119
+ }
120
+ const batch = {
121
+ members: [element],
122
+ step
123
+ };
124
+ batches.push(batch);
125
+ if (kind !== void 0) byKind.set(kind, batch);
126
+ }
97
127
  const taken = /* @__PURE__ */ new Map();
98
128
  const history = [];
99
129
  const showMore = () => {
@@ -113,23 +143,24 @@ function measure(root, allowWrap) {
113
143
  element.removeAttribute(stepAttribute(step));
114
144
  if (step === "more") showMore();
115
145
  };
116
- for (const { element, step } of queue) {
117
- if (fits(root)) break;
118
- if (taken.get(element)?.has("more")) continue;
119
- take(element, step);
146
+ for (const { members, step } of batches) {
147
+ if (fits(root, allowWrap)) break;
148
+ const giving = members.filter((element) => !taken.get(element)?.has("more"));
149
+ if (giving.length === 0) continue;
150
+ for (const element of giving) take(element, step);
120
151
  history.push({
121
- element,
152
+ members: giving,
122
153
  step
123
154
  });
124
155
  }
125
156
  for (let index = history.length - 1; index >= 0; index -= 1) {
126
- const { element, step } = history[index];
157
+ const { members, step } = history[index];
127
158
  if (step === "wrap") continue;
128
- const group = element.closest(`[${stepAttribute("wrap")}]`);
159
+ const group = members[0].closest(`[${stepAttribute("wrap")}]`);
129
160
  if (!group || !root.contains(group)) continue;
130
- if (history.findIndex((entry) => entry.element === group && entry.step === "wrap") < index) continue;
131
- undo(element, step);
132
- if (!fits(root)) take(element, step);
161
+ if (history.findIndex((entry) => entry.members.includes(group) && entry.step === "wrap") < index) continue;
162
+ for (const element of members) undo(element, step);
163
+ if (!fits(root, allowWrap)) for (const element of members) take(element, step);
133
164
  }
134
165
  const items = /* @__PURE__ */ new Map();
135
166
  const more = [];
@@ -142,6 +173,7 @@ function measure(root, allowWrap) {
142
173
  if (steps.has("more")) more.push(id);
143
174
  if (steps.has("wrap")) wrapping = true;
144
175
  }
176
+ for (const [scroller, left] of scrolled) scroller.scrollLeft = left;
145
177
  return {
146
178
  items,
147
179
  more,
@@ -172,6 +204,54 @@ function layOutLines(root, elements) {
172
204
  group.style.order = String(2 * number + 2);
173
205
  }
174
206
  }
207
+ /** What the arrow keys stop on: every control a person can reach, a segment each. */
208
+ const STOP = [
209
+ "button",
210
+ "a[href]",
211
+ "input",
212
+ "select",
213
+ "textarea",
214
+ "[role=button]",
215
+ "[role=radio]",
216
+ "[role=checkbox]",
217
+ "[role=switch]",
218
+ "[role=combobox]"
219
+ ].join(", ");
220
+ /** The line an element is on: the CSS `order` of the toolbar's flex item that holds it. */
221
+ function lineOf(element, root) {
222
+ const chain = [];
223
+ for (let node = element; node && node !== root; node = node.parentElement) chain.unshift(node);
224
+ const item = chain.find((node) => getComputedStyle(node).display !== "contents") ?? element;
225
+ return Number(getComputedStyle(item).order) || 0;
226
+ }
227
+ /**
228
+ * The toolbar's controls in reading order: line by line as they show, in the
229
+ * order of the DOM on each line. Wrapped groups move with CSS `order`, which
230
+ * neither the browser's focus order nor Base UI's list follows (WCAG 2.4.3).
231
+ * Leaves out what isn't shown (in More, a collapsed group's items), what is
232
+ * disabled natively, hidden from assistive technology, or a popup's focus guard.
233
+ */
234
+ function readingOrder(root) {
235
+ const controls = Array.from(root.querySelectorAll(STOP)).filter((control) => !control.matches(":disabled") && control.closest("[aria-hidden=true]") === null && !control.hasAttribute("data-base-ui-focus-guard") && control.getClientRects().length > 0);
236
+ const lines = new Map(controls.map((control) => [control, lineOf(control, root)]));
237
+ return controls.sort((a, b) => lines.get(a) - lines.get(b));
238
+ }
239
+ /**
240
+ * A text field keeps the arrow keys while its caret has somewhere to go, as
241
+ * in Base UI's composite: only at its start or end does an arrow leave it.
242
+ */
243
+ function keepsArrow(target, forward) {
244
+ if (!(target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement)) return false;
245
+ let start;
246
+ try {
247
+ start = target.selectionStart;
248
+ } catch {
249
+ return false;
250
+ }
251
+ if (start === null) return false;
252
+ if (start !== target.selectionEnd) return true;
253
+ return forward ? start < target.value.length : start > 0;
254
+ }
175
255
  const keyOf = (applied) => `${applied.wrapping}|${Array.from(applied.items, ([id, steps]) => `${id}:${[...steps].join(",")}`).join(";")}`;
176
256
  /**
177
257
  * What each item shows in More while it is there, outside React state: the
@@ -248,6 +328,100 @@ function FitToolbar({ className, wrap = false, ref, ...props }) {
248
328
  fonts?.removeEventListener?.("loadingdone", update);
249
329
  };
250
330
  }, [element, update]);
331
+ const tabbing = useRef(false);
332
+ const entered = useRef(false);
333
+ useLayoutEffect(() => {
334
+ const onKey = (event) => {
335
+ tabbing.current = event.key === "Tab";
336
+ };
337
+ const off = () => {
338
+ tabbing.current = false;
339
+ };
340
+ document.addEventListener("keydown", onKey, true);
341
+ document.addEventListener("keyup", off, true);
342
+ document.addEventListener("pointerdown", off, true);
343
+ return () => {
344
+ document.removeEventListener("keydown", onKey, true);
345
+ document.removeEventListener("keyup", off, true);
346
+ document.removeEventListener("pointerdown", off, true);
347
+ };
348
+ }, []);
349
+ const horizontal = (props.orientation ?? "horizontal") === "horizontal";
350
+ const { onKeyDownCapture, onFocus, loopFocus = true } = props;
351
+ const handleKeyDown = useCallback((event) => {
352
+ onKeyDownCapture?.(event);
353
+ const current = rootRef.current;
354
+ if (!current || !horizontal || event.defaultPrevented) return;
355
+ if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;
356
+ const target = event.target;
357
+ if (event.key === "ArrowUp" || event.key === "ArrowDown") {
358
+ if (target.closest("[role=radiogroup]") && current.contains(target)) {
359
+ event.preventDefault();
360
+ event.stopPropagation();
361
+ }
362
+ return;
363
+ }
364
+ if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
365
+ const rtl = getComputedStyle(current).direction === "rtl";
366
+ const forward = event.key === "ArrowRight" !== rtl;
367
+ if (keepsArrow(target, forward)) return;
368
+ const order = readingOrder(current);
369
+ const index = order.findIndex((control) => control === target || control.contains(target));
370
+ if (index < 0) return;
371
+ let next = index + (forward ? 1 : -1);
372
+ if (next < 0 || next >= order.length) {
373
+ if (!loopFocus) return;
374
+ next = (next + order.length) % order.length;
375
+ }
376
+ event.preventDefault();
377
+ event.stopPropagation();
378
+ if (next !== index) order[next].focus();
379
+ }, [
380
+ horizontal,
381
+ loopFocus,
382
+ onKeyDownCapture
383
+ ]);
384
+ const handleFocus = useCallback((event) => {
385
+ onFocus?.(event);
386
+ const current = rootRef.current;
387
+ if (!current) return;
388
+ const from = event.relatedTarget;
389
+ if (!(from instanceof Node && current.contains(from)) && tabbing.current && !entered.current) {
390
+ entered.current = true;
391
+ const [first] = readingOrder(current);
392
+ if (first && first !== event.target) {
393
+ first.focus();
394
+ return;
395
+ }
396
+ }
397
+ entered.current = true;
398
+ }, [onFocus]);
399
+ const lastFocused = useRef(null);
400
+ useLayoutEffect(() => {
401
+ if (!element) return;
402
+ const onFocusIn = (event) => {
403
+ lastFocused.current = event.target;
404
+ };
405
+ const onFocusOut = (event) => {
406
+ if (event.target.getClientRects().length > 0) lastFocused.current = null;
407
+ };
408
+ element.addEventListener("focusin", onFocusIn);
409
+ element.addEventListener("focusout", onFocusOut);
410
+ return () => {
411
+ element.removeEventListener("focusin", onFocusIn);
412
+ element.removeEventListener("focusout", onFocusOut);
413
+ };
414
+ }, [element]);
415
+ useLayoutEffect(() => {
416
+ const current = rootRef.current;
417
+ const last = lastFocused.current;
418
+ if (!current || !last || last.getClientRects().length > 0) return;
419
+ const active = document.activeElement;
420
+ if (active !== last && active !== document.body && active !== null) return;
421
+ const more = Array.from(current.querySelectorAll(`[${MORE_TRIGGER}]`)).find((trigger) => trigger.getClientRects().length > 0);
422
+ lastFocused.current = null;
423
+ more?.focus();
424
+ }, [applied]);
251
425
  const context = useMemo(() => ({
252
426
  applied,
253
427
  store
@@ -259,7 +433,9 @@ function FitToolbar({ className, wrap = false, ref, ...props }) {
259
433
  "data-slot": "fit-toolbar",
260
434
  "data-fit-wrapping": applied.wrapping ? "" : void 0,
261
435
  className: root({ className }),
262
- ...props
436
+ ...props,
437
+ onKeyDownCapture: handleKeyDown,
438
+ onFocus: handleFocus
263
439
  })
264
440
  });
265
441
  }
@@ -338,6 +514,12 @@ function useScrollGroup(group) {
338
514
  if (next) group.setAttribute("data-overflow", next);
339
515
  else group.removeAttribute("data-overflow");
340
516
  };
517
+ let placed = group.scrollLeft;
518
+ let scrolledByPerson = false;
519
+ const onScroll = () => {
520
+ if (Math.abs(group.scrollLeft - placed) > 1) scrolledByPerson = true;
521
+ edges();
522
+ };
341
523
  const reveal = (item) => {
342
524
  if (!(item instanceof HTMLElement) || !group.contains(item)) return;
343
525
  const fade = parseFloat(getComputedStyle(group).getPropertyValue("--fade-edge")) || 24;
@@ -345,11 +527,13 @@ function useScrollGroup(group) {
345
527
  const end = start + item.offsetWidth;
346
528
  if (start - fade < group.scrollLeft) group.scrollLeft = Math.max(0, start - fade);
347
529
  else if (end + fade > group.scrollLeft + group.clientWidth) group.scrollLeft = end + fade - group.clientWidth;
530
+ placed = group.scrollLeft;
348
531
  edges();
349
532
  };
533
+ let target = group.querySelector("[data-active]");
350
534
  const revealActive = () => {
351
- const active = group.querySelectorAll("[data-active]");
352
- for (const item of Array.from(active).reverse()) reveal(item);
535
+ if (!target || !group.contains(target) || !target.hasAttribute("data-active")) target = group.querySelector("[data-active]");
536
+ reveal(target);
353
537
  };
354
538
  const onWheel = (event) => {
355
539
  if (group.scrollWidth <= group.clientWidth) return;
@@ -361,16 +545,22 @@ function useScrollGroup(group) {
361
545
  const onFocus = (event) => reveal(event.target);
362
546
  revealActive();
363
547
  edges();
364
- group.addEventListener("scroll", edges, { passive: true });
548
+ group.addEventListener("scroll", onScroll, { passive: true });
365
549
  group.addEventListener("wheel", onWheel, { passive: false });
366
550
  group.addEventListener("focusin", onFocus);
367
551
  const resize = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(() => {
368
- revealActive();
552
+ if (!scrolledByPerson) revealActive();
369
553
  edges();
370
554
  });
371
555
  resize?.observe(group);
556
+ const activated = (node) => node instanceof Element ? node.hasAttribute("data-active") ? node : node.querySelector("[data-active]") : null;
372
557
  const changes = new MutationObserver((records) => {
373
- if (records.some((record) => record.type !== "attributes" || record.attributeName === "data-active")) revealActive();
558
+ const newly = records.flatMap((record) => record.type === "attributes" ? [record.target instanceof Element && record.target.hasAttribute("data-active") ? record.target : null] : Array.from(record.addedNodes, activated)).filter((node) => node !== null).at(-1);
559
+ if (newly) {
560
+ target = newly;
561
+ scrolledByPerson = false;
562
+ revealActive();
563
+ }
374
564
  edges();
375
565
  });
376
566
  changes.observe(group, {
@@ -381,7 +571,7 @@ function useScrollGroup(group) {
381
571
  attributeFilter: ["data-active"]
382
572
  });
383
573
  return () => {
384
- group.removeEventListener("scroll", edges);
574
+ group.removeEventListener("scroll", onScroll);
385
575
  group.removeEventListener("wheel", onWheel);
386
576
  group.removeEventListener("focusin", onFocus);
387
577
  resize?.disconnect();
@@ -437,27 +627,47 @@ function FitToolbarGroup({ className, wrapOrder = 0, firstLine = false, wrapLine
437
627
  /**
438
628
  * A toolbar button that can give way: an `md` Button (ghost unless you say)
439
629
  * with an icon and a label, an icon square with its label as a tooltip from
440
- * `labelOrder` on, and hidden from `moreOrder` on.
630
+ * `labelOrder` on, a + square with its label as a tooltip from `plusOrder`
631
+ * on, and hidden from `moreOrder` on.
441
632
  */
442
- function FitToolbarButton({ icon, children, labelOrder, moreOrder, moreItem, variant = "ghost", className, ...props }) {
443
- const { button, label } = fitToolbar();
633
+ function FitToolbarButton({ icon, collapsedIcon, children, labelOrder, plusOrder, moreOrder, moreItem, variant = "ghost", className, ...props }) {
634
+ const { button, label, icon: iconSlot, plus, fullIcon, collapsedIcon: collapsedSlot } = fitToolbar();
444
635
  const item = useFitItem({
445
636
  label: labelOrder,
446
637
  more: moreOrder,
638
+ plus: plusOrder,
447
639
  moreItem
448
640
  });
641
+ const icons = collapsedIcon === void 0 ? icon : /* @__PURE__ */ jsxs(Fragment$1, { children: [icon === void 0 ? null : /* @__PURE__ */ jsx("span", {
642
+ "data-slot": "fit-toolbar-button-full-icon",
643
+ className: fullIcon(),
644
+ children: icon
645
+ }), /* @__PURE__ */ jsx("span", {
646
+ "data-slot": "fit-toolbar-button-collapsed-icon",
647
+ className: collapsedSlot(),
648
+ children: collapsedIcon
649
+ })] });
449
650
  return /* @__PURE__ */ jsxs(Tooltip, {
450
- disabled: !item.taken("label"),
651
+ disabled: !item.taken("label") && !item.taken("plus"),
451
652
  children: [/* @__PURE__ */ jsxs(TooltipTrigger, {
452
653
  "data-slot": "button",
453
654
  render: /* @__PURE__ */ jsx(Toolbar.Button, { render: /* @__PURE__ */ jsx(Button, {
454
655
  variant,
455
656
  size: "md",
456
657
  className: button({ className }),
658
+ "data-fit-needs-icon": "",
457
659
  ...item.props,
458
660
  ...props
459
661
  }) }),
460
- children: [icon, /* @__PURE__ */ jsx("span", {
662
+ children: [plusOrder === void 0 ? icons : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
663
+ "data-slot": "fit-toolbar-button-icon",
664
+ className: iconSlot(),
665
+ children: icons
666
+ }), /* @__PURE__ */ jsx(Plus, {
667
+ "data-slot": "fit-toolbar-button-plus",
668
+ "aria-hidden": true,
669
+ className: plus()
670
+ })] }), /* @__PURE__ */ jsx("span", {
461
671
  "data-slot": "fit-toolbar-button-label",
462
672
  className: label(),
463
673
  children
@@ -465,5 +675,62 @@ function FitToolbarButton({ icon, children, labelOrder, moreOrder, moreItem, var
465
675
  }), /* @__PURE__ */ jsx(TooltipContent, { children })]
466
676
  });
467
677
  }
678
+ /** A count: a number, or a string of digits. Space Mono is for machine-written text only. */
679
+ const isCount = (node) => typeof node === "number" || typeof node === "string" && /^\d+$/.test(node.trim());
680
+ const text = (node) => typeof node === "string" || typeof node === "number" ? String(node) : void 0;
681
+ /**
682
+ * A picker's trigger as one toolbar item (a filter: Assignee, Channel): a
683
+ * PickerTriggerButton with an icon, a label, what is chosen and a chevron,
684
+ * filled `info-soft` once a value is chosen, as an applied filter is
685
+ * (Select › Filters). From `labelOrder` on its label gives way, while its
686
+ * icon, what is chosen and the chevron stay ("# ⌄", "👤 1 ⌄"); its label
687
+ * stays its accessible name, read before the value ("Assignee 1"), and shows
688
+ * as its tooltip.
689
+ */
690
+ function FitToolbarPicker({ trigger, icon, children, value, labelOrder, moreOrder, moreItem, className, ...props }) {
691
+ const { picker, label, pickerValue, chevron } = fitToolbar({ count: isCount(value) });
692
+ const item = useFitItem({
693
+ label: labelOrder,
694
+ more: moreOrder,
695
+ moreItem
696
+ });
697
+ const hasValue = value !== void 0 && value !== null && value !== false && value !== "";
698
+ const name = text(children) !== void 0 && (!hasValue || text(value) !== void 0) ? [text(children), hasValue ? text(value) : void 0].filter(Boolean).join(" ") : void 0;
699
+ const ownTrigger = cloneElement(trigger, { render: /* @__PURE__ */ jsx(PickerTriggerButton, {
700
+ className: picker({ className }),
701
+ "aria-label": name,
702
+ "data-has-value": hasValue ? "" : void 0,
703
+ "data-fit-needs-icon": "",
704
+ ...item.props,
705
+ ...props
706
+ }) });
707
+ return /* @__PURE__ */ jsxs(Tooltip, {
708
+ disabled: !item.taken("label"),
709
+ children: [/* @__PURE__ */ jsxs(TooltipTrigger, {
710
+ "data-slot": "button",
711
+ render: /* @__PURE__ */ jsx(Toolbar.Button, { render: ownTrigger }),
712
+ children: [
713
+ icon,
714
+ /* @__PURE__ */ jsxs("span", {
715
+ "data-slot": "fit-toolbar-picker-label",
716
+ className: label(),
717
+ children: [children, hasValue && name === void 0 ? " " : null]
718
+ }),
719
+ hasValue ? /* @__PURE__ */ jsx("span", {
720
+ "data-slot": "fit-toolbar-picker-value",
721
+ className: pickerValue(),
722
+ children: value
723
+ }) : null,
724
+ /* @__PURE__ */ jsx("span", {
725
+ "data-slot": "fit-toolbar-picker-chevron",
726
+ "data-picker-trigger-indicator": "",
727
+ "aria-hidden": true,
728
+ className: chevron(),
729
+ children: /* @__PURE__ */ jsx(ChevronDown, {})
730
+ })
731
+ ]
732
+ }), /* @__PURE__ */ jsx(TooltipContent, { children })]
733
+ });
734
+ }
468
735
  //#endregion
469
- export { FitToolbar, FitToolbarButton, FitToolbarGroup, FitToolbarMoreItems, useFitItem, useFitMore, useInFitToolbar };
736
+ export { FitToolbar, FitToolbarButton, FitToolbarGroup, FitToolbarMoreItems, FitToolbarPicker, useFitItem, useFitMore, useInFitToolbar };
@@ -6,16 +6,30 @@ const fitToolbar = tv({
6
6
  group: ["group/fit-group flex shrink-0 flex-nowrap items-center", "data-fit-wrap:mt-(--fit-gap)"],
7
7
  full: ["contents group-data-fit-collapse/fit-group:hidden"],
8
8
  short: ["hidden group-data-fit-collapse/fit-group:contents"],
9
- button: ["data-fit-label:w-8 data-fit-label:px-0 md:pointer-fine:data-fit-label:w-(--control-height)", "data-fit-more:hidden"],
10
- label: ["in-data-fit-label:sr-only"],
9
+ button: [
10
+ "data-fit-label:w-8 data-fit-label:px-0 md:pointer-fine:data-fit-label:w-(--control-height)",
11
+ "data-fit-plus:w-8 data-fit-plus:px-0 md:pointer-fine:data-fit-plus:w-(--control-height)",
12
+ "data-fit-more:hidden"
13
+ ],
14
+ label: ["in-data-fit-label:sr-only in-data-fit-plus:sr-only"],
15
+ icon: ["contents in-data-fit-plus:hidden"],
16
+ fullIcon: ["contents in-data-fit-label:hidden"],
17
+ collapsedIcon: ["hidden in-data-fit-label:contents"],
18
+ picker: ["data-fit-label:gap-1 data-fit-label:px-2", "data-fit-more:hidden"],
19
+ pickerValue: [],
20
+ chevron: ["inline-flex"],
21
+ plus: ["hidden in-data-fit-plus:block"],
11
22
  scroller: [],
12
23
  scrollContent: []
13
24
  },
14
- variants: { scroll: { true: {
15
- group: ["block min-w-0 flex-1 shrink basis-0"],
16
- scroller: ["-my-1.5 w-full gap-[inherit] scroll-px-6 mask-fade-x"],
17
- scrollContent: ["flex w-max items-center gap-[inherit] py-1.5"]
18
- } } }
25
+ variants: {
26
+ count: { true: { pickerValue: ["font-mono text-mono-meta"] } },
27
+ scroll: { true: {
28
+ group: ["block min-w-0 flex-1 shrink basis-0"],
29
+ scroller: ["-my-1.5 w-full gap-[inherit] scroll-px-6 mask-fade-x"],
30
+ scrollContent: ["flex w-max items-center gap-[inherit] py-1.5"]
31
+ } }
32
+ }
19
33
  });
20
34
  //#endregion
21
35
  export { fitToolbar };
@@ -1,2 +1,2 @@
1
- import { FitItemOptions, FitStep, FitSteps, FitToolbar, FitToolbarButton, FitToolbarButtonProps, FitToolbarGroup, FitToolbarGroupProps, FitToolbarMoreItems, FitToolbarProps, useFitItem, useFitMore, useInFitToolbar } from "./fit-toolbar.mjs";
2
- export { type FitItemOptions, type FitStep, type FitSteps, FitToolbar, FitToolbarButton, type FitToolbarButtonProps, FitToolbarGroup, type FitToolbarGroupProps, FitToolbarMoreItems, type FitToolbarProps, useFitItem, useFitMore, useInFitToolbar };
1
+ import { FitItemOptions, FitStep, FitSteps, FitToolbar, FitToolbarButton, FitToolbarButtonProps, FitToolbarGroup, FitToolbarGroupProps, FitToolbarMoreItems, FitToolbarPicker, FitToolbarPickerProps, FitToolbarProps, useFitItem, useFitMore, useInFitToolbar } from "./fit-toolbar.mjs";
2
+ export { type FitItemOptions, type FitStep, type FitSteps, FitToolbar, FitToolbarButton, type FitToolbarButtonProps, FitToolbarGroup, type FitToolbarGroupProps, FitToolbarMoreItems, FitToolbarPicker, type FitToolbarPickerProps, type FitToolbarProps, useFitItem, useFitMore, useInFitToolbar };
@@ -18,17 +18,17 @@ const inboxItem = tv({
18
18
  "has-[[data-slot=inbox-item-open]:focus-visible]:keyboard:before:inset-ring-2 has-[[data-slot=inbox-item-open]:focus-visible]:keyboard:before:inset-ring-foreground",
19
19
  "not-data-[density=compact]:has-data-[slot=inbox-item-trailing]:[--inbox-item-content-display:grid] not-data-[density=compact]:has-data-[slot=inbox-item-trailing]:[--inbox-item-content-end:-1]"
20
20
  ],
21
- itemTrigger: ["absolute inset-0 z-0 outline-none"],
22
- itemLeading: ["pointer-events-none relative z-10 flex size-9 shrink-0 items-center justify-center"],
21
+ itemTrigger: ["absolute inset-0 z-base outline-none"],
22
+ itemLeading: ["pointer-events-none relative z-raised flex size-9 shrink-0 items-center justify-center"],
23
23
  itemLeadingBadge: [
24
- "absolute -right-0.5 -bottom-0.5 z-10 inline-flex size-3.5 items-center justify-center",
24
+ "absolute -right-0.5 -bottom-0.5 z-raised inline-flex size-3.5 items-center justify-center",
25
25
  "rounded-[3px] border border-sticker-ink outline-2 outline-layer-canvas outline-solid",
26
26
  "bg-layer-canvas bg-[linear-gradient(var(--info-soft),var(--info-soft))] text-foreground",
27
27
  "[&_svg]:size-2.5 [&_svg]:shrink-0"
28
28
  ],
29
- itemSelection: ["relative z-10 flex size-5 shrink-0 items-center justify-center"],
29
+ itemSelection: ["relative z-raised flex size-5 shrink-0 items-center justify-center"],
30
30
  itemContent: [
31
- "pointer-events-none relative z-10 min-w-0 flex-1 text-left",
31
+ "pointer-events-none relative z-raised min-w-0 flex-1 text-left",
32
32
  "-col-start-3 col-end-(--inbox-item-content-end) row-start-1",
33
33
  "[display:var(--inbox-item-content-display,block)] grid-cols-subgrid"
34
34
  ],
@@ -57,7 +57,7 @@ const inboxItem = tv({
57
57
  "[&>svg]:text-foreground-hint"
58
58
  ],
59
59
  itemTrailing: [
60
- "pointer-events-none relative z-10 col-[-2/-1] row-start-1",
60
+ "pointer-events-none relative z-raised col-[-2/-1] row-start-1",
61
61
  "flex shrink-0",
62
62
  "mt-0 flex-col items-end gap-2 pt-px",
63
63
  "max-md:flex-col max-md:items-end max-md:gap-2",
@@ -138,7 +138,7 @@ const inboxItem = tv({
138
138
  } },
139
139
  reveal: { hover: {
140
140
  itemActions: [
141
- "absolute top-0 right-0 z-10",
141
+ "absolute top-0 right-0 z-raised",
142
142
  "pointer-events-none opacity-0 transition-opacity",
143
143
  "group-hover/inbox-item:pointer-events-auto group-hover/inbox-item:opacity-100",
144
144
  "group-focus-within/inbox-item:pointer-events-auto group-focus-within/inbox-item:opacity-100",
@@ -1,2 +1,2 @@
1
- import { Lightbox, LightboxActions, LightboxActionsProps, LightboxClose, LightboxCloseProps, LightboxContent, LightboxContentProps, LightboxHeader, LightboxHeaderLabel, LightboxHeaderLabelProps, LightboxHeaderMeta, LightboxHeaderMetaProps, LightboxHeaderProps, LightboxInfoActions, LightboxInfoActionsProps, LightboxInfoBar, LightboxInfoBarProps, LightboxInfoDock, LightboxInfoDockProps, LightboxInfoTitle, LightboxInfoTitleProps, LightboxMedia, LightboxMediaProps, LightboxNavigationButton, LightboxNavigationButtonProps, LightboxPortal, LightboxPortalProps, LightboxProps, LightboxStage, LightboxStageProps, LightboxThumbnail, LightboxThumbnailProps, LightboxThumbnailStrip, LightboxThumbnailStripProps, LightboxTitle, LightboxTitleProps, LightboxToolbar, LightboxToolbarProps, LightboxTrigger, LightboxTriggerProps } from "./lightbox.mjs";
2
- export { Lightbox, LightboxActions, type LightboxActionsProps, LightboxClose, type LightboxCloseProps, LightboxContent, type LightboxContentProps, LightboxHeader, LightboxHeaderLabel, type LightboxHeaderLabelProps, LightboxHeaderMeta, type LightboxHeaderMetaProps, type LightboxHeaderProps, LightboxInfoActions, type LightboxInfoActionsProps, LightboxInfoBar, type LightboxInfoBarProps, LightboxInfoDock, type LightboxInfoDockProps, LightboxInfoTitle, type LightboxInfoTitleProps, LightboxMedia, type LightboxMediaProps, LightboxNavigationButton, type LightboxNavigationButtonProps, LightboxPortal, type LightboxPortalProps, type LightboxProps, LightboxStage, type LightboxStageProps, LightboxThumbnail, type LightboxThumbnailProps, LightboxThumbnailStrip, type LightboxThumbnailStripProps, LightboxTitle, type LightboxTitleProps, LightboxToolbar, type LightboxToolbarProps, LightboxTrigger, type LightboxTriggerProps };
1
+ import { Lightbox, LightboxActions, LightboxActionsProps, LightboxBody, LightboxBodyProps, LightboxClose, LightboxCloseProps, LightboxContent, LightboxContentProps, LightboxHeader, LightboxHeaderLabel, LightboxHeaderLabelProps, LightboxHeaderMeta, LightboxHeaderMetaProps, LightboxHeaderProps, LightboxInfoActions, LightboxInfoActionsProps, LightboxInfoBar, LightboxInfoBarProps, LightboxInfoDock, LightboxInfoDockProps, LightboxInfoTitle, LightboxInfoTitleProps, LightboxMedia, LightboxMediaProps, LightboxNavigationButton, LightboxNavigationButtonProps, LightboxPortal, LightboxPortalProps, LightboxProps, LightboxStage, LightboxStageProps, LightboxThumbnail, LightboxThumbnailProps, LightboxThumbnailStrip, LightboxThumbnailStripProps, LightboxTitle, LightboxTitleProps, LightboxToolbar, LightboxToolbarProps, LightboxTrigger, LightboxTriggerProps } from "./lightbox.mjs";
2
+ export { Lightbox, LightboxActions, type LightboxActionsProps, LightboxBody, type LightboxBodyProps, LightboxClose, type LightboxCloseProps, LightboxContent, type LightboxContentProps, LightboxHeader, LightboxHeaderLabel, type LightboxHeaderLabelProps, LightboxHeaderMeta, type LightboxHeaderMetaProps, type LightboxHeaderProps, LightboxInfoActions, type LightboxInfoActionsProps, LightboxInfoBar, type LightboxInfoBarProps, LightboxInfoDock, type LightboxInfoDockProps, LightboxInfoTitle, type LightboxInfoTitleProps, LightboxMedia, type LightboxMediaProps, LightboxNavigationButton, type LightboxNavigationButtonProps, LightboxPortal, type LightboxPortalProps, type LightboxProps, LightboxStage, type LightboxStageProps, LightboxThumbnail, type LightboxThumbnailProps, LightboxThumbnailStrip, type LightboxThumbnailStripProps, LightboxTitle, type LightboxTitleProps, LightboxToolbar, type LightboxToolbarProps, LightboxTrigger, type LightboxTriggerProps };