@botiverse/raft-ui 0.15.0 → 0.17.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 +37 -0
- package/dist/index.d.mts +16 -4
- package/dist/index.mjs +188 -29
- package/dist/{message-list-03unCFnE.mjs → touch-drag-CbWqiMlZ.mjs} +96 -1
- package/dist/wip.d.mts +19 -2
- package/dist/wip.mjs +57 -40
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,40 @@
|
|
|
1
|
+
## [0.17.0](https://github.com/botiverse/raft-design/compare/v0.16.0...v0.17.0) (2026-10-21)
|
|
2
|
+
|
|
3
|
+
### Design System
|
|
4
|
+
|
|
5
|
+
- **Dragging on a touch screen**: on an item that can be moved, holding still opens its menu and holding then moving drags it. The item arms after 500ms, the same moment raft-ui's long-press menu opens, and the drag starts on the first 10px of movement; moving earlier is a scroll, and a second finger cancels. The component that owns the item's menu closes it when the drag starts. ([#662](https://github.com/botiverse/raft-design/pull/662))
|
|
6
|
+
- Why: on a phone, a long press on a draggable item opened its context menu first, and a scroll that started on a tab, task card or attachment picked the item up instead.
|
|
7
|
+
- Affects: touch drag hold 0ms (tabs, cards and attachments picked up at once) → 500ms, then 10px; held items get `select-none`, `-webkit-touch-callout: none` and `touch-manipulation`. Mouse and pen still start a drag after 5–6px, with no hold.
|
|
8
|
+
- Docs: https://raft-design.botiverse.build/foundations/states/#dragging-on-a-touch-screen
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **SortableTabsList** `onDragStart`: called when a tab drag starts, so an app that puts its own menu on a tab can close it. ([#662](https://github.com/botiverse/raft-design/pull/662))
|
|
13
|
+
- Why: the component that owns an item's menu closes it when the item's drag starts.
|
|
14
|
+
- Docs: https://raft-design.botiverse.build/components/tabs/
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **SortableTabsList, task drag-and-drop and SortableComposerAttachments**: on a touch screen a finger no longer starts a drag at once; it has to hold still for 500ms and then move. Scrolling that starts on a card or an attachment scrolls again. ([#662](https://github.com/botiverse/raft-design/pull/662))
|
|
19
|
+
- Why: the pointer sensor took every touch before the touch sensor could, and `touch-none` stopped the page from scrolling.
|
|
20
|
+
- Docs: https://raft-design.botiverse.build/foundations/states/#dragging-on-a-touch-screen
|
|
21
|
+
|
|
22
|
+
## [0.16.0](https://github.com/botiverse/raft-design/compare/v0.15.0...v0.16.0) (2026-10-21)
|
|
23
|
+
|
|
24
|
+
### Breaking Changes
|
|
25
|
+
|
|
26
|
+
- **VirtualList** `items`: compared by reference. A row renders again only when its item, its index or one of the list's render props changes, so replace an item to change it instead of editing it in place. ([#658](https://github.com/botiverse/raft-design/pull/658))
|
|
27
|
+
- Why: keeping rows across range updates is what removes the per-frame re-render.
|
|
28
|
+
- Migrate: if your code edits an item object in place, or a render function reads mutable state outside the props, replace the item (a new object) when it changes.
|
|
29
|
+
- Docs: https://raft-design.botiverse.build/components/virtual-list/
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- **VirtualList**: scrolling no longer restyles or re-renders every mounted row. Rows that stay in range keep their elements; rows are mounted ahead of the view in batches and passed rows stay mounted for a while; only the rows that fill the view below are mounted; rows above the view are planned at the average measured height; resize passes where nothing changed size write nothing. ([#658](https://github.com/botiverse/raft-design/pull/658))
|
|
34
|
+
- Why: in a long chat timeline the list dropped more frames than a plain list while scrolling, most with a slow CPU: every row was restyled on almost every frame, and each range change re-rendered every row.
|
|
35
|
+
- Affects: with a 4x slower CPU, frames over 33ms scrolling 320 messages 9 → 0 down and 24 → 0 up, p95 33ms → 16.7ms; main-thread time per frame 10–17ms → 2–5ms. Up to 2.5 viewports of passed rows stay mounted; after a jump only the rows in view mount, as before.
|
|
36
|
+
- Docs: https://raft-design.botiverse.build/components/virtual-list/
|
|
37
|
+
|
|
1
38
|
## [0.15.0](https://github.com/botiverse/raft-design/compare/v0.14.0...v0.15.0) (2026-10-21)
|
|
2
39
|
|
|
3
40
|
### Design System
|
package/dist/index.d.mts
CHANGED
|
@@ -3890,8 +3890,12 @@ export declare function TabsList({ className, children, variant, ref, ...props }
|
|
|
3890
3890
|
type SortableTabsListProps<T extends string = string> = TabsListProps & {
|
|
3891
3891
|
value: readonly T[];
|
|
3892
3892
|
onReorder: (value: T[]) => void;
|
|
3893
|
+
/** A tab was picked up. On a touch screen the long press that armed it may
|
|
3894
|
+
* have opened a menu the caller owns: close it here (DS › States ›
|
|
3895
|
+
* Dragging on a touch screen). */
|
|
3896
|
+
onDragStart?: (value: T) => void;
|
|
3893
3897
|
};
|
|
3894
|
-
export declare function SortableTabsList<T extends string = string>({ value, onReorder, children, variant, ...props }: SortableTabsListProps<T>): import("react").JSX.Element;
|
|
3898
|
+
export declare function SortableTabsList<T extends string = string>({ value, onReorder, onDragStart, children, variant, ...props }: SortableTabsListProps<T>): import("react").JSX.Element;
|
|
3895
3899
|
type TabsTabProps<T extends string = string> = StyledProps<Tabs$1.Tab.Props> & {
|
|
3896
3900
|
value: T;
|
|
3897
3901
|
};
|
|
@@ -4387,11 +4391,17 @@ interface VirtualListProps<T> extends VirtualListNativeProps {
|
|
|
4387
4391
|
* result is the row's `renderedItemWithCols`.
|
|
4388
4392
|
*/
|
|
4389
4393
|
renderItemWithCols?: ((info: RenderItemWithColsInfo) => ReactNode) | undefined;
|
|
4390
|
-
/**
|
|
4394
|
+
/**
|
|
4395
|
+
* The items, one row each, in order. Treat them as immutable: a mounted row is rendered again only
|
|
4396
|
+
* when its item (by reference) or its index changes, or a render prop does (`children`,
|
|
4397
|
+
* `ItemComponent`, `getKey`, `renderCol`, `renderItemWithCols`, `itemResizable`, `ignoreItemResize`,
|
|
4398
|
+
* `sessionId`, `virtual`), so replace an item's object to change its row.
|
|
4399
|
+
*/
|
|
4391
4400
|
items?: readonly T[] | undefined;
|
|
4392
4401
|
/**
|
|
4393
|
-
* The shortest a row can be, in px, or less:
|
|
4394
|
-
* size, plus one, so a larger value leaves the end of the
|
|
4402
|
+
* The shortest a row can be, in px, or less: to fill the view, rows not measured yet count at this
|
|
4403
|
+
* size (after a jump, as many rows as fit at it, plus one), so a larger value leaves the end of the
|
|
4404
|
+
* view empty. While the list scrolls, rows ahead of the view are mounted on top of these.
|
|
4395
4405
|
*/
|
|
4396
4406
|
itemMinSize: number;
|
|
4397
4407
|
/** The size, in px, counted for a row not measured yet (its offset and the spacer's height). */
|
|
@@ -4450,6 +4460,8 @@ interface VirtualListProps<T> extends VirtualListNativeProps {
|
|
|
4450
4460
|
/**
|
|
4451
4461
|
* Renders a row: called for each item in or near the view with the item, its index, the `domRef` to
|
|
4452
4462
|
* put on the row's root element and the `style` to spread on it. Give the element the `key` it gets.
|
|
4463
|
+
* A mounted row is rendered again only when its item, its index or a render prop changes (see
|
|
4464
|
+
* `items`): read no other mutable state here.
|
|
4453
4465
|
*/
|
|
4454
4466
|
children?: ((props: ItemRenderProps<T> & {
|
|
4455
4467
|
key: string | undefined;
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { cn } from "./cn.mjs";
|
|
2
|
-
import { $ as
|
|
2
|
+
import { $ as MessageReactionTooltipMeta, $n as ThemeStateContext, $t as DropdownMenuRadioItem, A as messageEmbed, An as Badge, At as MessageItemContinuationTime, B as MessageReferencePending, Bn as DialogPortal, Bt as useOverflowTitle, C as MessageQuotedPreviewThread, Cn as CheckboxIndicator, Ct as MessageItemBody, D as MessageEmbedContent, Dn as TooltipPortal, Dt as MessageItemFooter, E as MessageEmbed, En as TooltipContent, Et as MessageItemEmbedded, F as MessageMultiSelectCheckbox, Fn as DialogDescription, Ft as AvatarBadge, G as MessageReactionCount, Gn as buttonVariants, Gt as DropdownMenuFooter, H as MessageReferenceUnavailable, Hn as DialogTitle, Ht as DropdownMenuCheckboxItem, I as MessageMultiSelectToolbar, In as DialogFooter, It as AvatarFallback, J as MessageReactionPickerContent, Jn as useParentOverlayKind, Jt as DropdownMenuItemLabel, K as MessageReactionErrorIndicator, Kn as Spinner, Kt as DropdownMenuItem, L as MessageReferenceChip, Ln as DialogHeader, Lt as AvatarGroup, M as MessageTextSelectionToolbarButton, Mn as DialogBody, Mt as MessageItem, N as MessageTextSelectionToolbarSeparator, Nn as DialogClose, Nt as searchHit, O as MessageEmbedFooter, On as TooltipProvider, Ot as MessageItemSearchMatch, P as MessageMultiSelect, Pn as DialogContent, Pt as Avatar, Q as MessageReactionTooltipContent, Qn as useTheme, Qt as DropdownMenuRadioGroup, R as MessageReferenceIndicator, Rn as DialogNav, Rt as AvatarGroupCount, S as MessageQuotedPreviewText, Sn as Checkbox, St as MessageItemAttachments, T as MessageQuotedPreviewUnavailable, Tn as Tooltip, Tt as MessageItemEdited, U as MessageReactionAddButton, Un as DialogTrigger, Ut as DropdownMenuCheckboxItemIndicator, V as MessageReferenceText, Vn as DialogSurface, Vt as DropdownMenu, W as MessageReactionChip, Wn as Button, Wt as DropdownMenuContent, X as MessageReactionQuickButton, Xn as resolveThemeScopeProps, Xt as DropdownMenuPopup, Y as MessageReactionPickerStatic, Yn as withOverlayLayer, Yt as DropdownMenuLabel, Z as MessageReactionQuickRow, Zn as useThemeScopeProps, Zt as DropdownMenuPortal, _ as MessageQuotedPreviewAuthorName, _n as ScrollAreaCorner, _t as MessageItemSender, an as DropdownMenuTrigger, at as MessageItemBadgeSeparator, b as MessageQuotedPreviewHeader, bn as ScrollAreaViewport, bt as MessageItemSenderGroup, c as MessageListHistoryLimited, cn as Panel, ct as MessageItemToolbar, d as MessageQuotedPreview, dn as PanelHeaderRoot, en as DropdownMenuRadioItemIndicator, er as tv, et as MessageReactionTooltipName, f as MessageQuotedPreviewAttachment, fn as PanelScrollContent, g as MessageQuotedPreviewAuthorMeta, gn as ScrollAreaContent, gt as MessageItemMeta, hn as ScrollArea, ht as MessageItemInlineStatus, in as DropdownMenuSubmenuTrigger, it as MessageItemBadge, j as MessageTextSelectionToolbar, jn as Dialog, jt as MessageItemGutter, k as MessageEmbedHeader, kn as TooltipTrigger, kt as MessageItemAvatarSlot, l as MessageListNotice, ln as PanelBody, lt as MessageItemToolbarButton, m as MessageQuotedPreviewAuthor, mn as PanelSection, mt as MessageItemHeader, n as useMouseOnlyPointerSensor, nn as DropdownMenuShortcut, nr as headEdgePull, nt as MessageReactionTooltipNames, on as Kbd, ot as MessageItemReadReceipt, p as MessageQuotedPreviewAttachments, pn as PanelScrollViewport, q as MessageReactionGlyph, qn as defaultNativeButton, qt as DropdownMenuItemCount, rn as DropdownMenuSubmenu, rr as headEdges, rt as MessageReactionTooltipSeparator, s as MessageListDateDivider, sn as KbdGroup, st as MessageItemSavedIndicator, t as useHoldThenMoveTouchSensor, tn as DropdownMenuSeparator, tr as controlIconInset, tt as MessageReactionTooltipNameText, u as MessageList, un as PanelFooter, v as MessageQuotedPreviewChannel, vn as ScrollAreaScrollbar, vt as MessageItemSenderBadges, w as MessageQuotedPreviewTimestamp, wn as PRESENCE_COLOR, wt as MessageItemContent, x as MessageQuotedPreviewLoading, xn as useMobileViewport, xt as MessageItemTime, y as MessageQuotedPreviewContent, yn as ScrollAreaThumb, yt as MessageItemSenderButton, z as MessageReferenceLabel, zn as DialogOverlay, zt as AvatarImage } from "./touch-drag-CbWqiMlZ.mjs";
|
|
3
3
|
import { Dialog as Dialog$1 } from "@base-ui/react/dialog";
|
|
4
4
|
import { AlertTriangle, Bell, Check, CheckCircle2, ChevronDown, ChevronLeft, ChevronRight, Copy, FileText, Image, Info, Upload, X, Zap } from "lucide-react";
|
|
5
5
|
import { Children, cloneElement, createContext, createElement, isValidElement, use, useCallback, useContext, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
@@ -23,7 +23,7 @@ import { Switch as Switch$1 } from "@base-ui/react/switch";
|
|
|
23
23
|
import { Toggle } from "@base-ui/react/toggle";
|
|
24
24
|
import { ToggleGroup as ToggleGroup$1 } from "@base-ui/react/toggle-group";
|
|
25
25
|
import { Progress as Progress$1 } from "@base-ui/react/progress";
|
|
26
|
-
import { DndContext, DragOverlay, KeyboardCode, KeyboardSensor,
|
|
26
|
+
import { DndContext, DragOverlay, KeyboardCode, KeyboardSensor, closestCenter, closestCorners, getFirstCollision, pointerWithin, useDraggable, useDroppable, useSensor, useSensors } from "@dnd-kit/core";
|
|
27
27
|
import { SortableContext, arrayMove, defaultAnimateLayoutChanges, horizontalListSortingStrategy, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
|
28
28
|
import { CSS } from "@dnd-kit/utilities";
|
|
29
29
|
import { Toast } from "@base-ui/react/toast";
|
|
@@ -9065,10 +9065,7 @@ function SortableComposerAttachmentsRoot({ items, onReorder, children, axis, str
|
|
|
9065
9065
|
const [listMinHeight, setListMinHeight] = useState();
|
|
9066
9066
|
const draftIdsRef = useRef([]);
|
|
9067
9067
|
const lastReorderPointerRef = useRef(null);
|
|
9068
|
-
const sensors = useSensors(
|
|
9069
|
-
delay: 350,
|
|
9070
|
-
tolerance: 8
|
|
9071
|
-
} }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }));
|
|
9068
|
+
const sensors = useSensors(useMouseOnlyPointerSensor(6), useHoldThenMoveTouchSensor(), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }));
|
|
9072
9069
|
const attachments = useMemo(() => attachmentsById(children), [children]);
|
|
9073
9070
|
const itemIds = useMemo(() => items.map(String), [items]);
|
|
9074
9071
|
const horizontal = axis === "x";
|
|
@@ -9189,7 +9186,7 @@ function SortableComposerAttachment({ id, label, children }) {
|
|
|
9189
9186
|
return /* @__PURE__ */ jsx("div", {
|
|
9190
9187
|
ref: setNodeRef,
|
|
9191
9188
|
"aria-label": `Reorder ${label}`,
|
|
9192
|
-
className: "w-max max-w-full shrink-0 cursor-grab touch-none active:cursor-grabbing",
|
|
9189
|
+
className: "w-max max-w-full shrink-0 cursor-grab touch-manipulation select-none [-webkit-touch-callout:none] active:cursor-grabbing",
|
|
9193
9190
|
"data-dragging": isDragging ? "true" : void 0,
|
|
9194
9191
|
"data-slot": "sortable-composer-attachment",
|
|
9195
9192
|
style,
|
|
@@ -10735,7 +10732,7 @@ const tabs = tv({
|
|
|
10735
10732
|
],
|
|
10736
10733
|
background: ["", "hidden"],
|
|
10737
10734
|
tab: [
|
|
10738
|
-
"relative z-10 flex shrink-0 cursor-pointer touch-manipulation items-center gap-1.5 whitespace-nowrap outline-none",
|
|
10735
|
+
"relative z-10 flex shrink-0 cursor-pointer touch-manipulation select-none [-webkit-touch-callout:none] items-center gap-1.5 whitespace-nowrap outline-none",
|
|
10739
10736
|
"data-[reorderable=true]:cursor-pointer data-[dragging-source=true]:cursor-grabbing",
|
|
10740
10737
|
"font-sans text-callout font-semibold text-foreground-muted",
|
|
10741
10738
|
"transition-[background-color,box-shadow,color] duration-100 ease-out",
|
|
@@ -10949,14 +10946,11 @@ function TabsList({ className, children, variant = "default", ref, ...props }) {
|
|
|
10949
10946
|
})
|
|
10950
10947
|
});
|
|
10951
10948
|
}
|
|
10952
|
-
function SortableTabsList({ value, onReorder, children, variant = "default", ...props }) {
|
|
10949
|
+
function SortableTabsList({ value, onReorder, onDragStart, children, variant = "default", ...props }) {
|
|
10953
10950
|
const [activeId, setActiveId] = useState(void 0);
|
|
10954
10951
|
const placement = use(TabsPlacementContext);
|
|
10955
10952
|
const dndContextId = useId();
|
|
10956
|
-
const sensors = useSensors(
|
|
10957
|
-
delay: 350,
|
|
10958
|
-
tolerance: 8
|
|
10959
|
-
} }));
|
|
10953
|
+
const sensors = useSensors(useMouseOnlyPointerSensor(6), useHoldThenMoveTouchSensor());
|
|
10960
10954
|
const tabIds = useMemo(() => value.map(String), [value]);
|
|
10961
10955
|
const reorderContext = useMemo(() => ({
|
|
10962
10956
|
enabled: true,
|
|
@@ -10964,7 +10958,8 @@ function SortableTabsList({ value, onReorder, children, variant = "default", ...
|
|
|
10964
10958
|
}), [activeId]);
|
|
10965
10959
|
const handleDragStart = useCallback(({ active }) => {
|
|
10966
10960
|
setActiveId(String(active.id));
|
|
10967
|
-
|
|
10961
|
+
onDragStart?.(String(active.id));
|
|
10962
|
+
}, [onDragStart]);
|
|
10968
10963
|
const handleDragCancel = useCallback(() => {
|
|
10969
10964
|
setActiveId(void 0);
|
|
10970
10965
|
}, []);
|
|
@@ -12505,7 +12500,7 @@ function TaskCardStatus({ busy = false, status, className, render, children, ...
|
|
|
12505
12500
|
//#region src/components/task-dnd/task-dnd.recipe.ts
|
|
12506
12501
|
const taskDnd = tv({ slots: {
|
|
12507
12502
|
draggable: [
|
|
12508
|
-
"group/task-draggable relative cursor-grab touch-none active:cursor-grabbing",
|
|
12503
|
+
"group/task-draggable relative cursor-grab touch-manipulation select-none [-webkit-touch-callout:none] active:cursor-grabbing",
|
|
12509
12504
|
"outline-none",
|
|
12510
12505
|
"data-[dragging=true]:z-20 data-[dragging=true]:will-change-transform",
|
|
12511
12506
|
"data-[disabled=true]:cursor-default data-[disabled=true]:touch-auto",
|
|
@@ -12541,10 +12536,7 @@ function TaskDndProvider({ children, accessibility, disabled = false, isDropAllo
|
|
|
12541
12536
|
const [activeId, setActiveId] = useState(null);
|
|
12542
12537
|
const [activeHeight, setActiveHeight] = useState(null);
|
|
12543
12538
|
const [overStore] = useState(createTaskDndOverStore);
|
|
12544
|
-
const sensors = useSensors(
|
|
12545
|
-
delay: 200,
|
|
12546
|
-
tolerance: 5
|
|
12547
|
-
} }), useSensor(KeyboardSensor, { coordinateGetter: taskKeyboardCoordinates }));
|
|
12539
|
+
const sensors = useSensors(useMouseOnlyPointerSensor(5), useHoldThenMoveTouchSensor(), useSensor(KeyboardSensor, { coordinateGetter: taskKeyboardCoordinates }));
|
|
12548
12540
|
const context = useMemo(() => ({
|
|
12549
12541
|
activeId,
|
|
12550
12542
|
activeHeight,
|
|
@@ -13399,10 +13391,45 @@ function toLayoutUnit(value) {
|
|
|
13399
13391
|
return Math.round(value * 64) / 64;
|
|
13400
13392
|
}
|
|
13401
13393
|
//#endregion
|
|
13394
|
+
//#region src/components/virtual-list/utils/size-cache.ts
|
|
13395
|
+
/**
|
|
13396
|
+
* Records `size` as the measured size of row `key` in `offsets`, kept even when it equals the estimate
|
|
13397
|
+
* (an offset of 0), so the row counts as measured. Returns whether the cache changed.
|
|
13398
|
+
*/
|
|
13399
|
+
function recordMeasuredSize(offsets, key, size, itemEstimatedSize) {
|
|
13400
|
+
const offset = size - itemEstimatedSize;
|
|
13401
|
+
if (offsets.get(key) === offset) return false;
|
|
13402
|
+
offsets.set(key, offset);
|
|
13403
|
+
return true;
|
|
13404
|
+
}
|
|
13405
|
+
function createTypicalSizeCache() {
|
|
13406
|
+
return {
|
|
13407
|
+
version: -1,
|
|
13408
|
+
itemEstimatedSize: NaN,
|
|
13409
|
+
size: 0
|
|
13410
|
+
};
|
|
13411
|
+
}
|
|
13412
|
+
/**
|
|
13413
|
+
* The size rows typically are: the mean size of the measured rows (`offsets`), or `itemEstimatedSize`
|
|
13414
|
+
* before any is measured, and at least `itemMinSize` (and 1). Kept in `cache` and computed again only
|
|
13415
|
+
* when `version` (bumped whenever `offsets` changes) or `itemEstimatedSize` changes.
|
|
13416
|
+
*/
|
|
13417
|
+
function getTypicalSize(cache, offsets, version, itemEstimatedSize, itemMinSize) {
|
|
13418
|
+
if (cache.version !== version || cache.itemEstimatedSize !== itemEstimatedSize) {
|
|
13419
|
+
let total = 0;
|
|
13420
|
+
for (const offset of offsets.values()) total += offset;
|
|
13421
|
+
cache.size = itemEstimatedSize + (offsets.size === 0 ? 0 : total / offsets.size);
|
|
13422
|
+
cache.version = version;
|
|
13423
|
+
cache.itemEstimatedSize = itemEstimatedSize;
|
|
13424
|
+
}
|
|
13425
|
+
return Math.max(cache.size, itemMinSize, 1);
|
|
13426
|
+
}
|
|
13427
|
+
//#endregion
|
|
13402
13428
|
//#region src/components/virtual-list/utils/range.ts
|
|
13403
13429
|
/**
|
|
13404
13430
|
* The items to render: from the item at `scrollTop - paddingTop` through as many items as fit in
|
|
13405
|
-
* `listHeight` at their minimum size, plus one (
|
|
13431
|
+
* `listHeight` at their minimum size, plus one (the overscan is `getOverscannedRange`'s). Empty
|
|
13432
|
+
* (`0..-1`) before the list has been
|
|
13406
13433
|
* measured or when there are no items.
|
|
13407
13434
|
*/
|
|
13408
13435
|
function getVerticalRange({ fenwickTree, scrollTop, listHeight, itemMinSize, itemCount, paddingTop }) {
|
|
@@ -13416,6 +13443,60 @@ function getVerticalRange({ fenwickTree, scrollTop, listHeight, itemMinSize, ite
|
|
|
13416
13443
|
endIndex: Math.min(startIndex + Math.ceil(listHeight / itemMinSize + 1), itemCount - 1)
|
|
13417
13444
|
};
|
|
13418
13445
|
}
|
|
13446
|
+
const OVERSCAN_MIN = .25;
|
|
13447
|
+
const OVERSCAN_TARGET = 1;
|
|
13448
|
+
const OVERSCAN_MAX = 2.5;
|
|
13449
|
+
const OVERSCAN_BELOW = .25;
|
|
13450
|
+
/**
|
|
13451
|
+
* The last row needed to fill the view from row `startIndex` down: measured rows (`isMeasured`) count at
|
|
13452
|
+
* their size, the others at `itemMinSize`, so the view is filled whatever the unmeasured rows turn out
|
|
13453
|
+
* to be. `viewTop` is the view's top in item offsets.
|
|
13454
|
+
*/
|
|
13455
|
+
function getNeededEnd({ fenwickTree, startIndex, viewTop, listHeight, itemMinSize, itemCount, isMeasured }) {
|
|
13456
|
+
const bottom = viewTop + listHeight;
|
|
13457
|
+
let offset = fenwickTree.sum(startIndex);
|
|
13458
|
+
let index = startIndex;
|
|
13459
|
+
for (; index < itemCount - 1; ++index) {
|
|
13460
|
+
offset += isMeasured(index) ? fenwickTree.get(index) : Math.max(itemMinSize, 1);
|
|
13461
|
+
if (offset >= bottom) break;
|
|
13462
|
+
}
|
|
13463
|
+
return index;
|
|
13464
|
+
}
|
|
13465
|
+
/**
|
|
13466
|
+
* The items to render with overscan (see above), from the range in view (`view`, as `getVerticalRange`
|
|
13467
|
+
* gives it), the last row needed to fill the view (`neededEnd`) and the rendered range (`prev`).
|
|
13468
|
+
* `viewTop` is the view's top in item offsets, `moved` how far it moved since the last range update,
|
|
13469
|
+
* `movingUp` whether it last moved up, `settling` whether a jump is settling, `plannedSize` the size a
|
|
13470
|
+
* row is counted at when planning how many rows to mount above the view. With a `listHeight` of 0
|
|
13471
|
+
* (not laid out) there is no overscan: `view` is returned.
|
|
13472
|
+
*/
|
|
13473
|
+
function getOverscannedRange({ fenwickTree, view, prev, neededEnd, viewTop, moved, movingUp, settling, plannedSize, listHeight, itemCount }) {
|
|
13474
|
+
const last = itemCount - 1;
|
|
13475
|
+
const min = OVERSCAN_MIN * listHeight;
|
|
13476
|
+
const target = OVERSCAN_TARGET * listHeight;
|
|
13477
|
+
const max = OVERSCAN_MAX * listHeight;
|
|
13478
|
+
const apart = view.startIndex > prev.endIndex || view.endIndex < prev.startIndex;
|
|
13479
|
+
if (listHeight <= 0 || view.endIndex < view.startIndex || prev.endIndex < prev.startIndex || prev.endIndex > last || moved > max || apart && moved === 0) return view;
|
|
13480
|
+
let ahead = view.startIndex;
|
|
13481
|
+
for (let above = viewTop - fenwickTree.sum(ahead); ahead > 0 && above < target;) {
|
|
13482
|
+
ahead -= 1;
|
|
13483
|
+
above += plannedSize(ahead);
|
|
13484
|
+
}
|
|
13485
|
+
let startIndex = prev.startIndex;
|
|
13486
|
+
if (prev.startIndex > view.startIndex) startIndex = movingUp && !settling ? ahead : view.startIndex;
|
|
13487
|
+
else if (!settling) {
|
|
13488
|
+
const above = viewTop - fenwickTree.sum(prev.startIndex);
|
|
13489
|
+
if (movingUp ? above < min : above > max) startIndex = movingUp ? Math.min(ahead, prev.startIndex) : ahead;
|
|
13490
|
+
}
|
|
13491
|
+
const needed = Math.min(neededEnd, view.endIndex);
|
|
13492
|
+
const neededBottom = fenwickTree.sum(needed + 1);
|
|
13493
|
+
let endIndex = prev.endIndex;
|
|
13494
|
+
if (prev.endIndex < needed || movingUp && !settling && fenwickTree.sum(prev.endIndex + 1) - neededBottom > max) endIndex = Math.min(Math.max(fenwickTree.getBound(neededBottom + OVERSCAN_BELOW * listHeight), needed), last);
|
|
13495
|
+
return {
|
|
13496
|
+
startIndex,
|
|
13497
|
+
endIndex
|
|
13498
|
+
};
|
|
13499
|
+
}
|
|
13419
13500
|
/** The columns to render: the ones in `scrollLeft .. scrollLeft + listWidth`, plus one on each side. */
|
|
13420
13501
|
function getHorizontalRange({ xFenwickTree, scrollLeft, listWidth, columnCount }) {
|
|
13421
13502
|
if (xFenwickTree === null || columnCount === 0) return {
|
|
@@ -13866,7 +13947,11 @@ function VirtualList(props) {
|
|
|
13866
13947
|
const keptKeysRef = useRef([]);
|
|
13867
13948
|
/** The key of the row that holds focus (`document.activeElement` is in it), tracked by focusin / focusout. */
|
|
13868
13949
|
const focusedKeyRef = useRef(null);
|
|
13950
|
+
/** The measured rows' sizes, by key (see `utils/size-cache.ts`). */
|
|
13869
13951
|
const keyToHeightOffsetRef = useRef(/* @__PURE__ */ new Map());
|
|
13952
|
+
/** Bumped whenever `keyToHeightOffsetRef` changes (see `getTypicalSize`). */
|
|
13953
|
+
const measuredVersionRef = useRef(0);
|
|
13954
|
+
const typicalSizeRef = useRef(createTypicalSizeCache());
|
|
13870
13955
|
const leadingMarginRef = useRef(0);
|
|
13871
13956
|
/**
|
|
13872
13957
|
* Where the first mounted row in flow starts in the visible-items element (its top margin there,
|
|
@@ -13877,6 +13962,8 @@ function VirtualList(props) {
|
|
|
13877
13962
|
const pendingScrollTaskTimeoutRef = useRef(null);
|
|
13878
13963
|
const pendingScrollTaskRafRef = useRef(null);
|
|
13879
13964
|
const wheelScrollingRef = useRef(false);
|
|
13965
|
+
/** Whether the view last moved up (see `updateRange`). */
|
|
13966
|
+
const scrollingUpRef = useRef(false);
|
|
13880
13967
|
/** Clears `wheelScrollingRef` in the next frame; this list's own, so other lists cannot replace it. */
|
|
13881
13968
|
const [runLatestInNextFrame] = useState(createLatestInNextFrame);
|
|
13882
13969
|
/** The wheel is only taken over (catchWheel) where the pointer is not coarse. */
|
|
@@ -13944,6 +14031,7 @@ function VirtualList(props) {
|
|
|
13944
14031
|
keptKeysRef.current = [];
|
|
13945
14032
|
focusedKeyRef.current = null;
|
|
13946
14033
|
keyToHeightOffsetRef.current.clear();
|
|
14034
|
+
measuredVersionRef.current += 1;
|
|
13947
14035
|
leadingMarginRef.current = 0;
|
|
13948
14036
|
keyToIndexRef.current = /* @__PURE__ */ new Map();
|
|
13949
14037
|
rangeRef.current = {
|
|
@@ -14135,9 +14223,12 @@ function VirtualList(props) {
|
|
|
14135
14223
|
}
|
|
14136
14224
|
}
|
|
14137
14225
|
}, [virtual]);
|
|
14226
|
+
/** The size rows typically are: the mean of the measured rows (`getTypicalSize`). */
|
|
14227
|
+
const getTypicalRowSize = useEventCallback(() => getTypicalSize(typicalSizeRef.current, keyToHeightOffsetRef.current, measuredVersionRef.current, itemEstimatedSize, itemMinSize));
|
|
14138
14228
|
/**
|
|
14139
14229
|
* The viewport sync. The range starts at the item at `scrollTop + compensation - paddingTop` and
|
|
14140
|
-
* holds as many items as fit at `itemMinSize`, plus one
|
|
14230
|
+
* holds as many items as fit at `itemMinSize`, plus one, and is overscanned in batches
|
|
14231
|
+
* (`getOverscannedRange`). Items holding the text selection, and the item
|
|
14141
14232
|
* holding focus, stay
|
|
14142
14233
|
* mounted ("kept"). When the new range is inside the old one, only the layout styles are written;
|
|
14143
14234
|
* otherwise the items entering above the old start are recorded (`prependIndexes`) and the range is
|
|
@@ -14163,7 +14254,7 @@ function VirtualList(props) {
|
|
|
14163
14254
|
startIndex: range.verticalStartIndex,
|
|
14164
14255
|
endIndex: range.verticalEndIndex
|
|
14165
14256
|
};
|
|
14166
|
-
const
|
|
14257
|
+
const viewRange = getVerticalRange({
|
|
14167
14258
|
fenwickTree,
|
|
14168
14259
|
scrollTop: effectiveScrollTop,
|
|
14169
14260
|
listHeight: viewport.listHeight,
|
|
@@ -14171,8 +14262,37 @@ function VirtualList(props) {
|
|
|
14171
14262
|
itemCount: items.length,
|
|
14172
14263
|
paddingTop: getRowsTop()
|
|
14173
14264
|
});
|
|
14265
|
+
const overscanned = viewport.listHeight > 0;
|
|
14266
|
+
if (scrollTop !== viewport.scrollTop) scrollingUpRef.current = scrollTop < viewport.scrollTop;
|
|
14267
|
+
const nextRange = overscanned ? getOverscannedRange({
|
|
14268
|
+
fenwickTree,
|
|
14269
|
+
view: viewRange,
|
|
14270
|
+
prev: prevRange,
|
|
14271
|
+
neededEnd: getNeededEnd({
|
|
14272
|
+
fenwickTree,
|
|
14273
|
+
startIndex: viewRange.startIndex,
|
|
14274
|
+
viewTop: effectiveScrollTop - getRowsTop(),
|
|
14275
|
+
listHeight: viewport.listHeight,
|
|
14276
|
+
itemMinSize,
|
|
14277
|
+
itemCount: items.length,
|
|
14278
|
+
isMeasured: (index) => {
|
|
14279
|
+
const item = items[index];
|
|
14280
|
+
return item !== void 0 && keyToHeightOffsetRef.current.has(getKey(item));
|
|
14281
|
+
}
|
|
14282
|
+
}),
|
|
14283
|
+
viewTop: effectiveScrollTop - getRowsTop(),
|
|
14284
|
+
moved: Math.abs(scrollTop - viewport.scrollTop),
|
|
14285
|
+
movingUp: scrollingUpRef.current,
|
|
14286
|
+
settling: pendingScrollTaskRef.current !== null,
|
|
14287
|
+
plannedSize: (index) => {
|
|
14288
|
+
const item = items[index];
|
|
14289
|
+
return item !== void 0 && keyToHeightOffsetRef.current.has(getKey(item)) ? fenwickTree.get(index) : getTypicalRowSize();
|
|
14290
|
+
},
|
|
14291
|
+
listHeight: viewport.listHeight,
|
|
14292
|
+
itemCount: items.length
|
|
14293
|
+
}) : viewRange;
|
|
14174
14294
|
const rangeChanged = prevRange.startIndex !== nextRange.startIndex || prevRange.endIndex !== nextRange.endIndex;
|
|
14175
|
-
const nextInsidePrev = nextRange.startIndex >= prevRange.startIndex && prevRange.endIndex >= nextRange.endIndex;
|
|
14295
|
+
const nextInsidePrev = !overscanned && nextRange.startIndex >= prevRange.startIndex && prevRange.endIndex >= nextRange.endIndex;
|
|
14176
14296
|
const prevColRange = {
|
|
14177
14297
|
startIndex: range.horizontalStartIndex,
|
|
14178
14298
|
endIndex: range.horizontalEndIndex
|
|
@@ -14406,9 +14526,8 @@ function VirtualList(props) {
|
|
|
14406
14526
|
rows.forEach(({ key, index }, i) => {
|
|
14407
14527
|
const size = spans[i];
|
|
14408
14528
|
const previousSize = fenwickTree.get(index);
|
|
14529
|
+
if (recordMeasuredSize(keyToHeightOffsetRef.current, key, size, itemEstimatedSize)) measuredVersionRef.current += 1;
|
|
14409
14530
|
if (size === previousSize) return;
|
|
14410
|
-
if (size - itemEstimatedSize === 0) keyToHeightOffsetRef.current.delete(key);
|
|
14411
|
-
else keyToHeightOffsetRef.current.set(key, size - itemEstimatedSize);
|
|
14412
14531
|
fenwickTree.add(index, size - previousSize);
|
|
14413
14532
|
resizes.push({
|
|
14414
14533
|
index,
|
|
@@ -14728,6 +14847,10 @@ function VirtualList(props) {
|
|
|
14728
14847
|
const isFirstOfKey = /* @__PURE__ */ new Map();
|
|
14729
14848
|
for (const { key, isFirst } of queue) if (!isFirstOfKey.has(key)) isFirstOfKey.set(key, isFirst);
|
|
14730
14849
|
const { resizes, leadingMarginDelta } = measureFlowRun();
|
|
14850
|
+
if (resizes.length === 0 && leadingMarginDelta === 0 && pendingScrollTaskRef.current === null) {
|
|
14851
|
+
clearItemResizeQueue();
|
|
14852
|
+
return;
|
|
14853
|
+
}
|
|
14731
14854
|
applyLayoutStyles(startIndex);
|
|
14732
14855
|
onContentResize?.();
|
|
14733
14856
|
const listEl = listElRef.current;
|
|
@@ -15001,7 +15124,45 @@ function VirtualList(props) {
|
|
|
15001
15124
|
ItemComponent,
|
|
15002
15125
|
renderItemChild
|
|
15003
15126
|
]);
|
|
15004
|
-
const
|
|
15127
|
+
const renderInputs = useMemo(() => ({}), [
|
|
15128
|
+
virtual,
|
|
15129
|
+
getKey,
|
|
15130
|
+
renderCol,
|
|
15131
|
+
renderItemWithCols,
|
|
15132
|
+
itemResizable,
|
|
15133
|
+
sessionId,
|
|
15134
|
+
ignoreItemResize,
|
|
15135
|
+
ItemComponent,
|
|
15136
|
+
renderItemChild
|
|
15137
|
+
]);
|
|
15138
|
+
const [flowElements] = useState(() => /* @__PURE__ */ new Map());
|
|
15139
|
+
const renderedVisibleItems = useMemo(() => {
|
|
15140
|
+
const nodes = visibleItems.map((item) => {
|
|
15141
|
+
const key = getKey(item);
|
|
15142
|
+
const index = keyToIndexRef.current.get(key);
|
|
15143
|
+
const cached = flowElements.get(key);
|
|
15144
|
+
if (cached !== void 0 && cached.item === item && cached.index === index && cached.inputs === renderInputs) return cached.node;
|
|
15145
|
+
const node = renderItem(item, false);
|
|
15146
|
+
flowElements.set(key, {
|
|
15147
|
+
item,
|
|
15148
|
+
index,
|
|
15149
|
+
inputs: renderInputs,
|
|
15150
|
+
node
|
|
15151
|
+
});
|
|
15152
|
+
return node;
|
|
15153
|
+
});
|
|
15154
|
+
if (flowElements.size > visibleItems.length) {
|
|
15155
|
+
const inRange = new Set(visibleItems.map(getKey));
|
|
15156
|
+
for (const key of flowElements.keys()) if (!inRange.has(key)) flowElements.delete(key);
|
|
15157
|
+
}
|
|
15158
|
+
return nodes;
|
|
15159
|
+
}, [
|
|
15160
|
+
renderItem,
|
|
15161
|
+
visibleItems,
|
|
15162
|
+
getKey,
|
|
15163
|
+
flowElements,
|
|
15164
|
+
renderInputs
|
|
15165
|
+
]);
|
|
15005
15166
|
const renderedKeptBefore = useMemo(() => keptItemsBefore.map((item) => renderItem(item, true)), [renderItem, keptItemsBefore]);
|
|
15006
15167
|
const renderedKeptAfter = useMemo(() => keptItemsAfter.map((item) => renderItem(item, true)), [renderItem, keptItemsAfter]);
|
|
15007
15168
|
useNonVirtualContentResize(itemsElRef, onContentResize, !virtual);
|
|
@@ -15045,9 +15206,7 @@ function VirtualList(props) {
|
|
|
15045
15206
|
const { spans, firstStart } = measureRowSpans(visibleItemsEl, rows.map((row) => row.el));
|
|
15046
15207
|
if (rows[0] !== void 0 && keyToIndexRef.current.get(rows[0].key) === 0) leadingMarginRef.current = firstStart;
|
|
15047
15208
|
rows.forEach(({ key }, i) => {
|
|
15048
|
-
|
|
15049
|
-
if (offset === 0) keyToHeightOffsetRef.current.delete(key);
|
|
15050
|
-
else keyToHeightOffsetRef.current.set(key, offset);
|
|
15209
|
+
if (recordMeasuredSize(keyToHeightOffsetRef.current, key, spans[i], itemEstimatedSize)) measuredVersionRef.current += 1;
|
|
15051
15210
|
});
|
|
15052
15211
|
}
|
|
15053
15212
|
}
|
|
@@ -14,6 +14,7 @@ import { Avatar } from "@base-ui/react";
|
|
|
14
14
|
import { Menu } from "@base-ui/react/menu";
|
|
15
15
|
import { Toggle } from "@base-ui/react/toggle";
|
|
16
16
|
import { Toolbar } from "@base-ui/react/toolbar";
|
|
17
|
+
import { PointerSensor, TouchSensor, useSensor } from "@dnd-kit/core";
|
|
17
18
|
//#region src/lib/head-edges.ts
|
|
18
19
|
/**
|
|
19
20
|
* The ends of a header (design/foundations/alignment.md#the-ends-of-a-header).
|
|
@@ -3287,5 +3288,99 @@ function MessageListJumpPillDismiss({ className, children, render, nativeButton,
|
|
|
3287
3288
|
})
|
|
3288
3289
|
});
|
|
3289
3290
|
}
|
|
3291
|
+
const holds = /* @__PURE__ */ new WeakMap();
|
|
3292
|
+
function touchPoint(event) {
|
|
3293
|
+
const touch = event.touches?.[0] ?? event.changedTouches?.[0];
|
|
3294
|
+
return touch ? {
|
|
3295
|
+
x: touch.clientX,
|
|
3296
|
+
y: touch.clientY
|
|
3297
|
+
} : null;
|
|
3298
|
+
}
|
|
3299
|
+
function farther(from, to, limit) {
|
|
3300
|
+
return Math.hypot(to.x - from.x, to.y - from.y) > limit;
|
|
3301
|
+
}
|
|
3302
|
+
var HoldThenMoveTouchSensorImpl = class extends TouchSensor {
|
|
3303
|
+
static activators = TouchSensor.activators;
|
|
3304
|
+
static setup() {
|
|
3305
|
+
return TouchSensor.setup();
|
|
3306
|
+
}
|
|
3307
|
+
attach() {
|
|
3308
|
+
super.attach();
|
|
3309
|
+
const self = this;
|
|
3310
|
+
const { holdMs = 500 } = self.props.options;
|
|
3311
|
+
const state = {
|
|
3312
|
+
heldAt: null,
|
|
3313
|
+
last: null,
|
|
3314
|
+
timer: setTimeout(() => {
|
|
3315
|
+
state.heldAt = state.last ?? self.initialCoordinates;
|
|
3316
|
+
}, holdMs)
|
|
3317
|
+
};
|
|
3318
|
+
holds.set(this, state);
|
|
3319
|
+
}
|
|
3320
|
+
handleMove(event) {
|
|
3321
|
+
const self = this;
|
|
3322
|
+
const state = holds.get(this);
|
|
3323
|
+
if (self.activated || !state) {
|
|
3324
|
+
super.handleMove(event);
|
|
3325
|
+
return;
|
|
3326
|
+
}
|
|
3327
|
+
if ((event.touches?.length ?? 1) > 1) {
|
|
3328
|
+
self.handleCancel();
|
|
3329
|
+
return;
|
|
3330
|
+
}
|
|
3331
|
+
const point = touchPoint(event);
|
|
3332
|
+
const start = self.initialCoordinates;
|
|
3333
|
+
if (!point || !start) return;
|
|
3334
|
+
state.last = point;
|
|
3335
|
+
const { distance = 10, tolerance = 10 } = self.props.options;
|
|
3336
|
+
if (!state.heldAt) {
|
|
3337
|
+
if (farther(start, point, tolerance)) self.handleCancel();
|
|
3338
|
+
return;
|
|
3339
|
+
}
|
|
3340
|
+
if (event.cancelable) event.preventDefault();
|
|
3341
|
+
if (farther(state.heldAt, point, distance)) self.handleStart();
|
|
3342
|
+
}
|
|
3343
|
+
detach() {
|
|
3344
|
+
const state = holds.get(this);
|
|
3345
|
+
if (state) clearTimeout(state.timer);
|
|
3346
|
+
holds.delete(this);
|
|
3347
|
+
super.detach();
|
|
3348
|
+
}
|
|
3349
|
+
};
|
|
3350
|
+
/** A TouchSensor that starts a drag on the first move after a still hold (see above). */
|
|
3351
|
+
const HoldThenMoveTouchSensor = HoldThenMoveTouchSensorImpl;
|
|
3352
|
+
/**
|
|
3353
|
+
* dnd-kit's PointerSensor for a mouse and a pen only: a finger's pointerdown is
|
|
3354
|
+
* left to `HoldThenMoveTouchSensor`.
|
|
3355
|
+
*/
|
|
3356
|
+
var MouseOnlyPointerSensor = class extends PointerSensor {
|
|
3357
|
+
static activators = [{
|
|
3358
|
+
eventName: "onPointerDown",
|
|
3359
|
+
handler: (event, options) => event.nativeEvent.pointerType === "touch" ? false : PointerSensor.activators[0].handler(event, options)
|
|
3360
|
+
}];
|
|
3361
|
+
};
|
|
3362
|
+
/**
|
|
3363
|
+
* `useSensor(HoldThenMoveTouchSensor, …)` with the activation the sensor relies
|
|
3364
|
+
* on: no constraint dnd-kit would act on by itself, so only a move after the
|
|
3365
|
+
* hold starts the drag.
|
|
3366
|
+
*/
|
|
3367
|
+
function useHoldThenMoveTouchSensor({ holdMs = 500, distance = 10, tolerance = 10 } = {}) {
|
|
3368
|
+
const options = useMemo(() => ({
|
|
3369
|
+
holdMs,
|
|
3370
|
+
distance,
|
|
3371
|
+
tolerance,
|
|
3372
|
+
activationConstraint: { distance: Number.POSITIVE_INFINITY }
|
|
3373
|
+
}), [
|
|
3374
|
+
holdMs,
|
|
3375
|
+
distance,
|
|
3376
|
+
tolerance
|
|
3377
|
+
]);
|
|
3378
|
+
return useSensor(HoldThenMoveTouchSensor, options);
|
|
3379
|
+
}
|
|
3380
|
+
/** The mouse / pen sensor that goes with it: picks up after `distance` px. */
|
|
3381
|
+
function useMouseOnlyPointerSensor(distance) {
|
|
3382
|
+
const options = useMemo(() => ({ activationConstraint: { distance } }), [distance]);
|
|
3383
|
+
return useSensor(MouseOnlyPointerSensor, options);
|
|
3384
|
+
}
|
|
3290
3385
|
//#endregion
|
|
3291
|
-
export {
|
|
3386
|
+
export { MessageReactionTooltipMeta as $, ThemeStateContext as $n, DropdownMenuRadioItem as $t, messageEmbed as A, Badge as An, MessageItemContinuationTime as At, MessageReferencePending as B, DialogPortal as Bn, useOverflowTitle as Bt, MessageQuotedPreviewThread as C, CheckboxIndicator as Cn, MessageItemBody as Ct, MessageEmbedContent as D, TooltipPortal as Dn, MessageItemFooter as Dt, MessageEmbed as E, TooltipContent as En, MessageItemEmbedded as Et, MessageMultiSelectCheckbox as F, DialogDescription as Fn, AvatarBadge as Ft, MessageReactionCount as G, buttonVariants as Gn, DropdownMenuFooter as Gt, MessageReferenceUnavailable as H, DialogTitle as Hn, DropdownMenuCheckboxItem as Ht, MessageMultiSelectToolbar as I, DialogFooter as In, AvatarFallback as It, MessageReactionPickerContent as J, useParentOverlayKind as Jn, DropdownMenuItemLabel as Jt, MessageReactionErrorIndicator as K, Spinner as Kn, DropdownMenuItem as Kt, MessageReferenceChip as L, DialogHeader as Ln, AvatarGroup as Lt, MessageTextSelectionToolbarButton as M, DialogBody as Mn, MessageItem as Mt, MessageTextSelectionToolbarSeparator as N, DialogClose as Nn, searchHit as Nt, MessageEmbedFooter as O, TooltipProvider as On, MessageItemSearchMatch as Ot, MessageMultiSelect as P, DialogContent as Pn, Avatar$1 as Pt, MessageReactionTooltipContent as Q, useTheme as Qn, DropdownMenuRadioGroup as Qt, MessageReferenceIndicator as R, DialogNav as Rn, AvatarGroupCount as Rt, MessageQuotedPreviewText as S, Checkbox$1 as Sn, MessageItemAttachments as St, MessageQuotedPreviewUnavailable as T, Tooltip$1 as Tn, MessageItemEdited as Tt, MessageReactionAddButton as U, DialogTrigger as Un, DropdownMenuCheckboxItemIndicator as Ut, MessageReferenceText as V, DialogSurface as Vn, DropdownMenu as Vt, MessageReactionChip as W, Button$1 as Wn, DropdownMenuContent as Wt, MessageReactionQuickButton as X, resolveThemeScopeProps as Xn, DropdownMenuPopup as Xt, MessageReactionPickerStatic as Y, withOverlayLayer as Yn, DropdownMenuLabel as Yt, MessageReactionQuickRow as Z, useThemeScopeProps as Zn, DropdownMenuPortal as Zt, MessageQuotedPreviewAuthorName as _, ScrollAreaCorner as _n, MessageItemSender as _t, MessageListJumpPillDismiss as a, DropdownMenuTrigger as an, MessageItemBadgeSeparator as at, MessageQuotedPreviewHeader as b, ScrollAreaViewport as bn, MessageItemSenderGroup as bt, MessageListHistoryLimited as c, Panel as cn, MessageItemToolbar as ct, MessageQuotedPreview as d, PanelHeaderRoot as dn, MessageItemDelegatedBy as dt, DropdownMenuRadioItemIndicator as en, tv$1 as er, MessageReactionTooltipName as et, MessageQuotedPreviewAttachment as f, PanelScrollContent as fn, MessageItemDelegatedByButton as ft, MessageQuotedPreviewAuthorMeta as g, ScrollAreaContent as gn, MessageItemMeta as gt, MessageQuotedPreviewAuthorDelegatedBy as h, ScrollArea$1 as hn, MessageItemInlineStatus as ht, MessageListJumpPill as i, DropdownMenuSubmenuTrigger as in, MessageItemBadge as it, MessageTextSelectionToolbar as j, Dialog$1 as jn, MessageItemGutter as jt, MessageEmbedHeader as k, TooltipTrigger as kn, MessageItemAvatarSlot as kt, MessageListNotice as l, PanelBody as ln, MessageItemToolbarButton as lt, MessageQuotedPreviewAuthor as m, PanelSection as mn, MessageItemHeader as mt, useMouseOnlyPointerSensor as n, DropdownMenuShortcut as nn, headEdgePull as nr, MessageReactionTooltipNames as nt, MessageListJumpPillGroup as o, Kbd as on, MessageItemReadReceipt as ot, MessageQuotedPreviewAttachments as p, PanelScrollViewport as pn, MessageItemDelegatedByName as pt, MessageReactionGlyph as q, defaultNativeButton as qn, DropdownMenuItemCount as qt, MessageListJumpControls as r, DropdownMenuSubmenu as rn, headEdges as rr, MessageReactionTooltipSeparator as rt, MessageListDateDivider as s, KbdGroup as sn, MessageItemSavedIndicator as st, useHoldThenMoveTouchSensor as t, DropdownMenuSeparator as tn, controlIconInset as tr, MessageReactionTooltipNameText as tt, MessageList as u, PanelFooter as un, MessageItemDelegatedAvatar as ut, MessageQuotedPreviewChannel as v, ScrollAreaScrollbar as vn, MessageItemSenderBadges as vt, MessageQuotedPreviewTimestamp as w, PRESENCE_COLOR as wn, MessageItemContent as wt, MessageQuotedPreviewLoading as x, useMobileViewport as xn, MessageItemTime as xt, MessageQuotedPreviewContent as y, ScrollAreaThumb as yn, MessageItemSenderButton as yt, MessageReferenceLabel as z, DialogOverlay as zn, AvatarImage as zt };
|
package/dist/wip.d.mts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { C as MessageQuotedPreviewAuthorDelegatedBy, Hi as DialogTitleProps, Ki as ButtonProps, Si as DialogClose, Ti as DialogContentProps, Ui as DialogTrigger, Xi as StyledProps, Yi as DataAttributes, a as MessageListJumpPillDismissProps, c as MessageListJumpPillProps, cn as MessageItemDelegatedAvatar, dn as MessageItemDelegatedByButton, fn as MessageItemDelegatedByButtonProps, hn as MessageItemDelegatedByProps, i as MessageListJumpPillDismiss, ln as MessageItemDelegatedAvatarProps, mn as MessageItemDelegatedByNameProps, n as MessageListJumpControlsProps, o as MessageListJumpPillGroup, pn as MessageItemDelegatedByName, r as MessageListJumpPill, s as MessageListJumpPillGroupProps, t as MessageListJumpControls, un as MessageItemDelegatedBy, w as MessageQuotedPreviewAuthorDelegatedByProps, yi as Dialog } from "./index-DsVVBZ5T.mjs";
|
|
2
|
-
import { CSSProperties, ComponentPropsWithRef, ReactNode } from "react";
|
|
2
|
+
import { CSSProperties, ComponentPropsWithRef, PointerEvent, ReactNode } from "react";
|
|
3
3
|
import { Button } from "@base-ui/react/button";
|
|
4
4
|
import { useRender } from "@base-ui/react/use-render";
|
|
5
5
|
import { Combobox } from "@base-ui/react/combobox";
|
|
6
|
+
import { PointerSensor, TouchSensor, TouchSensorOptions } from "@dnd-kit/core";
|
|
6
7
|
//#region src/components/combobox-select/combobox-select.types.d.ts
|
|
7
8
|
type ComboboxSelectValue = string | number;
|
|
8
9
|
interface ComboboxSelectOption {
|
|
@@ -265,4 +266,20 @@ type UseCommentMarginFitsOptions = {
|
|
|
265
266
|
*/
|
|
266
267
|
export declare function useCommentMarginFits(element: Element | null, { minWidth }?: UseCommentMarginFitsOptions): boolean;
|
|
267
268
|
//#endregion
|
|
268
|
-
|
|
269
|
+
//#region src/lib/touch-drag.d.ts
|
|
270
|
+
type HoldThenMoveOptions = {
|
|
271
|
+
/** How long the finger stays still before a move can start the drag. */
|
|
272
|
+
holdMs?: number;
|
|
273
|
+
/** How far the finger moves after the hold to start the drag. */
|
|
274
|
+
distance?: number;
|
|
275
|
+
/** How far the finger may drift during the hold before it counts as a scroll. */
|
|
276
|
+
tolerance?: number;
|
|
277
|
+
};
|
|
278
|
+
/**
|
|
279
|
+
* `useSensor(HoldThenMoveTouchSensor, …)` with the activation the sensor relies
|
|
280
|
+
* on: no constraint dnd-kit would act on by itself, so only a move after the
|
|
281
|
+
* hold starts the drag.
|
|
282
|
+
*/
|
|
283
|
+
export declare function useHoldThenMoveTouchSensor({ holdMs, distance, tolerance }?: HoldThenMoveOptions): import("@dnd-kit/core").SensorDescriptor<TouchSensorOptions>;
|
|
284
|
+
//#endregion
|
|
285
|
+
export { Dialog as AnnouncementDialog, DialogClose as AnnouncementDialogClose, DialogTrigger as AnnouncementDialogTrigger, type ComboboxSelectGroup, type ComboboxSelectMessages, type ComboboxSelectOption, type ComboboxSelectProps, type ComboboxSelectSelection, type ComboboxSelectValue, type CommentCardMetaProps, type CommentCardProps, type CommentCardQuoteProps, type CommentCardTriggerProps, type CommentEntryAuthorProps, type CommentEntryBodyProps, type CommentEntryHeaderProps, type CommentEntryProps, type CommentEntryTimeProps, type CommentMarginItemProps, type CommentMarginLayout, type CommentMarginLayoutItem, type CommentMarginLayoutOptions, type CommentMarginProps, type CommentMarkerProps, type HoldThenMoveOptions, MessageItemDelegatedAvatar, type MessageItemDelegatedAvatarProps, MessageItemDelegatedBy, MessageItemDelegatedByButton, type MessageItemDelegatedByButtonProps, MessageItemDelegatedByName, type MessageItemDelegatedByNameProps, type MessageItemDelegatedByProps, MessageListJumpControls, type MessageListJumpControlsProps, MessageListJumpPill, MessageListJumpPillDismiss, type MessageListJumpPillDismissProps, MessageListJumpPillGroup, type MessageListJumpPillGroupProps, type MessageListJumpPillProps, MessageQuotedPreviewAuthorDelegatedBy, type MessageQuotedPreviewAuthorDelegatedByProps, type UseCommentMarginFitsOptions };
|
package/dist/wip.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Bt as useOverflowTitle, Hn as DialogTitle, Nn as DialogClose, Pn as DialogContent, Qn as useTheme, Un as DialogTrigger, Wn as Button$1, Zn as useThemeScopeProps, a as MessageListJumpPillDismiss, dt as MessageItemDelegatedBy, er as tv, ft as MessageItemDelegatedByButton, h as MessageQuotedPreviewAuthorDelegatedBy, i as MessageListJumpPill, jn as Dialog, o as MessageListJumpPillGroup, pt as MessageItemDelegatedByName, qn as defaultNativeButton, r as MessageListJumpControls, t as useHoldThenMoveTouchSensor, ut as MessageItemDelegatedAvatar } from "./touch-drag-CbWqiMlZ.mjs";
|
|
2
2
|
import { Check, ChevronDown, LoaderCircle, Plus, X } from "lucide-react";
|
|
3
3
|
import { createContext, use, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
4
4
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -27,17 +27,20 @@ function getSnapshot() {
|
|
|
27
27
|
function usePhoneMedia() {
|
|
28
28
|
return useSyncExternalStore(subscribe, getSnapshot, () => false);
|
|
29
29
|
}
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/components/combobox-select/combobox-select.list-side.ts
|
|
32
|
+
/** One option row (36px) and the list's 6px padding above and below. */
|
|
33
|
+
const ROW = 36;
|
|
34
|
+
const LIST_PADDING = 12;
|
|
35
|
+
/** The gap between the field and the list (the positioner's sideOffset). */
|
|
36
|
+
const SIDE_OFFSET = 4;
|
|
30
37
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* short of the list and there is more above. `listHeight` is the list's full
|
|
35
|
-
* height and `maxHeight` its cap (`listMaxHeight`), both in pixels.
|
|
38
|
+
* Whether fewer than three rows of a phone's list fit between the field and the
|
|
39
|
+
* bottom of the visual viewport. The list always opens below the field on a
|
|
40
|
+
* phone and never flips; when this is true the field is scrolled up instead.
|
|
36
41
|
*/
|
|
37
|
-
function
|
|
38
|
-
|
|
39
|
-
const above = anchor.top - viewport.top;
|
|
40
|
-
return below >= Math.min(listHeight, maxHeight) || below >= above ? "bottom" : "top";
|
|
42
|
+
function roomBelowIsShort(field, viewport, rows = 3) {
|
|
43
|
+
return viewport.top + viewport.height - field.bottom - SIDE_OFFSET < rows * ROW + LIST_PADDING;
|
|
41
44
|
}
|
|
42
45
|
//#endregion
|
|
43
46
|
//#region src/components/combobox-select/combobox-select.options.ts
|
|
@@ -106,37 +109,41 @@ function renderSelectedChip({ record, index, tagLimit, strings, renderLabel, ren
|
|
|
106
109
|
}, optionKey(record.value));
|
|
107
110
|
}
|
|
108
111
|
/**
|
|
109
|
-
* On a phone the list
|
|
110
|
-
*
|
|
112
|
+
* On a phone the list always opens below the field, between the field and the
|
|
113
|
+
* keyboard, as a native autocomplete does, and never flips: which side has more
|
|
114
|
+
* room depends on whether the keyboard happens to be up, so choosing a side made
|
|
115
|
+
* the same field open up one time and down the next.
|
|
111
116
|
*/
|
|
112
117
|
const PHONE_LIST_COLLISION = {
|
|
113
118
|
side: "none",
|
|
114
119
|
align: "shift",
|
|
115
120
|
fallbackAxisSide: "none"
|
|
116
121
|
};
|
|
117
|
-
/**
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
return
|
|
126
|
-
}
|
|
127
|
-
/** The side the phone's list opens on, from the room around the field and the list's cap. */
|
|
128
|
-
function measureListSide(anchor, groups, maxHeight) {
|
|
129
|
-
const view = anchor?.ownerDocument.defaultView;
|
|
130
|
-
if (!anchor || !view) return "bottom";
|
|
122
|
+
/**
|
|
123
|
+
* Keeps room for the list below the field on a phone: when fewer than three
|
|
124
|
+
* rows fit between the field and the bottom of the visual viewport (the
|
|
125
|
+
* keyboard's top edge while it is up), scrolls the field up to the top of
|
|
126
|
+
* what is visible instead of flipping the list above it.
|
|
127
|
+
*/
|
|
128
|
+
function keepRoomBelow(field) {
|
|
129
|
+
const view = field?.ownerDocument.defaultView;
|
|
130
|
+
if (!field || !view) return;
|
|
131
131
|
const visual = view.visualViewport;
|
|
132
|
-
const
|
|
133
|
-
return preferredListSide(anchor.getBoundingClientRect(), visual ? {
|
|
132
|
+
const viewport = visual ? {
|
|
134
133
|
top: visual.offsetTop,
|
|
135
134
|
height: visual.height
|
|
136
135
|
} : {
|
|
137
136
|
top: 0,
|
|
138
137
|
height: view.innerHeight
|
|
139
|
-
}
|
|
138
|
+
};
|
|
139
|
+
if (roomBelowIsShort(field.getBoundingClientRect(), viewport)) {
|
|
140
|
+
if (!field.style.scrollMarginTop) field.style.scrollMarginTop = "2.5rem";
|
|
141
|
+
field.scrollIntoView({
|
|
142
|
+
block: "start",
|
|
143
|
+
inline: "nearest",
|
|
144
|
+
behavior: "instant"
|
|
145
|
+
});
|
|
146
|
+
}
|
|
140
147
|
}
|
|
141
148
|
/** Options-driven composition. The stable compound Combobox API stays unchanged. */
|
|
142
149
|
function isItemEqualToValue(a, b) {
|
|
@@ -422,17 +429,27 @@ function ComboboxSelect({ options, multiple, value, defaultValue, onValueChange,
|
|
|
422
429
|
const valueChange = handleValueChange;
|
|
423
430
|
const listHeight = typeof listMaxHeight === "number" ? `${listMaxHeight}px` : listMaxHeight;
|
|
424
431
|
const fieldRef = useRef(null);
|
|
425
|
-
const [
|
|
426
|
-
const groups = normalized.groups;
|
|
432
|
+
const [phoneOpen, setPhoneOpen] = useState(false);
|
|
427
433
|
const handleOpenChange = useCallback((next, details) => {
|
|
428
434
|
onOpenChange?.(next, details);
|
|
429
|
-
if (
|
|
430
|
-
}, [
|
|
431
|
-
|
|
432
|
-
phone
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
435
|
+
if (phone && !details.isCanceled) setPhoneOpen(next);
|
|
436
|
+
}, [onOpenChange, phone]);
|
|
437
|
+
useEffect(() => {
|
|
438
|
+
if (!phone || !phoneOpen) return;
|
|
439
|
+
const field = fieldRef.current;
|
|
440
|
+
const visual = field?.ownerDocument.defaultView?.visualViewport;
|
|
441
|
+
let frame = 0;
|
|
442
|
+
const check = () => {
|
|
443
|
+
cancelAnimationFrame(frame);
|
|
444
|
+
frame = requestAnimationFrame(() => keepRoomBelow(field));
|
|
445
|
+
};
|
|
446
|
+
check();
|
|
447
|
+
visual?.addEventListener("resize", check);
|
|
448
|
+
return () => {
|
|
449
|
+
cancelAnimationFrame(frame);
|
|
450
|
+
visual?.removeEventListener("resize", check);
|
|
451
|
+
};
|
|
452
|
+
}, [phone, phoneOpen]);
|
|
436
453
|
const popupPropsStyle = popupProps?.style;
|
|
437
454
|
const popupStyle = useMemo(() => {
|
|
438
455
|
if (listHeight === void 0) return popupPropsStyle;
|
|
@@ -473,7 +490,7 @@ function ComboboxSelect({ options, multiple, value, defaultValue, onValueChange,
|
|
|
473
490
|
align: "start",
|
|
474
491
|
collisionPadding: 8,
|
|
475
492
|
...phone ? {
|
|
476
|
-
side:
|
|
493
|
+
side: "bottom",
|
|
477
494
|
collisionAvoidance: PHONE_LIST_COLLISION
|
|
478
495
|
} : null,
|
|
479
496
|
...positionerProps,
|
|
@@ -1106,4 +1123,4 @@ function useCommentMarginFits(element, { minWidth = 784 } = {}) {
|
|
|
1106
1123
|
return desktop && element !== null && width >= minWidth;
|
|
1107
1124
|
}
|
|
1108
1125
|
//#endregion
|
|
1109
|
-
export { Dialog as AnnouncementDialog, AnnouncementDialogAction, AnnouncementDialogBody, DialogClose as AnnouncementDialogClose, AnnouncementDialogContent, AnnouncementDialogFooter, AnnouncementDialogHeader, AnnouncementDialogLabel, AnnouncementDialogPicture, AnnouncementDialogSecondaryAction, AnnouncementDialogText, AnnouncementDialogTitle, DialogTrigger as AnnouncementDialogTrigger, COMMENT_HIGHLIGHT, COMMENT_HIGHLIGHT_ACTIVE, ComboboxSelect, CommentCard, CommentCardMeta, CommentCardQuote, CommentCardTrigger, CommentEntry, CommentEntryAuthor, CommentEntryBody, CommentEntryHeader, CommentEntryTime, CommentMargin, CommentMarginItem, CommentMarker, MessageItemDelegatedAvatar, MessageItemDelegatedBy, MessageItemDelegatedByButton, MessageItemDelegatedByName, MessageListJumpControls, MessageListJumpPill, MessageListJumpPillDismiss, MessageListJumpPillGroup, MessageQuotedPreviewAuthorDelegatedBy, resolveCommentMarginLayout, setCommentHighlights, useCommentMarginFits };
|
|
1126
|
+
export { Dialog as AnnouncementDialog, AnnouncementDialogAction, AnnouncementDialogBody, DialogClose as AnnouncementDialogClose, AnnouncementDialogContent, AnnouncementDialogFooter, AnnouncementDialogHeader, AnnouncementDialogLabel, AnnouncementDialogPicture, AnnouncementDialogSecondaryAction, AnnouncementDialogText, AnnouncementDialogTitle, DialogTrigger as AnnouncementDialogTrigger, COMMENT_HIGHLIGHT, COMMENT_HIGHLIGHT_ACTIVE, ComboboxSelect, CommentCard, CommentCardMeta, CommentCardQuote, CommentCardTrigger, CommentEntry, CommentEntryAuthor, CommentEntryBody, CommentEntryHeader, CommentEntryTime, CommentMargin, CommentMarginItem, CommentMarker, MessageItemDelegatedAvatar, MessageItemDelegatedBy, MessageItemDelegatedByButton, MessageItemDelegatedByName, MessageListJumpControls, MessageListJumpPill, MessageListJumpPillDismiss, MessageListJumpPillGroup, MessageQuotedPreviewAuthorDelegatedBy, resolveCommentMarginLayout, setCommentHighlights, useCommentMarginFits, useHoldThenMoveTouchSensor };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@botiverse/raft-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "React components and CSS foundations for the Raft Design System.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent-skills",
|
|
@@ -86,6 +86,7 @@
|
|
|
86
86
|
"react-dom": "^19"
|
|
87
87
|
},
|
|
88
88
|
"scripts": {
|
|
89
|
+
"bench:virtual-list": "node bench/virtual-list/run.mjs",
|
|
89
90
|
"build:tokens": "node scripts/build-tokens.mjs",
|
|
90
91
|
"build": "vp pack",
|
|
91
92
|
"check": "vp check",
|