@botiverse/raft-ui 0.82.0 → 0.84.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 +147 -0
- package/dist/components/app-link-card/app-link-card.mjs +19 -37
- package/dist/components/banner/banner.mjs +15 -2
- 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/drag/drag.recipe.mjs +1 -1
- package/dist/components/fit-toolbar/fit-toolbar.d.mts +165 -0
- package/dist/components/fit-toolbar/fit-toolbar.mjs +736 -0
- package/dist/components/fit-toolbar/fit-toolbar.recipe.mjs +35 -0
- package/dist/components/fit-toolbar/index.d.mts +2 -0
- package/dist/components/lightbox/index.d.mts +2 -2
- package/dist/components/lightbox/lightbox.d.mts +70 -4
- package/dist/components/lightbox/lightbox.mjs +285 -91
- package/dist/components/list-row/list-row.d.mts +14 -1
- package/dist/components/list-row/list-row.mjs +4 -2
- package/dist/components/list-row/list-row.recipe.d.mts +15 -3
- package/dist/components/list-row/list-row.recipe.mjs +18 -1
- package/dist/components/message-action-card/message-action-card-content.recipe.mjs +3 -3
- package/dist/components/message-action-card/message-action-card.mjs +7 -3
- package/dist/components/message-action-card/message-action-card.recipe.mjs +2 -6
- package/dist/components/message-embed/index.d.mts +2 -1
- package/dist/components/message-embed/message-embed.d.mts +8 -1
- package/dist/components/message-embed/message-embed.mjs +2 -2
- package/dist/components/message-embed/message-embed.recipe.d.mts +160 -0
- package/dist/components/message-embed/message-embed.recipe.mjs +49 -10
- package/dist/components/message-forwarded-bundle/message-forwarded-bundle.mjs +1 -0
- package/dist/components/message-forwarded-bundle/message-forwarded-bundle.recipe.mjs +1 -1
- package/dist/components/message-quoted-preview/message-quoted-preview.mjs +2 -0
- package/dist/components/message-replies/message-replies.mjs +1 -1
- package/dist/components/message-replies/message-replies.recipe.mjs +1 -1
- 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/popover/popover.d.mts +11 -2
- package/dist/components/popover/popover.mjs +51 -38
- package/dist/components/preview-card/preview-card.d.mts +5 -1
- package/dist/components/preview-card/preview-card.mjs +24 -27
- package/dist/components/save-status/save-status.d.mts +16 -9
- package/dist/components/save-status/save-status.mjs +19 -30
- package/dist/components/segmented-control/segmented-control.d.mts +1 -1
- package/dist/components/segmented-control/segmented-control.mjs +56 -10
- 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/task-board/task-board.d.mts +5 -0
- package/dist/components/task-board/task-board.mjs +8 -1
- package/dist/components/task-dnd/task-dnd.d.mts +10 -1
- package/dist/components/task-dnd/task-dnd.mjs +2 -1
- package/dist/components/toast/toast.mjs +2 -2
- package/dist/index.d.mts +4 -2
- package/dist/index.mjs +4 -2
- package/dist/lib/label-value-fit.mjs +33 -6
- package/dist/lib/laid-out-children.mjs +32 -0
- package/dist/lib/stacked-actions.mjs +7 -4
- package/dist/lib/use-mobile-viewport.mjs +5 -2
- package/dist/lib/use-soft-keyboard.mjs +72 -0
- package/dist/wip.d.mts +8 -3
- package/dist/wip.mjs +5 -3
- package/package.json +1 -1
|
@@ -2,8 +2,10 @@ import { tv } from "../../lib/tv.mjs";
|
|
|
2
2
|
import { useMergedRef } from "../../lib/use-merged-ref.mjs";
|
|
3
3
|
import { useTapActivation } from "../../lib/tap-activation.mjs";
|
|
4
4
|
import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip/tooltip.mjs";
|
|
5
|
-
import {
|
|
5
|
+
import { useInFitToolbar } from "../fit-toolbar/fit-toolbar.mjs";
|
|
6
|
+
import { cloneElement, createContext, use, useCallback, useLayoutEffect, useRef, useState } from "react";
|
|
6
7
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
import { Toolbar } from "@base-ui/react/toolbar";
|
|
7
9
|
import { Radio } from "@base-ui/react/radio";
|
|
8
10
|
import { RadioGroup } from "@base-ui/react/radio-group";
|
|
9
11
|
//#region src/components/segmented-control/segmented-control.tsx
|
|
@@ -51,6 +53,11 @@ const segmentedControl = tv({
|
|
|
51
53
|
xl: { root: ["@max-xl:**:data-[slot=segmented-control-item]:relative @max-xl:**:data-[slot=segmented-control-item]:w-(--segment-size) @max-xl:**:data-[slot=segmented-control-item]:px-0", "@max-xl:**:data-[slot=segmented-control-label]:sr-only @max-xl:**:data-[slot=segmented-control-count]:sr-only"] },
|
|
52
54
|
"2xl": { root: ["@max-2xl:**:data-[slot=segmented-control-item]:relative @max-2xl:**:data-[slot=segmented-control-item]:w-(--segment-size) @max-2xl:**:data-[slot=segmented-control-item]:px-0", "@max-2xl:**:data-[slot=segmented-control-label]:sr-only @max-2xl:**:data-[slot=segmented-control-count]:sr-only"] }
|
|
53
55
|
},
|
|
56
|
+
inFitToolbar: { true: { root: [
|
|
57
|
+
"shrink-0",
|
|
58
|
+
"data-fit-label:**:data-[slot=segmented-control-item]:relative data-fit-label:**:data-[slot=segmented-control-item]:w-(--segment-size) data-fit-label:**:data-[slot=segmented-control-item]:px-0",
|
|
59
|
+
"data-fit-label:**:data-[slot=segmented-control-label]:sr-only data-fit-label:**:data-[slot=segmented-control-count]:sr-only"
|
|
60
|
+
] } },
|
|
54
61
|
iconsOnlyMeasured: { true: { root: ["data-icons-only:**:data-[slot=segmented-control-item]:relative data-icons-only:**:data-[slot=segmented-control-item]:w-(--segment-size) data-icons-only:**:data-[slot=segmented-control-item]:px-0", "data-icons-only:**:data-[slot=segmented-control-label]:sr-only data-icons-only:**:data-[slot=segmented-control-count]:sr-only"] } }
|
|
55
62
|
}
|
|
56
63
|
});
|
|
@@ -67,6 +74,11 @@ function isIconsOnlyPreset(value) {
|
|
|
67
74
|
}
|
|
68
75
|
const SegmentedControlIconsOnlyContext = createContext(false);
|
|
69
76
|
/**
|
|
77
|
+
* In a FitToolbar: whether the control holds the toolbar's one tab stop
|
|
78
|
+
* (`undefined` outside a toolbar, where the radio group keeps its own).
|
|
79
|
+
*/
|
|
80
|
+
const SegmentedControlToolbarStopContext = createContext(void 0);
|
|
81
|
+
/**
|
|
70
82
|
* A length threshold in CSS pixels, or `undefined` for a value that isn't a
|
|
71
83
|
* `px`, `rem` or `em` length. `em` is the container's font size, as in a
|
|
72
84
|
* container query.
|
|
@@ -216,11 +228,14 @@ function useIconsOnlyWarning(ref, iconsOnlyBelow) {
|
|
|
216
228
|
}
|
|
217
229
|
}, [ref, iconsOnlyBelow]);
|
|
218
230
|
}
|
|
219
|
-
function SegmentedControl({ disabled = false, iconsOnlyBelow, variant = "default", className, ref, ...props }) {
|
|
231
|
+
function SegmentedControl({ disabled = false, iconsOnlyBelow: iconsOnlyBelowProp, variant = "default", className, ref, ...props }) {
|
|
232
|
+
const inFitToolbar = useInFitToolbar();
|
|
233
|
+
const iconsOnlyBelow = inFitToolbar ? void 0 : iconsOnlyBelowProp;
|
|
220
234
|
const preset = isIconsOnlyPreset(iconsOnlyBelow);
|
|
221
235
|
const { root } = segmentedControl({
|
|
222
236
|
iconsOnlyBelow: preset ? iconsOnlyBelow : void 0,
|
|
223
237
|
iconsOnlyMeasured: iconsOnlyBelow !== void 0 && !preset ? true : void 0,
|
|
238
|
+
inFitToolbar: inFitToolbar || void 0,
|
|
224
239
|
variant
|
|
225
240
|
});
|
|
226
241
|
const rootRef = useRef(null);
|
|
@@ -241,21 +256,51 @@ function SegmentedControl({ disabled = false, iconsOnlyBelow, variant = "default
|
|
|
241
256
|
if (start < track.scrollLeft) track.scrollLeft = start;
|
|
242
257
|
else if (end > track.scrollLeft + track.clientWidth) track.scrollLeft = end - track.clientWidth;
|
|
243
258
|
}, [overflowing, value]);
|
|
259
|
+
const group = /* @__PURE__ */ jsx(RadioGroup, {
|
|
260
|
+
ref: setRef,
|
|
261
|
+
disabled,
|
|
262
|
+
"data-slot": "segmented-control",
|
|
263
|
+
"data-overflowing": overflowing ? "" : void 0,
|
|
264
|
+
"data-fit-needs-icon": inFitToolbar ? "" : void 0,
|
|
265
|
+
className: root({ className }),
|
|
266
|
+
...props
|
|
267
|
+
});
|
|
244
268
|
return /* @__PURE__ */ jsx(SegmentedControlIconsOnlyContext, {
|
|
245
|
-
value: iconsOnlyBelow !== void 0,
|
|
246
|
-
children: /* @__PURE__ */ jsx(
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
269
|
+
value: inFitToolbar || iconsOnlyBelow !== void 0,
|
|
270
|
+
children: inFitToolbar ? /* @__PURE__ */ jsx(ToolbarItem, { group }) : group
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* In a FitToolbar the control is one item of the toolbar's list (Base UI
|
|
275
|
+
* Toolbar), so the toolbar keeps one tab stop: its segments are reachable by
|
|
276
|
+
* Tab only while the control holds it, and the toolbar's arrow keys reach
|
|
277
|
+
* them. Registered through `Toolbar.Link`'s render function, the one toolbar
|
|
278
|
+
* part that adds no role or keys of its own: the radio group is the item. Its
|
|
279
|
+
* own tab stop is the segments', never the group's.
|
|
280
|
+
*/
|
|
281
|
+
function ToolbarItem({ group }) {
|
|
282
|
+
const render = useCallback(({ ref, tabIndex, ...itemProps }) => /* @__PURE__ */ jsx(ToolbarItemGroup, {
|
|
283
|
+
group,
|
|
284
|
+
itemProps,
|
|
285
|
+
itemRef: ref,
|
|
286
|
+
stop: tabIndex === 0
|
|
287
|
+
}), [group]);
|
|
288
|
+
return /* @__PURE__ */ jsx(Toolbar.Link, { render });
|
|
289
|
+
}
|
|
290
|
+
function ToolbarItemGroup({ group, itemProps, itemRef, stop }) {
|
|
291
|
+
const [setRef] = useMergedRef(itemRef, group.props.ref);
|
|
292
|
+
return /* @__PURE__ */ jsx(SegmentedControlToolbarStopContext, {
|
|
293
|
+
value: stop,
|
|
294
|
+
children: cloneElement(group, {
|
|
295
|
+
...itemProps,
|
|
296
|
+
ref: setRef
|
|
253
297
|
})
|
|
254
298
|
});
|
|
255
299
|
}
|
|
256
300
|
function SegmentedControlItem({ className, ref, ...props }) {
|
|
257
301
|
const { item } = segmentedControl();
|
|
258
302
|
const iconsOnly = use(SegmentedControlIconsOnlyContext);
|
|
303
|
+
const toolbarStop = use(SegmentedControlToolbarStopContext);
|
|
259
304
|
const tapRef = useTapActivation();
|
|
260
305
|
const [setRef] = useMergedRef(tapRef, ref);
|
|
261
306
|
const radio = /* @__PURE__ */ jsx(Radio.Root, {
|
|
@@ -264,6 +309,7 @@ function SegmentedControlItem({ className, ref, ...props }) {
|
|
|
264
309
|
nativeButton: true,
|
|
265
310
|
"data-slot": "segmented-control-item",
|
|
266
311
|
className: item({ className: `group/segmented-control-item ${className ?? ""}`.trim() }),
|
|
312
|
+
...toolbarStop === false ? { tabIndex: -1 } : {},
|
|
267
313
|
...props
|
|
268
314
|
});
|
|
269
315
|
return iconsOnly ? /* @__PURE__ */ jsx(SegmentedControlItemTooltip, { children: radio }) : radio;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { SelectionToolbar, SelectionToolbarAction, SelectionToolbarActionProps, SelectionToolbarActions, SelectionToolbarActionsProps, SelectionToolbarClose, SelectionToolbarCloseProps, SelectionToolbarCount, SelectionToolbarCountProps, SelectionToolbarCountValue, SelectionToolbarCountValueProps, SelectionToolbarDock, SelectionToolbarDockProps, SelectionToolbarHint, SelectionToolbarHintProps, SelectionToolbarItem, SelectionToolbarItemProps, SelectionToolbarMore, SelectionToolbarMoreItems, SelectionToolbarMoreProps, SelectionToolbarProps } from "./selection-toolbar.mjs";
|
|
2
|
+
export { SelectionToolbar, SelectionToolbarAction, type SelectionToolbarActionProps, SelectionToolbarActions, type SelectionToolbarActionsProps, SelectionToolbarClose, type SelectionToolbarCloseProps, SelectionToolbarCount, type SelectionToolbarCountProps, SelectionToolbarCountValue, type SelectionToolbarCountValueProps, SelectionToolbarDock, type SelectionToolbarDockProps, SelectionToolbarHint, type SelectionToolbarHintProps, SelectionToolbarItem, type SelectionToolbarItemProps, SelectionToolbarMore, SelectionToolbarMoreItems, type SelectionToolbarMoreProps, type SelectionToolbarProps };
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { ButtonProps } from "../button/button.mjs";
|
|
2
|
+
import "../button/index.mjs";
|
|
3
|
+
import { FitToolbarGroupProps, FitToolbarMoreItems, FitToolbarProps } from "../fit-toolbar/fit-toolbar.mjs";
|
|
4
|
+
import "../fit-toolbar/index.mjs";
|
|
5
|
+
import { ReactElement, ReactNode } from "react";
|
|
6
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
7
|
+
//#region src/components/selection-toolbar/selection-toolbar.d.ts
|
|
8
|
+
export type SelectionToolbarDockProps = useRender.ComponentProps<"div">;
|
|
9
|
+
/**
|
|
10
|
+
* Where the toolbar floats: centered, 12px above the bottom of the list or
|
|
11
|
+
* conversation it sits in (plus the safe area), 20px from its sides. Its
|
|
12
|
+
* parent must be positioned.
|
|
13
|
+
*/
|
|
14
|
+
export declare function SelectionToolbarDock({ className, render, ...props }: SelectionToolbarDockProps): ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
15
|
+
export type SelectionToolbarProps = Omit<FitToolbarProps, "wrap" | "orientation">;
|
|
16
|
+
/**
|
|
17
|
+
* The floating toolbar for selected items (messages, tasks): the count, an
|
|
18
|
+
* optional hint, the actions on the selection, More while some are out of
|
|
19
|
+
* the row, then ×. One row at any width: what doesn't fit gives way, measured
|
|
20
|
+
* in the width it floats in, and never wraps. A `role="toolbar"`, one Tab
|
|
21
|
+
* stop with the arrow keys inside: give it an `aria-label`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function SelectionToolbar({ className, ...props }: SelectionToolbarProps): import("react").JSX.Element;
|
|
24
|
+
export type SelectionToolbarCountProps = useRender.ComponentProps<"span">;
|
|
25
|
+
/**
|
|
26
|
+
* What the actions act on ("3 selected"), first. Announced when it changes.
|
|
27
|
+
* Put the number in `SelectionToolbarCountValue`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function SelectionToolbarCount({ className, render, ...props }: SelectionToolbarCountProps): ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
30
|
+
export type SelectionToolbarCountValueProps = useRender.ComponentProps<"span">;
|
|
31
|
+
/** The number in the count, in Space Mono. */
|
|
32
|
+
export declare function SelectionToolbarCountValue({ className, render, ...props }: SelectionToolbarCountValueProps): ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
33
|
+
export type SelectionToolbarHintProps = useRender.ComponentProps<"span">;
|
|
34
|
+
/**
|
|
35
|
+
* A short line after the count, such as "1 will be skipped". The first to
|
|
36
|
+
* give way: it is then read by screen readers only.
|
|
37
|
+
*/
|
|
38
|
+
export declare function SelectionToolbarHint({ className, render, ...props }: SelectionToolbarHintProps): ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
39
|
+
export type SelectionToolbarItemProps = {
|
|
40
|
+
/** The control, such as `<Checkbox />`. It keeps its own role and keys. */
|
|
41
|
+
children: ReactElement<Record<string, unknown>>;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Any other control of the toolbar, such as a thread's select-all checkbox
|
|
45
|
+
* before the count, so the arrow keys reach it. It takes the toolbar's focus
|
|
46
|
+
* handling only: the control keeps its role and its own keys.
|
|
47
|
+
*/
|
|
48
|
+
export declare function SelectionToolbarItem({ children }: SelectionToolbarItemProps): import("react").JSX.Element;
|
|
49
|
+
export type SelectionToolbarActionsProps = Omit<FitToolbarGroupProps, "wrapOrder" | "firstLine" | "collapseOrder" | "collapsed">;
|
|
50
|
+
/** The actions on the selection, in one row. */
|
|
51
|
+
export declare function SelectionToolbarActions({ className, ...props }: SelectionToolbarActionsProps): import("react").JSX.Element;
|
|
52
|
+
export type SelectionToolbarActionProps = Omit<ButtonProps, "variant" | "size" | "children"> & {
|
|
53
|
+
/** The action's 14px icon. */
|
|
54
|
+
icon: ReactNode;
|
|
55
|
+
/** The label. While only the icon shows, the label is its accessible name and its tooltip. */
|
|
56
|
+
children: ReactNode;
|
|
57
|
+
/** Moves into More before the labels give way, the last first. */
|
|
58
|
+
secondary?: boolean;
|
|
59
|
+
} & ({
|
|
60
|
+
menu?: false;
|
|
61
|
+
/**
|
|
62
|
+
* What the action is in More's menu while it is there. By default a
|
|
63
|
+
* `DropdownMenuItem` with its icon and label that calls its `onClick`.
|
|
64
|
+
*/
|
|
65
|
+
moreItem?: ReactNode;
|
|
66
|
+
} | {
|
|
67
|
+
/** The action opens a menu: a 12px chevron follows the label. Open the menu upward. */
|
|
68
|
+
menu: true;
|
|
69
|
+
/**
|
|
70
|
+
* Required: its menu in More's (a `DropdownMenuSubmenu` with the same
|
|
71
|
+
* choices). Its `onClick` only opens its own menu, so it has no
|
|
72
|
+
* default, and without one it would vanish into More.
|
|
73
|
+
*/
|
|
74
|
+
moreItem: ReactNode;
|
|
75
|
+
});
|
|
76
|
+
/**
|
|
77
|
+
* One action on the selection: a 14px icon and a label, an `md` ghost button
|
|
78
|
+
* with a mouse on a desktop, a 44px bar button showing its icon alone on a
|
|
79
|
+
* phone and a touch screen.
|
|
80
|
+
*/
|
|
81
|
+
export declare function SelectionToolbarAction({ icon, children, secondary, menu, moreItem, className, onClick, disabled, ...props }: SelectionToolbarActionProps): import("react").JSX.Element;
|
|
82
|
+
export type SelectionToolbarMoreProps = Omit<ButtonProps, "variant" | "size" | "aria-label"> & {
|
|
83
|
+
/** Its accessible name, such as "More actions". */
|
|
84
|
+
"aria-label": string;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* More, after the actions, shown only while some are in it. Render it as a
|
|
88
|
+
* `DropdownMenuTrigger` whose menu opens upward (`side="top"`), with
|
|
89
|
+
* `SelectionToolbarMoreItems` in the menu.
|
|
90
|
+
*/
|
|
91
|
+
export declare function SelectionToolbarMore({ className, children, ...props }: SelectionToolbarMoreProps): import("react").JSX.Element;
|
|
92
|
+
/** The actions in More, in their order, each as its `moreItem`: put it in More's menu. */
|
|
93
|
+
export declare const SelectionToolbarMoreItems: typeof FitToolbarMoreItems;
|
|
94
|
+
export type SelectionToolbarCloseProps = Omit<ButtonProps, "variant" | "size" | "aria-label"> & {
|
|
95
|
+
/** Its accessible name, such as "Cancel". */
|
|
96
|
+
"aria-label": string;
|
|
97
|
+
};
|
|
98
|
+
/** ×, last: ends the selection, as Escape does. */
|
|
99
|
+
export declare function SelectionToolbarClose({ className, children, ...props }: SelectionToolbarCloseProps): import("react").JSX.Element;
|
|
100
|
+
//#endregion
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { Button } from "../button/button.mjs";
|
|
2
|
+
import { DropdownMenuItem } from "../dropdown-menu/dropdown-menu.mjs";
|
|
3
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip/tooltip.mjs";
|
|
4
|
+
import { FitToolbar, FitToolbarGroup, FitToolbarMoreItems, useFitItem, useFitMore } from "../fit-toolbar/fit-toolbar.mjs";
|
|
5
|
+
import { selectionToolbar } from "./selection-toolbar.recipe.mjs";
|
|
6
|
+
import { ChevronDown, MoreHorizontal, X } from "lucide-react";
|
|
7
|
+
import { cloneElement, useCallback } from "react";
|
|
8
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
import { mergeProps } from "@base-ui/react/merge-props";
|
|
10
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
11
|
+
import { Toolbar } from "@base-ui/react/toolbar";
|
|
12
|
+
//#region src/components/selection-toolbar/selection-toolbar.tsx
|
|
13
|
+
/**
|
|
14
|
+
* The order things give way in as the toolbar runs out of room: the hint,
|
|
15
|
+
* then the secondary actions into More (the last first), then the labels (the
|
|
16
|
+
* last first, so the first action's goes last), then the other actions into
|
|
17
|
+
* More, the last first. The count and × never do.
|
|
18
|
+
*/
|
|
19
|
+
const HINT_STEP = 10;
|
|
20
|
+
const SECONDARY_STEP = 20;
|
|
21
|
+
const LABEL_STEP = 30;
|
|
22
|
+
const MORE_STEP = 40;
|
|
23
|
+
/**
|
|
24
|
+
* Where the toolbar floats: centered, 12px above the bottom of the list or
|
|
25
|
+
* conversation it sits in (plus the safe area), 20px from its sides. Its
|
|
26
|
+
* parent must be positioned.
|
|
27
|
+
*/
|
|
28
|
+
function SelectionToolbarDock({ className, render, ...props }) {
|
|
29
|
+
const { dock } = selectionToolbar();
|
|
30
|
+
const defaultProps = { className: dock({ className }) };
|
|
31
|
+
return useRender({
|
|
32
|
+
defaultTagName: "div",
|
|
33
|
+
render,
|
|
34
|
+
props: mergeProps(defaultProps, props),
|
|
35
|
+
state: { slot: "selection-toolbar-dock" }
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The floating toolbar for selected items (messages, tasks): the count, an
|
|
40
|
+
* optional hint, the actions on the selection, More while some are out of
|
|
41
|
+
* the row, then ×. One row at any width: what doesn't fit gives way, measured
|
|
42
|
+
* in the width it floats in, and never wraps. A `role="toolbar"`, one Tab
|
|
43
|
+
* stop with the arrow keys inside: give it an `aria-label`.
|
|
44
|
+
*/
|
|
45
|
+
function SelectionToolbar({ className, ...props }) {
|
|
46
|
+
const { root } = selectionToolbar();
|
|
47
|
+
return /* @__PURE__ */ jsx(FitToolbar, {
|
|
48
|
+
"data-slot": "selection-toolbar",
|
|
49
|
+
className: root({ className }),
|
|
50
|
+
...props
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* What the actions act on ("3 selected"), first. Announced when it changes.
|
|
55
|
+
* Put the number in `SelectionToolbarCountValue`.
|
|
56
|
+
*/
|
|
57
|
+
function SelectionToolbarCount({ className, render, ...props }) {
|
|
58
|
+
const { count } = selectionToolbar();
|
|
59
|
+
const defaultProps = {
|
|
60
|
+
"aria-live": "polite",
|
|
61
|
+
"aria-atomic": true,
|
|
62
|
+
className: count({ className })
|
|
63
|
+
};
|
|
64
|
+
return useRender({
|
|
65
|
+
defaultTagName: "span",
|
|
66
|
+
render,
|
|
67
|
+
props: mergeProps(defaultProps, props),
|
|
68
|
+
state: { slot: "selection-toolbar-count" }
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/** The number in the count, in Space Mono. */
|
|
72
|
+
function SelectionToolbarCountValue({ className, render, ...props }) {
|
|
73
|
+
const { countValue } = selectionToolbar();
|
|
74
|
+
const defaultProps = { className: countValue({ className }) };
|
|
75
|
+
return useRender({
|
|
76
|
+
defaultTagName: "span",
|
|
77
|
+
render,
|
|
78
|
+
props: mergeProps(defaultProps, props),
|
|
79
|
+
state: { slot: "selection-toolbar-count-value" }
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* A short line after the count, such as "1 will be skipped". The first to
|
|
84
|
+
* give way: it is then read by screen readers only.
|
|
85
|
+
*/
|
|
86
|
+
function SelectionToolbarHint({ className, render, ...props }) {
|
|
87
|
+
const { hint } = selectionToolbar();
|
|
88
|
+
const item = useFitItem({ label: HINT_STEP });
|
|
89
|
+
const defaultProps = {
|
|
90
|
+
className: hint({ className }),
|
|
91
|
+
...item.props
|
|
92
|
+
};
|
|
93
|
+
return useRender({
|
|
94
|
+
defaultTagName: "span",
|
|
95
|
+
render,
|
|
96
|
+
props: mergeProps(defaultProps, props),
|
|
97
|
+
state: { slot: "selection-toolbar-hint" }
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Any other control of the toolbar, such as a thread's select-all checkbox
|
|
102
|
+
* before the count, so the arrow keys reach it. It takes the toolbar's focus
|
|
103
|
+
* handling only: the control keeps its role and its own keys.
|
|
104
|
+
*/
|
|
105
|
+
function SelectionToolbarItem({ children }) {
|
|
106
|
+
const renderItem = useCallback((itemProps) => {
|
|
107
|
+
const { role: _role, type: _type, onClick: _onClick, onKeyDown: _onKeyDown, onKeyUp: _onKeyUp, onMouseDown: _onMouseDown, onPointerDown: _onPointerDown, "aria-disabled": _ariaDisabled, ...composite } = itemProps;
|
|
108
|
+
return cloneElement(children, mergeProps(children.props, {
|
|
109
|
+
...composite,
|
|
110
|
+
"data-selection-toolbar-item": ""
|
|
111
|
+
}));
|
|
112
|
+
}, [children]);
|
|
113
|
+
return /* @__PURE__ */ jsx(Toolbar.Button, {
|
|
114
|
+
nativeButton: false,
|
|
115
|
+
render: renderItem
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
/** The actions on the selection, in one row. */
|
|
119
|
+
function SelectionToolbarActions({ className, ...props }) {
|
|
120
|
+
const { actions } = selectionToolbar();
|
|
121
|
+
return /* @__PURE__ */ jsx(FitToolbarGroup, {
|
|
122
|
+
"data-slot": "selection-toolbar-actions",
|
|
123
|
+
className: actions({ className }),
|
|
124
|
+
...props
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
let warnedMenuWithoutMoreItem = false;
|
|
128
|
+
function warnMenuWithoutMoreItem() {
|
|
129
|
+
if (warnedMenuWithoutMoreItem) return;
|
|
130
|
+
warnedMenuWithoutMoreItem = true;
|
|
131
|
+
console.warn("raft-ui: a SelectionToolbarAction with `menu` has no `moreItem`; it would vanish when it moves into More. Give it its menu as a DropdownMenuSubmenu.");
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* One action on the selection: a 14px icon and a label, an `md` ghost button
|
|
135
|
+
* with a mouse on a desktop, a 44px bar button showing its icon alone on a
|
|
136
|
+
* phone and a touch screen.
|
|
137
|
+
*/
|
|
138
|
+
function SelectionToolbarAction({ icon, children, secondary = false, menu = false, moreItem, className, onClick, disabled, ...props }) {
|
|
139
|
+
const { action, actionLabel, actionChevron } = selectionToolbar({ menu });
|
|
140
|
+
if (process.env.NODE_ENV !== "production" && menu && moreItem === void 0) warnMenuWithoutMoreItem();
|
|
141
|
+
const item = useFitItem({
|
|
142
|
+
label: LABEL_STEP,
|
|
143
|
+
more: secondary ? SECONDARY_STEP : MORE_STEP,
|
|
144
|
+
moreItem: moreItem !== void 0 ? moreItem : menu ? null : /* @__PURE__ */ jsxs(DropdownMenuItem, {
|
|
145
|
+
disabled,
|
|
146
|
+
onClick,
|
|
147
|
+
children: [icon, children]
|
|
148
|
+
})
|
|
149
|
+
});
|
|
150
|
+
return /* @__PURE__ */ jsxs(Tooltip, {
|
|
151
|
+
disabled: !item.taken("label"),
|
|
152
|
+
children: [/* @__PURE__ */ jsxs(TooltipTrigger, {
|
|
153
|
+
"data-slot": "button",
|
|
154
|
+
render: /* @__PURE__ */ jsx(Toolbar.Button, { render: /* @__PURE__ */ jsx(Button, {
|
|
155
|
+
"data-selection-toolbar-action": "",
|
|
156
|
+
"data-secondary": secondary ? "" : void 0,
|
|
157
|
+
variant: "ghost",
|
|
158
|
+
size: "md",
|
|
159
|
+
className: action({ className }),
|
|
160
|
+
onClick,
|
|
161
|
+
disabled,
|
|
162
|
+
...item.props,
|
|
163
|
+
...props
|
|
164
|
+
}) }),
|
|
165
|
+
children: [
|
|
166
|
+
icon,
|
|
167
|
+
/* @__PURE__ */ jsx("span", {
|
|
168
|
+
"data-slot": "selection-toolbar-action-label",
|
|
169
|
+
className: actionLabel(),
|
|
170
|
+
children
|
|
171
|
+
}),
|
|
172
|
+
menu ? /* @__PURE__ */ jsx(ChevronDown, {
|
|
173
|
+
"aria-hidden": true,
|
|
174
|
+
className: actionChevron()
|
|
175
|
+
}) : null
|
|
176
|
+
]
|
|
177
|
+
}), /* @__PURE__ */ jsx(TooltipContent, { children })]
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* More, after the actions, shown only while some are in it. Render it as a
|
|
182
|
+
* `DropdownMenuTrigger` whose menu opens upward (`side="top"`), with
|
|
183
|
+
* `SelectionToolbarMoreItems` in the menu.
|
|
184
|
+
*/
|
|
185
|
+
function SelectionToolbarMore({ className, children, ...props }) {
|
|
186
|
+
const { more } = selectionToolbar();
|
|
187
|
+
const fit = useFitMore();
|
|
188
|
+
return /* @__PURE__ */ jsx(Toolbar.Button, {
|
|
189
|
+
render: /* @__PURE__ */ jsx(Button, {
|
|
190
|
+
"data-selection-toolbar-more": "",
|
|
191
|
+
variant: "ghost",
|
|
192
|
+
size: "md",
|
|
193
|
+
className: more({ className }),
|
|
194
|
+
...fit.props,
|
|
195
|
+
...props
|
|
196
|
+
}),
|
|
197
|
+
children: children ?? /* @__PURE__ */ jsx(MoreHorizontal, {})
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
/** The actions in More, in their order, each as its `moreItem`: put it in More's menu. */
|
|
201
|
+
const SelectionToolbarMoreItems = FitToolbarMoreItems;
|
|
202
|
+
/** ×, last: ends the selection, as Escape does. */
|
|
203
|
+
function SelectionToolbarClose({ className, children, ...props }) {
|
|
204
|
+
const { close } = selectionToolbar();
|
|
205
|
+
return /* @__PURE__ */ jsx(Toolbar.Button, {
|
|
206
|
+
render: /* @__PURE__ */ jsx(Button, {
|
|
207
|
+
"data-selection-toolbar-close": "",
|
|
208
|
+
variant: "ghost",
|
|
209
|
+
size: "icon-md",
|
|
210
|
+
className: close({ className }),
|
|
211
|
+
...props
|
|
212
|
+
}),
|
|
213
|
+
children: children ?? /* @__PURE__ */ jsx(X, {})
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
//#endregion
|
|
217
|
+
export { SelectionToolbar, SelectionToolbarAction, SelectionToolbarActions, SelectionToolbarClose, SelectionToolbarCount, SelectionToolbarCountValue, SelectionToolbarDock, SelectionToolbarHint, SelectionToolbarItem, SelectionToolbarMore, SelectionToolbarMoreItems };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { tv } from "../../lib/tv.mjs";
|
|
2
|
+
//#region src/components/selection-toolbar/selection-toolbar.recipe.ts
|
|
3
|
+
const selectionToolbar = tv({
|
|
4
|
+
slots: {
|
|
5
|
+
dock: [
|
|
6
|
+
"pointer-events-none absolute inset-x-0 bottom-0 z-10 flex justify-center",
|
|
7
|
+
"px-5 pb-[calc(--spacing(3)+env(safe-area-inset-bottom))]",
|
|
8
|
+
"*:pointer-events-auto"
|
|
9
|
+
],
|
|
10
|
+
root: [
|
|
11
|
+
"w-fit gap-0 md:pointer-fine:gap-1",
|
|
12
|
+
"h-(--header-height) px-(--header-air) py-0",
|
|
13
|
+
"rounded-container bg-layer-popover ring-1 ring-line-seam shadow-float",
|
|
14
|
+
"**:data-[slot=button]:after:inset-x-[-1.5px] md:pointer-fine:**:data-[slot=button]:after:inset-x-[-3px]",
|
|
15
|
+
"*:data-[slot=button]:first:after:-left-[(var(--header-air)+1.5px)] *:data-[slot=button]:last:after:-right-[(var(--header-air)+1.5px)]"
|
|
16
|
+
],
|
|
17
|
+
count: ["shrink-0 pr-0 pl-2 md:pointer-fine:px-2", "whitespace-nowrap text-callout font-bold text-foreground tabular-nums"],
|
|
18
|
+
countValue: ["font-mono"],
|
|
19
|
+
hint: ["shrink-0 pr-1 whitespace-nowrap text-caption text-foreground-hint", "data-fit-label:sr-only"],
|
|
20
|
+
actions: ["gap-0 md:pointer-fine:gap-1"],
|
|
21
|
+
action: [
|
|
22
|
+
"[&_svg:not([class*='size-'])]:size-3.5",
|
|
23
|
+
"size-(--bar-button-size) justify-center px-0 after:inset-y-[-1.5px]",
|
|
24
|
+
"rounded-(--bar-button-radius) md:pointer-fine:rounded-control",
|
|
25
|
+
"md:pointer-fine:w-auto md:pointer-fine:pr-3 md:pointer-fine:pl-2.5",
|
|
26
|
+
"md:pointer-fine:data-fit-label:w-(--control-height) md:pointer-fine:data-fit-label:px-0",
|
|
27
|
+
"data-fit-more:hidden"
|
|
28
|
+
],
|
|
29
|
+
actionLabel: ["sr-only md:pointer-fine:not-sr-only md:pointer-fine:in-data-fit-label:sr-only"],
|
|
30
|
+
actionChevron: ["-ml-1 hidden size-3 md:pointer-fine:block md:pointer-fine:in-data-fit-label:hidden"],
|
|
31
|
+
more: [
|
|
32
|
+
"hidden data-fit-shown:inline-flex [&_svg:not([class*='size-'])]:size-3.5",
|
|
33
|
+
"size-(--bar-button-size) px-0 after:inset-y-[-1.5px]",
|
|
34
|
+
"rounded-(--bar-button-radius) md:pointer-fine:rounded-control",
|
|
35
|
+
"md:pointer-fine:w-(--control-height)"
|
|
36
|
+
],
|
|
37
|
+
close: ["size-(--bar-button-size) after:inset-y-[-1.5px]", "rounded-(--bar-button-radius) md:pointer-fine:rounded-control"]
|
|
38
|
+
},
|
|
39
|
+
variants: { menu: { true: { action: "md:pointer-fine:pr-2.5" } } }
|
|
40
|
+
});
|
|
41
|
+
//#endregion
|
|
42
|
+
export { selectionToolbar };
|
|
@@ -22,6 +22,11 @@ export declare function TaskBoardColumnBadge({ status, children, className, rend
|
|
|
22
22
|
export type TaskBoardColumnCountProps = useRender.ComponentProps<"span">;
|
|
23
23
|
export declare function TaskBoardColumnCount({ className, render, ...props }: TaskBoardColumnCountProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
24
24
|
export type TaskBoardColumnChevronProps = useRender.ComponentProps<"span">;
|
|
25
|
+
/**
|
|
26
|
+
* The arrow that shows whether the column is open: right while it is collapsed, down while it is open. It draws a
|
|
27
|
+
* `ChevronDown` when given no children, so a column heading always has it: a collapsed column shows only its heading,
|
|
28
|
+
* and without the arrow "Closed 2" over an empty space reads as two tasks that failed to load.
|
|
29
|
+
*/
|
|
25
30
|
export declare function TaskBoardColumnChevron({ className, render, ...props }: TaskBoardColumnChevronProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
26
31
|
export type TaskBoardColumnPanelProps = Omit<ComponentProps<typeof Collapsible.Panel>, "className"> & {
|
|
27
32
|
className?: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { cn } from "../../cn.mjs";
|
|
2
2
|
import { TaskStatusIcon } from "../tasks/task-status-icon.mjs";
|
|
3
3
|
import { taskBoardColumn } from "./task-board-column.recipe.mjs";
|
|
4
|
+
import { ChevronDown } from "lucide-react";
|
|
4
5
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
6
|
import { mergeProps } from "@base-ui/react/merge-props";
|
|
6
7
|
import { useRender } from "@base-ui/react/use-render";
|
|
@@ -71,11 +72,17 @@ function TaskBoardColumnCount({ className, render, ...props }) {
|
|
|
71
72
|
props: mergeProps(defaultProps, props)
|
|
72
73
|
});
|
|
73
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* The arrow that shows whether the column is open: right while it is collapsed, down while it is open. It draws a
|
|
77
|
+
* `ChevronDown` when given no children, so a column heading always has it: a collapsed column shows only its heading,
|
|
78
|
+
* and without the arrow "Closed 2" over an empty space reads as two tasks that failed to load.
|
|
79
|
+
*/
|
|
74
80
|
function TaskBoardColumnChevron({ className, render, ...props }) {
|
|
75
81
|
const { chevron } = taskBoardColumn();
|
|
76
82
|
const defaultProps = {
|
|
77
83
|
"data-slot": "task-board-column-chevron",
|
|
78
|
-
className: chevron({ className })
|
|
84
|
+
className: chevron({ className }),
|
|
85
|
+
children: /* @__PURE__ */ jsx(ChevronDown, { "aria-hidden": true })
|
|
79
86
|
};
|
|
80
87
|
return useRender({
|
|
81
88
|
defaultTagName: "span",
|
|
@@ -52,8 +52,17 @@ export type TaskDropZoneProps = useRender.ComponentProps<"div", TaskDropZoneStat
|
|
|
52
52
|
id: UniqueIdentifier;
|
|
53
53
|
disabled?: boolean;
|
|
54
54
|
data?: TaskDropData;
|
|
55
|
+
/**
|
|
56
|
+
* The zone is itself the place, as a column sorted by a field is: its
|
|
57
|
+
* cards carry no `dropId` and no placeholder opens between them. While a
|
|
58
|
+
* card it allows is over it, it carries `data-drag-into` and takes
|
|
59
|
+
* `fill-subtle`; never at rest, and never while it refuses the card.
|
|
60
|
+
* Without it the zone is never filled, and only a `TaskDropPlaceholder`
|
|
61
|
+
* says where the card lands.
|
|
62
|
+
*/
|
|
63
|
+
dragInto?: boolean;
|
|
55
64
|
};
|
|
56
|
-
export declare function TaskDropZone({ id, data, disabled, render, ...props }: TaskDropZoneProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
65
|
+
export declare function TaskDropZone({ id, data, disabled, dragInto, render, ...props }: TaskDropZoneProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
57
66
|
export type TaskDropPlaceholderProps = useRender.ComponentProps<"div", TaskDropZoneState> & {
|
|
58
67
|
id: UniqueIdentifier;
|
|
59
68
|
placement?: TaskDropPlacement;
|
|
@@ -174,7 +174,7 @@ function assignRef(ref, value) {
|
|
|
174
174
|
if (typeof ref === "function") ref(value);
|
|
175
175
|
else if (ref) ref.current = value;
|
|
176
176
|
}
|
|
177
|
-
function TaskDropZone({ id, data, disabled = false, render, ...props }) {
|
|
177
|
+
function TaskDropZone({ id, data, disabled = false, dragInto = false, render, ...props }) {
|
|
178
178
|
const context = use(TaskDndContext);
|
|
179
179
|
const { setNodeRef, isOver } = useDroppable({
|
|
180
180
|
id,
|
|
@@ -190,6 +190,7 @@ function TaskDropZone({ id, data, disabled = false, render, ...props }) {
|
|
|
190
190
|
props: {
|
|
191
191
|
"data-slot": "task-drop-zone",
|
|
192
192
|
"data-drop-state": dropState,
|
|
193
|
+
"data-drag-into": dragInto && dropState === "allowed" ? "" : void 0,
|
|
193
194
|
...props
|
|
194
195
|
}
|
|
195
196
|
});
|
|
@@ -10,8 +10,8 @@ import { Toast } from "@base-ui/react/toast";
|
|
|
10
10
|
//#region src/components/toast/toast.tsx
|
|
11
11
|
const toastStyles = tv({
|
|
12
12
|
slots: {
|
|
13
|
-
viewport: ["pointer-events-none isolate z-
|
|
14
|
-
positioner: ["isolate z-[calc(
|
|
13
|
+
viewport: ["pointer-events-none isolate z-80 w-[min(24rem,calc(100vw-2rem))]", "outline-none"],
|
|
14
|
+
positioner: ["isolate z-[calc(80-var(--toast-index))] outline-none"],
|
|
15
15
|
root: [
|
|
16
16
|
"group/toast pointer-events-auto",
|
|
17
17
|
"select-text",
|