Files
paseo/packages/app/src/components/draggable-list.web.tsx
T
Mohamed BoudraandGitHub 1b8af2e58e Refine tool call summaries and scrolling (#2069)
* feat(app): refine tool call summaries and scrolling

Keep summary groups stable from the first tool call, preserve live activity feedback, and scroll expanded groups to the latest entry. Consolidate web scrollbar styling around the native browser mechanism.

* refactor(app): reuse tool call height constant

* fix(app): preserve tool call summary state

* fix(app): refresh web tool groups on expand

* fix(app): preserve grouped tool errors
2026-07-14 21:16:29 +02:00

248 lines
6.8 KiB
TypeScript

import { memo, useCallback, useMemo, useRef, type ReactElement } from "react";
import { ScrollView, View } from "react-native";
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
type Modifier,
useSensor,
useSensors,
} from "@dnd-kit/core";
import {
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
useSortable,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import type { DraggableListProps, DraggableRenderItemInfo } from "./draggable-list.types";
import { getPointerActivationConstraint, useDragReorderState } from "./drag-reorder";
export type { DraggableListProps, DraggableRenderItemInfo };
const restrictToVerticalAxis: Modifier = ({ transform }) => ({
...transform,
x: 0,
});
const DND_MODIFIERS = [restrictToVerticalAxis];
const POINTER_ACTIVATION_CONFIG = {
defaultDistance: 6,
holdDelayMs: 250,
holdTolerance: 8,
};
interface SortableItemProps<T> {
id: string;
item: T;
index: number;
renderItem: (info: DraggableRenderItemInfo<T>) => ReactElement;
activeId: string | null;
useDragHandle: boolean;
}
function SortableItemInner<T>({
id,
item,
index,
renderItem,
activeId,
useDragHandle,
}: SortableItemProps<T>) {
const {
attributes,
listeners,
setNodeRef,
setActivatorNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id });
const dragRef = useRef<(() => void) | null>(null);
const drag = useCallback(() => {
// dnd-kit handles drag initiation via listeners
// This is a no-op but matches the mobile API
}, []);
// Store listeners in ref so drag handle can access them
dragRef.current = () => {
// Trigger drag - handled by dnd-kit's listeners
};
// dnd-kit can set `scaleX/scaleY` on the active item when dragging over a
// differently-sized droppable. For variable-height rows this can look like
// the "ghost" stretches. Keep the dragged item's size stable by zeroing
// out the dnd-kit scaling component.
const baseTransform = CSS.Transform.toString(
transform && isDragging ? { ...transform, scaleX: 1, scaleY: 1 } : transform,
);
const scaleTransform = isDragging ? "scale(1.02)" : "";
const combinedTransform = [baseTransform, scaleTransform].filter(Boolean).join(" ");
const style = useMemo(
() => ({
transform: combinedTransform || undefined,
transition,
opacity: isDragging ? 0.9 : 1,
zIndex: isDragging ? 1000 : 1,
}),
[combinedTransform, transition, isDragging],
);
const info: DraggableRenderItemInfo<T> = {
item,
index,
drag,
isActive: activeId === id,
dragHandleProps: useDragHandle
? {
attributes: attributes as unknown as Record<string, unknown>,
listeners: listeners as unknown as Record<string, unknown>,
setActivatorNodeRef: setActivatorNodeRef as unknown as (node: unknown) => void,
}
: undefined,
};
const wrapperProps = useDragHandle
? { ref: setNodeRef }
: { ref: setNodeRef, ...attributes, ...listeners };
return (
<div {...wrapperProps} style={style}>
{renderItem(info)}
</div>
);
}
const SortableItem = memo(SortableItemInner) as typeof SortableItemInner;
export function DraggableList<T>({
data,
keyExtractor,
renderItem,
onDragEnd,
style,
containerStyle,
contentContainerStyle,
testID,
ListFooterComponent,
ListHeaderComponent,
ListEmptyComponent,
showsVerticalScrollIndicator = true,
scrollEnabled = true,
extraData: _extraData,
useDragHandle = false,
// simultaneousGestureRef is native-only, ignored on web
onDragBegin,
nestable: _nestable = false,
}: DraggableListProps<T>) {
const { activeId, items, handlers } = useDragReorderState({
data,
keyExtractor,
onDragEnd,
onDragBegin,
});
const pointerActivationConstraint = getPointerActivationConstraint(
useDragHandle,
POINTER_ACTIVATION_CONFIG,
);
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: pointerActivationConstraint,
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
}),
);
const ids = useMemo(
() => items.map((item, index) => keyExtractor(item, index)),
[items, keyExtractor],
);
const wrapperStyle = useMemo(
() => [
{ position: "relative" as const },
scrollEnabled ? { flex: 1, minHeight: 0 } : null,
containerStyle,
],
[scrollEnabled, containerStyle],
);
return (
<View style={wrapperStyle}>
{scrollEnabled ? (
<ScrollView
testID={testID}
style={style}
contentContainerStyle={contentContainerStyle}
showsVerticalScrollIndicator={showsVerticalScrollIndicator}
>
{ListHeaderComponent}
{items.length === 0 && ListEmptyComponent}
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
modifiers={DND_MODIFIERS}
onDragStart={handlers.onDragStart}
onDragCancel={handlers.onDragCancel}
onDragEnd={handlers.onDragEnd}
>
<SortableContext items={ids} strategy={verticalListSortingStrategy}>
{items.map((item, index) => {
const id = keyExtractor(item, index);
return (
<SortableItem
key={id}
id={id}
item={item}
index={index}
renderItem={renderItem}
activeId={activeId}
useDragHandle={useDragHandle}
/>
);
})}
</SortableContext>
</DndContext>
{ListFooterComponent}
</ScrollView>
) : (
<>
{ListHeaderComponent}
{items.length === 0 && ListEmptyComponent}
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
modifiers={DND_MODIFIERS}
onDragStart={handlers.onDragStart}
onDragCancel={handlers.onDragCancel}
onDragEnd={handlers.onDragEnd}
>
<SortableContext items={ids} strategy={verticalListSortingStrategy}>
{items.map((item, index) => {
const id = keyExtractor(item, index);
return (
<SortableItem
key={id}
id={id}
item={item}
index={index}
renderItem={renderItem}
activeId={activeId}
useDragHandle={useDragHandle}
/>
);
})}
</SortableContext>
</DndContext>
{ListFooterComponent}
</>
)}
</View>
);
}