@botiverse/raft-ui 0.81.0 → 0.83.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 (50) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/dist/components/choice-list/choice-list.d.mts +8 -0
  3. package/dist/components/choice-list/choice-list.mjs +37 -16
  4. package/dist/components/choice-list/choice-list.recipe.mjs +1 -0
  5. package/dist/components/composer/composer-drop-overlay.d.mts +9 -4
  6. package/dist/components/composer/composer-drop-overlay.mjs +2 -5
  7. package/dist/components/dialog/dialog-sheet-context.mjs +7 -2
  8. package/dist/components/dialog/dialog-sheet.d.mts +3 -2
  9. package/dist/components/dialog/dialog-sheet.mjs +29 -43
  10. package/dist/components/dialog/dialog.d.mts +12 -1
  11. package/dist/components/dialog/dialog.mjs +50 -10
  12. package/dist/components/fit-toolbar/fit-toolbar.d.mts +110 -0
  13. package/dist/components/fit-toolbar/fit-toolbar.mjs +469 -0
  14. package/dist/components/fit-toolbar/fit-toolbar.recipe.mjs +21 -0
  15. package/dist/components/fit-toolbar/index.d.mts +2 -0
  16. package/dist/components/list-row/index.d.mts +2 -2
  17. package/dist/components/list-row/list-row.d.mts +9 -0
  18. package/dist/components/list-row/list-row.mjs +20 -1
  19. package/dist/components/list-row/list-row.recipe.d.mts +6 -3
  20. package/dist/components/list-row/list-row.recipe.mjs +8 -2
  21. package/dist/components/message-action-card/index.d.mts +2 -2
  22. package/dist/components/message-action-card/message-action-card.d.mts +8 -0
  23. package/dist/components/message-action-card/message-action-card.mjs +16 -1
  24. package/dist/components/message-action-card/message-action-card.recipe.mjs +5 -0
  25. package/dist/components/mobile-nav/mobile-nav-server.d.mts +21 -8
  26. package/dist/components/mobile-nav/mobile-nav-server.mjs +142 -13
  27. package/dist/components/resizable/resizable.recipe.mjs +4 -4
  28. package/dist/components/scroll-area/use-scrollbars.mjs +2 -1
  29. package/dist/components/segmented-control/segmented-control.d.mts +1 -1
  30. package/dist/components/segmented-control/segmented-control.mjs +11 -2
  31. package/dist/components/selection-toolbar/index.d.mts +2 -0
  32. package/dist/components/selection-toolbar/selection-toolbar.d.mts +100 -0
  33. package/dist/components/selection-toolbar/selection-toolbar.mjs +217 -0
  34. package/dist/components/selection-toolbar/selection-toolbar.recipe.mjs +42 -0
  35. package/dist/components/steps/otter-animation-visibility.mjs +45 -0
  36. package/dist/components/steps/otter-indicator.css +131 -106
  37. package/dist/components/steps/otter-indicator.d.mts +5 -8
  38. package/dist/components/steps/otter-indicator.mjs +38 -314
  39. package/dist/components/steps/ottie-assets/error.webp +0 -0
  40. package/dist/components/steps/ottie-assets/finish.webp +0 -0
  41. package/dist/components/steps/ottie-assets/process.webp +0 -0
  42. package/dist/components/steps/ottie-assets/wait.webp +0 -0
  43. package/dist/index.d.mts +5 -4
  44. package/dist/index.mjs +5 -4
  45. package/dist/lib/label-value-fit.mjs +33 -6
  46. package/dist/lib/use-mobile-viewport.mjs +5 -2
  47. package/dist/lib/use-soft-keyboard.mjs +72 -0
  48. package/dist/wip.d.mts +6 -3
  49. package/dist/wip.mjs +4 -3
  50. package/package.json +1 -1
@@ -0,0 +1,110 @@
1
+ import { StyledProps } from "../../lib/styled-props.mjs";
2
+ import { ButtonProps } from "../button/button.mjs";
3
+ import { ReactNode, Ref } from "react";
4
+ import { Toolbar } from "@base-ui/react/toolbar";
5
+ //#region src/components/fit-toolbar/fit-toolbar.d.ts
6
+ /**
7
+ * The ways an item gives way, each one step:
8
+ * - `label`: its label hides and its icon stays (the label stays its accessible name);
9
+ * - `more`: it moves into More;
10
+ * - `collapse`: a group shows its one button instead of its items (a Filter button for its filters);
11
+ * - `wrap`: a group moves to a line of its own, only in a toolbar that allows it.
12
+ */
13
+ export type FitStep = "label" | "more" | "collapse" | "wrap";
14
+ /** When each of an item's steps comes: lower gives way first; at the same number, the last item first. */
15
+ export type FitSteps = Partial<Record<FitStep, number>>;
16
+ export type FitToolbarProps = StyledProps<Toolbar.Root.Props> & {
17
+ ref?: Ref<HTMLDivElement>;
18
+ /**
19
+ * Groups may move to a line of their own (the `wrap` step) when the row
20
+ * runs out of room. Off for a toolbar that floats over content: a second
21
+ * line would cover it, so its last step is into More.
22
+ */
23
+ wrap?: boolean;
24
+ };
25
+ /**
26
+ * A toolbar whose items give way, in named steps and an order the items
27
+ * declare, as the region it sits in narrows: it measures its content in its
28
+ * own width, never the window's. The steps are written on each item as
29
+ * `data-fit-<step>` for its styles. A Base UI Toolbar: one Tab stop, the
30
+ * arrow keys inside.
31
+ */
32
+ export declare function FitToolbar({ className, wrap, ref, ...props }: FitToolbarProps): import("react").JSX.Element;
33
+ /**
34
+ * Inside a FitToolbar: a control that would measure its own fit (a
35
+ * SegmentedControl's `iconsOnlyBelow`) leaves it to the toolbar's steps.
36
+ */
37
+ export declare function useInFitToolbar(): boolean;
38
+ export type FitItemOptions = FitSteps & {
39
+ /** What the item is in More's menu while it is there (`FitToolbarMoreItems`). */
40
+ moreItem?: ReactNode;
41
+ };
42
+ /**
43
+ * Makes an element an item of the nearest FitToolbar: spread `props` on it.
44
+ * Says which steps it has taken, for what it renders (a tooltip once its
45
+ * label is gone, say).
46
+ */
47
+ export declare function useFitItem({ moreItem, ...steps }?: FitItemOptions): {
48
+ id: string;
49
+ props: Record<string, string | undefined>;
50
+ /** Steps it has taken. */
51
+ taken: (step: FitStep) => boolean;
52
+ };
53
+ /**
54
+ * Makes an element the toolbar's More: spread `props` on it. It shows only
55
+ * while an item is in More (`data-fit-shown`); hide it otherwise.
56
+ */
57
+ export declare function useFitMore(): {
58
+ shown: boolean;
59
+ props: Record<string, string | undefined>;
60
+ };
61
+ /** The items in More, in their order, each as its `moreItem`: put it in More's menu. */
62
+ export declare function FitToolbarMoreItems(): import("react").JSX.Element[] | null;
63
+ export type FitToolbarGroupProps = StyledProps<Toolbar.Group.Props> & {
64
+ /** When it moves to a line of its own (the `wrap` step), in a toolbar that allows it. */
65
+ wrapOrder?: number;
66
+ /** Stays on the first line: it never wraps. The page decides which groups do. */
67
+ firstLine?: boolean;
68
+ /**
69
+ * Groups with the same `wrapLine` wrap onto the same line, in their order, a
70
+ * fixed gap apart; a group without one takes a line of its own.
71
+ */
72
+ wrapLine?: string;
73
+ /** When it shows `collapsed` instead of its items (the `collapse` step). */
74
+ collapseOrder?: number;
75
+ /** The group as one button (a Filter button for its filters), shown once it collapses. */
76
+ collapsed?: ReactNode;
77
+ /**
78
+ * `scroll`: the group takes the rest of its line and scrolls sideways
79
+ * instead of giving way, its items whole, the side that hides more fading.
80
+ * Mark an applied or chosen item `data-active`: it is kept in view.
81
+ */
82
+ overflow?: "scroll";
83
+ };
84
+ /**
85
+ * A group of items in one row, a fixed gap apart. A toolbar wraps only
86
+ * between groups, the last first; groups with the same `wrapLine` share a
87
+ * line, any other takes one of its own.
88
+ */
89
+ export declare function FitToolbarGroup({ className, wrapOrder, firstLine, wrapLine, collapseOrder, collapsed, overflow, children, ref, ...props }: FitToolbarGroupProps & {
90
+ ref?: Ref<HTMLDivElement>;
91
+ }): import("react").JSX.Element;
92
+ export type FitToolbarButtonProps = Omit<ButtonProps, "children" | "size"> & {
93
+ /** Its icon, which stays when its label gives way. */
94
+ icon: ReactNode;
95
+ /** Its label: its accessible name and, once it has given way, its tooltip. */
96
+ children: ReactNode;
97
+ /** When its label gives way (the `label` step). */
98
+ labelOrder?: number;
99
+ /** When it moves into More (the `more` step). */
100
+ moreOrder?: number;
101
+ /** What it is in More's menu. */
102
+ moreItem?: ReactNode;
103
+ };
104
+ /**
105
+ * A toolbar button that can give way: an `md` Button (ghost unless you say)
106
+ * with an icon and a label, an icon square with its label as a tooltip from
107
+ * `labelOrder` on, and hidden from `moreOrder` on.
108
+ */
109
+ export declare function FitToolbarButton({ icon, children, labelOrder, moreOrder, moreItem, variant, className, ...props }: FitToolbarButtonProps): import("react").JSX.Element;
110
+ //#endregion
@@ -0,0 +1,469 @@
1
+ import { Button } from "../button/button.mjs";
2
+ import { useMergedRef } from "../../lib/use-merged-ref.mjs";
3
+ import { ScrollArea } from "../scroll-area/scroll-area.mjs";
4
+ import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip/tooltip.mjs";
5
+ import { fitToolbar } from "./fit-toolbar.recipe.mjs";
6
+ import { Fragment, createContext, useCallback, useContext, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
7
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
8
+ import { Toolbar } from "@base-ui/react/toolbar";
9
+ //#region src/components/fit-toolbar/fit-toolbar.tsx
10
+ const NONE = {
11
+ items: /* @__PURE__ */ new Map(),
12
+ more: [],
13
+ wrapping: false
14
+ };
15
+ const STEPS = [
16
+ "label",
17
+ "more",
18
+ "collapse",
19
+ "wrap"
20
+ ];
21
+ const ID = "data-fit-id";
22
+ const DECLARED = "data-fit-steps";
23
+ const MORE_TRIGGER = "data-fit-more-trigger";
24
+ const SHOWN = "data-fit-shown";
25
+ const WRAPPING = "data-fit-wrapping";
26
+ const ROW = "data-fit-row";
27
+ const NO_WRAP = "data-fit-first-line";
28
+ const WRAP_LINE = "data-fit-wrap-line";
29
+ const BREAK = "data-fit-break";
30
+ const stepAttribute = (step) => `data-fit-${step}`;
31
+ function declare(steps) {
32
+ return STEPS.filter((step) => steps[step] !== void 0).map((step) => `${step}:${steps[step]}`).join(" ");
33
+ }
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
+ /**
39
+ * Nothing pokes out of the toolbar or out of any of its rows, and no line has
40
+ * broken by itself: while groups wrap, each line (one `order`) stays one line.
41
+ */
42
+ function fits(root) {
43
+ if (![root, ...root.querySelectorAll(`[${ROW}]`)].every((row) => {
44
+ if (row.getClientRects().length === 0) return true;
45
+ const style = getComputedStyle(row);
46
+ 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);
48
+ })) 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;
53
+ }
54
+ 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;
58
+ 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;
63
+ }
64
+ return true;
65
+ }
66
+ /**
67
+ * Takes the steps one at a time, in their order, until the toolbar fits its
68
+ * region; writes each on the DOM as it goes (React renders the same after)
69
+ * and returns them. It starts from nothing taken every time, so the outcome
70
+ * depends on the width alone.
71
+ */
72
+ function measure(root, allowWrap) {
73
+ const elements = Array.from(root.querySelectorAll(`[${ID}]`));
74
+ const triggers = Array.from(root.querySelectorAll(`[${MORE_TRIGGER}]`));
75
+ for (const element of elements) {
76
+ for (const step of STEPS) element.removeAttribute(stepAttribute(step));
77
+ element.style.removeProperty("order");
78
+ }
79
+ for (const trigger of triggers) trigger.removeAttribute(SHOWN);
80
+ root.removeAttribute(WRAPPING);
81
+ for (const line of Array.from(root.querySelectorAll(`:scope > [${BREAK}]`))) line.remove();
82
+ root.style.setProperty("--fit-gap", getComputedStyle(root).columnGap);
83
+ const queue = elements.flatMap((element, index) => (element.getAttribute(DECLARED) ?? "").split(" ").filter(Boolean).map((entry) => {
84
+ const [step, order] = entry.split(":");
85
+ return {
86
+ element,
87
+ step,
88
+ order: Number(order),
89
+ index
90
+ };
91
+ }).filter(({ step, element: item }) => {
92
+ if (step === "more") return triggers.length > 0;
93
+ if (step === "wrap") return allowWrap && !item.hasAttribute(NO_WRAP);
94
+ return true;
95
+ }));
96
+ queue.sort((a, b) => a.order - b.order || b.index - a.index);
97
+ const taken = /* @__PURE__ */ new Map();
98
+ const history = [];
99
+ const showMore = () => {
100
+ const shown = Array.from(taken.values()).some((steps) => steps.has("more"));
101
+ for (const trigger of triggers) trigger.toggleAttribute(SHOWN, shown);
102
+ };
103
+ const take = (element, step) => {
104
+ const steps = taken.get(element) ?? /* @__PURE__ */ new Set();
105
+ steps.add(step);
106
+ taken.set(element, steps);
107
+ element.setAttribute(stepAttribute(step), "");
108
+ if (step === "more") showMore();
109
+ if (step === "wrap") layOutLines(root, elements);
110
+ };
111
+ const undo = (element, step) => {
112
+ taken.get(element)?.delete(step);
113
+ element.removeAttribute(stepAttribute(step));
114
+ if (step === "more") showMore();
115
+ };
116
+ for (const { element, step } of queue) {
117
+ if (fits(root)) break;
118
+ if (taken.get(element)?.has("more")) continue;
119
+ take(element, step);
120
+ history.push({
121
+ element,
122
+ step
123
+ });
124
+ }
125
+ for (let index = history.length - 1; index >= 0; index -= 1) {
126
+ const { element, step } = history[index];
127
+ if (step === "wrap") continue;
128
+ const group = element.closest(`[${stepAttribute("wrap")}]`);
129
+ 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);
133
+ }
134
+ const items = /* @__PURE__ */ new Map();
135
+ const more = [];
136
+ let wrapping = false;
137
+ for (const element of elements) {
138
+ const steps = taken.get(element);
139
+ if (!steps || steps.size === 0) continue;
140
+ const id = element.getAttribute(ID);
141
+ items.set(id, steps);
142
+ if (steps.has("more")) more.push(id);
143
+ if (steps.has("wrap")) wrapping = true;
144
+ }
145
+ return {
146
+ items,
147
+ more,
148
+ wrapping
149
+ };
150
+ }
151
+ /**
152
+ * Puts the wrapped groups on the lines after the first: groups with the same
153
+ * `wrapLine` share a line, in their order; any other takes one of its own.
154
+ * A zero-height break before each line ends the one before it.
155
+ */
156
+ function layOutLines(root, elements) {
157
+ root.setAttribute(WRAPPING, "");
158
+ for (const line of Array.from(root.querySelectorAll(`:scope > [${BREAK}]`))) line.remove();
159
+ const lines = [];
160
+ for (const group of elements) {
161
+ if (!group.hasAttribute(stepAttribute("wrap"))) continue;
162
+ const line = group.getAttribute(WRAP_LINE) ?? `#${group.getAttribute(ID)}`;
163
+ let number = lines.indexOf(line);
164
+ if (number < 0) {
165
+ number = lines.push(line) - 1;
166
+ const lineBreak = document.createElement("span");
167
+ lineBreak.setAttribute(BREAK, "");
168
+ lineBreak.setAttribute("aria-hidden", "true");
169
+ lineBreak.style.cssText = `flex-basis: 100%; height: 0; order: ${2 * number + 1}`;
170
+ root.append(lineBreak);
171
+ }
172
+ group.style.order = String(2 * number + 2);
173
+ }
174
+ }
175
+ const keyOf = (applied) => `${applied.wrapping}|${Array.from(applied.items, ([id, steps]) => `${id}:${[...steps].join(",")}`).join(";")}`;
176
+ /**
177
+ * What each item shows in More while it is there, outside React state: the
178
+ * items write it on every render, and only `FitToolbarMoreItems` (in an open
179
+ * menu) reads it.
180
+ */
181
+ function createMoreItemStore() {
182
+ const items = /* @__PURE__ */ new Map();
183
+ const listeners = /* @__PURE__ */ new Set();
184
+ let version = 0;
185
+ const notify = () => {
186
+ version += 1;
187
+ for (const listener of listeners) listener();
188
+ };
189
+ return {
190
+ set(id, item) {
191
+ items.set(id, item);
192
+ notify();
193
+ },
194
+ delete(id) {
195
+ if (items.delete(id)) notify();
196
+ },
197
+ get: (id) => items.get(id),
198
+ subscribe: (listener) => {
199
+ listeners.add(listener);
200
+ return () => {
201
+ listeners.delete(listener);
202
+ };
203
+ },
204
+ version: () => version
205
+ };
206
+ }
207
+ const FitToolbarContext = createContext(null);
208
+ /**
209
+ * A toolbar whose items give way, in named steps and an order the items
210
+ * declare, as the region it sits in narrows: it measures its content in its
211
+ * own width, never the window's. The steps are written on each item as
212
+ * `data-fit-<step>` for its styles. A Base UI Toolbar: one Tab stop, the
213
+ * arrow keys inside.
214
+ */
215
+ function FitToolbar({ className, wrap = false, ref, ...props }) {
216
+ const { root } = fitToolbar();
217
+ const rootRef = useRef(null);
218
+ const [element, setElement] = useState(null);
219
+ const [setRef] = useMergedRef(rootRef, setElement, ref);
220
+ const [applied, setApplied] = useState(NONE);
221
+ const [store] = useState(createMoreItemStore);
222
+ const update = useCallback(() => {
223
+ const current = rootRef.current;
224
+ if (!current) return;
225
+ const next = measure(current, wrap);
226
+ setApplied((previous) => keyOf(previous) === keyOf(next) ? previous : next);
227
+ }, [wrap]);
228
+ useLayoutEffect(update);
229
+ useLayoutEffect(() => {
230
+ if (!element) return;
231
+ const region = element.parentElement;
232
+ const resize = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(update);
233
+ if (region) resize?.observe(region);
234
+ const content = typeof MutationObserver === "undefined" ? null : new MutationObserver((records) => {
235
+ const own = (node) => node instanceof Element && node.hasAttribute(BREAK);
236
+ if (records.some((record) => record.type === "characterData" || ![...record.addedNodes, ...record.removedNodes].every(own))) update();
237
+ });
238
+ content?.observe(element, {
239
+ childList: true,
240
+ subtree: true,
241
+ characterData: true
242
+ });
243
+ const fonts = typeof document === "undefined" ? void 0 : document.fonts;
244
+ fonts?.addEventListener?.("loadingdone", update);
245
+ return () => {
246
+ resize?.disconnect();
247
+ content?.disconnect();
248
+ fonts?.removeEventListener?.("loadingdone", update);
249
+ };
250
+ }, [element, update]);
251
+ const context = useMemo(() => ({
252
+ applied,
253
+ store
254
+ }), [applied, store]);
255
+ return /* @__PURE__ */ jsx(FitToolbarContext.Provider, {
256
+ value: context,
257
+ children: /* @__PURE__ */ jsx(Toolbar.Root, {
258
+ ref: setRef,
259
+ "data-slot": "fit-toolbar",
260
+ "data-fit-wrapping": applied.wrapping ? "" : void 0,
261
+ className: root({ className }),
262
+ ...props
263
+ })
264
+ });
265
+ }
266
+ /**
267
+ * Inside a FitToolbar: a control that would measure its own fit (a
268
+ * SegmentedControl's `iconsOnlyBelow`) leaves it to the toolbar's steps.
269
+ */
270
+ function useInFitToolbar() {
271
+ return useContext(FitToolbarContext) !== null;
272
+ }
273
+ /**
274
+ * Makes an element an item of the nearest FitToolbar: spread `props` on it.
275
+ * Says which steps it has taken, for what it renders (a tooltip once its
276
+ * label is gone, say).
277
+ */
278
+ function useFitItem({ moreItem, ...steps } = {}) {
279
+ const id = useId();
280
+ const context = useContext(FitToolbarContext);
281
+ const taken = context?.applied.items.get(id);
282
+ const store = context?.store;
283
+ const declared = declare(steps);
284
+ useLayoutEffect(() => {
285
+ if (moreItem !== void 0) store?.set(id, moreItem);
286
+ });
287
+ useLayoutEffect(() => () => store?.delete(id), [store, id]);
288
+ const props = {
289
+ [ID]: id,
290
+ [DECLARED]: declared || void 0
291
+ };
292
+ for (const step of STEPS) props[stepAttribute(step)] = taken?.has(step) ? "" : void 0;
293
+ return {
294
+ id,
295
+ props,
296
+ /** Steps it has taken. */
297
+ taken: (step) => taken?.has(step) ?? false
298
+ };
299
+ }
300
+ /**
301
+ * Makes an element the toolbar's More: spread `props` on it. It shows only
302
+ * while an item is in More (`data-fit-shown`); hide it otherwise.
303
+ */
304
+ function useFitMore() {
305
+ const shown = (useContext(FitToolbarContext)?.applied.more.length ?? 0) > 0;
306
+ return {
307
+ shown,
308
+ props: {
309
+ [MORE_TRIGGER]: "",
310
+ [SHOWN]: shown ? "" : void 0
311
+ }
312
+ };
313
+ }
314
+ /** The items in More, in their order, each as its `moreItem`: put it in More's menu. */
315
+ function FitToolbarMoreItems() {
316
+ const context = useContext(FitToolbarContext);
317
+ const store = context?.store;
318
+ useSyncExternalStore(store?.subscribe ?? noSubscription, store?.version ?? noVersion, store?.version ?? noVersion);
319
+ if (!context) return null;
320
+ return context.applied.more.map((id) => /* @__PURE__ */ jsx(Fragment, { children: context.store.get(id) }, id));
321
+ }
322
+ const noSubscription = () => () => {};
323
+ const noVersion = () => 0;
324
+ /**
325
+ * A scrolling group: which side hides more (`data-overflow`, for
326
+ * `mask-fade-x`), a vertical wheel scrolling it, and its `data-active` items
327
+ * and the focused one kept in view, clear of the fade, with no animation.
328
+ * The same as slock web's FilterStrip, so the two behave alike.
329
+ */
330
+ function useScrollGroup(group) {
331
+ useLayoutEffect(() => {
332
+ if (!group) return;
333
+ const edges = () => {
334
+ const max = group.scrollWidth - group.clientWidth;
335
+ const start = group.scrollLeft > 1;
336
+ const end = max > 1 && group.scrollLeft < max - 1;
337
+ const next = start && end ? "both" : start ? "start" : end ? "end" : null;
338
+ if (next) group.setAttribute("data-overflow", next);
339
+ else group.removeAttribute("data-overflow");
340
+ };
341
+ const reveal = (item) => {
342
+ if (!(item instanceof HTMLElement) || !group.contains(item)) return;
343
+ const fade = parseFloat(getComputedStyle(group).getPropertyValue("--fade-edge")) || 24;
344
+ const start = item.getBoundingClientRect().left - group.getBoundingClientRect().left + group.scrollLeft;
345
+ const end = start + item.offsetWidth;
346
+ if (start - fade < group.scrollLeft) group.scrollLeft = Math.max(0, start - fade);
347
+ else if (end + fade > group.scrollLeft + group.clientWidth) group.scrollLeft = end + fade - group.clientWidth;
348
+ edges();
349
+ };
350
+ const revealActive = () => {
351
+ const active = group.querySelectorAll("[data-active]");
352
+ for (const item of Array.from(active).reverse()) reveal(item);
353
+ };
354
+ const onWheel = (event) => {
355
+ if (group.scrollWidth <= group.clientWidth) return;
356
+ if (Math.abs(event.deltaY) <= Math.abs(event.deltaX)) return;
357
+ const before = group.scrollLeft;
358
+ group.scrollLeft += event.deltaY;
359
+ if (group.scrollLeft !== before) event.preventDefault();
360
+ };
361
+ const onFocus = (event) => reveal(event.target);
362
+ revealActive();
363
+ edges();
364
+ group.addEventListener("scroll", edges, { passive: true });
365
+ group.addEventListener("wheel", onWheel, { passive: false });
366
+ group.addEventListener("focusin", onFocus);
367
+ const resize = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(() => {
368
+ revealActive();
369
+ edges();
370
+ });
371
+ resize?.observe(group);
372
+ const changes = new MutationObserver((records) => {
373
+ if (records.some((record) => record.type !== "attributes" || record.attributeName === "data-active")) revealActive();
374
+ edges();
375
+ });
376
+ changes.observe(group, {
377
+ childList: true,
378
+ subtree: true,
379
+ characterData: true,
380
+ attributes: true,
381
+ attributeFilter: ["data-active"]
382
+ });
383
+ return () => {
384
+ group.removeEventListener("scroll", edges);
385
+ group.removeEventListener("wheel", onWheel);
386
+ group.removeEventListener("focusin", onFocus);
387
+ resize?.disconnect();
388
+ changes.disconnect();
389
+ };
390
+ }, [group]);
391
+ }
392
+ /**
393
+ * A group of items in one row, a fixed gap apart. A toolbar wraps only
394
+ * between groups, the last first; groups with the same `wrapLine` share a
395
+ * line, any other takes one of its own.
396
+ */
397
+ function FitToolbarGroup({ className, wrapOrder = 0, firstLine = false, wrapLine, collapseOrder, collapsed, overflow, children, ref, ...props }) {
398
+ const { group, full, short, scroller, scrollContent } = fitToolbar({ scroll: overflow === "scroll" });
399
+ const [scrollerElement, setScroller] = useState(null);
400
+ useScrollGroup(overflow === "scroll" ? scrollerElement : null);
401
+ const item = useFitItem({
402
+ wrap: wrapOrder,
403
+ collapse: collapsed === void 0 ? void 0 : collapseOrder ?? 0
404
+ });
405
+ return /* @__PURE__ */ jsx(Toolbar.Group, {
406
+ ref,
407
+ "data-slot": "fit-toolbar-group",
408
+ "data-fit-row": overflow === "scroll" ? void 0 : "",
409
+ "data-fit-scroll": overflow === "scroll" ? "" : void 0,
410
+ "data-fit-first-line": firstLine ? "" : void 0,
411
+ "data-fit-wrap-line": wrapLine,
412
+ className: group({ className }),
413
+ ...item.props,
414
+ ...props,
415
+ children: overflow === "scroll" ? /* @__PURE__ */ jsx(ScrollArea, {
416
+ ref: setScroller,
417
+ orientation: "horizontal",
418
+ overscroll: "contain",
419
+ size: "small",
420
+ trigger: "scroll",
421
+ tabIndex: -1,
422
+ "data-fit-scroller": "",
423
+ className: scroller(),
424
+ contentClassName: scrollContent(),
425
+ children
426
+ }) : collapsed === void 0 ? children : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
427
+ "data-slot": "fit-toolbar-group-items",
428
+ className: full(),
429
+ children
430
+ }), /* @__PURE__ */ jsx("span", {
431
+ "data-slot": "fit-toolbar-group-collapsed",
432
+ className: short(),
433
+ children: collapsed
434
+ })] })
435
+ });
436
+ }
437
+ /**
438
+ * A toolbar button that can give way: an `md` Button (ghost unless you say)
439
+ * with an icon and a label, an icon square with its label as a tooltip from
440
+ * `labelOrder` on, and hidden from `moreOrder` on.
441
+ */
442
+ function FitToolbarButton({ icon, children, labelOrder, moreOrder, moreItem, variant = "ghost", className, ...props }) {
443
+ const { button, label } = fitToolbar();
444
+ const item = useFitItem({
445
+ label: labelOrder,
446
+ more: moreOrder,
447
+ moreItem
448
+ });
449
+ return /* @__PURE__ */ jsxs(Tooltip, {
450
+ disabled: !item.taken("label"),
451
+ children: [/* @__PURE__ */ jsxs(TooltipTrigger, {
452
+ "data-slot": "button",
453
+ render: /* @__PURE__ */ jsx(Toolbar.Button, { render: /* @__PURE__ */ jsx(Button, {
454
+ variant,
455
+ size: "md",
456
+ className: button({ className }),
457
+ ...item.props,
458
+ ...props
459
+ }) }),
460
+ children: [icon, /* @__PURE__ */ jsx("span", {
461
+ "data-slot": "fit-toolbar-button-label",
462
+ className: label(),
463
+ children
464
+ })]
465
+ }), /* @__PURE__ */ jsx(TooltipContent, { children })]
466
+ });
467
+ }
468
+ //#endregion
469
+ export { FitToolbar, FitToolbarButton, FitToolbarGroup, FitToolbarMoreItems, useFitItem, useFitMore, useInFitToolbar };
@@ -0,0 +1,21 @@
1
+ import { tv } from "../../lib/tv.mjs";
2
+ //#region src/components/fit-toolbar/fit-toolbar.recipe.ts
3
+ const fitToolbar = tv({
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)"],
7
+ full: ["contents group-data-fit-collapse/fit-group:hidden"],
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"],
11
+ scroller: [],
12
+ scrollContent: []
13
+ },
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
+ } } }
19
+ });
20
+ //#endregion
21
+ export { fitToolbar };
@@ -0,0 +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,2 +1,2 @@
1
- import { ListRow, ListRowBody, ListRowBodyProps, ListRowBottom, ListRowBottomProps, ListRowChevron, ListRowChevronProps, ListRowDescription, ListRowDescriptionProps, ListRowGroup, ListRowGroupProps, ListRowIcon, ListRowIconProps, ListRowMeta, ListRowMetaProps, ListRowProps, ListRowTitle, ListRowTitleProps, ListRowTop, ListRowTopProps, ListRowValue, ListRowValueProps } from "./list-row.mjs";
2
- export { ListRow, ListRowBody, type ListRowBodyProps, ListRowBottom, type ListRowBottomProps, ListRowChevron, type ListRowChevronProps, ListRowDescription, type ListRowDescriptionProps, ListRowGroup, type ListRowGroupProps, ListRowIcon, type ListRowIconProps, ListRowMeta, type ListRowMetaProps, type ListRowProps, ListRowTitle, type ListRowTitleProps, ListRowTop, type ListRowTopProps, ListRowValue, type ListRowValueProps };
1
+ import { ListRow, ListRowBody, ListRowBodyProps, ListRowBottom, ListRowBottomProps, ListRowChevron, ListRowChevronProps, ListRowDescription, ListRowDescriptionProps, ListRowGroup, ListRowGroupProps, ListRowIcon, ListRowIconProps, ListRowMeta, ListRowMetaProps, ListRowProps, ListRowSubtitle, ListRowSubtitleProps, ListRowTitle, ListRowTitleProps, ListRowTop, ListRowTopProps, ListRowValue, ListRowValueProps } from "./list-row.mjs";
2
+ export { ListRow, ListRowBody, type ListRowBodyProps, ListRowBottom, type ListRowBottomProps, ListRowChevron, type ListRowChevronProps, ListRowDescription, type ListRowDescriptionProps, ListRowGroup, type ListRowGroupProps, ListRowIcon, type ListRowIconProps, ListRowMeta, type ListRowMetaProps, type ListRowProps, ListRowSubtitle, type ListRowSubtitleProps, ListRowTitle, type ListRowTitleProps, ListRowTop, type ListRowTopProps, ListRowValue, type ListRowValueProps };
@@ -47,6 +47,15 @@ export declare function ListRowBottom({ className, render, ...props }: ListRowBo
47
47
  export type ListRowTitleProps = useRender.ComponentProps<"span">;
48
48
  /** 16/24 600 on one line; the row's tone sets its weight and ink. */
49
49
  export declare function ListRowTitle({ className, render, ...props }: ListRowTitleProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
50
+ export type ListRowSubtitleProps = useRender.ComponentProps<"span">;
51
+ /**
52
+ * Data about the row's subject on the line under the title: an agent's or a
53
+ * channel's description, a channel's topic, a person's role, an app's byline.
54
+ * 400 in the row's ink, a size under the title, cut to one line. A setting's
55
+ * consequence goes in `ListRowDescription`; a time, a date or a gray count in
56
+ * `ListRowMeta`.
57
+ */
58
+ export declare function ListRowSubtitle({ className, render, ...props }: ListRowSubtitleProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
50
59
  export type ListRowDescriptionProps = useRender.ComponentProps<"span"> & {
51
60
  /** Let the text run onto more lines instead of truncating: a setting's
52
61
  * consequence. */
@@ -109,6 +109,25 @@ function ListRowTitle({ className, render, ...props }) {
109
109
  props: mergeProps(defaultProps, props)
110
110
  });
111
111
  }
112
+ /**
113
+ * Data about the row's subject on the line under the title: an agent's or a
114
+ * channel's description, a channel's topic, a person's role, an app's byline.
115
+ * 400 in the row's ink, a size under the title, cut to one line. A setting's
116
+ * consequence goes in `ListRowDescription`; a time, a date or a gray count in
117
+ * `ListRowMeta`.
118
+ */
119
+ function ListRowSubtitle({ className, render, ...props }) {
120
+ const { subtitle } = listRow();
121
+ const defaultProps = {
122
+ "data-slot": "list-row-subtitle",
123
+ className: subtitle({ className })
124
+ };
125
+ return useRender({
126
+ defaultTagName: "span",
127
+ render,
128
+ props: mergeProps(defaultProps, props)
129
+ });
130
+ }
112
131
  /** The preview (who spoke first: "Wug: …") or a setting's consequence, 14/20 ink-2. */
113
132
  function ListRowDescription({ className, render, wrap = false, ...props }) {
114
133
  const { description } = listRow({ wrap });
@@ -164,4 +183,4 @@ function ListRowChevron({ className, render, ...props }) {
164
183
  });
165
184
  }
166
185
  //#endregion
167
- export { ListRow, ListRowBody, ListRowBottom, ListRowChevron, ListRowDescription, ListRowGroup, ListRowIcon, ListRowMeta, ListRowTitle, ListRowTop, ListRowValue };
186
+ export { ListRow, ListRowBody, ListRowBottom, ListRowChevron, ListRowDescription, ListRowGroup, ListRowIcon, ListRowMeta, ListRowSubtitle, ListRowTitle, ListRowTop, ListRowValue };