@botiverse/raft-ui 0.82.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 (28) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/components/dialog/dialog-sheet-context.mjs +7 -2
  3. package/dist/components/dialog/dialog-sheet.d.mts +3 -2
  4. package/dist/components/dialog/dialog-sheet.mjs +29 -43
  5. package/dist/components/dialog/dialog.d.mts +12 -1
  6. package/dist/components/dialog/dialog.mjs +50 -10
  7. package/dist/components/fit-toolbar/fit-toolbar.d.mts +110 -0
  8. package/dist/components/fit-toolbar/fit-toolbar.mjs +469 -0
  9. package/dist/components/fit-toolbar/fit-toolbar.recipe.mjs +21 -0
  10. package/dist/components/fit-toolbar/index.d.mts +2 -0
  11. package/dist/components/list-row/list-row.recipe.d.mts +3 -3
  12. package/dist/components/list-row/list-row.recipe.mjs +7 -2
  13. package/dist/components/mobile-nav/mobile-nav-server.d.mts +21 -8
  14. package/dist/components/mobile-nav/mobile-nav-server.mjs +142 -13
  15. package/dist/components/segmented-control/segmented-control.d.mts +1 -1
  16. package/dist/components/segmented-control/segmented-control.mjs +11 -2
  17. package/dist/components/selection-toolbar/index.d.mts +2 -0
  18. package/dist/components/selection-toolbar/selection-toolbar.d.mts +100 -0
  19. package/dist/components/selection-toolbar/selection-toolbar.mjs +217 -0
  20. package/dist/components/selection-toolbar/selection-toolbar.recipe.mjs +42 -0
  21. package/dist/index.d.mts +3 -2
  22. package/dist/index.mjs +3 -2
  23. package/dist/lib/label-value-fit.mjs +33 -6
  24. package/dist/lib/use-mobile-viewport.mjs +5 -2
  25. package/dist/lib/use-soft-keyboard.mjs +72 -0
  26. package/dist/wip.d.mts +6 -3
  27. package/dist/wip.mjs +4 -3
  28. package/package.json +1 -1
@@ -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 };
@@ -5,7 +5,7 @@ export declare const listRow: import("tailwind-variants/lite").TVReturnType<{
5
5
  root: string;
6
6
  };
7
7
  plain: {
8
- root: string;
8
+ root: string[];
9
9
  };
10
10
  entry: {
11
11
  root: string;
@@ -47,7 +47,7 @@ export declare const listRow: import("tailwind-variants/lite").TVReturnType<{
47
47
  root: string;
48
48
  };
49
49
  plain: {
50
- root: string;
50
+ root: string[];
51
51
  };
52
52
  entry: {
53
53
  root: string;
@@ -89,7 +89,7 @@ export declare const listRow: import("tailwind-variants/lite").TVReturnType<{
89
89
  root: string;
90
90
  };
91
91
  plain: {
92
- root: string;
92
+ root: string[];
93
93
  };
94
94
  entry: {
95
95
  root: string;
@@ -7,6 +7,7 @@ const listRow = tv({
7
7
  "group/list-row flex w-full min-w-0 items-center gap-3 py-3 text-left",
8
8
  "font-sans no-underline",
9
9
  "[--list-row-ink:var(--foreground)] [--list-row-ink-2:var(--foreground-muted)] [--list-row-ink-3:var(--foreground-hint)]",
10
+ "[--list-row-status-space:0px]",
10
11
  "text-(--list-row-ink)",
11
12
  "not-disabled:not-aria-disabled:data-[tone=unread]:[--list-row-ink-2:var(--foreground)]",
12
13
  "not-disabled:not-aria-disabled:data-[tone=muted]:[--list-row-ink:var(--foreground-muted)]",
@@ -18,7 +19,7 @@ const listRow = tv({
18
19
  "aria-disabled:[--list-row-ink-2:var(--foreground-disabled)] aria-disabled:[--list-row-ink-3:var(--foreground-disabled)]"
19
20
  ],
20
21
  icon: ["flex shrink-0 items-center justify-center", "text-(--list-row-ink-3) [&>svg]:size-5 [&>svg]:shrink-0"],
21
- body: ["flex min-w-0 flex-1 flex-col gap-0.5"],
22
+ body: ["flex min-w-0 flex-1 flex-col gap-0.5", "-mb-(--list-row-status-space,0px)"],
22
23
  line: ["flex min-w-0 items-center gap-2"],
23
24
  title: [
24
25
  "min-w-0 flex-1 truncate",
@@ -34,7 +35,11 @@ const listRow = tv({
34
35
  variants: {
35
36
  variant: {
36
37
  default: { root: "min-h-16 pointer-coarse:min-h-14 pointer-coarse:py-2 [--list-row-rule-start:63px]" },
37
- plain: { root: "py-(--settings-row-inset) [--list-row-rule-start:16px]" },
38
+ plain: { root: [
39
+ "py-(--settings-row-inset) pb-[calc(var(--settings-row-inset)+var(--list-row-status-space))]",
40
+ "has-[>[data-slot=list-row-body]>[data-slot=save-status]]:[--list-row-status-space:calc(--spacing(1)+var(--text-footnote--line-height))]",
41
+ "[--list-row-rule-start:16px]"
42
+ ] },
38
43
  entry: { root: "" }
39
44
  },
40
45
  interactive: {
@@ -19,19 +19,32 @@ export declare function MobileNavServerItem({ className, id, ref, onClick, onCon
19
19
  export type MobileNavServerPopoverContentProps = PopoverContentProps;
20
20
  /**
21
21
  * The popover above the tab bar, 8px from each side of it and 8px above it,
22
- * as tall as its content and at most 60% of the screen. It stops below the
23
- * top safe area (a notch in a standalone web app): Base UI's collision padding
24
- * takes only pixels, so the height cap subtracts `env(safe-area-inset-top)`.
25
- * The bottom needs none: the tab bar under the popover already pads the
26
- * bottom safe area. A column:
22
+ * as tall as its content and at most 60% of the screen. The one exception:
23
+ * the list always keeps one whole server row, so where the parts after it
24
+ * leave less, the popover grows past 60% as far as that row needs (the
25
+ * floor, measured from its children into `--mobile-nav-server-popover-floor`).
26
+ * Where even that doesn't fit, the whole popover scrolls, the footer and the
27
+ * pages with it: the children sit in a ScrollArea
28
+ * (`data-slot="mobile-nav-server-popover-scroll"`) inside the popup. It stops below the top safe area (a notch in a standalone
29
+ * web app): Base UI's collision padding takes only pixels, so the height cap
30
+ * subtracts `env(safe-area-inset-top)`. The bottom needs none: the tab bar
31
+ * under the popover already pads the bottom safe area. A column:
27
32
  * `MobileNavServerPopoverList` takes the room left and scrolls, and the
28
33
  * destinations and the footer after it stay in view.
34
+ *
35
+ * Focus on open goes to the current server's row (`aria-current="true"` in
36
+ * the list), which the list has already scrolled into view, so focusing it
37
+ * never scrolls the list back to the top. A touch opens it with focus on the
38
+ * popover itself, without scrolling, so no keyboard comes up. `initialFocus`
39
+ * overrides this.
29
40
  */
30
- export declare function MobileNavServerPopoverContent({ anchor, side, align, sideOffset, collisionPadding, className, ...props }: MobileNavServerPopoverContentProps): import("react").JSX.Element;
41
+ export declare function MobileNavServerPopoverContent({ anchor, side, align, sideOffset, collisionPadding, initialFocus, className, children, ref, ...props }: MobileNavServerPopoverContentProps): import("react").JSX.Element;
31
42
  export type MobileNavServerPopoverListProps = ScrollAreaProps;
32
43
  /** The server list: takes the room left in the popover and scrolls inside it,
33
- * a ScrollArea (a column with no gap, so its gutters move nothing). */
34
- export declare function MobileNavServerPopoverList({ className, ...props }: MobileNavServerPopoverListProps): import("react").JSX.Element;
44
+ * a ScrollArea (a column with no gap, so its gutters move nothing). It is
45
+ * never shorter than one whole two-line row, and it opens scrolled to the
46
+ * current server's row (`aria-current="true"`), with no animation. */
47
+ export declare function MobileNavServerPopoverList({ className, ref, ...props }: MobileNavServerPopoverListProps): import("react").JSX.Element;
35
48
  export type MobileNavServerPopoverGroupProps = ComponentPropsWithRef<"div">;
36
49
  /** Rows that always stay in view, such as the footer actions. */
37
50
  export declare function MobileNavServerPopoverGroup({ className, ...props }: MobileNavServerPopoverGroupProps): import("react").JSX.Element;