@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.
- package/CHANGELOG.md +80 -0
- package/dist/components/choice-list/choice-list.d.mts +8 -0
- package/dist/components/choice-list/choice-list.mjs +37 -16
- package/dist/components/choice-list/choice-list.recipe.mjs +1 -0
- package/dist/components/composer/composer-drop-overlay.d.mts +9 -4
- package/dist/components/composer/composer-drop-overlay.mjs +2 -5
- 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/index.d.mts +2 -2
- package/dist/components/list-row/list-row.d.mts +9 -0
- package/dist/components/list-row/list-row.mjs +20 -1
- package/dist/components/list-row/list-row.recipe.d.mts +6 -3
- package/dist/components/list-row/list-row.recipe.mjs +8 -2
- package/dist/components/message-action-card/index.d.mts +2 -2
- package/dist/components/message-action-card/message-action-card.d.mts +8 -0
- package/dist/components/message-action-card/message-action-card.mjs +16 -1
- package/dist/components/message-action-card/message-action-card.recipe.mjs +5 -0
- 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/resizable/resizable.recipe.mjs +4 -4
- package/dist/components/scroll-area/use-scrollbars.mjs +2 -1
- 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/components/steps/otter-animation-visibility.mjs +45 -0
- package/dist/components/steps/otter-indicator.css +131 -106
- package/dist/components/steps/otter-indicator.d.mts +5 -8
- package/dist/components/steps/otter-indicator.mjs +38 -314
- package/dist/components/steps/ottie-assets/error.webp +0 -0
- package/dist/components/steps/ottie-assets/finish.webp +0 -0
- package/dist/components/steps/ottie-assets/process.webp +0 -0
- package/dist/components/steps/ottie-assets/wait.webp +0 -0
- package/dist/index.d.mts +5 -4
- package/dist/index.mjs +5 -4
- 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,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 };
|