diff --git a/apps/designer/app/shared/design-system/components/primitives/dnd/canvas.stories.tsx b/apps/designer/app/shared/design-system/components/primitives/dnd/canvas.stories.tsx
new file mode 100644
index 0000000000..0877e02305
--- /dev/null
+++ b/apps/designer/app/shared/design-system/components/primitives/dnd/canvas.stories.tsx
@@ -0,0 +1,234 @@
+import { ComponentMeta } from "@storybook/react";
+import React, { useState, useRef } from "react";
+import { Box } from "../../box";
+import { useDropTarget } from "./use-drop-target";
+import { useDrag } from "./use-drag";
+import { usePlacement, PlacementIndicator, type Rect } from "./placement";
+import { useAutoScroll } from "./use-auto-scroll";
+
+const ROOT_ID = "root";
+
+type ItemData = {
+ id: string;
+ style: React.CSSProperties;
+ acceptsChildren: boolean;
+ children: ItemData[];
+};
+
+const Item = ({
+ data,
+ dragItemId,
+}: {
+ data: ItemData;
+ dragItemId: string | undefined;
+}) => {
+ return (
+
+
+
+ );
+};
+
+const Items = ({
+ data,
+ dragItemId,
+}: {
+ data: ItemData[];
+ dragItemId: string | undefined;
+}) => {
+ return (
+ <>
+ {data.map((child) => (
+
+ ))}
+ >
+ );
+};
+
+const findItem = (data: ItemData[], id: string): ItemData | undefined => {
+ for (const item of data) {
+ if (item.id === id) {
+ return item;
+ }
+ const found = findItem(item.children, id);
+ if (found) {
+ return found;
+ }
+ }
+ return undefined;
+};
+
+const mapItems = (
+ data: ItemData[],
+ fn: (item: ItemData) => ItemData
+): ItemData[] => {
+ return fn({
+ id: ROOT_ID,
+ style: {},
+ acceptsChildren: true,
+ children: data.map((item) => {
+ return fn({
+ ...item,
+ children: mapItems(item.children, fn),
+ });
+ }),
+ }).children;
+};
+
+export const Canvas = () => {
+ const [data, setData] = useState([
+ {
+ id: "0",
+ style: {},
+ children: [],
+ acceptsChildren: true,
+ },
+ { id: "1", style: {}, children: [], acceptsChildren: true },
+ { id: "2", style: {}, children: [], acceptsChildren: true },
+ { id: "3", style: {}, children: [], acceptsChildren: true },
+ { id: "4", style: {}, children: [], acceptsChildren: true },
+ { id: "5", style: {}, children: [], acceptsChildren: true },
+ ]);
+
+ const [placement, setPalcement] = useState<{
+ index: number;
+ placementRect: Rect;
+ }>();
+
+ const [dragItemId, setDragItemId] = useState();
+
+ const dropTargetId = useRef();
+
+ const dropTargetHandlers = useDropTarget({
+ isDropTarget(element: HTMLElement) {
+ const id = element.dataset.id;
+ return (
+ id != null &&
+ (id === ROOT_ID || findItem(data, id)?.acceptsChildren === true)
+ );
+ },
+ onDropTargetChange(event) {
+ dropTargetId.current = event.target.dataset.id;
+ placementHandlers.handleTargetChange(event.target);
+ },
+ });
+
+ const autoScrollHandlers = useAutoScroll();
+
+ const dragProps = useDrag({
+ onStart(event) {
+ const id = event.target.dataset.id;
+ if (id == null) {
+ event.cancel();
+ return;
+ }
+ setDragItemId(id);
+ autoScrollHandlers.setEnabled(true);
+ },
+ onMove: (poiterCoordinate) => {
+ dropTargetHandlers.handleMove(poiterCoordinate);
+ autoScrollHandlers.handleMove(poiterCoordinate);
+ placementHandlers.handleMove(poiterCoordinate);
+ },
+ onEnd() {
+ const currentDropTargetId = dropTargetId.current;
+ if (
+ placement !== undefined &&
+ dragItemId !== undefined &&
+ currentDropTargetId !== undefined
+ ) {
+ setData((current) => {
+ const dragItem = findItem(current, dragItemId);
+
+ // shouldn't happen, for TypeScript
+ if (dragItem === undefined) {
+ return current;
+ }
+
+ return mapItems(current, (item) => {
+ let children = item.children;
+
+ const oldIndex = children.findIndex(
+ (child) => child.id === dragItemId
+ );
+
+ if (oldIndex !== -1) {
+ children = children.slice();
+ children.splice(oldIndex, 1);
+ }
+
+ if (item.id === currentDropTargetId) {
+ // placement.index does not take into account the fact that the drag item will be removed.
+ // we need to do this to account for it.
+ const newIndex =
+ oldIndex !== -1 && oldIndex < placement.index
+ ? placement.index - 1
+ : placement.index;
+
+ children = children.slice();
+ children.splice(newIndex, 0, dragItem);
+ }
+
+ return children === item.children ? item : { ...item, children };
+ });
+ });
+ }
+
+ dropTargetHandlers.handleEnd();
+ autoScrollHandlers.setEnabled(false);
+ placementHandlers.handleEnd();
+ setDragItemId(undefined);
+ setPalcement(undefined);
+ dropTargetId.current = undefined;
+ },
+ });
+
+ const placementHandlers = usePlacement({
+ onPalcementChange: (event) => {
+ setPalcement(event);
+ },
+ });
+
+ return (
+ <>
+ {
+ dropTargetHandlers.rootRef(element);
+ autoScrollHandlers.targetRef(element);
+ }}
+ onScroll={() => {
+ dropTargetHandlers.handleScroll();
+ placementHandlers.handleScroll();
+ }}
+ data-id={ROOT_ID}
+ {...dragProps}
+ >
+
+
+ {placement && }
+ >
+ );
+};
+
+export default {} as ComponentMeta;
diff --git a/apps/designer/app/shared/design-system/components/primitives/dnd/handlers.stories.tsx b/apps/designer/app/shared/design-system/components/primitives/dnd/handlers.stories.tsx
deleted file mode 100644
index 62640500c5..0000000000
--- a/apps/designer/app/shared/design-system/components/primitives/dnd/handlers.stories.tsx
+++ /dev/null
@@ -1,136 +0,0 @@
-import { ComponentMeta } from "@storybook/react";
-import React, { useRef, useState } from "react";
-import { Box } from "../../box";
-import { useDropTarget } from "./use-drop-target";
-import { useDrag } from "./use-drag";
-import { getPlacement, PlacementIndicator, type Rect } from "./placement";
-import { useAutoScroll } from "./use-auto-scroll";
-
-export const Playground = () => {
- const [dropTargetRect, setDropTargetRect] = useState();
- const [placementIndicatorRect, setPlacementIndicatorRect] = useState();
- const holdRef = useRef();
- const dragItemRef = useRef();
-
- const handleHoldEnd = () => {
- holdRef.current?.style.removeProperty("background");
- };
-
- const dropTargetHandlers = useDropTarget({
- isDropTarget(element: HTMLElement) {
- return element.dataset.draggable === "true";
- },
- onDropTargetChange({ rect, target }) {
- setDropTargetRect(rect);
- const placementRect = getPlacement({ target });
- setPlacementIndicatorRect(placementRect);
- },
- onHold({ target }: { target: HTMLElement }) {
- handleHoldEnd();
- holdRef.current = target;
- target.style.background = "red";
- },
- });
-
- const autoScrollHandlers = useAutoScroll();
-
- const dragProps = useDrag({
- onStart(event) {
- if (event.target.dataset.draggable === "false") {
- event.cancel();
- return;
- }
- dragItemRef.current = event.target;
- autoScrollHandlers.setEnabled(true);
- },
- onMove: (poiterCoordinate) => {
- dropTargetHandlers.handleMove(poiterCoordinate);
- autoScrollHandlers.handleMove(poiterCoordinate);
- },
- onEnd() {
- dropTargetHandlers.handleEnd();
- setDropTargetRect(undefined);
- setPlacementIndicatorRect(undefined);
- handleHoldEnd();
- autoScrollHandlers.setEnabled(false);
- },
- onShiftChange({ shifts }) {
- if (dragItemRef.current) {
- dragItemRef.current.textContent = `shifted ${shifts}`;
- }
- },
- });
-
- return (
- {
- dropTargetHandlers.rootRef(el);
- autoScrollHandlers.targetRef(el);
- }}
- css={{ display: "block", overflow: "auto", height: 500 }}
- onScroll={dropTargetHandlers.handleScroll}
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
- Not Draggable
-
-
-
-
- );
-};
-
-export default {} as ComponentMeta;
-
-const Item = ({
- background,
- draggable = true,
- children,
-}: {
- background: string;
- draggable?: boolean;
- children?: React.ReactNode;
-}) => (
-
- {children}
-
-);
-
-const Outline = ({ rect }: { rect?: Rect }) => {
- if (rect === undefined) return null;
- const style = {
- top: rect.top,
- left: rect.left,
- width: rect.width,
- height: rect.height,
- };
- return (
-
- );
-};
diff --git a/apps/designer/app/shared/design-system/components/primitives/dnd/sortable-list.stories.tsx b/apps/designer/app/shared/design-system/components/primitives/dnd/sortable-list.stories.tsx
index 6882ef62a4..6fed0dc20f 100644
--- a/apps/designer/app/shared/design-system/components/primitives/dnd/sortable-list.stories.tsx
+++ b/apps/designer/app/shared/design-system/components/primitives/dnd/sortable-list.stories.tsx
@@ -102,7 +102,7 @@ export const SortableList = ({
let newIndex = placement.index;
// placement.index does not take into account the fact that the drag item will be removed.
- // we need to do this to account for that
+ // we need to do this to account for it.
if (oldIndex < newIndex) {
newIndex = Math.max(0, newIndex - 1);
}