@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.
- package/CHANGELOG.md +42 -0
- package/dist/components/dialog/dialog-sheet-context.mjs +7 -2
- package/dist/components/dialog/dialog-sheet.d.mts +3 -2
- package/dist/components/dialog/dialog-sheet.mjs +29 -43
- package/dist/components/dialog/dialog.d.mts +12 -1
- package/dist/components/dialog/dialog.mjs +50 -10
- package/dist/components/fit-toolbar/fit-toolbar.d.mts +110 -0
- package/dist/components/fit-toolbar/fit-toolbar.mjs +469 -0
- package/dist/components/fit-toolbar/fit-toolbar.recipe.mjs +21 -0
- package/dist/components/fit-toolbar/index.d.mts +2 -0
- package/dist/components/list-row/list-row.recipe.d.mts +3 -3
- package/dist/components/list-row/list-row.recipe.mjs +7 -2
- package/dist/components/mobile-nav/mobile-nav-server.d.mts +21 -8
- package/dist/components/mobile-nav/mobile-nav-server.mjs +142 -13
- package/dist/components/segmented-control/segmented-control.d.mts +1 -1
- package/dist/components/segmented-control/segmented-control.mjs +11 -2
- package/dist/components/selection-toolbar/index.d.mts +2 -0
- package/dist/components/selection-toolbar/selection-toolbar.d.mts +100 -0
- package/dist/components/selection-toolbar/selection-toolbar.mjs +217 -0
- package/dist/components/selection-toolbar/selection-toolbar.recipe.mjs +42 -0
- package/dist/index.d.mts +3 -2
- package/dist/index.mjs +3 -2
- package/dist/lib/label-value-fit.mjs +33 -6
- package/dist/lib/use-mobile-viewport.mjs +5 -2
- package/dist/lib/use-soft-keyboard.mjs +72 -0
- package/dist/wip.d.mts +6 -3
- package/dist/wip.mjs +4 -3
- 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:
|
|
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.
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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
|
-
|
|
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;
|