@botiverse/raft-ui 0.84.0 → 0.86.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 (65) hide show
  1. package/CHANGELOG.md +33 -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-shell/app-shell.mjs +2 -2
  6. package/dist/components/avatar/avatar.mjs +1 -1
  7. package/dist/components/button/button-split.mjs +2 -2
  8. package/dist/components/button/button.mjs +2 -2
  9. package/dist/components/choice-list/choice-list.recipe.mjs +1 -1
  10. package/dist/components/code-block/code-block.recipe.mjs +2 -2
  11. package/dist/components/combobox/combobox.mjs +1 -1
  12. package/dist/components/combobox-select/combobox-select.css +1 -1
  13. package/dist/components/comment/comment.recipe.mjs +2 -2
  14. package/dist/components/composer/composer-attachment.recipe.mjs +5 -5
  15. package/dist/components/composer/composer-drop-overlay.mjs +2 -2
  16. package/dist/components/composer/composer-drop-overlay.recipe.mjs +1 -1
  17. package/dist/components/composer/sortable-composer-attachments.mjs +1 -1
  18. package/dist/components/context-menu/context-menu.mjs +1 -1
  19. package/dist/components/conversation-panel/conversation-panel-empty-state.mjs +1 -1
  20. package/dist/components/dialog/dialog.mjs +4 -4
  21. package/dist/components/drag/drag-lift.d.mts +3 -2
  22. package/dist/components/drag/drag-lift.mjs +3 -2
  23. package/dist/components/drag/drag.recipe.mjs +1 -1
  24. package/dist/components/drawer/drawer.recipe.mjs +4 -4
  25. package/dist/components/dropdown-menu/dropdown-menu.mjs +1 -1
  26. package/dist/components/file-preview/file-preview.recipe.mjs +1 -1
  27. package/dist/components/fit-toolbar/fit-toolbar.d.mts +78 -2
  28. package/dist/components/fit-toolbar/fit-toolbar.mjs +331 -14
  29. package/dist/components/fit-toolbar/fit-toolbar.recipe.mjs +30 -4
  30. package/dist/components/fit-toolbar/index.d.mts +2 -2
  31. package/dist/components/inbox-item/inbox-item.recipe.mjs +7 -7
  32. package/dist/components/lightbox/lightbox.d.mts +2 -2
  33. package/dist/components/lightbox/lightbox.mjs +7 -7
  34. package/dist/components/message-attachment/message-image-gallery.recipe.mjs +1 -1
  35. package/dist/components/message-item/message-item.recipe.mjs +2 -2
  36. package/dist/components/message-list/message-list.recipe.mjs +2 -2
  37. package/dist/components/mobile-nav/mobile-nav.mjs +2 -2
  38. package/dist/components/notification-center/notification-center.mjs +1 -1
  39. package/dist/components/panel/panel-header.recipe.mjs +1 -1
  40. package/dist/components/panel/panel-split-action.recipe.mjs +2 -2
  41. package/dist/components/popover/popover.mjs +1 -1
  42. package/dist/components/preview-card/preview-card.mjs +1 -1
  43. package/dist/components/pull-to-refresh/pull-to-refresh-indicator.mjs +1 -1
  44. package/dist/components/resizable/resizable.recipe.mjs +1 -1
  45. package/dist/components/scroll-area/scroll-area.css +1 -0
  46. package/dist/components/select/select.mjs +2 -2
  47. package/dist/components/selection-toolbar/selection-toolbar.d.mts +1 -1
  48. package/dist/components/selection-toolbar/selection-toolbar.recipe.mjs +1 -1
  49. package/dist/components/sidebar/sidebar.recipe.mjs +1 -1
  50. package/dist/components/tabs/tabs.mjs +4 -4
  51. package/dist/components/task-dnd/task-dnd.recipe.mjs +2 -2
  52. package/dist/components/thread-panel/thread-panel.recipe.mjs +1 -1
  53. package/dist/components/toast/toast.mjs +5 -5
  54. package/dist/components/tooltip/tooltip.mjs +2 -2
  55. package/dist/lib/list-cell.mjs +1 -1
  56. package/dist/lint/no-raw-z-index.mjs +142 -0
  57. package/dist/lint/prefer-scroll-area.mjs +1 -1
  58. package/dist/lint/recommended.json +2 -1
  59. package/dist/lint.d.mts +8 -2
  60. package/dist/lint.mjs +16 -3
  61. package/dist/styles.css +28 -0
  62. package/dist/wip.d.mts +2 -2
  63. package/dist/wip.mjs +2 -2
  64. package/package.json +1 -1
  65. package/skills/raft-ui-guide/SKILL.md +1 -0
@@ -3,9 +3,10 @@ import { Button } from "../button/button.mjs";
3
3
  import { useMergedRef } from "../../lib/use-merged-ref.mjs";
4
4
  import { ScrollArea } from "../scroll-area/scroll-area.mjs";
5
5
  import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip/tooltip.mjs";
6
+ import { InputGroup, InputGroupAddon, InputGroupInput } from "../input-group/input-group.mjs";
6
7
  import { PickerTriggerButton } from "../picker-trigger-button/picker-trigger-button.mjs";
7
8
  import { fitToolbar } from "./fit-toolbar.recipe.mjs";
8
- import { ChevronDown, Plus } from "lucide-react";
9
+ import { ChevronDown, MoreHorizontal, Plus, Search, X } from "lucide-react";
9
10
  import { Fragment, cloneElement, createContext, useCallback, useContext, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
10
11
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
11
12
  import { Toolbar } from "@base-ui/react/toolbar";
@@ -32,6 +33,7 @@ const NO_WRAP = "data-fit-first-line";
32
33
  const WRAP_LINE = "data-fit-wrap-line";
33
34
  const BREAK = "data-fit-break";
34
35
  const NEEDS_ICON = "data-fit-needs-icon";
36
+ const PINNED_END = "[data-slot=fit-toolbar-group-end]";
35
37
  const stepAttribute = (step) => `data-fit-${step}`;
36
38
  const hasIcon = (item) => item.querySelector("svg, img") !== null;
37
39
  /** The group has a `wrap` step it may take: the toolbar wraps and the page lets it. */
@@ -55,10 +57,10 @@ function fits(root, allowWrap) {
55
57
  for (const group of root.querySelectorAll("[data-fit-scroll]")) {
56
58
  if (group.getClientRects().length === 0) continue;
57
59
  const content = group.querySelector("[data-slot=scroll-area-content]") ?? group;
60
+ const scroller = group.querySelector("[data-fit-scroller]") ?? group;
58
61
  const [first] = laidOutChildren(content);
59
- if (first && group.clientWidth + .5 < first.getBoundingClientRect().width) return false;
62
+ if (first && scroller.clientWidth + .5 < first.getBoundingClientRect().width) return false;
60
63
  if (canWrap(group, allowWrap) && !group.hasAttribute(stepAttribute("wrap"))) {
61
- const scroller = group.querySelector("[data-fit-scroller]") ?? group;
62
64
  if (scroller.scrollWidth > scroller.clientWidth + .5) return false;
63
65
  }
64
66
  }
@@ -145,7 +147,7 @@ function measure(root, allowWrap) {
145
147
  };
146
148
  for (const { members, step } of batches) {
147
149
  if (fits(root, allowWrap)) break;
148
- const giving = members.filter((element) => !taken.get(element)?.has("more"));
150
+ const giving = members.filter((element) => !taken.get(element)?.has("more") && element.parentElement?.closest(`[${stepAttribute("more")}]`) == null);
149
151
  if (giving.length === 0) continue;
150
152
  for (const element of giving) take(element, step);
151
153
  history.push({
@@ -159,8 +161,10 @@ function measure(root, allowWrap) {
159
161
  const group = members[0].closest(`[${stepAttribute("wrap")}]`);
160
162
  if (!group || !root.contains(group)) continue;
161
163
  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);
164
+ const back = members.filter((element) => element.closest(PINNED_END) === null);
165
+ if (back.length === 0) continue;
166
+ for (const element of back) undo(element, step);
167
+ if (!fits(root, allowWrap)) for (const element of back) take(element, step);
164
168
  }
165
169
  const items = /* @__PURE__ */ new Map();
166
170
  const more = [];
@@ -229,10 +233,11 @@ function lineOf(element, root) {
229
233
  * order of the DOM on each line. Wrapped groups move with CSS `order`, which
230
234
  * neither the browser's focus order nor Base UI's list follows (WCAG 2.4.3).
231
235
  * 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.
236
+ * disabled natively, hidden from assistive technology or `inert` (the first
237
+ * line under an open search), or a popup's focus guard.
233
238
  */
234
239
  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);
240
+ const controls = Array.from(root.querySelectorAll(STOP)).filter((control) => !control.matches(":disabled") && control.closest("[aria-hidden=true], [inert]") === null && !control.hasAttribute("data-base-ui-focus-guard") && control.getClientRects().length > 0);
236
241
  const lines = new Map(controls.map((control) => [control, lineOf(control, root)]));
237
242
  return controls.sort((a, b) => lines.get(a) - lines.get(b));
238
243
  }
@@ -495,6 +500,28 @@ function FitToolbarMoreItems() {
495
500
  if (!context) return null;
496
501
  return context.applied.more.map((id) => /* @__PURE__ */ jsx(Fragment, { children: context.store.get(id) }, id));
497
502
  }
503
+ /**
504
+ * The toolbar's More as one toolbar item: a ghost ⋯ icon button, shown only
505
+ * while an item is in More, its name as a tooltip. Put it where the items it
506
+ * holds were: on a list's toolbar, before the primary action.
507
+ */
508
+ function FitToolbarMore({ trigger, "aria-label": label = "More", children, className, ...props }) {
509
+ const { more } = fitToolbar();
510
+ const fit = useFitMore();
511
+ const ownTrigger = cloneElement(trigger, { render: /* @__PURE__ */ jsx(Button, {
512
+ variant: "ghost",
513
+ size: "icon-md",
514
+ "aria-label": label,
515
+ className: more({ className }),
516
+ ...fit.props,
517
+ ...props
518
+ }) });
519
+ return /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
520
+ "data-slot": "button",
521
+ render: /* @__PURE__ */ jsx(Toolbar.Button, { render: ownTrigger }),
522
+ children: children ?? /* @__PURE__ */ jsx(MoreHorizontal, {})
523
+ }), /* @__PURE__ */ jsx(TooltipContent, { children: label })] });
524
+ }
498
525
  const noSubscription = () => () => {};
499
526
  const noVersion = () => 0;
500
527
  /**
@@ -579,18 +606,31 @@ function useScrollGroup(group) {
579
606
  };
580
607
  }, [group]);
581
608
  }
609
+ let warnedEndWithoutScroll = false;
610
+ function warnEndWithoutScroll() {
611
+ if (warnedEndWithoutScroll) return;
612
+ warnedEndWithoutScroll = true;
613
+ console.warn("raft-ui: a FitToolbarGroup has `end` without `overflow=\"scroll\"`; it is not rendered. Pin an item at the end of a scrolling group only, or put it among the group's children.");
614
+ }
582
615
  /**
583
616
  * A group of items in one row, a fixed gap apart. A toolbar wraps only
584
617
  * between groups, the last first; groups with the same `wrapLine` share a
585
618
  * line, any other takes one of its own.
586
619
  */
587
- function FitToolbarGroup({ className, wrapOrder = 0, firstLine = false, wrapLine, collapseOrder, collapsed, overflow, children, ref, ...props }) {
588
- const { group, full, short, scroller, scrollContent } = fitToolbar({ scroll: overflow === "scroll" });
620
+ function FitToolbarGroup({ className, wrapOrder = 0, firstLine = false, wrapLine, collapseOrder, collapsed, moreOrder, moreItem, overflow, end, children, ref, ...props }) {
621
+ const pinned = overflow === "scroll" && end !== void 0 && end !== null;
622
+ if (process.env.NODE_ENV !== "production" && !pinned && end !== void 0 && end !== null) warnEndWithoutScroll();
623
+ const { group, full, short, scroller, scrollContent, end: endSlot } = fitToolbar({
624
+ scroll: overflow === "scroll",
625
+ pinned
626
+ });
589
627
  const [scrollerElement, setScroller] = useState(null);
590
628
  useScrollGroup(overflow === "scroll" ? scrollerElement : null);
591
629
  const item = useFitItem({
592
630
  wrap: wrapOrder,
593
- collapse: collapsed === void 0 ? void 0 : collapseOrder ?? 0
631
+ collapse: collapsed === void 0 ? void 0 : collapseOrder ?? 0,
632
+ more: moreOrder,
633
+ moreItem
594
634
  });
595
635
  return /* @__PURE__ */ jsx(Toolbar.Group, {
596
636
  ref,
@@ -602,7 +642,7 @@ function FitToolbarGroup({ className, wrapOrder = 0, firstLine = false, wrapLine
602
642
  className: group({ className }),
603
643
  ...item.props,
604
644
  ...props,
605
- children: overflow === "scroll" ? /* @__PURE__ */ jsx(ScrollArea, {
645
+ children: overflow === "scroll" ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(ScrollArea, {
606
646
  ref: setScroller,
607
647
  orientation: "horizontal",
608
648
  overscroll: "contain",
@@ -613,7 +653,11 @@ function FitToolbarGroup({ className, wrapOrder = 0, firstLine = false, wrapLine
613
653
  className: scroller(),
614
654
  contentClassName: scrollContent(),
615
655
  children
616
- }) : collapsed === void 0 ? children : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
656
+ }), pinned ? /* @__PURE__ */ jsx("span", {
657
+ "data-slot": "fit-toolbar-group-end",
658
+ className: endSlot(),
659
+ children: end
660
+ }) : null] }) : collapsed === void 0 ? children : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
617
661
  "data-slot": "fit-toolbar-group-items",
618
662
  className: full(),
619
663
  children
@@ -732,5 +776,278 @@ function FitToolbarPicker({ trigger, icon, children, value, labelOrder, moreOrde
732
776
  }), /* @__PURE__ */ jsx(TooltipContent, { children })]
733
777
  });
734
778
  }
779
+ const COVERED = "data-fit-search-covered";
780
+ /** How many FitToolbarSearch fields each toolbar root holds (`data-fit-search`). */
781
+ const searchesByRoot = /* @__PURE__ */ new WeakMap();
782
+ /**
783
+ * While a collapsed search is open in the scope's place, the first line's
784
+ * items before it (the scope) are under it: hidden (their room kept, so the
785
+ * toolbar measures the same) and `inert`, so neither the pointer, the arrow
786
+ * keys nor assistive technology reach what can't be seen. The items after it
787
+ * (More, the primary action) and the wrapped lines stay as they are.
788
+ */
789
+ function coverScope(root, search) {
790
+ const covered = [];
791
+ for (const child of Array.from(root.children)) {
792
+ if (!(child instanceof HTMLElement)) continue;
793
+ if (child === search || child.contains(search)) break;
794
+ if (child.hasAttribute(BREAK)) continue;
795
+ if ((Number(getComputedStyle(child).order) || 0) > 0) continue;
796
+ if (child.hasAttribute("inert")) continue;
797
+ child.setAttribute(COVERED, "");
798
+ child.setAttribute("inert", "");
799
+ child.style.visibility = "hidden";
800
+ covered.push(child);
801
+ }
802
+ return () => {
803
+ for (const child of covered) {
804
+ if (!child.hasAttribute(COVERED)) continue;
805
+ child.removeAttribute(COVERED);
806
+ child.removeAttribute("inert");
807
+ child.style.removeProperty("visibility");
808
+ }
809
+ };
810
+ }
811
+ /**
812
+ * The search field of a list's toolbar: it fills the rest of its line and
813
+ * keeps at least the room its icon and its whole placeholder take (measured,
814
+ * or `minWidth`), so the toolbar's other steps come before
815
+ * it is squeezed. From `collapseOrder` on it is a search icon button at the
816
+ * start of the line, which opens the field in the scope's place, More and the
817
+ * primary action kept at the line's end; the
818
+ * field closes once it is empty and loses focus, or on Esc. A search with
819
+ * text stays open. Put it straight in the FitToolbar, between its groups.
820
+ */
821
+ function FitToolbarSearch({ "aria-label": label, value, defaultValue = "", onValueChange, minWidth, collapseOrder, icon = /* @__PURE__ */ jsx(Search, {}), clearLabel, className, style, onKeyDown, disabled, ref, ...props }) {
822
+ const { search, searchButton, searchSpacer, searchField, searchInput, searchSizer } = fitToolbar();
823
+ const item = useFitItem({ collapse: collapseOrder });
824
+ const collapsed = item.taken("collapse");
825
+ const [own, setOwn] = useState(defaultValue);
826
+ const text = value ?? own;
827
+ const [opened, setOpened] = useState(false);
828
+ const open = collapsed && (opened || text !== "");
829
+ const wrapper = useRef(null);
830
+ const field = useRef(null);
831
+ const spacer = useRef(null);
832
+ const input = useRef(null);
833
+ const button = useRef(null);
834
+ const [setInput] = useMergedRef(input, ref);
835
+ const sizer = useRef(null);
836
+ const sizerText = useRef(null);
837
+ const focusOnOpen = useRef(false);
838
+ const focusButtonOnClose = useRef(false);
839
+ useLayoutEffect(() => {
840
+ const root = wrapper.current?.closest("[data-slot=fit-toolbar]");
841
+ if (!root) return;
842
+ const count = (searchesByRoot.get(root) ?? 0) + 1;
843
+ searchesByRoot.set(root, count);
844
+ root.setAttribute("data-fit-search", "");
845
+ return () => {
846
+ const left = (searchesByRoot.get(root) ?? 1) - 1;
847
+ if (left > 0) searchesByRoot.set(root, left);
848
+ else {
849
+ searchesByRoot.delete(root);
850
+ root.removeAttribute("data-fit-search");
851
+ }
852
+ };
853
+ }, []);
854
+ const [measured, setMeasured] = useState(null);
855
+ const placeholder = typeof props.placeholder === "string" ? props.placeholder : label;
856
+ useLayoutEffect(() => {
857
+ const copy = sizer.current;
858
+ const words = sizerText.current;
859
+ const real = input.current;
860
+ if (!copy || !words || !real || minWidth !== void 0) return;
861
+ const read = () => {
862
+ const style = getComputedStyle(real);
863
+ words.style.font = style.font;
864
+ words.style.letterSpacing = style.letterSpacing;
865
+ words.style.paddingInlineEnd = style.paddingInlineEnd;
866
+ const width = Math.ceil(copy.getBoundingClientRect().width);
867
+ if (width <= 0) return;
868
+ wrapper.current?.style.setProperty("--fit-search-min", `${width}px`);
869
+ setMeasured(width);
870
+ };
871
+ read();
872
+ const resize = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(read);
873
+ resize?.observe(copy);
874
+ const fonts = typeof document === "undefined" ? void 0 : document.fonts;
875
+ fonts?.addEventListener?.("loadingdone", read);
876
+ return () => {
877
+ resize?.disconnect();
878
+ fonts?.removeEventListener?.("loadingdone", read);
879
+ };
880
+ }, [
881
+ minWidth,
882
+ placeholder,
883
+ icon
884
+ ]);
885
+ const least = minWidth ?? measured;
886
+ const change = useCallback((next) => {
887
+ setOwn(next);
888
+ onValueChange?.(next);
889
+ }, [onValueChange]);
890
+ useLayoutEffect(() => {
891
+ const box = field.current;
892
+ const line = spacer.current;
893
+ const root = wrapper.current?.closest("[data-slot=fit-toolbar]");
894
+ if (!box || !line || !root || !open) return;
895
+ const place = () => {
896
+ const style = getComputedStyle(root);
897
+ const edge = root.getBoundingClientRect();
898
+ const slot = line.getBoundingClientRect();
899
+ const end = style.direction === "rtl" ? slot.left - edge.left - parseFloat(style.borderLeftWidth) : edge.right - parseFloat(style.borderRightWidth) - slot.right;
900
+ box.style.insetInlineStart = style.paddingInlineStart;
901
+ box.style.insetInlineEnd = `${end}px`;
902
+ box.style.top = `${line.offsetTop + (line.offsetHeight - box.offsetHeight) / 2}px`;
903
+ };
904
+ place();
905
+ const uncover = coverScope(root, wrapper.current);
906
+ const resize = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(place);
907
+ resize?.observe(root);
908
+ resize?.observe(line);
909
+ if (focusOnOpen.current) {
910
+ focusOnOpen.current = false;
911
+ input.current?.focus();
912
+ }
913
+ return () => {
914
+ resize?.disconnect();
915
+ uncover();
916
+ for (const side of [
917
+ "inset-inline-start",
918
+ "inset-inline-end",
919
+ "top"
920
+ ]) box.style.removeProperty(side);
921
+ };
922
+ }, [open]);
923
+ useLayoutEffect(() => {
924
+ if (open || !focusButtonOnClose.current) return;
925
+ focusButtonOnClose.current = false;
926
+ if (collapsed) button.current?.focus();
927
+ });
928
+ const close = useCallback(() => {
929
+ setOpened(false);
930
+ focusOnOpen.current = false;
931
+ }, []);
932
+ const handleFocus = useCallback(() => setOpened(true), []);
933
+ const handleBlur = useCallback((event) => {
934
+ const to = event.relatedTarget;
935
+ if (to instanceof Node && field.current?.contains(to)) return;
936
+ close();
937
+ }, [close]);
938
+ const handleKeyDown = useCallback((event) => {
939
+ onKeyDown?.(event);
940
+ if (event.defaultPrevented || event.key !== "Escape") return;
941
+ if (event.nativeEvent.isComposing) return;
942
+ if (text === "" && !collapsed) return;
943
+ event.preventDefault();
944
+ event.stopPropagation();
945
+ if (text !== "") {
946
+ change("");
947
+ return;
948
+ }
949
+ focusButtonOnClose.current = true;
950
+ close();
951
+ }, [
952
+ change,
953
+ close,
954
+ collapsed,
955
+ onKeyDown,
956
+ text
957
+ ]);
958
+ const clear = useCallback(() => {
959
+ change("");
960
+ input.current?.focus();
961
+ }, [change]);
962
+ const openField = useCallback(() => {
963
+ focusOnOpen.current = true;
964
+ setOpened(true);
965
+ }, []);
966
+ return /* @__PURE__ */ jsxs("div", {
967
+ ref: wrapper,
968
+ "data-slot": "fit-toolbar-search",
969
+ className: search(),
970
+ style: least === null ? void 0 : { "--fit-search-min": `${least}px` },
971
+ ...item.props,
972
+ children: [
973
+ /* @__PURE__ */ jsx("div", {
974
+ "aria-hidden": true,
975
+ inert: true,
976
+ "data-slot": "fit-toolbar-search-sizer",
977
+ className: searchSizer(),
978
+ children: /* @__PURE__ */ jsxs(InputGroup, {
979
+ ref: sizer,
980
+ size: "md",
981
+ className: "w-max",
982
+ children: [/* @__PURE__ */ jsx(InputGroupAddon, { children: icon }), /* @__PURE__ */ jsx("span", {
983
+ ref: sizerText,
984
+ className: "whitespace-pre",
985
+ children: placeholder
986
+ })]
987
+ })
988
+ }),
989
+ /* @__PURE__ */ jsxs(Tooltip, {
990
+ disabled: !collapsed || open,
991
+ children: [/* @__PURE__ */ jsx(TooltipTrigger, {
992
+ "data-slot": "button",
993
+ render: /* @__PURE__ */ jsx(Toolbar.Button, {
994
+ ref: button,
995
+ disabled,
996
+ render: /* @__PURE__ */ jsx(Button, {
997
+ variant: "ghost",
998
+ size: "icon-md",
999
+ "aria-label": label,
1000
+ "data-fit-search-button": "",
1001
+ inert: open || void 0,
1002
+ className: searchButton(),
1003
+ onClick: openField
1004
+ })
1005
+ }),
1006
+ children: icon
1007
+ }), /* @__PURE__ */ jsx(TooltipContent, { children: label })]
1008
+ }),
1009
+ /* @__PURE__ */ jsx("span", {
1010
+ ref: spacer,
1011
+ "aria-hidden": true,
1012
+ "data-slot": "fit-toolbar-search-spacer",
1013
+ className: searchSpacer()
1014
+ }),
1015
+ /* @__PURE__ */ jsxs(InputGroup, {
1016
+ ref: field,
1017
+ size: "md",
1018
+ "data-fit-search-field": "",
1019
+ "data-open": open ? "" : void 0,
1020
+ className: searchField({ className }),
1021
+ style,
1022
+ onFocus: handleFocus,
1023
+ onBlur: handleBlur,
1024
+ children: [
1025
+ /* @__PURE__ */ jsx(InputGroupAddon, { children: icon }),
1026
+ /* @__PURE__ */ jsx(Toolbar.Input, { render: /* @__PURE__ */ jsx(InputGroupInput, {
1027
+ ref: setInput,
1028
+ type: "search",
1029
+ "aria-label": label,
1030
+ className: searchInput(),
1031
+ disabled,
1032
+ ...props,
1033
+ value: text,
1034
+ onValueChange: change,
1035
+ onKeyDown: handleKeyDown
1036
+ }) }),
1037
+ clearLabel !== void 0 && text !== "" ? /* @__PURE__ */ jsx(InputGroupAddon, {
1038
+ align: "inline-end",
1039
+ children: /* @__PURE__ */ jsx(Toolbar.Button, { render: /* @__PURE__ */ jsx(Button, {
1040
+ variant: "ghost",
1041
+ size: "icon-md",
1042
+ "aria-label": clearLabel,
1043
+ onClick: clear,
1044
+ children: /* @__PURE__ */ jsx(X, {})
1045
+ }) })
1046
+ }) : null
1047
+ ]
1048
+ })
1049
+ ]
1050
+ });
1051
+ }
735
1052
  //#endregion
736
- export { FitToolbar, FitToolbarButton, FitToolbarGroup, FitToolbarMoreItems, FitToolbarPicker, useFitItem, useFitMore, useInFitToolbar };
1053
+ export { FitToolbar, FitToolbarButton, FitToolbarGroup, FitToolbarMore, FitToolbarMoreItems, FitToolbarPicker, FitToolbarSearch, useFitItem, useFitMore, useInFitToolbar };
@@ -2,8 +2,17 @@ import { tv } from "../../lib/tv.mjs";
2
2
  //#region src/components/fit-toolbar/fit-toolbar.recipe.ts
3
3
  const fitToolbar = tv({
4
4
  slots: {
5
- root: ["flex min-w-0 max-w-full flex-nowrap items-center", "data-fit-wrapping:flex-wrap data-fit-wrapping:gap-y-0"],
6
- group: ["group/fit-group flex shrink-0 flex-nowrap items-center", "data-fit-wrap:mt-(--fit-gap)"],
5
+ root: [
6
+ "flex min-w-0 max-w-full flex-nowrap items-center",
7
+ "data-fit-wrapping:flex-wrap data-fit-wrapping:gap-y-0",
8
+ "data-fit-search:relative",
9
+ "data-fit-search:[--fit-scroll-grow:0] data-fit-search:[--fit-scroll-basis:auto]"
10
+ ],
11
+ group: [
12
+ "group/fit-group flex shrink-0 flex-nowrap items-center",
13
+ "data-fit-wrap:mt-(--fit-gap)",
14
+ "data-fit-more:hidden"
15
+ ],
7
16
  full: ["contents group-data-fit-collapse/fit-group:hidden"],
8
17
  short: ["hidden group-data-fit-collapse/fit-group:contents"],
9
18
  button: [
@@ -19,15 +28,32 @@ const fitToolbar = tv({
19
28
  pickerValue: [],
20
29
  chevron: ["inline-flex"],
21
30
  plus: ["hidden in-data-fit-plus:block"],
31
+ more: ["hidden data-fit-shown:inline-flex"],
22
32
  scroller: [],
23
- scrollContent: []
33
+ scrollContent: [],
34
+ end: ["flex shrink-0 items-center gap-[inherit]"],
35
+ search: ["contents"],
36
+ searchButton: ["hidden order-first in-data-fit-collapse:inline-flex", "inert:invisible"],
37
+ searchSizer: ["pointer-events-none invisible absolute inset-s-0 top-0 size-0 overflow-hidden"],
38
+ searchSpacer: ["hidden min-w-0 flex-1 basis-0 self-stretch in-data-fit-collapse:block"],
39
+ searchField: [
40
+ "w-auto min-w-(--fit-search-min) flex-1 basis-0",
41
+ "in-data-fit-collapse:not-data-open:not-focus-within:hidden",
42
+ "in-data-fit-collapse:data-open:absolute in-data-fit-collapse:data-open:min-w-0",
43
+ "in-data-fit-collapse:focus-within:absolute in-data-fit-collapse:focus-within:min-w-0"
44
+ ],
45
+ searchInput: ["[&::-webkit-search-cancel-button]:appearance-none"]
24
46
  },
25
47
  variants: {
26
48
  count: { true: { pickerValue: ["font-mono text-mono-meta"] } },
27
49
  scroll: { true: {
28
- group: ["block min-w-0 flex-1 shrink basis-0"],
50
+ group: ["block min-w-0 shrink grow-(--fit-scroll-grow,1) basis-(--fit-scroll-basis,0px)", "data-fit-wrap:grow"],
29
51
  scroller: ["-my-1.5 w-full gap-[inherit] scroll-px-6 mask-fade-x"],
30
52
  scrollContent: ["flex w-max items-center gap-[inherit] py-1.5"]
53
+ } },
54
+ pinned: { true: {
55
+ group: ["flex"],
56
+ scroller: ["w-auto min-w-0 flex-auto"]
31
57
  } }
32
58
  }
33
59
  });
@@ -1,2 +1,2 @@
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 };
1
+ import { FitItemOptions, FitStep, FitSteps, FitToolbar, FitToolbarButton, FitToolbarButtonProps, FitToolbarGroup, FitToolbarGroupProps, FitToolbarMore, FitToolbarMoreItems, FitToolbarMoreProps, FitToolbarPicker, FitToolbarPickerProps, FitToolbarProps, FitToolbarSearch, FitToolbarSearchProps, useFitItem, useFitMore, useInFitToolbar } from "./fit-toolbar.mjs";
2
+ export { type FitItemOptions, type FitStep, type FitSteps, FitToolbar, FitToolbarButton, type FitToolbarButtonProps, FitToolbarGroup, type FitToolbarGroupProps, FitToolbarMore, FitToolbarMoreItems, type FitToolbarMoreProps, FitToolbarPicker, type FitToolbarPickerProps, type FitToolbarProps, FitToolbarSearch, type FitToolbarSearchProps, 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",
@@ -39,10 +39,10 @@ export type LightboxTriggerProps = Dialog.Trigger.Props;
39
39
  export declare function LightboxTrigger(props: LightboxTriggerProps): import("react").JSX.Element;
40
40
  export type LightboxPortalProps = StyledProps<Dialog.Portal.Props>;
41
41
  /**
42
- * The lightbox's own stacking context, at `z-70` above the page's dialogs
42
+ * The lightbox's own stacking context, at `z-lightbox` (70) above the page's dialogs
43
43
  * and popups: the scrim, the content, and every Base UI popup opened from
44
44
  * inside, which Base UI portals into this element by default, in DOM order.
45
- * A popup's own `z-50` then counts inside the lightbox, so it is drawn above
45
+ * A popup's own `z-popup` then counts inside the lightbox, so it is drawn above
46
46
  * the content instead of under the scrim, and a press on it is a press inside
47
47
  * the lightbox.
48
48
  */
@@ -15,11 +15,11 @@ import { useHotkey } from "@tanstack/react-hotkeys";
15
15
  //#region src/components/lightbox/lightbox.tsx
16
16
  const lightbox = tv({
17
17
  slots: {
18
- portal: ["relative isolate z-70"],
18
+ portal: ["relative isolate z-lightbox"],
19
19
  backdrop: ["fixed inset-0 bg-layer-backdrop data-closed:hidden"],
20
20
  content: [
21
21
  "pointer-events-none fixed inset-0 flex flex-col data-closed:hidden",
22
- "*:data-lightbox-close:pointer-events-auto *:data-lightbox-close:absolute *:data-lightbox-close:z-20",
22
+ "*:data-lightbox-close:pointer-events-auto *:data-lightbox-close:absolute *:data-lightbox-close:z-raised",
23
23
  "*:data-lightbox-close:top-[calc(16px+env(safe-area-inset-top,0px))] *:data-lightbox-close:right-[calc(16px+env(safe-area-inset-right,0px))]"
24
24
  ],
25
25
  header: [
@@ -61,7 +61,7 @@ const lightbox = tv({
61
61
  stage: ["pointer-events-none relative flex min-h-0 flex-1 items-center justify-center overflow-hidden"],
62
62
  media: ["pointer-events-auto max-h-[80vh] max-w-[86vw] border-0 bg-transparent shadow-none"],
63
63
  navigationButton: [
64
- "pointer-events-auto absolute z-10 inline-flex items-center justify-center",
64
+ "pointer-events-auto absolute z-raised inline-flex items-center justify-center",
65
65
  "transition-[background-color,color,opacity,box-shadow,transform] duration-150",
66
66
  "outline-[1px] outline-offset-2 outline-transparent",
67
67
  "data-[disabled=true]:cursor-default",
@@ -71,7 +71,7 @@ const lightbox = tv({
71
71
  "focus-visible:outline-transparent focus-visible:shadow-focus",
72
72
  "[&_svg]:size-5.5"
73
73
  ],
74
- infoDock: ["pointer-events-none absolute right-6 bottom-20 left-6 z-10 flex justify-center"],
74
+ infoDock: ["pointer-events-none absolute right-6 bottom-20 left-6 z-raised flex justify-center"],
75
75
  infoBar: [
76
76
  "pointer-events-auto flex items-center",
77
77
  "w-[clamp(144px,20vw,224px)] max-w-[calc(100vw-48px)] min-w-36",
@@ -87,7 +87,7 @@ const lightbox = tv({
87
87
  "*:data-[slot=button]:after:inset-x-[-3px] *:data-[slot=button]:first:after:left-[-9.5px] *:data-[slot=button]:last:after:right-[-9.5px]",
88
88
  "pointer-coarse:*:data-[slot=button]:after:inset-y-[-9.5px]"
89
89
  ],
90
- thumbnailStrip: ["pointer-events-none absolute right-0 bottom-4 left-0 z-10", "flex items-center justify-center gap-1.5"],
90
+ thumbnailStrip: ["pointer-events-none absolute right-0 bottom-4 left-0 z-raised", "flex items-center justify-center gap-1.5"],
91
91
  thumbnail: [
92
92
  "group/lightbox-thumb pointer-events-auto relative size-12 overflow-hidden",
93
93
  "transition-[border-color,opacity,box-shadow] duration-150",
@@ -155,10 +155,10 @@ function LightboxTrigger(props) {
155
155
  });
156
156
  }
157
157
  /**
158
- * The lightbox's own stacking context, at `z-70` above the page's dialogs
158
+ * The lightbox's own stacking context, at `z-lightbox` (70) above the page's dialogs
159
159
  * and popups: the scrim, the content, and every Base UI popup opened from
160
160
  * inside, which Base UI portals into this element by default, in DOM order.
161
- * A popup's own `z-50` then counts inside the lightbox, so it is drawn above
161
+ * A popup's own `z-popup` then counts inside the lightbox, so it is drawn above
162
162
  * the content instead of under the scrim, and a press on it is a press inside
163
163
  * the lightbox.
164
164
  */
@@ -13,7 +13,7 @@ const messageImageGallery = tv({
13
13
  "outline-none focus-visible:keyboard:inset-ring-2 focus-visible:keyboard:inset-ring-foreground"
14
14
  ],
15
15
  action: [
16
- "absolute z-10 hidden items-center justify-center",
16
+ "absolute z-raised hidden items-center justify-center",
17
17
  "focus:flex",
18
18
  "group-hover/img:flex",
19
19
  "right-[calc(var(--control-embedded-inset)-1px)] bottom-[calc(var(--control-embedded-inset)-1px)] size-8 rounded-chip border-[1.5px] border-line-strong",
@@ -81,7 +81,7 @@ const messageItem = tv({
81
81
  "dark:shadow-[0_0_0_3px_var(--mi-row-fill,transparent),0_0_0_3px_var(--layer-canvas)]"
82
82
  ],
83
83
  edited: ["ms-1 whitespace-nowrap", "text-caption text-foreground-hint"],
84
- trailing: ["absolute end-0 bottom-0 z-10 flex h-lh items-center", "select-none! **:select-none!"],
84
+ trailing: ["absolute end-0 bottom-0 z-raised flex h-lh items-center", "select-none! **:select-none!"],
85
85
  body: [
86
86
  "wrap-break-word select-text",
87
87
  "**:select-text",
@@ -93,7 +93,7 @@ const messageItem = tv({
93
93
  attachments: ["stack-y-2", "mt-2"],
94
94
  footer: ["mt-1.5 flex flex-wrap items-center gap-1.5"],
95
95
  toolbar: [
96
- "absolute z-20 flex items-center",
96
+ "absolute z-control flex items-center",
97
97
  "opacity-(--mi-toolbar-opacity,0)",
98
98
  "[pointer-events:var(--mi-toolbar-pe,none)]",
99
99
  "top-[-18px] md:pointer-fine:top-[calc(-1*(var(--control-height)+8px)/2)] right-3 gap-1 overflow-visible rounded-container p-1",
@@ -14,7 +14,7 @@ const messageList = tv({
14
14
  dateDividerRule: ["flex-1", "min-w-3 border-t border-line-hairline"],
15
15
  dateDividerChip: ["inline-flex items-center", "font-mono text-mono-meta whitespace-nowrap text-foreground-hint"],
16
16
  endFade: [
17
- "pointer-events-none absolute inset-x-0 bottom-0 z-5",
17
+ "pointer-events-none absolute inset-x-0 bottom-0 z-raised",
18
18
  "h-timeline-section",
19
19
  "bg-linear-to-t from-layer-canvas to-transparent"
20
20
  ],
@@ -35,7 +35,7 @@ const messageList = tv({
35
35
  });
36
36
  const messageListJump = tv({
37
37
  slots: {
38
- controls: ["absolute inset-x-0 z-10 mx-auto", "flex w-fit max-w-[calc(100%-1.5rem)] flex-wrap items-center justify-center gap-x-2 gap-y-3"],
38
+ controls: ["absolute inset-x-0 z-float mx-auto", "flex w-fit max-w-[calc(100%-1.5rem)] flex-wrap items-center justify-center gap-x-2 gap-y-3"],
39
39
  group: ["inline-flex shrink-0 items-stretch", "*:data-[slot=message-list-jump-pill]:rounded-e-none"],
40
40
  dismiss: [
41
41
  "rounded-s-none border-s-0",
@@ -17,8 +17,8 @@ const mobileNav = tv({ slots: {
17
17
  "[&_svg]:shrink-0",
18
18
  "isolate min-w-0 flex-col justify-start gap-[7px] border-0 px-0 pt-1 pb-0",
19
19
  "font-sans text-[10px]/3 font-semibold text-foreground-hint",
20
- "*:relative *:z-10 [&_svg]:size-[22px] [&_svg]:stroke-2",
21
- "before:pointer-events-none before:absolute before:top-0 before:left-1/2 before:z-0 before:-translate-x-1/2",
20
+ "*:relative *:z-raised [&_svg]:size-[22px] [&_svg]:stroke-2",
21
+ "before:pointer-events-none before:absolute before:top-0 before:left-1/2 before:z-base before:-translate-x-1/2",
22
22
  "before:h-[30px] before:w-14 before:rounded-control before:border-[1.5px] before:border-transparent before:content-['']",
23
23
  "active:before:bg-fill-medium",
24
24
  "data-[selected=true]:font-bold data-[selected=true]:text-foreground",