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..f5a9982083
--- /dev/null
+++ b/apps/designer/app/shared/design-system/components/primitives/dnd/canvas.stories.tsx
@@ -0,0 +1,309 @@
+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 elementToId = (element: HTMLElement) => element.dataset.id;
+
+const idToElement = (
+ root: HTMLElement,
+ id: string
+): HTMLElement | undefined => {
+ if (elementToId(root) === id) {
+ return root;
+ }
+ for (const child of root.children) {
+ if (child instanceof HTMLElement) {
+ const found = idToElement(child, 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;
+};
+
+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 findItemPath = (data: ItemData[], id: string): ItemData[] | undefined => {
+ for (const item of data) {
+ if (item.id === id) {
+ return [item];
+ }
+ const found = findItemPath(item.children, id);
+ if (found) {
+ return [...found, item];
+ }
+ }
+ return undefined;
+};
+
+export const Canvas = () => {
+ const [data, setData] = useState([
+ {
+ id: "0",
+ style: {},
+ children: [
+ { id: "1", style: { margin: 0 }, children: [], acceptsChildren: true },
+ { id: "2", style: { margin: 0 }, children: [], acceptsChildren: true },
+ { id: "3", style: { margin: 0 }, children: [], acceptsChildren: true },
+ ],
+ acceptsChildren: true,
+ },
+ { id: "4", style: {}, children: [], acceptsChildren: true },
+ { id: "5", style: {}, children: [], acceptsChildren: true },
+ { id: "6", style: {}, children: [], acceptsChildren: false },
+ ]);
+
+ const [placement, setPalcement] = useState<{
+ index: number;
+ placementRect: Rect;
+ }>();
+
+ const [dragItemId, setDragItemId] = useState();
+
+ const dropTargetId = useRef();
+ const rootRef = useRef(null);
+
+ const setDropTarget = (id: string, element: HTMLElement) => {
+ dropTargetId.current = id;
+ placementHandlers.handleTargetChange(element);
+ };
+
+ const dropTargetHandlers = useDropTarget({
+ edgeDistanceThreshold: 10,
+ isDropTarget(element: HTMLElement) {
+ return elementToId(element) !== undefined;
+ },
+ onDropTargetChange(event) {
+ const id = elementToId(event.target);
+ const rootElement = rootRef.current;
+
+ if (id === undefined || rootElement === null) {
+ return;
+ }
+
+ if (id !== ROOT_ID) {
+ let path = findItemPath(data, id) ?? [];
+
+ // NOTE: not sure we should always do this.
+ // Might depend on whether there's a parent with acceptsChildren, or something.
+ if (event.area !== "center") {
+ path = path.slice(1);
+ }
+
+ // to make sure we are not dropping on ourself
+ const dragItemIndex = path.findIndex((item) => item.id === dragItemId);
+ if (dragItemIndex !== -1) {
+ path = path.slice(dragItemIndex + 1);
+ }
+
+ for (const item of path) {
+ if (item.acceptsChildren) {
+ const element = idToElement(rootElement, item.id);
+ if (element) {
+ setDropTarget(item.id, element);
+ return;
+ }
+ }
+ }
+ }
+
+ setDropTarget(ROOT_ID, rootElement);
+ },
+ });
+
+ 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({
+ onPlacementChange: (event) => {
+ setPalcement(event);
+ },
+ });
+
+ return (
+ <>
+ {
+ dropTargetHandlers.rootRef(element);
+ autoScrollHandlers.targetRef(element);
+ rootRef.current = 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/placement.tsx b/apps/designer/app/shared/design-system/components/primitives/dnd/placement.tsx
new file mode 100644
index 0000000000..a20a29d724
--- /dev/null
+++ b/apps/designer/app/shared/design-system/components/primitives/dnd/placement.tsx
@@ -0,0 +1,234 @@
+import { useRef } from "react";
+import { Box } from "../../box";
+
+export type Rect = Pick;
+type Point = { x: number; y: number };
+
+const probe = document.createElement("div");
+
+/**
+ * We place a div at the position where future drop will happen.
+ * We learn if the indicator will render vertically or horizontally as well as its width/heigh.
+ */
+const getPlacement = ({
+ target,
+ index = 0,
+}: {
+ target: HTMLElement;
+ index?: number;
+}) => {
+ const { children } = target;
+ if (index > children.length - 1) {
+ target.appendChild(probe);
+ } else {
+ target.insertBefore(probe, children[index]);
+ }
+ const rect = probe.getBoundingClientRect();
+ target.removeChild(probe);
+ return rect;
+};
+
+// https://stackoverflow.com/a/18157551/478603
+const getDistance = (rect: Rect, { x, y }: Point) => {
+ const dx = Math.max(rect.left - x, 0, x - (rect.left + rect.width));
+ const dy = Math.max(rect.top - y, 0, y - (rect.top + rect.height));
+ return Math.sqrt(dx * dx + dy * dy);
+};
+
+const getClosestRectIndex = (rects: Rect[], point: Point) => {
+ if (rects.length === 0) {
+ return -1;
+ }
+
+ const sorted = rects
+ .map((rect, index) => ({
+ index,
+ distance: getDistance(rect, point),
+ }))
+ .sort((a, b) => a.distance - b.distance);
+
+ return sorted[0].index;
+};
+
+const isEqualRect = (a: Rect | undefined, b: Rect) => {
+ return (
+ a !== undefined &&
+ a.top === b.top &&
+ a.left === b.left &&
+ a.width === b.width &&
+ a.height === b.height
+ );
+};
+
+type UsePlacementProps = {
+ onPlacementChange: (event: { index: number; placementRect: Rect }) => void;
+};
+
+type Handlers = {
+ handleEnd: () => void;
+ handleMove: (pointerPoint: Point) => void;
+ handleScroll: () => void;
+ handleTargetChange: (target: HTMLElement) => void;
+};
+
+export const usePlacement = ({
+ onPlacementChange,
+}: UsePlacementProps): Handlers => {
+ const state = useRef({
+ childrenRectsCache: new WeakMap(),
+ target: undefined as HTMLElement | undefined,
+ targetRect: undefined as DOMRect | undefined,
+ pointerPoint: undefined as Point | undefined,
+ index: undefined as number | undefined,
+ placementRext: undefined as Rect | undefined,
+ });
+
+ const getChildrenRects = (parent: HTMLElement, parentRect: DOMRect) => {
+ const fromCache = state.current.childrenRectsCache.get(parent);
+ if (fromCache !== undefined) {
+ return fromCache;
+ }
+
+ const result: Rect[] = [];
+
+ for (const child of parent.children) {
+ if (child instanceof HTMLElement) {
+ const rect = child.getBoundingClientRect();
+ result.push({
+ top: rect.top - parentRect.top,
+ left: rect.left - parentRect.left,
+ width: rect.width,
+ height: rect.height,
+ });
+ }
+ }
+
+ state.current.childrenRectsCache.set(parent, result);
+ return result;
+ };
+
+ const getIndex = (
+ parent: HTMLElement,
+ parentRect: DOMRect,
+ pointerPoint: Point
+ ) => {
+ if (parent.children.length === 0) {
+ return 0;
+ }
+
+ const childrenRects = getChildrenRects(parent, parentRect);
+
+ const pointerAdjusted = {
+ x: pointerPoint.x - parentRect.left,
+ y: pointerPoint.y - parentRect.top,
+ };
+
+ const closestRectIndex = getClosestRectIndex(
+ childrenRects,
+ pointerAdjusted
+ );
+ const closesRect = childrenRects[closestRectIndex];
+
+ const isOrientationVertical =
+ childrenRects.length < 2 ||
+ childrenRects[0].left === childrenRects[1].left;
+
+ if (isOrientationVertical) {
+ const middleY = closesRect.top + closesRect.height / 2;
+ return pointerAdjusted.y < middleY
+ ? closestRectIndex
+ : closestRectIndex + 1;
+ } else {
+ const middleX = closesRect.left + closesRect.width / 2;
+ return pointerAdjusted.x < middleX
+ ? closestRectIndex
+ : closestRectIndex + 1;
+ }
+ };
+
+ const detectChange = (
+ reason: "pointer-move" | "scroll" | "target-change"
+ ) => {
+ const { target, pointerPoint } = state.current;
+
+ if (target === undefined || pointerPoint === undefined) {
+ return;
+ }
+
+ const nextTargetRect = target.getBoundingClientRect();
+ const targetRectChanged = !isEqualRect(
+ state.current.targetRect,
+ nextTargetRect
+ );
+
+ state.current.targetRect = nextTargetRect;
+
+ const nextIndex = getIndex(target, nextTargetRect, pointerPoint);
+ const indexChanged = nextIndex !== state.current.index;
+ state.current.index = nextIndex;
+
+ if (reason !== "target-change" && !indexChanged && !targetRectChanged) {
+ return;
+ }
+
+ const nextPlacementRect = getPlacement({ target, index: nextIndex });
+ const placementRectChanged = !isEqualRect(
+ state.current.placementRext,
+ nextPlacementRect
+ );
+ state.current.placementRext = nextPlacementRect;
+
+ if (placementRectChanged || indexChanged) {
+ onPlacementChange({ index: nextIndex, placementRect: nextPlacementRect });
+ }
+ };
+
+ return {
+ handleEnd() {
+ state.current = {
+ childrenRectsCache: new WeakMap(),
+ target: undefined,
+ targetRect: undefined,
+ pointerPoint: undefined,
+ index: undefined,
+ placementRext: undefined,
+ };
+ },
+ handleMove(pointerPoint) {
+ state.current.pointerPoint = pointerPoint;
+ detectChange("pointer-move");
+ },
+ handleScroll() {
+ detectChange("scroll");
+ },
+ handleTargetChange: (target) => {
+ state.current.target = target;
+ detectChange("target-change");
+ },
+ };
+};
+
+export const PlacementIndicator = ({ rect }: { rect?: Rect }) => {
+ if (rect === undefined) {
+ return null;
+ }
+
+ const style = {
+ top: rect.top - 1,
+ left: rect.left - 1,
+ 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
new file mode 100644
index 0000000000..adfa20c62e
--- /dev/null
+++ b/apps/designer/app/shared/design-system/components/primitives/dnd/sortable-list.stories.tsx
@@ -0,0 +1,182 @@
+import { ComponentMeta } from "@storybook/react";
+import { useState } from "react";
+import { Box } from "../../box";
+import { styled } from "../../../stitches.config";
+import { useDropTarget } from "./use-drop-target";
+import { useDrag } from "./use-drag";
+import { usePlacement, PlacementIndicator, type Rect } from "./placement";
+import { useAutoScroll } from "./use-auto-scroll";
+
+type ItemData = { id: string; text: string };
+
+const ListItem = styled("li", {
+ display: "block",
+ margin: 10,
+ background: "$mint12",
+ padding: 10,
+ userSelect: "none",
+});
+
+const List = styled("ul", {
+ display: "block",
+ margin: 0,
+ padding: 10,
+});
+
+const Item = ({
+ data,
+ isDragging,
+}: {
+ data: ItemData;
+ isDragging: boolean;
+}) => {
+ return (
+
+ {data.text}
+
+ );
+};
+
+export const SortableList = ({
+ direction = "vertical",
+}: {
+ direction?: "horizontal" | "vertical" | "wrap";
+}) => {
+ const [data, setData] = useState([
+ { id: "0", text: "First" },
+ { id: "1", text: "Second" },
+ { id: "2", text: "Third" },
+ { id: "3", text: "Fourth" },
+ { id: "4", text: "Fifth" },
+ { id: "5", text: "Sixth" },
+ { id: "6", text: "Seventh" },
+ { id: "7", text: "Eighth" },
+ { id: "8", text: "Ninth" },
+ { id: "9", text: "Tenth" },
+ { id: "10", text: "Eleventh" },
+ { id: "11", text: "Twelfth" },
+ { id: "12", text: "Thirteenth" },
+ { id: "13", text: "Fourteenth" },
+ { id: "14", text: "Fifteenth" },
+ { id: "15", text: "Sixteenth" },
+ { id: "16", text: "Seventeenth" },
+ { id: "17", text: "Eighteenth" },
+ ] as ItemData[]);
+
+ const [placement, setPalcement] = useState<{
+ index: number;
+ placementRect: Rect;
+ }>();
+
+ const [dragItemId, setDragItemId] = useState();
+
+ const dropTargetHandlers = useDropTarget({
+ isDropTarget(element: HTMLElement) {
+ return element instanceof HTMLUListElement;
+ },
+ onDropTargetChange(event) {
+ placementHandlers.handleTargetChange(event.target);
+ },
+ });
+
+ const autoScrollHandlers = useAutoScroll();
+
+ const dragProps = useDrag({
+ onStart(event) {
+ if (!(event.target instanceof HTMLLIElement)) {
+ event.cancel();
+ return;
+ }
+ setDragItemId(event.target.dataset.id);
+ autoScrollHandlers.setEnabled(true);
+ },
+ onMove: (poiterCoordinate) => {
+ dropTargetHandlers.handleMove(poiterCoordinate);
+ autoScrollHandlers.handleMove(poiterCoordinate);
+ placementHandlers.handleMove(poiterCoordinate);
+ },
+ onEnd() {
+ if (placement !== undefined && dragItemId !== undefined) {
+ const oldIndex = data.findIndex((item) => item.id === dragItemId);
+ if (oldIndex !== -1) {
+ 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 it.
+ if (oldIndex < newIndex) {
+ newIndex = Math.max(0, newIndex - 1);
+ }
+
+ if (oldIndex !== newIndex) {
+ const newData = [...data];
+ newData.splice(oldIndex, 1);
+ newData.splice(newIndex, 0, data[oldIndex]);
+ setData(newData);
+ }
+ }
+ }
+
+ dropTargetHandlers.handleEnd();
+ autoScrollHandlers.setEnabled(false);
+ placementHandlers.handleEnd();
+ setDragItemId(undefined);
+ setPalcement(undefined);
+ },
+ });
+
+ const placementHandlers = usePlacement({
+ onPlacementChange: (event) => {
+ setPalcement(event);
+ },
+ });
+
+ return (
+ <>
+ {
+ dropTargetHandlers.handleScroll();
+ placementHandlers.handleScroll();
+ }}
+ {...dragProps}
+ >
+
+ {data.map((item) => (
+
+ ))}
+
+
+ {placement && }
+ >
+ );
+};
+
+export const SortableListHorizontal = () => (
+
+);
+
+export const SortableListWrap = () => ;
+
+export default {} as ComponentMeta;
diff --git a/apps/designer/app/shared/design-system/components/primitives/dnd/use-auto-scroll.ts b/apps/designer/app/shared/design-system/components/primitives/dnd/use-auto-scroll.ts
new file mode 100644
index 0000000000..522c20247f
--- /dev/null
+++ b/apps/designer/app/shared/design-system/components/primitives/dnd/use-auto-scroll.ts
@@ -0,0 +1,152 @@
+import { useRef } from "react";
+
+// Time between frames of scroll animation in milliseconds
+const FRAME_PERIOD = 30;
+
+const getSpeed = (
+ pointerPosition: number,
+ containerStart: number,
+ containerEnd: number,
+ edgeDistanceThreshold: number,
+ minSpeed: number,
+ maxSpeed: number
+) => {
+ const thresholdSafe = Math.min(
+ (containerEnd - containerStart) / 2,
+ edgeDistanceThreshold
+ );
+
+ const startAdjusted = containerStart + thresholdSafe;
+ const endAdjusted = containerEnd - thresholdSafe;
+
+ const distanceFromEdgeToSpeed = (distance: number) => {
+ // between 0 and 1
+ const normalized = Math.min(distance, thresholdSafe) / thresholdSafe;
+
+ // speed in pixels per second
+ return minSpeed + normalized * (maxSpeed - minSpeed);
+ };
+
+ if (pointerPosition < startAdjusted) {
+ return -1 * distanceFromEdgeToSpeed(startAdjusted - pointerPosition);
+ }
+
+ if (pointerPosition > endAdjusted) {
+ return distanceFromEdgeToSpeed(pointerPosition - endAdjusted);
+ }
+
+ return 0;
+};
+
+export type Parameters = {
+ edgeDistanceThreshold?: number;
+
+ // min/max speed of the scroll animation in pixels per second
+ minSpeed?: number;
+ maxSpeed?: number;
+};
+
+export type Handlers = {
+ handleMove: (pointerCoordinate: { x: number; y: number }) => void;
+ setEnabled: (enabled: boolean) => void;
+ targetRef: (element: HTMLElement | null) => void;
+};
+
+export const useAutoScroll = ({
+ edgeDistanceThreshold = 100,
+ minSpeed = 1,
+ maxSpeed = 500,
+}: Parameters = {}): Handlers => {
+ const state = useRef({
+ target: null as HTMLElement | null,
+ enabled: false,
+ prevTimestamp: 0,
+ speedX: 0,
+ speedY: 0,
+ stepScheduled: false,
+ });
+
+ const step = (timestamp: number) => {
+ state.current.stepScheduled = false;
+
+ if (
+ !state.current.enabled ||
+ (Math.round((state.current.speedX / 1000) * FRAME_PERIOD) === 0 &&
+ Math.round((state.current.speedY / 1000) * FRAME_PERIOD) === 0)
+ ) {
+ return;
+ }
+
+ const elapsed = timestamp - state.current.prevTimestamp;
+
+ // to avoid a big jump when auto-scroll becomes enabled
+ if (elapsed > 3000) {
+ state.current.prevTimestamp = timestamp;
+ scheduleStep();
+ return;
+ }
+
+ if (elapsed < FRAME_PERIOD) {
+ scheduleStep();
+ return;
+ }
+
+ state.current.prevTimestamp = timestamp;
+
+ // for TypeScript
+ if (state.current.target === null) {
+ return;
+ }
+
+ state.current.target.scrollBy(
+ (state.current.speedX / 1000) * elapsed,
+ (state.current.speedY / 1000) * elapsed
+ );
+
+ scheduleStep();
+ };
+
+ const scheduleStep = () => {
+ if (!state.current.stepScheduled) {
+ state.current.stepScheduled = true;
+ window.requestAnimationFrame(step);
+ }
+ };
+
+ return {
+ handleMove({ x, y }) {
+ if (!state.current.enabled || state.current.target === null) {
+ return;
+ }
+
+ const rect = state.current.target.getBoundingClientRect();
+
+ state.current.speedY = getSpeed(
+ y,
+ rect.top,
+ rect.bottom,
+ edgeDistanceThreshold,
+ minSpeed,
+ maxSpeed
+ );
+
+ state.current.speedX = getSpeed(
+ x,
+ rect.left,
+ rect.right,
+ edgeDistanceThreshold,
+ minSpeed,
+ maxSpeed
+ );
+
+ scheduleStep();
+ },
+ setEnabled(newEnabled) {
+ state.current.enabled = newEnabled;
+ scheduleStep();
+ },
+ targetRef: (element) => {
+ state.current.target = element;
+ },
+ };
+};
diff --git a/apps/designer/app/shared/design-system/components/primitives/dnd/use-drag.ts b/apps/designer/app/shared/design-system/components/primitives/dnd/use-drag.ts
new file mode 100644
index 0000000000..e168b71eca
--- /dev/null
+++ b/apps/designer/app/shared/design-system/components/primitives/dnd/use-drag.ts
@@ -0,0 +1,120 @@
+import { useMove } from "./use-move";
+import { useRef } from "react";
+
+type State =
+ | {
+ status: "idle" | "canceled";
+ }
+ | {
+ status: "pending" | "dragging";
+ x: number;
+ y: number;
+ initialX: number;
+ initialY: number;
+ shifts: number;
+ };
+
+const initialState = {
+ status: "idle",
+} as const;
+
+export type Parameters = {
+ startDistanceThreashold?: number;
+ shiftDistanceThreshold?: number;
+ onStart: (event: { target: HTMLElement; cancel: () => void }) => void;
+ onMove: (event: { x: number; y: number }) => void;
+ onShiftChange?: (event: { shifts: number }) => void;
+ onEnd: () => void;
+};
+
+export const useDrag = ({
+ startDistanceThreashold = 3,
+ shiftDistanceThreshold = 20,
+ onStart,
+ onMove,
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
+ onShiftChange = () => {},
+ onEnd,
+}: Parameters): React.HTMLAttributes => {
+ const state = useRef(initialState);
+
+ const cancel = () => {
+ state.current = { status: "canceled" };
+ };
+
+ const detectShift = () => {
+ if (state.current.status !== "dragging") {
+ return;
+ }
+ const deltaX = state.current.x - state.current.initialX;
+ const shifts =
+ deltaX > 0
+ ? Math.floor(deltaX / shiftDistanceThreshold)
+ : Math.ceil(deltaX / shiftDistanceThreshold);
+
+ if (shifts !== state.current.shifts) {
+ state.current.shifts = shifts;
+ onShiftChange({ shifts });
+ }
+ };
+
+ const props = useMove({
+ onMoveStart({
+ pageX: x,
+ pageY: y,
+ target,
+ }: {
+ pageX: number;
+ pageY: number;
+ target: HTMLElement;
+ }) {
+ state.current = {
+ status: "pending",
+ x,
+ y,
+ initialX: x,
+ initialY: y,
+ shifts: 0,
+ };
+ onStart({ target, cancel });
+ },
+ onMove({ pageX: x, pageY: y }: { pageX: number; pageY: number }) {
+ if (state.current.status === "canceled") {
+ return;
+ }
+
+ // We want to start dragging only when the user has moved more than startDistanceThreashold.
+ if (
+ state.current.status === "pending" &&
+ Math.abs(x - state.current.x) < startDistanceThreashold &&
+ Math.abs(y - state.current.y) < startDistanceThreashold
+ ) {
+ return;
+ }
+
+ // We shouldn't be in non-pending state when we get here, unles user has ended the drag and yet somehow we ended up with onMove() call.
+ if (
+ state.current.status === "pending" ||
+ state.current.status === "dragging"
+ ) {
+ state.current = {
+ status: "dragging",
+ x,
+ y,
+ initialX: state.current.initialX,
+ initialY: state.current.initialY,
+ shifts: state.current.shifts,
+ };
+ }
+
+ onMove({ x, y });
+ detectShift();
+ },
+ onMoveEnd() {
+ state.current = initialState;
+ onEnd();
+ },
+ });
+
+ return props.moveProps;
+};
diff --git a/apps/designer/app/shared/design-system/components/primitives/dnd/use-drop-target.ts b/apps/designer/app/shared/design-system/components/primitives/dnd/use-drop-target.ts
new file mode 100644
index 0000000000..f3c5ea501b
--- /dev/null
+++ b/apps/designer/app/shared/design-system/components/primitives/dnd/use-drop-target.ts
@@ -0,0 +1,171 @@
+import { useRef } from "react";
+
+export type Area = "top" | "right" | "bottom" | "left" | "center";
+
+export type Parameters = {
+ isDropTarget: (element: HTMLElement) => boolean;
+ edgeDistanceThreshold?: number;
+ holdTimeThreshold?: number;
+ onDropTargetChange: (event: {
+ rect: DOMRect;
+ area: Area;
+ target: HTMLElement;
+ }) => void;
+ onHold?: (event: { target: HTMLElement }) => void;
+};
+
+export type Handlers = {
+ handleMove: (pointerCoordinate: Coordinate) => void;
+ handleScroll: () => void;
+ handleEnd: () => void;
+ rootRef: (element: HTMLElement | null) => void;
+};
+
+export const useDropTarget = ({
+ isDropTarget,
+ edgeDistanceThreshold = 3,
+ holdTimeThreshold = 500,
+ onDropTargetChange,
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
+ onHold = () => {},
+}: Parameters): Handlers => {
+ const state = useRef({
+ root: null as HTMLElement | null,
+ target: undefined as HTMLElement | undefined,
+ area: "center" as Area,
+ rect: undefined as DOMRect | undefined,
+ holdTimeout: undefined as NodeJS.Timeout | undefined,
+ pointerCoordinate: undefined as Coordinate | undefined,
+ dropIndex: undefined as number | undefined,
+ });
+
+ const clearHoldTimeout = () => {
+ if (state.current.holdTimeout) {
+ clearTimeout(state.current.holdTimeout);
+ state.current.holdTimeout = undefined;
+ }
+ };
+
+ const detectHold = (target: HTMLElement) => {
+ clearHoldTimeout();
+ state.current.holdTimeout = setTimeout(() => {
+ state.current.holdTimeout = undefined;
+ if (target === state.current.target) {
+ onHold({ target });
+ }
+ }, holdTimeThreshold);
+ };
+
+ const detectTarget = () => {
+ const pointerCoordinate = state.current.pointerCoordinate;
+ if (pointerCoordinate === undefined) {
+ return;
+ }
+
+ const target = elementFromPoint(pointerCoordinate);
+ if (target === undefined || state.current.root === null) {
+ return;
+ }
+
+ const nextTarget = findClosestDropTarget({
+ root: state.current.root,
+ target,
+ isDropTarget,
+ });
+
+ const hasTargetChanged = nextTarget !== state.current.target;
+ state.current.target = nextTarget;
+
+ if (hasTargetChanged) {
+ detectHold(nextTarget);
+ }
+
+ const nextRect = nextTarget.getBoundingClientRect();
+
+ const hasRectChanged = nextRect !== state.current.rect;
+ state.current.rect = nextRect;
+
+ const nextArea = getArea(
+ pointerCoordinate,
+ edgeDistanceThreshold,
+ nextRect
+ );
+
+ const hasAreaChanged = nextArea !== state.current.area;
+ state.current.area = nextArea;
+
+ if (hasTargetChanged || hasAreaChanged || hasRectChanged) {
+ onDropTargetChange({
+ rect: nextRect,
+ area: nextArea,
+ target: nextTarget,
+ });
+ }
+ };
+
+ return {
+ handleMove(pointerCoordinate: Coordinate) {
+ state.current.pointerCoordinate = pointerCoordinate;
+ detectTarget();
+ },
+
+ handleScroll() {
+ detectTarget();
+ },
+
+ handleEnd() {
+ // Clear onHold timeout if we ended interaction before it fired
+ clearHoldTimeout();
+ },
+
+ rootRef(rootElement: HTMLElement | null) {
+ state.current.root = rootElement;
+ },
+ };
+};
+
+type Coordinate = { x: number; y: number };
+
+const elementFromPoint = (coordinate: Coordinate): HTMLElement | undefined => {
+ const element = document.elementFromPoint(coordinate.x, coordinate.y);
+ if (element instanceof HTMLElement) return element;
+};
+
+const getArea = (
+ pointerCoordinate: Coordinate,
+ edgeDistanceThreshold: number,
+ targetRect?: DOMRect
+) => {
+ let area: Area = "center";
+ if (targetRect === undefined) return area;
+ // We are at the edge and this means user wants to insert after that element into its parent
+ if (pointerCoordinate.y - targetRect.top <= edgeDistanceThreshold)
+ area = "top";
+ if (targetRect.bottom - pointerCoordinate.y <= edgeDistanceThreshold)
+ area = "bottom";
+ if (pointerCoordinate.x - targetRect.left <= edgeDistanceThreshold)
+ area = "left";
+ if (targetRect.right - pointerCoordinate.x <= edgeDistanceThreshold)
+ area = "right";
+ return area;
+};
+
+const findClosestDropTarget = ({
+ root,
+ target,
+ isDropTarget,
+}: {
+ root: HTMLElement;
+ target: HTMLElement;
+ isDropTarget: (target: HTMLElement) => boolean;
+}): HTMLElement => {
+ let currentTarget: HTMLElement | null = target;
+ while (currentTarget !== null && currentTarget !== root) {
+ const isValid = isDropTarget(currentTarget);
+ if (isValid) {
+ return currentTarget;
+ }
+ currentTarget = currentTarget.parentElement;
+ }
+ return root;
+};
diff --git a/apps/designer/app/shared/design-system/components/primitives/dnd/use-move.ts b/apps/designer/app/shared/design-system/components/primitives/dnd/use-move.ts
new file mode 100644
index 0000000000..b16447981d
--- /dev/null
+++ b/apps/designer/app/shared/design-system/components/primitives/dnd/use-move.ts
@@ -0,0 +1,332 @@
+/* eslint-disable prefer-const */
+/* eslint-disable func-style */
+/*
+ * Copyright 2020 Adobe. All rights reserved.
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License. You may obtain a copy
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
+ * OF ANY KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+/**
+ * We had to copy this file from react-aria because we need pageX and pageY.
+ */
+
+import React, { HTMLAttributes, useMemo, useRef } from "react";
+import { useGlobalListeners } from "@react-aria/utils";
+
+interface MoveResult {
+ /** Props to spread on the target element. */
+ moveProps: HTMLAttributes;
+}
+
+interface EventBase {
+ shiftKey: boolean;
+ ctrlKey: boolean;
+ metaKey: boolean;
+ altKey: boolean;
+}
+
+export type PointerType = "mouse" | "pen" | "touch" | "keyboard" | "virtual";
+
+interface BaseMoveEvent {
+ /** The pointer type that triggered the move event. */
+ pointerType: PointerType;
+ /** Whether the shift keyboard modifier was held during the move event. */
+ shiftKey: boolean;
+ /** Whether the ctrl keyboard modifier was held during the move event. */
+ ctrlKey: boolean;
+ /** Whether the meta keyboard modifier was held during the move event. */
+ metaKey: boolean;
+ /** Whether the alt keyboard modifier was held during the move event. */
+ altKey: boolean;
+}
+
+interface MoveStartEvent extends BaseMoveEvent {
+ /** The type of move event being fired. */
+ type: "movestart";
+ target: HTMLElement;
+ pageX: number;
+ pageY: number;
+}
+
+interface MoveMoveEvent extends BaseMoveEvent {
+ /** The type of move event being fired. */
+ type: "move";
+ /** The amount moved in the X direction since the last event. */
+ deltaX: number;
+ /** The amount moved in the Y direction since the last event. */
+ deltaY: number;
+ pageX: number;
+ pageY: number;
+}
+
+interface MoveEndEvent extends BaseMoveEvent {
+ /** The type of move event being fired. */
+ type: "moveend";
+}
+
+interface MoveEvents {
+ /** Handler that is called when a move interaction starts. */
+ onMoveStart?: (e: MoveStartEvent) => void;
+ /** Handler that is called when the element is moved. */
+ onMove?: (e: MoveMoveEvent) => void;
+ /** Handler that is called when a move interaction ends. */
+ onMoveEnd?: (e: MoveEndEvent) => void;
+}
+
+/**
+ * Handles move interactions across mouse, touch, and keyboard, including dragging with
+ * the mouse or touch, and using the arrow keys. Normalizes behavior across browsers and
+ * platforms, and ignores emulated mouse events on touch devices.
+ */
+export function useMove(props: MoveEvents): MoveResult {
+ let state = useRef<{
+ didMove: boolean;
+ lastPosition: { pageX: number; pageY: number } | null;
+ id: number | null;
+ }>({ didMove: false, lastPosition: null, id: null });
+
+ let { addGlobalListener, removeGlobalListener } = useGlobalListeners();
+
+ // Because addGlobalListener is used to set callbacks,
+ // noramlly it will "see" the version of "props" at the time of addGlobalListener call.
+ // This in turn means that user's callbakcs will see old state variables etc.
+ // To workaround this, we use a ref that always points to the latest props.
+ let latestProps = useRef(props);
+ latestProps.current = props;
+
+ let moveProps = useMemo(() => {
+ let moveProps: HTMLAttributes = {};
+
+ let start = () => {
+ //disableTextSelection();
+ state.current.didMove = false;
+ };
+ let move = (
+ originalEvent: MouseEvent | TouchEvent | React.KeyboardEvent,
+ pointerType: PointerType,
+ deltaX: number,
+ deltaY: number
+ ) => {
+ if (deltaX === 0 && deltaY === 0) {
+ return;
+ }
+
+ // FIXME: this is a temporary fix
+ // we need to figure out how to properly support keyboard and touch
+ if (
+ !(originalEvent.target instanceof HTMLElement) ||
+ !("pageX" in originalEvent)
+ ) {
+ return;
+ }
+
+ if (!state.current.didMove) {
+ state.current.didMove = true;
+ latestProps.current.onMoveStart?.({
+ type: "movestart",
+ pointerType,
+ shiftKey: originalEvent.shiftKey,
+ metaKey: originalEvent.metaKey,
+ ctrlKey: originalEvent.ctrlKey,
+ altKey: originalEvent.altKey,
+ target: originalEvent.target,
+ pageX: originalEvent.pageX,
+ pageY: originalEvent.pageY,
+ });
+ }
+
+ latestProps.current.onMove?.({
+ type: "move",
+ pointerType,
+ deltaX: deltaX,
+ deltaY: deltaY,
+ shiftKey: originalEvent.shiftKey,
+ metaKey: originalEvent.metaKey,
+ ctrlKey: originalEvent.ctrlKey,
+ altKey: originalEvent.altKey,
+ pageX: originalEvent.pageX,
+ pageY: originalEvent.pageY,
+ });
+ };
+ let end = (originalEvent: EventBase, pointerType: PointerType) => {
+ //restoreTextSelection();
+ if (state.current.didMove) {
+ latestProps.current.onMoveEnd?.({
+ type: "moveend",
+ pointerType,
+ shiftKey: originalEvent.shiftKey,
+ metaKey: originalEvent.metaKey,
+ ctrlKey: originalEvent.ctrlKey,
+ altKey: originalEvent.altKey,
+ });
+ }
+ };
+
+ if (typeof PointerEvent === "undefined") {
+ let onMouseMove = (e: MouseEvent) => {
+ if (e.button === 0 && state.current.lastPosition !== null) {
+ move(
+ e,
+ "mouse",
+ e.pageX - state.current.lastPosition.pageX,
+ e.pageY - state.current.lastPosition.pageY
+ );
+ state.current.lastPosition = { pageX: e.pageX, pageY: e.pageY };
+ }
+ };
+ let onMouseUp = (e: MouseEvent) => {
+ if (e.button === 0) {
+ end(e, "mouse");
+ removeGlobalListener(window, "mousemove", onMouseMove, false);
+ removeGlobalListener(window, "mouseup", onMouseUp, false);
+ }
+ };
+ moveProps.onMouseDown = (e: React.MouseEvent) => {
+ if (e.button === 0) {
+ start();
+ e.stopPropagation();
+ e.preventDefault();
+ state.current.lastPosition = { pageX: e.pageX, pageY: e.pageY };
+ addGlobalListener(window, "mousemove", onMouseMove, false);
+ addGlobalListener(window, "mouseup", onMouseUp, false);
+ }
+ };
+
+ let onTouchMove = (e: TouchEvent) => {
+ let touch = [...e.changedTouches].findIndex(
+ ({ identifier }) => identifier === state.current.id
+ );
+ if (touch >= 0 && state.current.lastPosition !== null) {
+ let { pageX, pageY } = e.changedTouches[touch];
+ move(
+ e,
+ "touch",
+ pageX - state.current.lastPosition.pageX,
+ pageY - state.current.lastPosition.pageY
+ );
+ state.current.lastPosition = { pageX, pageY };
+ }
+ };
+ let onTouchEnd = (e: TouchEvent) => {
+ let touch = [...e.changedTouches].findIndex(
+ ({ identifier }) => identifier === state.current.id
+ );
+ if (touch >= 0) {
+ end(e, "touch");
+ state.current.id = null;
+ removeGlobalListener(window, "touchmove", onTouchMove);
+ removeGlobalListener(window, "touchend", onTouchEnd);
+ removeGlobalListener(window, "touchcancel", onTouchEnd);
+ }
+ };
+ moveProps.onTouchStart = (e: React.TouchEvent) => {
+ if (e.changedTouches.length === 0 || state.current.id != null) {
+ return;
+ }
+
+ let { pageX, pageY, identifier } = e.changedTouches[0];
+ start();
+ e.stopPropagation();
+ e.preventDefault();
+ state.current.lastPosition = { pageX, pageY };
+ state.current.id = identifier;
+ addGlobalListener(window, "touchmove", onTouchMove, false);
+ addGlobalListener(window, "touchend", onTouchEnd, false);
+ addGlobalListener(window, "touchcancel", onTouchEnd, false);
+ };
+ } else {
+ let onPointerMove = (e: PointerEvent) => {
+ if (
+ e.pointerId === state.current.id &&
+ state.current.lastPosition !== null
+ ) {
+ let pointerType = (e.pointerType || "mouse") as PointerType;
+
+ // Problems with PointerEvent#movementX/movementY:
+ // 1. it is always 0 on macOS Safari.
+ // 2. On Chrome Android, it's scaled by devicePixelRatio, but not on Chrome macOS
+ move(
+ e,
+ pointerType,
+ e.pageX - state.current.lastPosition.pageX,
+ e.pageY - state.current.lastPosition.pageY
+ );
+ state.current.lastPosition = { pageX: e.pageX, pageY: e.pageY };
+ }
+ };
+
+ let onPointerUp = (e: PointerEvent) => {
+ if (e.pointerId === state.current.id) {
+ let pointerType = (e.pointerType || "mouse") as PointerType;
+ end(e, pointerType);
+ state.current.id = null;
+ removeGlobalListener(window, "pointermove", onPointerMove, false);
+ removeGlobalListener(window, "pointerup", onPointerUp, false);
+ removeGlobalListener(window, "pointercancel", onPointerUp, false);
+ }
+ };
+
+ moveProps.onPointerDown = (e: React.PointerEvent) => {
+ if (e.button === 0 && state.current.id == null) {
+ start();
+ e.stopPropagation();
+ e.preventDefault();
+ state.current.lastPosition = { pageX: e.pageX, pageY: e.pageY };
+ state.current.id = e.pointerId;
+ addGlobalListener(window, "pointermove", onPointerMove, false);
+ addGlobalListener(window, "pointerup", onPointerUp, false);
+ addGlobalListener(window, "pointercancel", onPointerUp, false);
+ }
+ };
+ }
+
+ let triggerKeyboardMove = (
+ e: React.KeyboardEvent,
+ deltaX: number,
+ deltaY: number
+ ) => {
+ start();
+ move(e, "keyboard", deltaX, deltaY);
+ end(e, "keyboard");
+ };
+
+ moveProps.onKeyDown = (e) => {
+ switch (e.key) {
+ case "Left":
+ case "ArrowLeft":
+ e.preventDefault();
+ e.stopPropagation();
+ triggerKeyboardMove(e, -1, 0);
+ break;
+ case "Right":
+ case "ArrowRight":
+ e.preventDefault();
+ e.stopPropagation();
+ triggerKeyboardMove(e, 1, 0);
+ break;
+ case "Up":
+ case "ArrowUp":
+ e.preventDefault();
+ e.stopPropagation();
+ triggerKeyboardMove(e, 0, -1);
+ break;
+ case "Down":
+ case "ArrowDown":
+ e.preventDefault();
+ e.stopPropagation();
+ triggerKeyboardMove(e, 0, 1);
+ break;
+ }
+ };
+
+ return moveProps;
+ }, [addGlobalListener, removeGlobalListener]);
+
+ return { moveProps };
+}
diff --git a/apps/designer/package.json b/apps/designer/package.json
index d2d0240c6f..beb1fdbabe 100644
--- a/apps/designer/package.json
+++ b/apps/designer/package.json
@@ -48,6 +48,7 @@
"@radix-ui/react-toggle-group": "^0.1.5",
"@radix-ui/react-tooltip": "^0.1.7",
"@radix-ui/react-use-layout-effect": "^0.1.0",
+ "@react-aria/utils": "^3.13.1",
"@remix-run/node": "^1.6.4",
"@remix-run/react": "^1.6.4",
"@remix-run/serve": "^1.6.4",
diff --git a/yarn.lock b/yarn.lock
index 62cd43092e..6de5d29112 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1975,7 +1975,7 @@
dependencies:
regenerator-runtime "^0.13.2"
-"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.17.8", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
+"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.17.8", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.6.tgz#6a1ef59f838debd670421f8c7f2cbb8da9751580"
integrity sha512-t9wi7/AW6XtKahAe20Yw0/mMljKq0B1r2fPdvaAdV/KPDZewFXdaaa6K7lxmZBZ8FBNpCiAT6iHPmd6QO9bKfQ==
@@ -3277,6 +3277,24 @@
dependencies:
"@babel/runtime" "^7.13.10"
+"@react-aria/ssr@^3.2.0":
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/@react-aria/ssr/-/ssr-3.2.0.tgz#88460384b43204f91c972d5b0de24ee44d6a2984"
+ integrity sha512-wwJFdkl+Q8NU5yJ4NvdAOqx5LM3QtUVoSjuK7Ey8jZ4WS4bB0EqT3Kr3IInBs257HzZ5nXCiKXKE4NGXXuIRWA==
+ dependencies:
+ "@babel/runtime" "^7.6.2"
+
+"@react-aria/utils@^3.13.1":
+ version "3.13.1"
+ resolved "https://registry.yarnpkg.com/@react-aria/utils/-/utils-3.13.1.tgz#45557fdc7ae9de057a83014013bf09e54d074c96"
+ integrity sha512-usW6RoLKil4ylgDbRcaQ5YblNGv5ZihI4I9NB8pdazhw53cSRyLaygLdmHO33xgpPnAhb6Nb/tv8d5p6cAde+A==
+ dependencies:
+ "@babel/runtime" "^7.6.2"
+ "@react-aria/ssr" "^3.2.0"
+ "@react-stately/utils" "^3.5.0"
+ "@react-types/shared" "^3.13.1"
+ clsx "^1.1.1"
+
"@react-dnd/asap@4.0.1", "@react-dnd/asap@^4.0.0":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-4.0.1.tgz#5291850a6b58ce6f2da25352a64f1b0674871aab"
@@ -3297,6 +3315,18 @@
resolved "https://registry.yarnpkg.com/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz#a3031eb54129f2c66b2753f8404266ec7bf67f0a"
integrity sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==
+"@react-stately/utils@^3.5.0":
+ version "3.5.0"
+ resolved "https://registry.yarnpkg.com/@react-stately/utils/-/utils-3.5.0.tgz#e76231cfc93042814dcc5d96436e50c807c1d905"
+ integrity sha512-WzzwlQtJrf7egaN+lt02/f/wkFKbcscsbinmXs9pL7QyYm+BCQ9xXj01w0juzt93piZgB/kfIYJqInEdpudh1A==
+ dependencies:
+ "@babel/runtime" "^7.6.2"
+
+"@react-types/shared@^3.13.1":
+ version "3.13.1"
+ resolved "https://registry.yarnpkg.com/@react-types/shared/-/shared-3.13.1.tgz#eda5e3744971606f753baf7879136bf8e3f707ab"
+ integrity sha512-EHQqILDJeDvnloy5VV9lnnEjpCMwH1ghptCfa/lz9Ht9nwco3qGCvUABkWyND7yU1Adt3A/1oJxhpRUu3eTlyg==
+
"@remix-run/dev@^1.6.4":
version "1.6.4"
resolved "https://registry.yarnpkg.com/@remix-run/dev/-/dev-1.6.4.tgz#7a2a7fc24e66434c5cf64d6e79a62d35d09b73de"
@@ -7002,7 +7032,7 @@ clsx@1.1.0:
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.0.tgz#62937c6adfea771247c34b54d320fb99624f5702"
integrity sha512-3avwM37fSK5oP6M5rQ9CNe99lwxhXDOeSWVPAOYF6OazUTgZCMb0yWlJpmdD74REy1gkEaFiub2ULv4fq9GUhA==
-clsx@^1.0.4:
+clsx@^1.0.4, clsx@^1.1.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==