diff --git a/.storybook/main.ts b/.storybook/main.ts index 0ae6c082d..68debcee3 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,23 +1,33 @@ import type { StorybookConfig } from "@storybook/react-vite"; const config: StorybookConfig = { - stories: ["../lib/**/*.mdx", "../lib/**/*.stories.@(js|jsx|mjs|ts|tsx)"], + stories: ["../frontend/**/*.mdx", "../frontend/**/*.stories.@(js|jsx|mjs|ts|tsx)"], + addons: [ "@storybook/addon-links", "@storybook/addon-essentials", "@chromatic-com/storybook", "@storybook/addon-interactions", ], + + core: { + builder: "@storybook/builder-vite", + }, + framework: { name: "@storybook/react-vite", options: {}, }, - docs: { - autodocs: "tag", - }, + + docs: {}, + managerHead: (head) => ` ${head} `, + + typescript: { + reactDocgen: "react-docgen-typescript", + }, }; export default config; diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 000000000..03571dc88 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,18 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Start Storybook", + "type": "shell", + "command": "yarn storybook", + "presentation": { + "reveal": "silent", + "panel": "shared" + }, + "runOptions": { + "instanceLimit": 1, + "runOn": "folderOpen" + } + } + ] +} diff --git a/frontend/app/app.tsx b/frontend/app/app.tsx index 04870961a..290bab856 100644 --- a/frontend/app/app.tsx +++ b/frontend/app/app.tsx @@ -6,6 +6,8 @@ import { atoms, globalStore } from "@/store/global"; import * as jotai from "jotai"; import { Provider } from "jotai"; +import { DndProvider } from "react-dnd"; +import { HTML5Backend } from "react-dnd-html5-backend"; import "../../public/style.less"; import { CenteredDiv } from "./element/quickelems"; @@ -30,8 +32,10 @@ const AppInner = () => { } return (
-
- + +
+ +
); }; diff --git a/frontend/app/block/block.tsx b/frontend/app/block/block.tsx index a3cc83ae0..cfcce9e08 100644 --- a/frontend/app/block/block.tsx +++ b/frontend/app/block/block.tsx @@ -12,28 +12,17 @@ import * as React from "react"; import "./block.less"; -const Block = ({ tabId, blockId }: { tabId: string; blockId: string }) => { +interface BlockProps { + blockId: string; + onClose: () => void; +} + +const Block = ({ blockId, onClose }: BlockProps) => { const blockRef = React.useRef(null); - const [dims, setDims] = React.useState({ width: 0, height: 0 }); - - function handleClose() { - WOS.DeleteBlock(blockId); - } - - React.useEffect(() => { - if (!blockRef.current) { - return; - } - const rect = blockRef.current.getBoundingClientRect(); - const newWidth = Math.floor(rect.width); - const newHeight = Math.floor(rect.height); - if (newWidth !== dims.width || newHeight !== dims.height) { - setDims({ width: newWidth, height: newHeight }); - } - }, [blockRef.current]); let blockElem: JSX.Element = null; const [blockData, blockDataLoading] = WOS.useWaveObjectValue(WOS.makeORef("block", blockId)); + if (!blockId || !blockData) return null; console.log("blockData: ", blockData); if (blockDataLoading) { blockElem = Loading...; @@ -49,11 +38,9 @@ const Block = ({ tabId, blockId }: { tabId: string; blockId: string }) => { return (
-
- Block [{blockId.substring(0, 8)}] {dims.width}x{dims.height} -
+
Block [{blockId.substring(0, 8)}]
-
handleClose()}> +
diff --git a/frontend/app/store/wos.ts b/frontend/app/store/wos.ts index f43117630..5ab78133d 100644 --- a/frontend/app/store/wos.ts +++ b/frontend/app/store/wos.ts @@ -3,6 +3,7 @@ // WaveObjectStore +import { LayoutNode } from "@/faraday/index"; import { Call as $Call, Events } from "@wailsio/runtime"; import * as jotai from "jotai"; import * as React from "react"; @@ -131,19 +132,21 @@ function useWaveObjectValueWithSuspense(oref: string): T { return dataValue.value; } -function getWaveObjectAtom(oref: string): jotai.Atom { +function getWaveObjectAtom(oref: string): jotai.WritableAtom { let wov = waveObjectValueCache.get(oref); if (wov == null) { wov = createWaveValueObject(oref, true); waveObjectValueCache.set(oref, wov); } - return jotai.atom((get) => { - let dataValue = get(wov.dataAtom); - if (dataValue.loading) { - return null; + return jotai.atom( + (get) => { + let dataValue = get(wov.dataAtom); + return dataValue.value; + }, + (_get, set, value: T) => { + setObjectValue(value, set, true); } - return dataValue.value; - }); + ); } function getWaveObjectLoadingAtom(oref: string): jotai.Atom { @@ -177,7 +180,7 @@ function useWaveObjectValue(oref: string): [T, boolean] { return [atomVal.value, atomVal.loading]; } -function useWaveObject(oref: string): [T, boolean, (T) => void] { +function useWaveObject(oref: string): [T, boolean, (val: T) => void] { let wov = waveObjectValueCache.get(oref); if (wov == null) { wov = createWaveValueObject(oref, true); @@ -278,11 +281,13 @@ function wrapObjectServiceCall(fnName: string, ...args: any[]): Promise { // should provide getFn if it is available (e.g. inside of a jotai atom) // otherwise it will use the globalStore.get function function getObjectValue(oref: string, getFn?: jotai.Getter): T { + console.log("getObjectValue", oref); let wov = waveObjectValueCache.get(oref); if (wov == null) { return null; } if (getFn == null) { + console.log("getObjectValue", "getFn is null, using globalStore.get"); getFn = globalStore.get; } const atomVal = getFn(wov.dataAtom); @@ -299,8 +304,10 @@ function setObjectValue(value: WaveObj, setFn?: jotai.Setter, pushToServer?: return; } if (setFn == null) { + console.log("setter null"); setFn = globalStore.set; } + console.log("Setting", oref, "to", value); setFn(wov.dataAtom, { value: value, loading: false }); if (pushToServer) { UpdateObject(value, false); @@ -319,8 +326,8 @@ export function CreateBlock(blockDef: BlockDef, rtOpts: RuntimeOpts): Promise<{ return wrapObjectServiceCall("CreateBlock", blockDef, rtOpts); } -export function DeleteBlock(blockId: string): Promise { - return wrapObjectServiceCall("DeleteBlock", blockId); +export function DeleteBlock(blockId: string, newLayout?: LayoutNode): Promise { + return wrapObjectServiceCall("DeleteBlock", blockId, newLayout); } export function CloseTab(tabId: string): Promise { @@ -349,4 +356,5 @@ export { useWaveObject, useWaveObjectValue, useWaveObjectValueWithSuspense, + waveObjectValueCache, }; diff --git a/frontend/app/tab/tab.tsx b/frontend/app/tab/tab.tsx index d9b1f5db6..0333bba2c 100644 --- a/frontend/app/tab/tab.tsx +++ b/frontend/app/tab/tab.tsx @@ -4,14 +4,41 @@ import { Block } from "@/app/block/block"; import * as WOS from "@/store/wos"; +import { TileLayout } from "@/faraday/index"; +import { getLayoutStateAtomForTab } from "@/faraday/lib/layoutAtom"; +import { useAtomValue } from "jotai"; +import { useCallback, useMemo } from "react"; import { CenteredDiv, CenteredLoadingDiv } from "../element/quickelems"; import "./tab.less"; const TabContent = ({ tabId }: { tabId: string }) => { - const [tabData, tabLoading] = WOS.useWaveObjectValue(WOS.makeORef("tab", tabId)); + const oref = useMemo(() => WOS.makeORef("tab", tabId), [tabId]); + const loadingAtom = useMemo(() => WOS.getWaveObjectLoadingAtom(oref), [oref]); + const tabLoading = useAtomValue(loadingAtom); + const tabAtom = useMemo(() => WOS.getWaveObjectAtom(oref), [oref]); + const layoutStateAtom = useMemo(() => getLayoutStateAtomForTab(tabId, tabAtom), [tabAtom, tabId]); + const tabData = useAtomValue(tabAtom); + + const renderBlock = useCallback((tabData: TabLayoutData, onClose: () => void) => { + // console.log("renderBlock", tabData); + if (!tabData.blockId) { + return null; + } + return ; + }, []); + + const onNodeDelete = useCallback( + (data: TabLayoutData) => { + console.log("onNodeDelete", data, tabData); + WOS.DeleteBlock(data.blockId, tabData.layout); + }, + [tabData] + ); + if (tabLoading) { return ; } + if (!tabData) { return (
@@ -19,15 +46,15 @@ const TabContent = ({ tabId }: { tabId: string }) => {
); } + return (
- {tabData.blockids.map((blockId: string) => { - return ( -
- -
- ); - })} +
); }; diff --git a/frontend/app/workspace/workspace.tsx b/frontend/app/workspace/workspace.tsx index 325c4e0f6..87d794e11 100644 --- a/frontend/app/workspace/workspace.tsx +++ b/frontend/app/workspace/workspace.tsx @@ -8,6 +8,13 @@ import { clsx } from "clsx"; import * as jotai from "jotai"; import { CenteredDiv } from "../element/quickelems"; +import { LayoutTreeActionType, LayoutTreeInsertNodeAction, newLayoutNode } from "@/faraday/index"; +import { + deleteLayoutStateAtomForTab, + getLayoutStateAtomForTab, + useLayoutTreeStateReducerAtom, +} from "@/faraday/lib/layoutAtom"; +import { useCallback, useMemo } from "react"; import "./workspace.less"; function Tab({ tabId }: { tabId: string }) { @@ -18,6 +25,7 @@ function Tab({ tabId }: { tabId: string }) { } function handleCloseTab() { WOS.CloseTab(tabId); + deleteLayoutStateAtomForTab(tabId); } return (
{ + return WOS.getWaveObjectAtom(WOS.makeORef("tab", windowData.activetabid)); + }, [windowData.activetabid]); + const [, dispatchLayoutStateAction] = useLayoutTreeStateReducerAtom( + getLayoutStateAtomForTab(windowData.activetabid, activeTabAtom) + ); + + const addBlockToTab = useCallback( + (blockId: string) => { + const insertNodeAction: LayoutTreeInsertNodeAction = { + type: LayoutTreeActionType.InsertNode, + node: newLayoutNode(undefined, undefined, undefined, { blockId }), + }; + dispatchLayoutStateAction(insertNodeAction); + }, + [activeTabAtom] + ); async function createBlock(blockDef: BlockDef) { const rtOpts: RuntimeOpts = { termsize: { rows: 25, cols: 80 } }; - await WOS.CreateBlock(blockDef, rtOpts); + const { blockId } = await WOS.CreateBlock(blockDef, rtOpts); + addBlockToTab(blockId); } async function clickTerminal() { diff --git a/frontend/faraday/index.ts b/frontend/faraday/index.ts new file mode 100644 index 000000000..4986378f4 --- /dev/null +++ b/frontend/faraday/index.ts @@ -0,0 +1,38 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { TileLayout } from "./lib/TileLayout.jsx"; +import { newLayoutTreeStateAtom, useLayoutTreeStateReducerAtom, withLayoutTreeState } from "./lib/layoutAtom.js"; +import { newLayoutNode } from "./lib/layoutNode.js"; +import type { + LayoutNode, + LayoutTreeCommitPendingAction, + LayoutTreeComputeMoveNodeAction, + LayoutTreeDeleteNodeAction, + LayoutTreeInsertNodeAction, + LayoutTreeMoveNodeAction, + LayoutTreeState, + WritableLayoutNodeAtom, + WritableLayoutTreeStateAtom, +} from "./lib/model.js"; +import { LayoutTreeActionType } from "./lib/model.js"; + +export { + LayoutTreeActionType, + TileLayout, + newLayoutNode, + newLayoutTreeStateAtom, + useLayoutTreeStateReducerAtom, + withLayoutTreeState, +}; +export type { + LayoutNode, + LayoutTreeCommitPendingAction, + LayoutTreeComputeMoveNodeAction, + LayoutTreeDeleteNodeAction, + LayoutTreeInsertNodeAction, + LayoutTreeMoveNodeAction, + LayoutTreeState, + WritableLayoutNodeAtom, + WritableLayoutTreeStateAtom, +}; diff --git a/frontend/faraday/lib/TileLayout.stories.tsx b/frontend/faraday/lib/TileLayout.stories.tsx new file mode 100644 index 000000000..e7f9476e3 --- /dev/null +++ b/frontend/faraday/lib/TileLayout.stories.tsx @@ -0,0 +1,111 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import type { Meta, StoryObj } from "@storybook/react"; + +import { TileLayout } from "./TileLayout.jsx"; + +import { useState } from "react"; +import { newLayoutTreeStateAtom, useLayoutTreeStateReducerAtom } from "./layoutAtom.js"; +import { newLayoutNode } from "./layoutNode.js"; +import { LayoutTreeActionType, LayoutTreeInsertNodeAction } from "./model.js"; +import "./tilelayout.stories.less"; +import { FlexDirection } from "./utils.js"; + +interface TestData { + name: string; +} + +const renderTestData = (data: TestData) =>
{data.name}
; + +const meta = { + title: "TileLayout", + args: { + layoutTreeStateAtom: newLayoutTreeStateAtom( + newLayoutNode(FlexDirection.Row, undefined, undefined, { + name: "Hello world!", + }) + ), + renderContent: renderTestData, + }, + component: TileLayout, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ["autodocs"], +} satisfies Meta>; + +export default meta; +type Story = StoryObj; + +export const Basic: Story = { + args: { + layoutTreeStateAtom: newLayoutTreeStateAtom( + newLayoutNode(FlexDirection.Row, undefined, undefined, { name: "Hello world!" }) + ), + }, +}; + +export const More: Story = { + args: { + layoutTreeStateAtom: newLayoutTreeStateAtom( + newLayoutNode(FlexDirection.Row, undefined, [ + newLayoutNode(FlexDirection.Column, undefined, undefined, { name: "Hello world1!" }), + newLayoutNode(FlexDirection.Column, undefined, undefined, { name: "Hello world2!" }), + newLayoutNode(FlexDirection.Column, undefined, [ + newLayoutNode(FlexDirection.Column, undefined, undefined, { name: "Hello world3!" }), + newLayoutNode(FlexDirection.Column, undefined, undefined, { name: "Hello world4!" }), + ]), + ]) + ), + }, +}; + +const evenMoreRootNode = newLayoutNode(FlexDirection.Row, undefined, [ + newLayoutNode(FlexDirection.Column, undefined, undefined, { name: "Hello world1!" }), + newLayoutNode(FlexDirection.Column, undefined, [ + newLayoutNode(FlexDirection.Column, undefined, undefined, { name: "Hello world2!" }), + newLayoutNode(FlexDirection.Column, undefined, undefined, { name: "Hello world3!" }), + ]), + newLayoutNode(FlexDirection.Column, undefined, [ + newLayoutNode(FlexDirection.Column, undefined, undefined, { name: "Hello world4!" }), + newLayoutNode(FlexDirection.Column, undefined, undefined, { name: "Hello world5!" }), + newLayoutNode(FlexDirection.Column, undefined, [ + newLayoutNode(FlexDirection.Column, undefined, undefined, { name: "Hello world6!" }), + newLayoutNode(FlexDirection.Column, undefined, undefined, { name: "Hello world7!" }), + newLayoutNode(FlexDirection.Column, undefined, undefined, { name: "Hello world8!" }), + ]), + ]), +]); + +export const EvenMore: Story = { + args: { + layoutTreeStateAtom: newLayoutTreeStateAtom(evenMoreRootNode), + }, +}; + +const addNodeAtom = newLayoutTreeStateAtom(evenMoreRootNode); + +export const AddNode: Story = { + render: () => { + const [, dispatch] = useLayoutTreeStateReducerAtom(addNodeAtom); + const [numAddedNodes, setNumAddedNodes] = useState(0); + const dispatchAddNode = () => { + const newNode = newLayoutNode(FlexDirection.Column, undefined, undefined, { + name: "New Node" + numAddedNodes, + }); + const insertNodeAction: LayoutTreeInsertNodeAction = { + type: LayoutTreeActionType.InsertNode, + node: newNode, + }; + dispatch(insertNodeAction); + setNumAddedNodes(numAddedNodes + 1); + }; + return ( +
+
+ +
+ +
+ ); + }, +}; diff --git a/frontend/faraday/lib/TileLayout.tsx b/frontend/faraday/lib/TileLayout.tsx new file mode 100644 index 000000000..9f7e901ff --- /dev/null +++ b/frontend/faraday/lib/TileLayout.tsx @@ -0,0 +1,369 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import clsx from "clsx"; +import { CSSProperties, RefObject, useCallback, useEffect, useLayoutEffect, useRef, useState } from "react"; +import { useDrag, useDragLayer, useDrop } from "react-dnd"; + +import { useLayoutTreeStateReducerAtom } from "./layoutAtom.js"; +import { + ContentRenderer, + LayoutNode, + LayoutTreeAction, + LayoutTreeActionType, + LayoutTreeComputeMoveNodeAction, + LayoutTreeDeleteNodeAction, + LayoutTreeState, + WritableLayoutTreeStateAtom, +} from "./model.js"; +import "./tilelayout.less"; +import { setTransform as createTransform, debounce, determineDropDirection } from "./utils.js"; + +export interface TileLayoutProps { + layoutTreeStateAtom: WritableLayoutTreeStateAtom; + renderContent: ContentRenderer; + onNodeDelete?: (data: T) => void; + className?: string; +} + +export const TileLayout = ({ layoutTreeStateAtom, className, renderContent, onNodeDelete }: TileLayoutProps) => { + const overlayContainerRef = useRef(null); + const displayContainerRef = useRef(null); + + const [layoutTreeState, dispatch] = useLayoutTreeStateReducerAtom(layoutTreeStateAtom); + const [nodeRefs, setNodeRefs] = useState>>(new Map()); + + useEffect(() => { + console.log("layoutTreeState changed", layoutTreeState); + }, [layoutTreeState]); + + const setRef = useCallback( + (id: string, ref: RefObject) => { + setNodeRefs((prev) => { + prev.set(id, ref); + console.log("setRef", id, ref, prev); + return prev; + }); + }, + [setNodeRefs] + ); + + const deleteRef = useCallback( + (id: string) => { + if (nodeRefs.has(id)) { + setNodeRefs((prev) => { + prev.delete(id); + console.log("deleteRef", id, prev); + return prev; + }); + } else { + console.log("deleteRef id not found", id); + } + }, + [nodeRefs, setNodeRefs] + ); + + const [overlayTransform, setOverlayTransform] = useState(); + const [layoutLeafTransforms, setLayoutLeafTransforms] = useState>({}); + + const activeDrag = useDragLayer((monitor) => monitor.isDragging()); + + /** + * Callback to update the transforms on the displayed leafs and move the overlay over the display layer when dragging. + */ + const updateTransforms = useCallback( + debounce(() => { + if (overlayContainerRef.current && displayContainerRef.current) { + const displayBoundingRect = displayContainerRef.current.getBoundingClientRect(); + console.log("displayBoundingRect", displayBoundingRect); + const overlayBoundingRect = overlayContainerRef.current.getBoundingClientRect(); + + const newLayoutLeafTransforms: Record = {}; + + console.log( + "nodeRefs", + nodeRefs, + "layoutLeafs", + layoutTreeState.leafs, + "layoutTreeState", + layoutTreeState + ); + + for (const leaf of layoutTreeState.leafs) { + const leafRef = nodeRefs.get(leaf.id); + if (leafRef?.current) { + const leafBounding = leafRef.current.getBoundingClientRect(); + const transform = createTransform({ + top: leafBounding.top - overlayBoundingRect.top, + left: leafBounding.left - overlayBoundingRect.left, + width: leafBounding.width, + height: leafBounding.height, + }); + newLayoutLeafTransforms[leafRef.current.id] = transform; + } else { + console.warn("missing leaf", leaf.id); + } + } + + setLayoutLeafTransforms(newLayoutLeafTransforms); + + const newOverlayOffset = displayBoundingRect.top + 2 * displayBoundingRect.height; + console.log("overlayOffset", newOverlayOffset); + setOverlayTransform( + createTransform( + { + top: activeDrag ? 0 : newOverlayOffset, + left: 0, + width: overlayBoundingRect.width, + height: overlayBoundingRect.height, + }, + false + ) + ); + } + }, 30), + [activeDrag, overlayContainerRef, displayContainerRef, layoutTreeState, nodeRefs] + ); + + // Update the transforms whenever we drag something and whenever the layout updates. + useLayoutEffect(() => { + updateTransforms(); + }, [activeDrag, layoutTreeState]); + + // Update the transforms on first render and again whenever the window resizes. I had to do a slightly hacky thing + // because I noticed that the window handler wasn't updating when the callback changed so I remove it each time and + // reattach the new callback. + const [prevUpdateTransforms, setPrevUpdateTransforms] = useState<() => void>(undefined); + useEffect(() => { + if (prevUpdateTransforms) window.removeEventListener("resize", prevUpdateTransforms); + window.addEventListener("resize", updateTransforms); + setPrevUpdateTransforms(updateTransforms); + return () => { + window.removeEventListener("resize", updateTransforms); + }; + }, [updateTransforms]); + + // Ensure that we don't see any jostling in the layout when we're rendering it the first time. + // `animate` will be disabled until after the transforms have all applied the first time. + // `overlayVisible` will be disabled until after the overlay has been pushed out of view. + const [animate, setAnimate] = useState(false); + const [overlayVisible, setOverlayVisible] = useState(false); + useEffect(() => { + setTimeout(() => { + setAnimate(true); + }, 50); + setTimeout(() => { + setOverlayVisible(true); + }, 30); + }, []); + + const onLeafClose = useCallback( + (node: LayoutNode) => { + console.log("onLeafClose", node); + const deleteAction: LayoutTreeDeleteNodeAction = { + type: LayoutTreeActionType.DeleteNode, + nodeId: node.id, + }; + console.log("calling dispatch", deleteAction); + dispatch(deleteAction); + console.log("calling onNodeDelete", node); + onNodeDelete?.(node.data); + console.log("node deleted"); + }, + [onNodeDelete, dispatch] + ); + + return ( +
+
+ {layoutLeafTransforms && + layoutTreeState.leafs.map((leaf) => { + return ( + + ); + })} +
+
+ +
+
+ ); +}; + +interface TileNodeProps { + layoutNode: LayoutNode; + renderContent: ContentRenderer; + onLeafClose: (node: LayoutNode) => void; + transform: CSSProperties; +} + +const dragItemType = "TILE_ITEM"; + +const TileNode = ({ layoutNode, renderContent, transform, onLeafClose }: TileNodeProps) => { + const tileNodeRef = useRef(null); + + const [{ isDragging, dragItem }, drag, dragPreview] = useDrag( + () => ({ + type: dragItemType, + item: () => layoutNode, + collect: (monitor) => ({ + isDragging: monitor.isDragging(), + dragItem: monitor.getItem>(), + }), + }), + [layoutNode] + ); + + useEffect(() => { + if (isDragging) { + console.log("drag start", layoutNode.id, layoutNode, dragItem); + } + }, [isDragging]); + + // Register the tile item as a draggable component + useEffect(() => { + console.log("set drag", layoutNode); + drag(tileNodeRef); + dragPreview(tileNodeRef); + }, [tileNodeRef]); + + const onClose = useCallback(() => { + onLeafClose(layoutNode); + }, [layoutNode, onLeafClose]); + + return ( +
+ {layoutNode.data && ( +
+ {renderContent(layoutNode.data, onClose)} +
+ )} +
+ ); +}; + +interface OverlayNodeProps { + layoutNode: LayoutNode; + layoutTreeState: LayoutTreeState; + dispatch: (action: LayoutTreeAction) => void; + setRef: (id: string, ref: RefObject) => void; + deleteRef: (id: string) => void; +} + +const OverlayNode = ({ layoutNode, layoutTreeState, dispatch, setRef, deleteRef }: OverlayNodeProps) => { + const overlayRef = useRef(null); + const leafRef = useRef(null); + + const [, drop] = useDrop( + () => ({ + accept: dragItemType, + canDrop: (_, monitor) => { + const dragItem = monitor.getItem>(); + if (monitor.isOver({ shallow: true }) && dragItem?.id !== layoutNode.id) { + return true; + } + return false; + }, + drop: (_, monitor) => { + console.log("drop start", layoutNode.id, layoutTreeState.pendingAction); + if (!monitor.didDrop() && layoutTreeState.pendingAction) { + dispatch({ + type: LayoutTreeActionType.CommitPendingAction, + }); + } + }, + hover: (_, monitor) => { + if (monitor.isOver({ shallow: true }) && monitor.canDrop()) { + const dragItem = monitor.getItem>(); + console.log("computing operation", layoutNode, dragItem, layoutTreeState.pendingAction); + dispatch({ + type: LayoutTreeActionType.ComputeMove, + node: layoutNode, + nodeToMove: dragItem, + direction: determineDropDirection( + overlayRef.current?.getBoundingClientRect(), + monitor.getClientOffset() + ), + } as LayoutTreeComputeMoveNodeAction); + } + }, + }), + [overlayRef.current, layoutNode, layoutTreeState, dispatch] + ); + + // Register the tile item as a draggable component + useEffect(() => { + const layoutNodeId = layoutNode?.id; + if (overlayRef?.current) { + drop(overlayRef); + setRef(layoutNodeId, overlayRef); + } + + return () => { + deleteRef(layoutNodeId); + }; + }, [overlayRef]); + + const generateChildren = () => { + if (Array.isArray(layoutNode.children)) { + return layoutNode.children.map((childItem) => { + return ( + + ); + }); + } else { + return [
]; + } + }; + + if (!layoutNode) { + return null; + } + + return ( +
+ {generateChildren()} +
+ ); +}; diff --git a/frontend/faraday/lib/layoutAtom.ts b/frontend/faraday/lib/layoutAtom.ts new file mode 100644 index 000000000..b5ed97f97 --- /dev/null +++ b/frontend/faraday/lib/layoutAtom.ts @@ -0,0 +1,95 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { PrimitiveAtom, WritableAtom, atom, useAtom } from "jotai"; +import { useCallback } from "react"; +import { layoutTreeStateReducer, newLayoutTreeState } from "./layoutState.js"; +import { + LayoutNode, + LayoutTreeAction, + LayoutTreeState, + WritableLayoutNodeAtom, + WritableLayoutTreeStateAtom, +} from "./model.js"; + +/** + * Creates a new layout tree state wrapped as an atom. + * @param rootNode The root node for the tree. + * @returns The state wrapped as an atom. + * + * @template T The type of data associated with the nodes of the tree. + */ +export function newLayoutTreeStateAtom(rootNode: LayoutNode): PrimitiveAtom> { + return atom(newLayoutTreeState(rootNode)) as PrimitiveAtom>; +} + +/** + * Derives a WritableLayoutTreeStateAtom from a WritableLayoutNodeAtom, initializing the tree state. + * @param layoutNodeAtom The atom containing the root node for the LayoutTreeState. + * @returns The derived WritableLayoutTreeStateAtom. + */ +export function withLayoutTreeState(layoutNodeAtom: WritableLayoutNodeAtom): WritableLayoutTreeStateAtom { + return atom( + (get) => newLayoutTreeState(get(layoutNodeAtom)), + (_get, set, value) => set(layoutNodeAtom, value.rootNode) + ); +} + +export function withLayoutStateFromTab( + tabAtom: WritableAtom +): WritableLayoutTreeStateAtom { + return atom( + (get) => { + const tabData = get(tabAtom); + console.log("get layout state from tab", tabData); + return newLayoutTreeState(tabData?.layout); + }, + (get, set, value) => { + const tabValue = get(tabAtom); + const newTabValue = { ...tabValue }; + newTabValue.layout = value.rootNode; + console.log("set tab", tabValue, value); + set(tabAtom, newTabValue); + } + ); +} + +/** + * Hook to subscribe to the tree state and dispatch actions to its reducer functon. + * @param layoutTreeStateAtom The atom holding the layout tree state. + * @returns The current state of the tree and the dispatch function. + */ +export function useLayoutTreeStateReducerAtom( + layoutTreeStateAtom: WritableLayoutTreeStateAtom +): readonly [LayoutTreeState, (action: LayoutTreeAction) => void] { + const [state, setState] = useAtom(layoutTreeStateAtom); + const dispatch = useCallback( + (action: LayoutTreeAction) => setState(layoutTreeStateReducer(state, action)), + [state, setState] + ); + return [state, dispatch]; +} + +const tabLayoutAtomCache = new Map>(); + +export function getLayoutStateAtomForTab( + tabId: string, + tabAtom: WritableAtom +): WritableLayoutTreeStateAtom { + let atom = tabLayoutAtomCache.get(tabId); + if (atom) { + console.log("Reusing atom for tab", tabId); + return atom; + } + console.log("Creating new atom for tab", tabId); + atom = withLayoutStateFromTab(tabAtom); + tabLayoutAtomCache.set(tabId, atom); + return atom; +} + +export function deleteLayoutStateAtomForTab(tabId: string) { + const atom = tabLayoutAtomCache.get(tabId); + if (atom) { + tabLayoutAtomCache.delete(tabId); + } +} diff --git a/frontend/faraday/lib/layoutNode.ts b/frontend/faraday/lib/layoutNode.ts new file mode 100644 index 000000000..707717d22 --- /dev/null +++ b/frontend/faraday/lib/layoutNode.ts @@ -0,0 +1,245 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { LayoutNode } from "./model.js"; +import { FlexDirection, getCrypto, reverseFlexDirection } from "./utils.js"; + +const crypto = getCrypto(); + +/** + * Creates a new node. + * @param flexDirection The flex direction for the new node. + * @param size The size for the new node. + * @param children The children for the new node. + * @param data The data for the new node. + * @template T The type of data associated with the node. + * @returns The new node. + */ +export function newLayoutNode( + flexDirection?: FlexDirection, + size?: number, + children?: LayoutNode[], + data?: T +): LayoutNode { + const newNode: LayoutNode = { + id: crypto.randomUUID(), + flexDirection: flexDirection ?? FlexDirection.Column, + size, + children, + data, + }; + + if (!validateNode(newNode)) { + throw new Error("Invalid node"); + } + return newNode; +} + +/** + * Adds new nodes to the tree at the given index. + * @param node The parent node. + * @param idx The index to insert at. + * @param children The nodes to insert. + * @template T The type of data associated with the node. + * @returns The updated parent node. + */ +export function addChildAt(node: LayoutNode, idx: number, ...children: LayoutNode[]) { + console.log("adding", children, "to", node, "at index", idx); + if (children.length === 0) return; + + if (!node.children) { + addIntermediateNode(node); + } + const childrenToAdd = children.flatMap((v) => { + if (v.flexDirection !== node.flexDirection) { + return v; + } else if (v.children) { + return v.children; + } else { + v.flexDirection = reverseFlexDirection(node.flexDirection); + return v; + } + }); + + if (node.children.length <= idx) { + node.children.push(...childrenToAdd); + } else if (idx >= 0) { + node.children.splice(idx, 0, ...childrenToAdd); + } +} + +/** + * Adds an intermediate node as a direct child of the given node, moving the given node's children or data into it. + * + * If the node contains children, they are moved two levels deeper to preserve their flex direction. If the node only has data, it is moved one level deeper. + * @param node The node to add the intermediate node to. + * @template T The type of data associated with the node. + * @returns The updated node and the node that was added. + */ +export function addIntermediateNode(node: LayoutNode): LayoutNode { + let intermediateNode: LayoutNode; + console.log(node); + + if (node.data) { + intermediateNode = newLayoutNode(reverseFlexDirection(node.flexDirection), undefined, undefined, node.data); + node.children = [intermediateNode]; + node.data = undefined; + } else { + const intermediateNodeInner = newLayoutNode(node.flexDirection, undefined, node.children); + intermediateNode = newLayoutNode(reverseFlexDirection(node.flexDirection), undefined, [intermediateNodeInner]); + node.children = [intermediateNode]; + } + const intermediateNodeId = intermediateNode.id; + intermediateNode.id = node.id; + node.id = intermediateNodeId; + return intermediateNode; +} + +/** + * Attempts to remove the specified node from its parent. + * @param parent The parent node. + * @param childToRemove The node to remove. + * @template T The type of data associated with the node. + * @returns The updated parent node, or undefined if the node was not found. + */ +export function removeChild(parent: LayoutNode, childToRemove: LayoutNode) { + if (!parent.children) return; + const idx = parent.children.indexOf(childToRemove); + if (idx === -1) return; + parent.children?.splice(idx, 1); +} + +/** + * Finds the node with the given id. + * @param node The node to search in. + * @param id The id to search for. + * @template T The type of data associated with the node. + * @returns The node with the given id or undefined if no node with the given id was found. + */ +export function findNode(node: LayoutNode, id: string): LayoutNode | undefined { + if (node.id === id) return node; + if (!node.children) return; + for (const child of node.children) { + const result = findNode(child, id); + if (result) return result; + } + return; +} + +/** + * Finds the node whose children contains the node with the given id. + * @param node The node to start the search from. + * @param id The id to search for. + * @template T The type of data associated with the node. + * @returns The parent node, or undefined if no node with the given id was found. + */ +export function findParent(node: LayoutNode, id: string): LayoutNode | undefined { + if (node.id === id || !node.children) return; + for (const child of node.children) { + if (child.id === id) return node; + const retVal = findParent(child, id); + if (retVal) return retVal; + } + return; +} + +/** + * Determines whether a node is valid. + * @param node The node to validate. + * @template T The type of data associated with the node. + * @returns True if the node is valid, false otherwise. + */ +export function validateNode(node: LayoutNode): boolean { + if (!node.children == !node.data) { + console.error("Either children or data must be defined for node, not both"); + return false; + } + + if (node.children?.length === 0) { + console.error("Node cannot define an empty array of children"); + return false; + } + return true; +} + +/** + * Recursively corrects the tree to minimize nested single-child nodes, remove invalid nodes, and correct invalid flex direction order. + * Also finds all leaf nodes under the specified node. + * @param node The node to start the balancing from. + * @template T The type of data associated with the node. + * @returns The corrected node and an array of leaf nodes. + */ +export function balanceNode(node: LayoutNode): { node: LayoutNode; leafs: LayoutNode[] } | undefined { + const leafs: LayoutNode[] = []; + const newNode = balanceNodeHelper(node, leafs); + return { node: newNode, leafs }; +} + +function balanceNodeHelper(node: LayoutNode, leafs: LayoutNode[]): LayoutNode { + if (!node) return; + if (!node.children) { + leafs.push(node); + return node; + } + if (node.children.length === 0) return; + if (!validateNode(node)) throw new Error("Invalid node"); + node.children = node.children + .flatMap((child) => { + if (child.flexDirection === node.flexDirection) { + child.flexDirection = reverseFlexDirection(node.flexDirection); + } + if (child.children?.length === 1 && child.children[0].children) { + return child.children[0].children; + } + return child; + }) + .map((child) => { + return balanceNodeHelper(child, leafs); + }) + .filter((v) => v); + if (node.children.length === 1 && !node.children[0].children) { + node.data = node.children[0].data; + node.id = node.children[0].id; + node.children = undefined; + } + return node; +} + +/** + * Finds the first node in the tree where a new node can be inserted. + * + * This will attempt to fill each node until it has maxChildren children. If a node is full, it will move to its children and + * fill each of them until it has maxChildren children. It will ensure that each child fills evenly before moving to the next + * layer down. + * + * @param node The node to start the search from. + * @param maxChildren The maximum number of children a node can have. + * @returns The node to insert into and the index at which to insert. + */ +export function findNextInsertLocation( + node: LayoutNode, + maxChildren: number +): { node: LayoutNode; index: number } { + const insertLoc = findNextInsertLocationHelper(node, maxChildren, 1); + return { node: insertLoc?.node, index: insertLoc?.index }; +} + +function findNextInsertLocationHelper( + node: LayoutNode, + maxChildren: number, + curDepth: number = 1 +): { node: LayoutNode; index: number; depth: number } { + if (!node) return; + if (!node.children) return { node, index: 1, depth: curDepth }; + let insertLocs: { node: LayoutNode; index: number; depth: number }[] = []; + if (node.children.length < maxChildren) { + insertLocs.push({ node, index: node.children.length, depth: curDepth }); + } + for (const child of node.children.slice().reverse()) { + insertLocs.push(findNextInsertLocationHelper(child, maxChildren, curDepth + 1)); + } + insertLocs = insertLocs + .filter((a) => a) + .sort((a, b) => Math.pow(a.depth, a.index + maxChildren) - Math.pow(b.depth, b.index + maxChildren)); + return insertLocs[0]; +} diff --git a/frontend/faraday/lib/layoutState.ts b/frontend/faraday/lib/layoutState.ts new file mode 100644 index 000000000..cbeaa52a7 --- /dev/null +++ b/frontend/faraday/lib/layoutState.ts @@ -0,0 +1,273 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { + addChildAt, + addIntermediateNode, + balanceNode, + findNextInsertLocation, + findNode, + findParent, + removeChild, +} from "./layoutNode.js"; +import { + LayoutNode, + LayoutTreeAction, + LayoutTreeActionType, + LayoutTreeComputeMoveNodeAction, + LayoutTreeDeleteNodeAction, + LayoutTreeInsertNodeAction, + LayoutTreeMoveNodeAction, + LayoutTreeState, + MoveOperation, +} from "./model.js"; +import { DropDirection, FlexDirection, lazy } from "./utils.js"; + +/** + * Initializes a layout tree state. + * @param rootNode The root node for the tree. + * @returns The state of the tree. + * + * @template T The type of data associated with the nodes of the tree. + */ +export function newLayoutTreeState(rootNode: LayoutNode): LayoutTreeState { + const { node: balancedRootNode, leafs } = balanceNode(rootNode); + return { + rootNode: balancedRootNode, + leafs, + pendingAction: undefined, + }; +} + +/** + * Performs a specified action on the layout tree state. Uses Immer Produce internally to resolve deep changes to the tree. + * + * @param layoutTreeState The state of the tree. + * @param action The action to perform. + * + * @template T The type of data associated with the nodes of the tree. + * @returns The new state of the tree. + */ +export function layoutTreeStateReducer( + layoutTreeState: LayoutTreeState, + action: LayoutTreeAction +): LayoutTreeState { + layoutTreeStateReducerInner(layoutTreeState, action); + return layoutTreeState; +} + +/** + * Helper function for layoutTreeStateReducer. + * @param layoutTreeState The state of the tree. + * @param action The action to perform. + * @see layoutTreeStateReducer + * @template T The type of data associated with the nodes of the tree. + */ +function layoutTreeStateReducerInner(layoutTreeState: LayoutTreeState, action: LayoutTreeAction) { + switch (action.type) { + case LayoutTreeActionType.ComputeMove: + computeMoveNode(layoutTreeState, action as LayoutTreeComputeMoveNodeAction); + break; + case LayoutTreeActionType.CommitPendingAction: + if (!layoutTreeState?.pendingAction) { + console.error("unable to commit pending action, does not exist"); + break; + } + layoutTreeStateReducerInner(layoutTreeState, layoutTreeState.pendingAction); + break; + case LayoutTreeActionType.Move: + moveNode(layoutTreeState, action as LayoutTreeMoveNodeAction); + break; + case LayoutTreeActionType.InsertNode: + insertNode(layoutTreeState, action as LayoutTreeInsertNodeAction); + break; + case LayoutTreeActionType.DeleteNode: + deleteNode(layoutTreeState, action as LayoutTreeDeleteNodeAction); + break; + default: { + console.error("Invalid reducer action", layoutTreeState, action); + } + } +} + +/** + * Computes an operation for inserting a new node into the tree in the given direction relative to the specified node. + * + * @param layoutTreeState The state of the tree. + * @param computeInsertAction The operation to compute. + * + * @template T The type of data associated with the nodes of the tree. + */ +function computeMoveNode( + layoutTreeState: LayoutTreeState, + computeInsertAction: LayoutTreeComputeMoveNodeAction +) { + const rootNode = layoutTreeState.rootNode; + const { node, nodeToMove, direction } = computeInsertAction; + console.log("computeInsertOperation start", layoutTreeState.rootNode, node, nodeToMove, direction); + if (direction === undefined) { + console.warn("No direction provided for insertItemInDirection"); + return; + } + + let newOperation: MoveOperation; + const parent = lazy(() => findParent(rootNode, node.id)); + const indexInParent = lazy(() => parent()?.children.findIndex((child) => node.id === child.id)); + const isRoot = rootNode.id === node.id; + + switch (direction) { + case DropDirection.Top: + if (node.flexDirection === FlexDirection.Column) { + newOperation = { parentId: node.id, index: 0, node: nodeToMove }; + } else { + if (isRoot) + newOperation = { + node: nodeToMove, + index: 0, + insertAtRoot: true, + }; + + const parentNode = parent(); + if (parentNode) + newOperation = { + parentId: parentNode.id, + index: indexInParent() ?? 0, + node: nodeToMove, + }; + } + break; + case DropDirection.Bottom: + if (node.flexDirection === FlexDirection.Column) { + newOperation = { parentId: node.id, index: 1, node: nodeToMove }; + } else { + if (isRoot) + newOperation = { + node: nodeToMove, + index: 1, + insertAtRoot: true, + }; + + const parentNode = parent(); + if (parentNode) + newOperation = { + parentId: parentNode.id, + index: indexInParent() + 1, + node: nodeToMove, + }; + } + break; + case DropDirection.Left: + if (node.flexDirection === FlexDirection.Row) { + newOperation = { parentId: node.id, index: 0, node: nodeToMove }; + } else { + const parentNode = parent(); + if (parentNode) + newOperation = { + parentId: parentNode.id, + index: indexInParent(), + node: nodeToMove, + }; + } + break; + case DropDirection.Right: + if (node.flexDirection === FlexDirection.Row) { + newOperation = { parentId: node.id, index: 1, node: nodeToMove }; + } else { + const parentNode = parent(); + if (parentNode) + newOperation = { + parentId: parentNode.id, + index: indexInParent() + 1, + node: nodeToMove, + }; + } + break; + default: + throw new Error("Invalid direction"); + } + + if (newOperation) layoutTreeState.pendingAction = { type: LayoutTreeActionType.Move, ...newOperation }; +} + +function moveNode(layoutTreeState: LayoutTreeState, action: LayoutTreeMoveNodeAction) { + const rootNode = layoutTreeState.rootNode; + console.log("moveNode", action, layoutTreeState.rootNode); + if (!action) { + console.error("no move node action provided"); + return; + } + if (action.parentId && action.insertAtRoot) { + console.error("parent and insertAtRoot cannot both be defined in a move node action"); + return; + } + + let node = findNode(rootNode, action.node.id) ?? action.node; + let parent = findNode(rootNode, action.parentId); + let oldParent = findParent(rootNode, action.node.id); + + console.log(node, parent, oldParent); + + // Remove nodeToInsert from its old parent + if (oldParent) { + removeChild(oldParent, node); + } + + if (!parent && action.insertAtRoot) { + addIntermediateNode(rootNode); + addChildAt(rootNode, action.index, node); + } else if (parent) { + addChildAt(parent, action.index, node); + } else { + throw new Error("Invalid InsertOperation"); + } + const { node: newRootNode, leafs } = balanceNode(layoutTreeState.rootNode); + layoutTreeState.rootNode = newRootNode; + layoutTreeState.leafs = leafs; + layoutTreeState.pendingAction = undefined; +} + +function insertNode(layoutTreeState: LayoutTreeState, action: LayoutTreeInsertNodeAction) { + if (!action?.node) { + console.error("no insert node action provided"); + return; + } + if (!layoutTreeState.rootNode) { + const { node: balancedNode, leafs } = balanceNode(action.node); + layoutTreeState.rootNode = balancedNode; + layoutTreeState.leafs = leafs; + return; + } + const insertLoc = findNextInsertLocation(layoutTreeState.rootNode, 5); + addChildAt(insertLoc.node, insertLoc.index, action.node); + const { node: newRootNode, leafs } = balanceNode(layoutTreeState.rootNode); + layoutTreeState.rootNode = newRootNode; + layoutTreeState.leafs = leafs; +} + +function deleteNode(layoutTreeState: LayoutTreeState, action: LayoutTreeDeleteNodeAction) { + console.log("deleteNode", layoutTreeState, action); + if (!action?.nodeId) { + console.error("no delete node action provided"); + return; + } + if (!layoutTreeState.rootNode) { + console.error("no root node"); + return; + } + if (layoutTreeState.rootNode.id === action.nodeId) { + layoutTreeState.rootNode = undefined; + layoutTreeState.leafs = undefined; + return; + } + const parent = findParent(layoutTreeState.rootNode, action.nodeId); + if (parent) { + const node = parent.children.find((child) => child.id === action.nodeId); + removeChild(parent, node); + console.log("node deleted", parent, node); + } else { + console.error("unable to delete node, not found in tree"); + } + const { node: newRootNode, leafs } = balanceNode(layoutTreeState.rootNode); + layoutTreeState.rootNode = newRootNode; + layoutTreeState.leafs = leafs; +} diff --git a/frontend/faraday/lib/model.ts b/frontend/faraday/lib/model.ts new file mode 100644 index 000000000..2824ed091 --- /dev/null +++ b/frontend/faraday/lib/model.ts @@ -0,0 +1,133 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { WritableAtom } from "jotai"; +import { DropDirection, FlexDirection } from "./utils.js"; + +/** + * Represents an operation to insert a node into a tree. + */ +export type MoveOperation = { + /** + * The index at which the node will be inserted in the parent. + */ + index: number; + + /** + * The parent node. Undefined if inserting at root. + */ + parentId?: string; + + /** + * Whether the node will be inserted at the root of the tree. + */ + insertAtRoot?: boolean; + + /** + * The node to insert. + */ + node: LayoutNode; +}; + +/** + * Types of actions that modify the layout tree. + */ +export enum LayoutTreeActionType { + ComputeMove = "computeMove", + Move = "move", + CommitPendingAction = "commit", + ResizeNode = "resize", + InsertNode = "insert", + DeleteNode = "delete", +} + +/** + * Base class for actions that modify the layout tree. + */ +export interface LayoutTreeAction { + type: LayoutTreeActionType; +} + +/** + * Action for computing a move operation and saving it as a pending action in the tree state. + * + * @template T The type of data associated with the nodes of the tree. + * @see MoveOperation + * @see LayoutTreeMoveNodeAction + */ +export interface LayoutTreeComputeMoveNodeAction extends LayoutTreeAction { + type: LayoutTreeActionType.ComputeMove; + node: LayoutNode; + nodeToMove: LayoutNode; + direction: DropDirection; +} + +/** + * Action for moving a node within the layout tree. + * + * @template T The type of data associated with the nodes of the tree. + * @see MoveOperation + */ +export interface LayoutTreeMoveNodeAction extends LayoutTreeAction, MoveOperation { + type: LayoutTreeActionType.Move; +} + +/** + * Action for committing a pending action to the layout tree. + */ +export interface LayoutTreeCommitPendingAction extends LayoutTreeAction { + type: LayoutTreeActionType.CommitPendingAction; +} + +/** + * Action for inserting a new node to the layout tree. + * + * @template T The type of data associated with the nodes of the tree. + */ +export interface LayoutTreeInsertNodeAction extends LayoutTreeAction { + type: LayoutTreeActionType.InsertNode; + node: LayoutNode; +} + +/** + * Action for deleting a node from the layout tree. + */ +export interface LayoutTreeDeleteNodeAction extends LayoutTreeAction { + type: LayoutTreeActionType.DeleteNode; + nodeId: string; +} + +/** + * Represents the state of a layout tree. + * + * @template T The type of data associated with the nodes of the tree. + */ +export type LayoutTreeState = { + rootNode: LayoutNode; + leafs: LayoutNode[]; + pendingAction: LayoutTreeAction; +}; + +/** + * Represents a single node in the layout tree. + * @template T The type of data associated with the node. + */ +export interface LayoutNode { + id: string; + data?: T; + children?: LayoutNode[]; + flexDirection: FlexDirection; + size?: number; +} + +/** + * An abstraction of the type definition for a writable layout node atom. + */ +export type WritableLayoutNodeAtom = WritableAtom, [value: LayoutNode], void>; + +/** + * An abstraction of the type definition for a writable layout tree state atom. + */ +export type WritableLayoutTreeStateAtom = WritableAtom, [value: LayoutTreeState], void>; + +export type ContentRenderer = (data: T, onClose?: () => void) => React.ReactNode; diff --git a/frontend/faraday/lib/tilelayout.less b/frontend/faraday/lib/tilelayout.less new file mode 100644 index 000000000..9785663dd --- /dev/null +++ b/frontend/faraday/lib/tilelayout.less @@ -0,0 +1,93 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +.tile-layout { + position: relative; + height: 100%; + width: 100%; + overflow: hidden; + + .overlay-container, + .display-container, + .placeholder-container { + position: absolute; + display: flex; + top: 0; + left: 0; + height: 100%; + width: 100%; + } + + .placeholder-container { + z-index: 1; + } + + .overlay-container { + z-index: 2; + background-color: coral; + opacity: 0.5; + } + + .tile-node, + .overlay-node { + display: flex; + flex: 1 1 auto; + max-width: 100%; + max-height: 100%; + width: 100%; + height: 100%; + + &.is-dragging { + background-color: rgba(0, 255, 0, 0.3); + } + } + + .tile-node { + visibility: hidden; + } + + &.animate { + .tile-node { + transition-duration: 0.15s; + transition-timing-function: ease-in; + transition-property: transform, width, height; + } + } + + &.overlayVisible { + .tile-node { + visibility: unset; + } + } + + .tile-leaf, + .overlay-leaf { + display: flex; + flex: 1 1 auto; + max-height: 100%; + max-width: 100%; + margin: 5px; + } + + .tile-leaf { + border: 1px solid black; + } + + .overlay-leaf { + border: 10px solid red; + } + + // .tile-leaf { + // border: 1px solid black; + // } + + // .overlay-leaf { + // margin: 1px; + // } + + .placeholder { + display: flex; + flex: 1 1 auto; + background-color: aqua; + } +} diff --git a/frontend/faraday/lib/tilelayout.stories.less b/frontend/faraday/lib/tilelayout.stories.less new file mode 100644 index 000000000..b179ffc8f --- /dev/null +++ b/frontend/faraday/lib/tilelayout.stories.less @@ -0,0 +1,9 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +.placeholder-visible { + .overlay-container, + .placeholder-container { + transform: unset !important; + } +} diff --git a/frontend/faraday/lib/utils.ts b/frontend/faraday/lib/utils.ts new file mode 100644 index 000000000..0f6043965 --- /dev/null +++ b/frontend/faraday/lib/utils.ts @@ -0,0 +1,116 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { CSSProperties } from "react"; +import { XYCoord } from "react-dnd"; + +export interface Dimensions { + width: number; + height: number; + left: number; + top: number; +} + +export enum DropDirection { + Top = 0, + Right = 1, + Bottom = 2, + Left = 3, +} + +export enum FlexDirection { + Row = "row", + Column = "column", +} + +export function reverseFlexDirection(flexDirection: FlexDirection): FlexDirection { + return flexDirection === FlexDirection.Row ? FlexDirection.Column : FlexDirection.Row; +} + +export function determineDropDirection(dimensions?: Dimensions, offset?: XYCoord | null): DropDirection | undefined { + console.log("determineDropDirection", dimensions, offset); + if (!offset || !dimensions) return undefined; + const { width, height, left, top } = dimensions; + let { x, y } = offset; + x -= left; + y -= top; + + // Lies outside of the box + if (y < 0 || y > height || x < 0 || x > width) return undefined; + + const diagonal1 = y * width - x * height; + const diagonal2 = y * width + x * height - height * width; + + // Lies on diagonal + if (diagonal1 == 0 || diagonal2 == 0) return undefined; + + let code = 0; + + if (diagonal2 > 0) { + code += 1; + } + + if (diagonal1 > 0) { + code += 2; + code = 5 - code; + } + + return code; +} + +export function setTransform({ top, left, width, height }: Dimensions, setSize: boolean = true): CSSProperties { + // Replace unitless items with px + const translate = `translate(${left}px,${top}px)`; + return { + top: 0, + left: 0, + transform: translate, + WebkitTransform: translate, + MozTransform: translate, + msTransform: translate, + OTransform: translate, + width: setSize ? `${width}px` : undefined, + height: setSize ? `${height}px` : undefined, + position: "absolute", + }; +} + +export const debounce = any>(callback: T, waitFor: number) => { + let timeout: NodeJS.Timeout; + return (...args: Parameters): ReturnType => { + let result: any; + clearTimeout(timeout); + timeout = setTimeout(() => { + result = callback(...args); + }, waitFor); + return result; + }; +}; + +/** + * Simple wrapper function that lazily evaluates the provided function and caches its result for future calls. + * @param callback The function to lazily run. + * @returns The result of the function. + */ +export const lazy = any>(callback: T) => { + let res: ReturnType; + let processed = false; + return (...args: Parameters): ReturnType => { + if (processed) return res; + res = callback(...args); + processed = true; + return res; + }; +}; + +/** + * Workaround for NodeJS compatibility. Will attempt to resolve the Crypto API from the browser and fallback to NodeJS if it isn't present. + * @returns The Crypto API. + */ +export function getCrypto() { + try { + return window.crypto; + } catch { + return crypto; + } +} diff --git a/frontend/faraday/tests/layoutNode.test.ts b/frontend/faraday/tests/layoutNode.test.ts new file mode 100644 index 000000000..b134bd81e --- /dev/null +++ b/frontend/faraday/tests/layoutNode.test.ts @@ -0,0 +1,304 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { assert, test } from "vitest"; +import { + addChildAt, + addIntermediateNode, + balanceNode, + findNextInsertLocation, + newLayoutNode, +} from "../lib/layoutNode.js"; +import { LayoutNode } from "../lib/model.js"; +import { FlexDirection } from "../lib/utils.js"; +import { TestData } from "./model.js"; + +test("newLayoutNode", () => { + assert.throws( + () => newLayoutNode(FlexDirection.Column), + "Invalid node", + undefined, + "calls to the constructor without data or children should fail" + ); + assert.throws( + () => newLayoutNode(FlexDirection.Column, undefined, [], { name: "hello" }), + "Invalid node", + undefined, + "calls to the constructor with both data and children should fail" + ); + assert.doesNotThrow( + () => newLayoutNode(FlexDirection.Column, undefined, undefined, { name: "hello" }), + "Invalid node", + undefined, + "calls to the constructor with only data defined should succeed" + ); + assert.throws(() => newLayoutNode(FlexDirection.Column, undefined, [], undefined)), + "Invalid node", + undefined, + "calls to the constructor with empty children array should fail"; + assert.doesNotThrow(() => + newLayoutNode( + FlexDirection.Column, + undefined, + [newLayoutNode(FlexDirection.Column, undefined, undefined, { name: "hello" })], + undefined + ) + ), + "Invalid node", + undefined, + "calls to the constructor with children array containing at least one child should succeed"; +}); + +test("addIntermediateNode", () => { + let node1: LayoutNode = newLayoutNode(FlexDirection.Column, undefined, [ + newLayoutNode(FlexDirection.Row, undefined, undefined, { name: "hello" }), + ]); + assert(node1.children![0].data!.name === "hello", "node1 should have one child which should have data"); + const intermediateNode1 = addIntermediateNode(node1); + assert( + node1.children !== undefined && node1.children.length === 1 && node1.children?.includes(intermediateNode1), + "node1 should have a single child intermediateNode1" + ); + assert(intermediateNode1.flexDirection === FlexDirection.Row, "intermediateNode1 should have flexDirection Row"); + assert( + intermediateNode1.children![0].children![0].data!.name === "hello" && + intermediateNode1.children![0].children![0].flexDirection === FlexDirection.Row, + "intermediateNode1 should have a nested child which should have data and flexDirection Row" + ); + let node2: LayoutNode = newLayoutNode(FlexDirection.Column, undefined, undefined, { + name: "hello", + }); + const intermediateNode2 = addIntermediateNode(node2); + assert( + node2.children !== undefined && + node2.data === undefined && + node2.children.length === 1 && + node2.children.includes(intermediateNode2), + "node2 should have no data and a single child intermediateNode2" + ); + assert( + intermediateNode2.data.name === "hello" && intermediateNode2.children === undefined, + "intermediateNode2 should have no children and should have data matching the old value of node2" + ); +}); + +test("addChildAt - same flexDirection, no children", () => { + let node1 = newLayoutNode(FlexDirection.Row, undefined, undefined, { name: "node1" }); + let node2 = newLayoutNode(FlexDirection.Row, undefined, undefined, { name: "node2" }); + addChildAt(node1, 1, node2); + assert(node1.data === undefined, "node1 should have no data"); + assert(node1.children!.length === 2, "node1 should have two children"); + assert(node1.children![0].data!.name === "node1", "node1's first child should have node1's data"); + assert(node1.children![1].id === node2.id, "node1's second child should be node2"); + assert(node1.children![1].flexDirection === FlexDirection.Column, "node2 should now have flexDirection Column"); +}); + +test("addChildAt - different flexDirection, no children", () => { + let node1 = newLayoutNode(FlexDirection.Row, undefined, undefined, { name: "node1" }); + let node2 = newLayoutNode(FlexDirection.Column, undefined, undefined, { name: "node2" }); + addChildAt(node1, 1, node2); + assert(node1.data === undefined, "node1 should have no data"); + assert(node1.children!.length === 2, "node1 should have two children"); + assert(node1.children![0].data!.name === "node1", "node1's first child should have node1's data"); + assert(node1.children![0].data!.name === "node1", "node1's first child should have flexDirection Column"); + assert(node1.children![1].id === node2.id, "node1's second child should be node2"); + assert(node1.children![1].flexDirection === FlexDirection.Column, "node2 should have flexDirection Row"); +}); + +test("addChildAt - same flexDirection, first node has children, second doesn't", () => { + let node1 = newLayoutNode(FlexDirection.Row, undefined, [ + newLayoutNode(FlexDirection.Column, undefined, undefined, { name: "node1" }), + ]); + let node2 = newLayoutNode(FlexDirection.Column, undefined, undefined, { name: "node2" }); + addChildAt(node1, 1, node2); + assert(node1.data === undefined, "node1 should have no data"); + assert(node1.children!.length === 2, "node1 should have two children"); + assert(node1.children![0].data!.name === "node1", "node1's first child should have node1's data"); + assert( + node1.children![0].flexDirection === FlexDirection.Column, + "node1's first child should have flexDirection Column" + ); + assert(node1.children![1].id === node2.id, "node1's second child should be node2"); + assert(node1.children![1].flexDirection === FlexDirection.Column, "node2 should have flexDirection Column"); +}); + +test("addChildAt - different flexDirection, first node has children, second doesn't", () => { + let node1 = newLayoutNode(FlexDirection.Row, undefined, [ + newLayoutNode(FlexDirection.Column, undefined, undefined, { name: "node1" }), + ]); + let node2 = newLayoutNode(FlexDirection.Row, undefined, undefined, { name: "node2" }); + addChildAt(node1, 1, node2); + assert(node1.data === undefined, "node1 should have no data"); + assert(node1.children!.length === 2, "node1 should have two children"); + assert(node1.children![0].data!.name === "node1", "node1's first child should have node1's data"); + assert(node1.children![1].id === node2.id, "node1's second child should be node2"); + assert(node1.children![1].flexDirection === FlexDirection.Column, "node2 should now have flexDirection Column"); +}); + +test("addChildAt - same flexDirection, first node has children, second has children", () => { + let node1 = newLayoutNode(FlexDirection.Row, undefined, [ + newLayoutNode(FlexDirection.Column, undefined, undefined, { name: "node1" }), + ]); + let node2 = newLayoutNode(FlexDirection.Row, undefined, [ + newLayoutNode(FlexDirection.Column, undefined, undefined, { name: "node2" }), + ]); + addChildAt(node1, 1, node2); + assert(node1.data === undefined, "node1 should have no data"); + assert(node1.children!.length === 2, "node1 should have two children"); + assert(node1.children![0].data!.name === "node1", "node1's first child should have node1's data"); + assert( + node1.children![0].flexDirection === FlexDirection.Column, + "node1's first child should have flexDirection Column" + ); + assert(node1.children![1].id === node2.children![0].id, "node1's second child should be node2's child"); + assert( + node1.children![1].flexDirection === FlexDirection.Column, + "node1's second child should have flexDirection Column" + ); +}); + +test("addChildAt - different flexDirection, first node has children, second has children", () => { + let node1 = newLayoutNode(FlexDirection.Row, undefined, [ + newLayoutNode(FlexDirection.Column, undefined, undefined, { name: "node1" }), + ]); + let node2 = newLayoutNode(FlexDirection.Column, undefined, [ + newLayoutNode(FlexDirection.Row, undefined, undefined, { name: "node2" }), + ]); + addChildAt(node1, 1, node2); + assert(node1.data === undefined, "node1 should have no data"); + assert(node1.children!.length === 2, "node1 should have two children"); + assert(node1.children![0].data!.name === "node1", "node1's first child should have node1's data"); + assert( + node1.children![0].flexDirection === FlexDirection.Column, + "node1's first child should have flexDirection Column" + ); + assert(node1.children![1].id === node2.id, "node1's second child should be node2"); + assert( + node1.children![1].flexDirection === FlexDirection.Column, + "node1's second child should have flexDirection Column" + ); +}); + +test("balanceNode - corrects flex directions", () => { + let node1 = newLayoutNode(FlexDirection.Row, undefined, [ + newLayoutNode(FlexDirection.Row, undefined, undefined, { name: "node1Inner1" }), + newLayoutNode(FlexDirection.Row, undefined, undefined, { name: "node1Inner2" }), + ]); + const newNode1 = balanceNode(node1).node; + assert(newNode1 !== undefined, "newNode1 should not be undefined"); + node1 = newNode1; + assert(node1.data === undefined, "node1 should have no data"); + assert(node1.children![0].flexDirection !== node1.flexDirection); +}); + +test("balanceNode - collapses nodes with single grandchild 1", () => { + let node1 = newLayoutNode(FlexDirection.Row, undefined, [ + newLayoutNode(FlexDirection.Column, undefined, [ + newLayoutNode(FlexDirection.Row, undefined, undefined, { name: "node1" }), + ]), + ]); + const newNode1 = balanceNode(node1).node; + assert(newNode1 !== undefined, "newNode1 should not be undefined"); + node1 = newNode1; + assert(node1.children === undefined, "node1 should have no children"); + assert(node1.data!.name === "node1", "node1 should have data 'node1'"); +}); + +test("balanceNode - collapses nodes with single grandchild 2", () => { + let node2 = newLayoutNode(FlexDirection.Row, undefined, [ + newLayoutNode(FlexDirection.Column, undefined, [ + newLayoutNode(FlexDirection.Row, undefined, [ + newLayoutNode(FlexDirection.Column, undefined, undefined, { name: "node2Inner1" }), + newLayoutNode(FlexDirection.Column, undefined, undefined, { name: "node2Inner2" }), + ]), + ]), + ]); + const { node: newNode2, leafs } = balanceNode(node2); + assert(newNode2 !== undefined, "newNode2 should not be undefined"); + node2 = newNode2; + assert(node2.children!.length === 2, "node2 should have two children"); + assert(node2.children[0].data!.name === "node2Inner1", "node2's first child should have data 'node2Inner1'"); + assert(leafs.length === 2, "leafs should have two leafs"); + assert(leafs[0].data!.name === "node2Inner1", "leafs[0] should have data 'node2Inner1'"); + assert(leafs[1].data!.name === "node2Inner2", "leafs[1] should have data 'node2Inner2'"); +}); + +test("balanceNode - collapses nodes with single grandchild 3", () => { + let node3 = newLayoutNode(FlexDirection.Row, undefined, [ + newLayoutNode(FlexDirection.Column, undefined, [ + newLayoutNode(FlexDirection.Row, undefined, [ + newLayoutNode(FlexDirection.Column, undefined, undefined, { name: "node3" }), + ]), + ]), + ]); + const newNode3 = balanceNode(node3).node; + assert(newNode3 !== undefined, "newNode3 should not be undefined"); + node3 = newNode3; + assert(node3.children === undefined, "node3 should have no children"); + assert(node3.data!.name === "node3", "node3 should have data 'node3'"); +}); + +test("balanceNode - collapses nodes with single grandchild 4", () => { + let node4 = newLayoutNode(FlexDirection.Row, undefined, [ + newLayoutNode(FlexDirection.Column, undefined, [ + newLayoutNode(FlexDirection.Row, undefined, [ + newLayoutNode(FlexDirection.Column, undefined, [ + newLayoutNode(FlexDirection.Row, undefined, undefined, { name: "node4Inner1" }), + newLayoutNode(FlexDirection.Row, undefined, undefined, { name: "node4Inner2" }), + ]), + ]), + ]), + ]); + const newNode4 = balanceNode(node4); + assert(newNode4 !== undefined, "newNode4 should not be undefined"); + node4 = newNode4.node; + assert(node4.children!.length === 1, "node4 should have one child"); + assert(node4.children![0].children!.length === 2, "node4 should have two grandchildren"); + assert( + node4.children[0].children![0].data!.name === "node4Inner1", + "node4's first child should have data 'node4Inner1'" + ); +}); + +test("findNextInsertLocation", () => { + const node1 = newLayoutNode(FlexDirection.Row, undefined, [ + newLayoutNode(FlexDirection.Row, undefined, undefined, { name: "node1" }), + newLayoutNode(FlexDirection.Row, undefined, undefined, { name: "node1" }), + newLayoutNode(FlexDirection.Row, undefined, undefined, { name: "node1" }), + newLayoutNode(FlexDirection.Row, undefined, undefined, { name: "node1" }), + ]); + + const insertLoc1 = findNextInsertLocation(node1, 5); + assert(insertLoc1.node.id === node1.id, "should insert into node1"); + assert(insertLoc1.index === 4, "should insert into index 4 of node1"); + + const node2Inner5 = newLayoutNode(FlexDirection.Row, undefined, undefined, { name: "node2Inner5" }); + const node2 = newLayoutNode(FlexDirection.Row, undefined, [ + newLayoutNode(FlexDirection.Row, undefined, undefined, { name: "node1" }), + newLayoutNode(FlexDirection.Row, undefined, undefined, { name: "node1" }), + newLayoutNode(FlexDirection.Row, undefined, undefined, { name: "node1" }), + newLayoutNode(FlexDirection.Row, undefined, undefined, { name: "node1" }), + node2Inner5, + ]); + + const insertLoc2 = findNextInsertLocation(node2, 5); + assert(insertLoc2.node.id === node2Inner5.id, "should insert into node2Inner5"); + assert(insertLoc2.index === 1, "should insert into index 1 of node2Inner1"); + + const node3Inner5 = newLayoutNode(FlexDirection.Row, undefined, [ + newLayoutNode(FlexDirection.Row, undefined, undefined, { name: "node1" }), + newLayoutNode(FlexDirection.Row, undefined, undefined, { name: "node1" }), + ]); + const node3Inner4 = newLayoutNode(FlexDirection.Row, undefined, undefined, { name: "node3Inner4" }); + const node3 = newLayoutNode(FlexDirection.Row, undefined, [ + newLayoutNode(FlexDirection.Row, undefined, undefined, { name: "node1" }), + newLayoutNode(FlexDirection.Row, undefined, undefined, { name: "node1" }), + newLayoutNode(FlexDirection.Row, undefined, undefined, { name: "node1" }), + node3Inner4, + node3Inner5, + ]); + + const insertLoc3 = findNextInsertLocation(node3, 5); + assert(insertLoc3.node.id === node3Inner4.id, "should insert into node3Inner4"); + assert(insertLoc3.index === 1, "should insert into index 1 of node3Inner4"); +}); diff --git a/frontend/faraday/tests/layoutState.test.ts b/frontend/faraday/tests/layoutState.test.ts new file mode 100644 index 000000000..3b6333511 --- /dev/null +++ b/frontend/faraday/tests/layoutState.test.ts @@ -0,0 +1,57 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { assert, test } from "vitest"; +import { newLayoutNode } from "../lib/layoutNode.js"; +import { layoutTreeStateReducer, newLayoutTreeState } from "../lib/layoutState.js"; +import { LayoutTreeActionType, LayoutTreeComputeMoveNodeAction, LayoutTreeMoveNodeAction } from "../lib/model.js"; +import { DropDirection, FlexDirection } from "../lib/utils.js"; +import { TestData } from "./model.js"; + +test("layoutTreeStateReducer - compute move", () => { + let treeState = newLayoutTreeState( + newLayoutNode(FlexDirection.Row, undefined, undefined, { name: "root" }) + ); + assert(treeState.rootNode.data!.name === "root", "root should have no children and should have data"); + let node1 = newLayoutNode(FlexDirection.Column, undefined, undefined, { name: "node1" }); + treeState = layoutTreeStateReducer(treeState, { + type: LayoutTreeActionType.ComputeMove, + node: treeState.rootNode, + nodeToMove: node1, + direction: DropDirection.Bottom, + } as LayoutTreeComputeMoveNodeAction); + const insertOperation = treeState.pendingAction as LayoutTreeMoveNodeAction; + assert(insertOperation.node === node1, "insert operation node should equal node1"); + assert(!insertOperation.parentId, "insert operation parent should not be defined"); + assert(insertOperation.index === 1, "insert operation index should equal 1"); + assert(insertOperation.insertAtRoot, "insert operation insertAtRoot should be true"); + treeState = layoutTreeStateReducer(treeState, { + type: LayoutTreeActionType.CommitPendingAction, + }); + assert( + treeState.rootNode.data === undefined && treeState.rootNode.children!.length === 2, + "root node should now have no data and should have two children" + ); + assert(treeState.rootNode.children![1].data!.name === "node1", "root's second child should be node1"); + + let node2 = newLayoutNode(FlexDirection.Column, undefined, undefined, { name: "node2" }); + treeState = layoutTreeStateReducer(treeState, { + type: LayoutTreeActionType.ComputeMove, + node: node1, + nodeToMove: node2, + direction: DropDirection.Bottom, + } as LayoutTreeComputeMoveNodeAction); + const insertOperation2 = treeState.pendingAction as LayoutTreeMoveNodeAction; + assert(insertOperation2.node === node2, "insert operation node should equal node2"); + assert(insertOperation2.parentId === node1.id, "insert operation parent id should be node1 id"); + assert(insertOperation2.index === 1, "insert operation index should equal 1"); + assert(!insertOperation2.insertAtRoot, "insert operation insertAtRoot should be false"); + treeState = layoutTreeStateReducer(treeState, { + type: LayoutTreeActionType.CommitPendingAction, + }); + assert( + treeState.rootNode.data === undefined && treeState.rootNode.children!.length === 2, + "root node should still have three children" + ); + assert(treeState.rootNode.children![1].children!.length === 2, "root's second child should now have two children"); +}); diff --git a/frontend/faraday/tests/model.ts b/frontend/faraday/tests/model.ts new file mode 100644 index 000000000..f014b4daf --- /dev/null +++ b/frontend/faraday/tests/model.ts @@ -0,0 +1,6 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +export type TestData = { + name: string; +}; diff --git a/frontend/faraday/tests/utils.test.ts b/frontend/faraday/tests/utils.test.ts new file mode 100644 index 000000000..7c95ca642 --- /dev/null +++ b/frontend/faraday/tests/utils.test.ts @@ -0,0 +1,65 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { assert, test } from "vitest"; +import { + Dimensions, + DropDirection, + FlexDirection, + determineDropDirection, + reverseFlexDirection, +} from "../lib/utils.js"; + +test("determineDropDirection", () => { + const dimensions: Dimensions = { + top: 0, + left: 0, + height: 3, + width: 3, + }; + + assert.equal( + determineDropDirection(dimensions, { + x: 1.5, + y: 0.5, + }), + DropDirection.Top + ); + + assert.equal( + determineDropDirection(dimensions, { + x: 1.5, + y: 2.5, + }), + DropDirection.Bottom + ); + + assert.equal( + determineDropDirection(dimensions, { + x: 2.5, + y: 1.5, + }), + DropDirection.Right + ); + + assert.equal( + determineDropDirection(dimensions, { + x: 0.5, + y: 1.5, + }), + DropDirection.Left + ); + + assert.equal( + determineDropDirection(dimensions, { + x: 1.5, + y: 1.5, + }), + undefined + ); +}); + +test("reverseFlexDirection", () => { + assert.equal(reverseFlexDirection(FlexDirection.Row), FlexDirection.Column); + assert.equal(reverseFlexDirection(FlexDirection.Column), FlexDirection.Row); +}); diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index de115715f..78f1dea92 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -1,6 +1,8 @@ // Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 +import { LayoutNode } from "../faraday"; + declare global { type UIContext = { windowid: string; @@ -14,11 +16,11 @@ declare global { oid: string; }; - type WaveObj = { + interface WaveObj { otype: string; oid: string; version: number; - }; + } type WaveObjUpdate = { updatetype: "update" | "delete"; @@ -68,6 +70,7 @@ declare global { version: number; name: string; blockids: string[]; + layout: LayoutNode; }; type Point = { @@ -104,6 +107,10 @@ declare global { winsize: WinSize; lastfocusts: number; }; + + type TabLayoutData = { + blockId: string; + }; } export {}; diff --git a/frontend/wave.ts b/frontend/wave.ts index 59fd7d825..45e97ad32 100644 --- a/frontend/wave.ts +++ b/frontend/wave.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { Client } from "@/gopkg/wstore"; +import { globalStore } from "@/store/global"; import * as WOS from "@/store/wos"; import * as React from "react"; import { createRoot } from "react-dom/client"; @@ -17,6 +18,7 @@ loadFonts(); console.log("Wave Starting"); (window as any).WOS = WOS; +(window as any).globalStore = globalStore; function matchViewportSize() { document.body.style.width = window.visualViewport.width + "px"; diff --git a/package.json b/package.json index 476668b66..f76484669 100644 --- a/package.json +++ b/package.json @@ -7,18 +7,23 @@ "dev": "vite", "build": "vite build --minify false --mode development", "build:prod": "vite build --mode production", - "preview": "vite preview" + "preview": "vite preview", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build", + "coverage": "vitest run --coverage", + "test": "vitest" }, "devDependencies": { - "@chromatic-com/storybook": "^1.3.3", + "@chromatic-com/storybook": "^1.5.0", "@eslint/js": "^9.2.0", - "@storybook/addon-essentials": "^8.0.10", - "@storybook/addon-interactions": "^8.0.10", - "@storybook/addon-links": "^8.0.10", - "@storybook/blocks": "^8.0.10", - "@storybook/react": "^8.0.10", - "@storybook/react-vite": "^8.0.10", - "@storybook/test": "^8.0.10", + "@storybook/addon-essentials": "^8.1.4", + "@storybook/addon-interactions": "^8.1.4", + "@storybook/addon-links": "^8.1.4", + "@storybook/blocks": "^8.1.4", + "@storybook/builder-vite": "^8.1.4", + "@storybook/react": "^8.1.4", + "@storybook/react-vite": "^8.1.4", + "@storybook/test": "^8.1.4", "@types/node": "^20.12.12", "@types/react": "^18.3.2", "@types/uuid": "^9.0.8", @@ -31,7 +36,7 @@ "prettier": "^3.2.5", "prettier-plugin-jsdoc": "^1.3.0", "prettier-plugin-organize-imports": "^3.2.4", - "storybook": "^8.0.10", + "storybook": "^8.1.4", "ts-node": "^10.9.2", "tslib": "^2.6.2", "typescript": "^5.4.5", @@ -54,6 +59,8 @@ "jotai": "^2.8.0", "monaco-editor": "^0.49.0", "react": "^18.3.1", + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.3.1", "react-markdown": "^9.0.1", "remark-gfm": "^4.0.0", diff --git a/pkg/service/objectservice/objectservice.go b/pkg/service/objectservice/objectservice.go index 6fb3c844b..3f569cab2 100644 --- a/pkg/service/objectservice/objectservice.go +++ b/pkg/service/objectservice/objectservice.go @@ -143,15 +143,15 @@ func (svc *ObjectService) CreateBlock(uiContext wstore.UIContext, blockDef *wsto } } rtn := make(map[string]any) - rtn["blockid"] = blockData.OID + rtn["blockId"] = blockData.OID return updatesRtn(ctx, rtn) } -func (svc *ObjectService) DeleteBlock(uiContext wstore.UIContext, blockId string) (any, error) { +func (svc *ObjectService) DeleteBlock(uiContext wstore.UIContext, blockId string, newLayout any) (any, error) { ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) defer cancelFn() ctx = wstore.ContextWithUpdates(ctx) - err := wstore.DeleteBlock(ctx, uiContext.ActiveTabId, blockId) + err := wstore.DeleteBlock(ctx, uiContext.ActiveTabId, blockId, newLayout) if err != nil { return nil, fmt.Errorf("error deleting block: %w", err) } diff --git a/pkg/wstore/wstore.go b/pkg/wstore/wstore.go index 62c38a425..a9d95e486 100644 --- a/pkg/wstore/wstore.go +++ b/pkg/wstore/wstore.go @@ -210,7 +210,7 @@ func findStringInSlice(slice []string, val string) int { return -1 } -func DeleteBlock(ctx context.Context, tabId string, blockId string) error { +func DeleteBlock(ctx context.Context, tabId string, blockId string, newLayout any) error { return WithTx(ctx, func(tx *TxWrap) error { tab, _ := DBGet[*Tab](tx.Context(), tabId) if tab == nil { @@ -221,11 +221,13 @@ func DeleteBlock(ctx context.Context, tabId string, blockId string) error { return nil } tab.BlockIds = append(tab.BlockIds[:blockIdx], tab.BlockIds[blockIdx+1:]...) + if newLayout != nil { + tab.Layout = newLayout + } DBUpdate(tx.Context(), tab) DBDelete(tx.Context(), "block", blockId) return nil }) - } func CloseTab(ctx context.Context, workspaceId string, tabId string) error { diff --git a/pkg/wstore/wstore_types.go b/pkg/wstore/wstore_types.go index 18b4f1f81..036d16ce7 100644 --- a/pkg/wstore/wstore_types.go +++ b/pkg/wstore/wstore_types.go @@ -88,6 +88,7 @@ type Tab struct { OID string `json:"oid"` Version int `json:"version"` Name string `json:"name"` + Layout any `json:"layout,omitempty"` BlockIds []string `json:"blockids"` Meta map[string]any `json:"meta"` } diff --git a/tsconfig.json b/tsconfig.json index 4e0f7b6a7..05f554e80 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,10 +17,11 @@ "paths": { "@/app/*": ["frontend/app/*"], "@/util/*": ["frontend/util/*"], + "@/faraday/*": ["frontend/faraday/*"], "@/store/*": ["frontend/app/store/*"], "@/element/*": ["frontend/app/element/*"], "@/bindings/*": ["frontend/bindings/github.com/wavetermdev/thenextwave/pkg/service/*"], - "@/gopkg/*": ["frontend/bindings/github.com/wavetermdev/thenextwave/pkg/*"] + "@/gopkg/*": ["frontend/bindings/github.com/wavetermdev/thenextwave/pkg/*"], } } } diff --git a/yarn.lock b/yarn.lock index d8512509a..405ce6364 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1514,16 +1514,16 @@ __metadata: languageName: node linkType: hard -"@chromatic-com/storybook@npm:^1.3.3": - version: 1.4.0 - resolution: "@chromatic-com/storybook@npm:1.4.0" +"@chromatic-com/storybook@npm:^1.5.0": + version: 1.5.0 + resolution: "@chromatic-com/storybook@npm:1.5.0" dependencies: - chromatic: "npm:^11.3.2" + chromatic: "npm:^11.4.0" filesize: "npm:^10.0.12" jsonfile: "npm:^6.1.0" react-confetti: "npm:^6.1.0" strip-ansi: "npm:^7.1.0" - checksum: 10c0/4b4ae546358b46d1a515fb5eb7110263eda9f59062702aea3deabd27a0f1554c778a2735d26824080ed9e833890808afbd603b89cc093bda83b7de96ad4102fd + checksum: 10c0/e1656e2f73756db0fc1e5dbc5367485f24d5e9d0643ff5259fe32fb42441b3c250250b208cf58cf1aa0a55334e59146b0ff9b8161c2838bd6ee2a8f36ccada58 languageName: node linkType: hard @@ -2303,6 +2303,27 @@ __metadata: languageName: node linkType: hard +"@react-dnd/asap@npm:^5.0.1": + version: 5.0.2 + resolution: "@react-dnd/asap@npm:5.0.2" + checksum: 10c0/0063db616db9801c9be18f11a912c3e214f87e714b1e4bf9462952af7ead65cba0b43e1f7c34bc8748811b6926e74d929e5e126f85ebb91b870faf809ceb5177 + languageName: node + linkType: hard + +"@react-dnd/invariant@npm:^4.0.1": + version: 4.0.2 + resolution: "@react-dnd/invariant@npm:4.0.2" + checksum: 10c0/b303cc53fc5074cefb2a76b45b9c73ebb5d35630b18f5dc282ed9a9ac9b0287b9da1f6ac63acfdea2341b8f8187f615afc12d5eb14ec6015964f5c1b167332e2 + languageName: node + linkType: hard + +"@react-dnd/shallowequal@npm:^4.0.1": + version: 4.0.2 + resolution: "@react-dnd/shallowequal@npm:4.0.2" + checksum: 10c0/9a352fd176752e5d9c2797d598aca034b7829111ae0c992d80f40d5f068fcd6a039b1841c741dfa1ab67a36a00664310aec4f0ce216e4112f80875c9fe6fd8dc + languageName: node + linkType: hard + "@rollup/pluginutils@npm:^5.0.2": version: 5.1.0 resolution: "@rollup/pluginutils@npm:5.1.0" @@ -2445,60 +2466,60 @@ __metadata: languageName: node linkType: hard -"@storybook/addon-actions@npm:8.1.3": - version: 8.1.3 - resolution: "@storybook/addon-actions@npm:8.1.3" +"@storybook/addon-actions@npm:8.1.4": + version: 8.1.4 + resolution: "@storybook/addon-actions@npm:8.1.4" dependencies: - "@storybook/core-events": "npm:8.1.3" + "@storybook/core-events": "npm:8.1.4" "@storybook/global": "npm:^5.0.0" "@types/uuid": "npm:^9.0.1" dequal: "npm:^2.0.2" polished: "npm:^4.2.2" uuid: "npm:^9.0.0" - checksum: 10c0/4d4ffbd17fd79c1fb63b00a11aa742a234fabf1417321bb500fd0225258268befbd3a5ee8ea3788fa585a9b5e3bf24be4716c1960033f15f406594a7b970e022 + checksum: 10c0/3b8589cfbacdda157463d467e5623923b224ebaf68183d75caf218d9b282119b5f74eeef1539b839b5729433cb8b337d48cf9256b05da891a3bb6924873c76da languageName: node linkType: hard -"@storybook/addon-backgrounds@npm:8.1.3": - version: 8.1.3 - resolution: "@storybook/addon-backgrounds@npm:8.1.3" +"@storybook/addon-backgrounds@npm:8.1.4": + version: 8.1.4 + resolution: "@storybook/addon-backgrounds@npm:8.1.4" dependencies: "@storybook/global": "npm:^5.0.0" memoizerific: "npm:^1.11.3" ts-dedent: "npm:^2.0.0" - checksum: 10c0/a1a90d2ac9ba4319fc9330330e70ab08d09aa4f1fff7713d5f7ce2e429910720b061c18e99107bb5ad3b0ecbdcc563ac8bb979e04214960d4409f4122ab70045 + checksum: 10c0/fb1bd44c3a22791585c0d62d91cd36df598541f94d580659ce9c79a76ef934bf88f34e3470bfc0bd41ceda0ddb9db1b62fe87ca5286df6f32e8dc9e0745fe6e1 languageName: node linkType: hard -"@storybook/addon-controls@npm:8.1.3": - version: 8.1.3 - resolution: "@storybook/addon-controls@npm:8.1.3" +"@storybook/addon-controls@npm:8.1.4": + version: 8.1.4 + resolution: "@storybook/addon-controls@npm:8.1.4" dependencies: - "@storybook/blocks": "npm:8.1.3" + "@storybook/blocks": "npm:8.1.4" dequal: "npm:^2.0.2" lodash: "npm:^4.17.21" ts-dedent: "npm:^2.0.0" - checksum: 10c0/54dd6082dfdc8f1931e6c1f6455ba40de6840d20e05d91a530146417b2e355cd2cce864d21fce5115347ff5002fd66ce940775756678992f8dd423bc97bc3af5 + checksum: 10c0/a9d2b32df38e4275bebc3e48bc8ab7e1173c9fe0dc78235627d177c7a4b80d0d33bff4ae77c50abf31ce88150e65b3cc8a646f12af4b7cead5487c0760d5a63b languageName: node linkType: hard -"@storybook/addon-docs@npm:8.1.3": - version: 8.1.3 - resolution: "@storybook/addon-docs@npm:8.1.3" +"@storybook/addon-docs@npm:8.1.4": + version: 8.1.4 + resolution: "@storybook/addon-docs@npm:8.1.4" dependencies: "@babel/core": "npm:^7.24.4" "@mdx-js/react": "npm:^3.0.0" - "@storybook/blocks": "npm:8.1.3" - "@storybook/client-logger": "npm:8.1.3" - "@storybook/components": "npm:8.1.3" - "@storybook/csf-plugin": "npm:8.1.3" - "@storybook/csf-tools": "npm:8.1.3" + "@storybook/blocks": "npm:8.1.4" + "@storybook/client-logger": "npm:8.1.4" + "@storybook/components": "npm:8.1.4" + "@storybook/csf-plugin": "npm:8.1.4" + "@storybook/csf-tools": "npm:8.1.4" "@storybook/global": "npm:^5.0.0" - "@storybook/node-logger": "npm:8.1.3" - "@storybook/preview-api": "npm:8.1.3" - "@storybook/react-dom-shim": "npm:8.1.3" - "@storybook/theming": "npm:8.1.3" - "@storybook/types": "npm:8.1.3" + "@storybook/node-logger": "npm:8.1.4" + "@storybook/preview-api": "npm:8.1.4" + "@storybook/react-dom-shim": "npm:8.1.4" + "@storybook/theming": "npm:8.1.4" + "@storybook/types": "npm:8.1.4" "@types/react": "npm:^16.8.0 || ^17.0.0 || ^18.0.0" fs-extra: "npm:^11.1.0" react: "npm:^16.8.0 || ^17.0.0 || ^18.0.0" @@ -2506,58 +2527,58 @@ __metadata: rehype-external-links: "npm:^3.0.0" rehype-slug: "npm:^6.0.0" ts-dedent: "npm:^2.0.0" - checksum: 10c0/7622907c7cecc4222f9f47c97d25336134c110dead3810ed2bab5a2adeb53cd2f6e87fc8444c1586fd7535d9b95d0aa36f77c74cb24c3f86ee9c933d793499ba + checksum: 10c0/1c174248ff0c49336c7873f23f872e8e666ff63aecbc081e819a7d56fcf5ffbd58d8e5ee41b4218ec2f993fd342a40d5fece3fa722330fdb88fa83832eea3216 languageName: node linkType: hard -"@storybook/addon-essentials@npm:^8.0.10": - version: 8.1.3 - resolution: "@storybook/addon-essentials@npm:8.1.3" +"@storybook/addon-essentials@npm:^8.1.4": + version: 8.1.4 + resolution: "@storybook/addon-essentials@npm:8.1.4" dependencies: - "@storybook/addon-actions": "npm:8.1.3" - "@storybook/addon-backgrounds": "npm:8.1.3" - "@storybook/addon-controls": "npm:8.1.3" - "@storybook/addon-docs": "npm:8.1.3" - "@storybook/addon-highlight": "npm:8.1.3" - "@storybook/addon-measure": "npm:8.1.3" - "@storybook/addon-outline": "npm:8.1.3" - "@storybook/addon-toolbars": "npm:8.1.3" - "@storybook/addon-viewport": "npm:8.1.3" - "@storybook/core-common": "npm:8.1.3" - "@storybook/manager-api": "npm:8.1.3" - "@storybook/node-logger": "npm:8.1.3" - "@storybook/preview-api": "npm:8.1.3" + "@storybook/addon-actions": "npm:8.1.4" + "@storybook/addon-backgrounds": "npm:8.1.4" + "@storybook/addon-controls": "npm:8.1.4" + "@storybook/addon-docs": "npm:8.1.4" + "@storybook/addon-highlight": "npm:8.1.4" + "@storybook/addon-measure": "npm:8.1.4" + "@storybook/addon-outline": "npm:8.1.4" + "@storybook/addon-toolbars": "npm:8.1.4" + "@storybook/addon-viewport": "npm:8.1.4" + "@storybook/core-common": "npm:8.1.4" + "@storybook/manager-api": "npm:8.1.4" + "@storybook/node-logger": "npm:8.1.4" + "@storybook/preview-api": "npm:8.1.4" ts-dedent: "npm:^2.0.0" - checksum: 10c0/fe178230ca397611b79039ac55101d6a8f7216296b8f011996e3903aa0ca187852e02cc4707cbdd7b0fc5375edcd033bb49441cdd892694d82f3ee2910d6ce56 + checksum: 10c0/75c9963e7581bce0fbcf4461c6217bdaa22f233436d449e9d06d6f5574d6035e3c7517c052aa6b1c73868880f95f5733194d7d36a4082cccba79318bd60e6892 languageName: node linkType: hard -"@storybook/addon-highlight@npm:8.1.3": - version: 8.1.3 - resolution: "@storybook/addon-highlight@npm:8.1.3" +"@storybook/addon-highlight@npm:8.1.4": + version: 8.1.4 + resolution: "@storybook/addon-highlight@npm:8.1.4" dependencies: "@storybook/global": "npm:^5.0.0" - checksum: 10c0/e501e1d363e81bc82dcafd61d1fdc1a09f5261c1b699f52810ec913e10864a30c727c10b95b572430956ab3e52af0eb0941dec3f4593faa70cc31407faf77385 + checksum: 10c0/761a75d48624faea3f496091c41a8afdd06e46e0ebbb2a8bd6205e549830180c9dcd17db48039012103c55ceb15a997a52a35397f070aa132d5c9584fdd84a59 languageName: node linkType: hard -"@storybook/addon-interactions@npm:^8.0.10": - version: 8.1.3 - resolution: "@storybook/addon-interactions@npm:8.1.3" +"@storybook/addon-interactions@npm:^8.1.4": + version: 8.1.4 + resolution: "@storybook/addon-interactions@npm:8.1.4" dependencies: "@storybook/global": "npm:^5.0.0" - "@storybook/instrumenter": "npm:8.1.3" - "@storybook/test": "npm:8.1.3" - "@storybook/types": "npm:8.1.3" + "@storybook/instrumenter": "npm:8.1.4" + "@storybook/test": "npm:8.1.4" + "@storybook/types": "npm:8.1.4" polished: "npm:^4.2.2" ts-dedent: "npm:^2.2.0" - checksum: 10c0/7a0e5e95c8266d3b15de77714911a61db0dd501bb5463c6d0b6dcd0a5617ef461f943cbb63593538a32d168299533028ac62de942eb67c8bfa4fb03ff3b632d1 + checksum: 10c0/5fad209450ba560a3e67e1eace3eae1b8d05848b71f0010dcd0db2fe91d107ebb362b4411801db025893391212c680f1921a7bfeb8ddbfd074ca56f825f57fb1 languageName: node linkType: hard -"@storybook/addon-links@npm:^8.0.10": - version: 8.1.3 - resolution: "@storybook/addon-links@npm:8.1.3" +"@storybook/addon-links@npm:^8.1.4": + version: 8.1.4 + resolution: "@storybook/addon-links@npm:8.1.4" dependencies: "@storybook/csf": "npm:^0.1.7" "@storybook/global": "npm:^5.0.0" @@ -2567,62 +2588,62 @@ __metadata: peerDependenciesMeta: react: optional: true - checksum: 10c0/c0e160b82e8032de988be2d6b0d5d3f01b017e7aa46cb4099277393e74dc0fa537cfcfc92578997f0c2dc455c9ce33453c41bb7f09d527df9fe9fd1de63247e8 + checksum: 10c0/710675143cfb534258dd76efd7cba5007dad8cce021f086b817eb08fbfbeada52b0428636d8f67cb9765ea183a7da9979c497b34ba747925f3813499b12b9347 languageName: node linkType: hard -"@storybook/addon-measure@npm:8.1.3": - version: 8.1.3 - resolution: "@storybook/addon-measure@npm:8.1.3" +"@storybook/addon-measure@npm:8.1.4": + version: 8.1.4 + resolution: "@storybook/addon-measure@npm:8.1.4" dependencies: "@storybook/global": "npm:^5.0.0" tiny-invariant: "npm:^1.3.1" - checksum: 10c0/88c936485e36fbbcdc932b09e73133b232f0b9c96a83ca0f69dbcd92db6949ee58cf8f63e114bd12582ca7f60712ec932efad517d37c4aca209d547a1ba6be59 + checksum: 10c0/4cb3f18d94d567337e7a37f5d7565f2113cb7c36f3790748cf152492b64e8c3825b7b2e3f9c86b384ed8969a34e74196ad5b1f3433be2a77734821f47399d7d1 languageName: node linkType: hard -"@storybook/addon-outline@npm:8.1.3": - version: 8.1.3 - resolution: "@storybook/addon-outline@npm:8.1.3" +"@storybook/addon-outline@npm:8.1.4": + version: 8.1.4 + resolution: "@storybook/addon-outline@npm:8.1.4" dependencies: "@storybook/global": "npm:^5.0.0" ts-dedent: "npm:^2.0.0" - checksum: 10c0/ab2e83b0a785df6b79e482b28a942d27da19fc48f5f55e47f46d2fe5adc1a021e09d77eb1e5f89cbbb605b1128876cd0725ebd55439a41538d15dc033585b61f + checksum: 10c0/c0fa6634f05dcfb8bc52ce3626f7e0f99aaacacab23e43d44ddd4e08129e2ea67bbbe390a6ab3148cd338edd483cf94d65ea79f96cb88dd61bf897c32d0f3ef0 languageName: node linkType: hard -"@storybook/addon-toolbars@npm:8.1.3": - version: 8.1.3 - resolution: "@storybook/addon-toolbars@npm:8.1.3" - checksum: 10c0/c5f515b0d54b47bded1cfbb608a263998fcdb77d5e12790c15bb6f2403530ea08bd5b48e516b5562dbfca3a758d6fc383901d47e524a7090ccacb78b51a3e3ee +"@storybook/addon-toolbars@npm:8.1.4": + version: 8.1.4 + resolution: "@storybook/addon-toolbars@npm:8.1.4" + checksum: 10c0/cf6fd8a4de64100e0779fdc5e6754bf0ed750108d9205041dcf22b79b7ebd2e8ccec6c089656655375d55ab81d694a644dbc42cb2875fa4023dd502c965c60b7 languageName: node linkType: hard -"@storybook/addon-viewport@npm:8.1.3": - version: 8.1.3 - resolution: "@storybook/addon-viewport@npm:8.1.3" +"@storybook/addon-viewport@npm:8.1.4": + version: 8.1.4 + resolution: "@storybook/addon-viewport@npm:8.1.4" dependencies: memoizerific: "npm:^1.11.3" - checksum: 10c0/1f24b7c02617fdf04c5fc5be4f95e88c885ec0384fe6723bfd815bb4382e28f95d3d847d216fd44d3a4ae7c53987584f68dceeb3b747f865a79fdd88f4eb401f + checksum: 10c0/5344b6e447da4cbd4c75cd08f06abeec3215269624e1f86ebb81eda8466ec665d307fca78412649bc3d389678d3de755aa1c3ce68d466aab15a0200493c86fee languageName: node linkType: hard -"@storybook/blocks@npm:8.1.3, @storybook/blocks@npm:^8.0.10": - version: 8.1.3 - resolution: "@storybook/blocks@npm:8.1.3" +"@storybook/blocks@npm:8.1.4, @storybook/blocks@npm:^8.1.4": + version: 8.1.4 + resolution: "@storybook/blocks@npm:8.1.4" dependencies: - "@storybook/channels": "npm:8.1.3" - "@storybook/client-logger": "npm:8.1.3" - "@storybook/components": "npm:8.1.3" - "@storybook/core-events": "npm:8.1.3" + "@storybook/channels": "npm:8.1.4" + "@storybook/client-logger": "npm:8.1.4" + "@storybook/components": "npm:8.1.4" + "@storybook/core-events": "npm:8.1.4" "@storybook/csf": "npm:^0.1.7" - "@storybook/docs-tools": "npm:8.1.3" + "@storybook/docs-tools": "npm:8.1.4" "@storybook/global": "npm:^5.0.0" "@storybook/icons": "npm:^1.2.5" - "@storybook/manager-api": "npm:8.1.3" - "@storybook/preview-api": "npm:8.1.3" - "@storybook/theming": "npm:8.1.3" - "@storybook/types": "npm:8.1.3" + "@storybook/manager-api": "npm:8.1.4" + "@storybook/preview-api": "npm:8.1.4" + "@storybook/theming": "npm:8.1.4" + "@storybook/types": "npm:8.1.4" "@types/lodash": "npm:^4.14.167" color-convert: "npm:^2.0.1" dequal: "npm:^2.0.2" @@ -2643,18 +2664,18 @@ __metadata: optional: true react-dom: optional: true - checksum: 10c0/c32e55efb94f0274013be7c7a898d84af892d9a98ead3f16f9ab04c60b15288f315a1cdcde31189081dba60b251ff16df76358fea1bb6c5e039230cf9c56847d + checksum: 10c0/01d6fcfd902782eed2ccd0310bd9cdafbe35163156a681f82c6969a171ba6f3f266c0d0333f90a1275ee2f6f1124767f89b27049a064660f85d47cb6113a7db6 languageName: node linkType: hard -"@storybook/builder-manager@npm:8.1.3": - version: 8.1.3 - resolution: "@storybook/builder-manager@npm:8.1.3" +"@storybook/builder-manager@npm:8.1.4": + version: 8.1.4 + resolution: "@storybook/builder-manager@npm:8.1.4" dependencies: "@fal-works/esbuild-plugin-global-externals": "npm:^2.1.2" - "@storybook/core-common": "npm:8.1.3" - "@storybook/manager": "npm:8.1.3" - "@storybook/node-logger": "npm:8.1.3" + "@storybook/core-common": "npm:8.1.4" + "@storybook/manager": "npm:8.1.4" + "@storybook/node-logger": "npm:8.1.4" "@types/ejs": "npm:^3.1.1" "@yarnpkg/esbuild-plugin-pnp": "npm:^3.0.0-rc.10" browser-assert: "npm:^1.2.1" @@ -2665,23 +2686,23 @@ __metadata: fs-extra: "npm:^11.1.0" process: "npm:^0.11.10" util: "npm:^0.12.4" - checksum: 10c0/2cbfdfbdbe431a029a168299c51bf62c0d82c431035dbf2ed698b560a13663378b83660722c6d08869ef328d7a6ab3bec73cb3b5f48d4bdbcc9575a350753e99 + checksum: 10c0/57c2a2b1102fc02430e3b108e225c7e6e8613d9dc8df6add356178a9a06f2be4ec7379b3a62858b1d4b833200482a1a319f265af102835a049d2424ff7e70c93 languageName: node linkType: hard -"@storybook/builder-vite@npm:8.1.3": - version: 8.1.3 - resolution: "@storybook/builder-vite@npm:8.1.3" +"@storybook/builder-vite@npm:8.1.4, @storybook/builder-vite@npm:^8.1.4": + version: 8.1.4 + resolution: "@storybook/builder-vite@npm:8.1.4" dependencies: - "@storybook/channels": "npm:8.1.3" - "@storybook/client-logger": "npm:8.1.3" - "@storybook/core-common": "npm:8.1.3" - "@storybook/core-events": "npm:8.1.3" - "@storybook/csf-plugin": "npm:8.1.3" - "@storybook/node-logger": "npm:8.1.3" - "@storybook/preview": "npm:8.1.3" - "@storybook/preview-api": "npm:8.1.3" - "@storybook/types": "npm:8.1.3" + "@storybook/channels": "npm:8.1.4" + "@storybook/client-logger": "npm:8.1.4" + "@storybook/core-common": "npm:8.1.4" + "@storybook/core-events": "npm:8.1.4" + "@storybook/csf-plugin": "npm:8.1.4" + "@storybook/node-logger": "npm:8.1.4" + "@storybook/preview": "npm:8.1.4" + "@storybook/preview-api": "npm:8.1.4" + "@storybook/types": "npm:8.1.4" "@types/find-cache-dir": "npm:^3.2.1" browser-assert: "npm:^1.2.1" es-module-lexer: "npm:^1.5.0" @@ -2702,38 +2723,38 @@ __metadata: optional: true vite-plugin-glimmerx: optional: true - checksum: 10c0/f59b795fa0685cddea330294c8d9fd652b395a132cc794d3b98d0265d6ba1691bc31df8451296b94ada088b8d4ec39f1469e12b6c0af791f006123f1ea232f06 + checksum: 10c0/610f627f6bc0a1840b1ba544466b1503943c7e02127af270128841d45347be9c57d854621103f7a411a2e7627e8e7113623a3e05f7637fe6a336791579e198a2 languageName: node linkType: hard -"@storybook/channels@npm:8.1.3": - version: 8.1.3 - resolution: "@storybook/channels@npm:8.1.3" +"@storybook/channels@npm:8.1.4": + version: 8.1.4 + resolution: "@storybook/channels@npm:8.1.4" dependencies: - "@storybook/client-logger": "npm:8.1.3" - "@storybook/core-events": "npm:8.1.3" + "@storybook/client-logger": "npm:8.1.4" + "@storybook/core-events": "npm:8.1.4" "@storybook/global": "npm:^5.0.0" telejson: "npm:^7.2.0" tiny-invariant: "npm:^1.3.1" - checksum: 10c0/10b65df4e8c6cbbdb9a9cf90766ee4003b91f6bbb8208dabe3614198a4d378b730fa7e28df4a33f0090d2dc91f0d0f9810845c3ca40affa81ae11e1041fabbcb + checksum: 10c0/2b025725f84290c83b24767f04ae5a5547773fc98b5bfd3acf20942002885552596bcb4feceb31e857751bbb88636c653858b326399f574d529897da88277522 languageName: node linkType: hard -"@storybook/cli@npm:8.1.3": - version: 8.1.3 - resolution: "@storybook/cli@npm:8.1.3" +"@storybook/cli@npm:8.1.4": + version: 8.1.4 + resolution: "@storybook/cli@npm:8.1.4" dependencies: "@babel/core": "npm:^7.24.4" "@babel/types": "npm:^7.24.0" "@ndelangen/get-tarball": "npm:^3.0.7" - "@storybook/codemod": "npm:8.1.3" - "@storybook/core-common": "npm:8.1.3" - "@storybook/core-events": "npm:8.1.3" - "@storybook/core-server": "npm:8.1.3" - "@storybook/csf-tools": "npm:8.1.3" - "@storybook/node-logger": "npm:8.1.3" - "@storybook/telemetry": "npm:8.1.3" - "@storybook/types": "npm:8.1.3" + "@storybook/codemod": "npm:8.1.4" + "@storybook/core-common": "npm:8.1.4" + "@storybook/core-events": "npm:8.1.4" + "@storybook/core-server": "npm:8.1.4" + "@storybook/csf-tools": "npm:8.1.4" + "@storybook/node-logger": "npm:8.1.4" + "@storybook/telemetry": "npm:8.1.4" + "@storybook/types": "npm:8.1.4" "@types/semver": "npm:^7.3.4" "@yarnpkg/fslib": "npm:2.10.3" "@yarnpkg/libzip": "npm:2.3.0" @@ -2762,30 +2783,30 @@ __metadata: bin: getstorybook: ./bin/index.js sb: ./bin/index.js - checksum: 10c0/610aeca5422b84f83fda658a226300d094ce2fa38f584bcf9bc317caacb1b02171cd2fb75a923b6e57e41af06859fdd689b8d4551f445556264f46ff1e5a2a9a + checksum: 10c0/67ed446e57e0f52b6cfcd9806d828239ed6af86e9c710e60c92ebc98fa8858ca48b2a6e02e01df92b557e759647d006af11df23811bd7ac5b1adedfbb6cb9030 languageName: node linkType: hard -"@storybook/client-logger@npm:8.1.3": - version: 8.1.3 - resolution: "@storybook/client-logger@npm:8.1.3" +"@storybook/client-logger@npm:8.1.4": + version: 8.1.4 + resolution: "@storybook/client-logger@npm:8.1.4" dependencies: "@storybook/global": "npm:^5.0.0" - checksum: 10c0/5d89c79d12461438da4f57972f311d19e12cdbac21ba3e3993beb6dad69be3343e0bc8a6bbd08b3ce05c7324767269ca81dd85d99695b76a5ccb4c69349eae73 + checksum: 10c0/63743aa160ac0eaabca21c5ca483ad85c0ddefc1f9987b7f323e033e0667257f328f2c3b6ef61d1fc7c4913ff227f8af4abeda7358b1250856419fa5c3007585 languageName: node linkType: hard -"@storybook/codemod@npm:8.1.3": - version: 8.1.3 - resolution: "@storybook/codemod@npm:8.1.3" +"@storybook/codemod@npm:8.1.4": + version: 8.1.4 + resolution: "@storybook/codemod@npm:8.1.4" dependencies: "@babel/core": "npm:^7.24.4" "@babel/preset-env": "npm:^7.24.4" "@babel/types": "npm:^7.24.0" "@storybook/csf": "npm:^0.1.7" - "@storybook/csf-tools": "npm:8.1.3" - "@storybook/node-logger": "npm:8.1.3" - "@storybook/types": "npm:8.1.3" + "@storybook/csf-tools": "npm:8.1.4" + "@storybook/node-logger": "npm:8.1.4" + "@storybook/types": "npm:8.1.4" "@types/cross-spawn": "npm:^6.0.2" cross-spawn: "npm:^7.0.3" globby: "npm:^14.0.1" @@ -2794,39 +2815,39 @@ __metadata: prettier: "npm:^3.1.1" recast: "npm:^0.23.5" tiny-invariant: "npm:^1.3.1" - checksum: 10c0/4d9afca04079b3484208f81658504ad065b2b2bf4b68cfadddf409c7dd04edbcb8fd822ab512e5d8d2235350e7bde5d20c15754331328c49aed5fb84e170489e + checksum: 10c0/35f207b19572a68620b257aac78ce34f3684de1d19e744302029a987b443853415a8f37f4c666f8fb9477f6b4f893d48726b708e21279a2366d9b062e0c7714d languageName: node linkType: hard -"@storybook/components@npm:8.1.3": - version: 8.1.3 - resolution: "@storybook/components@npm:8.1.3" +"@storybook/components@npm:8.1.4": + version: 8.1.4 + resolution: "@storybook/components@npm:8.1.4" dependencies: "@radix-ui/react-dialog": "npm:^1.0.5" "@radix-ui/react-slot": "npm:^1.0.2" - "@storybook/client-logger": "npm:8.1.3" + "@storybook/client-logger": "npm:8.1.4" "@storybook/csf": "npm:^0.1.7" "@storybook/global": "npm:^5.0.0" "@storybook/icons": "npm:^1.2.5" - "@storybook/theming": "npm:8.1.3" - "@storybook/types": "npm:8.1.3" + "@storybook/theming": "npm:8.1.4" + "@storybook/types": "npm:8.1.4" memoizerific: "npm:^1.11.3" util-deprecate: "npm:^1.0.2" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - checksum: 10c0/c7d579ef6b2e825d7a72ceca7f01bf608a84c084afabffeb737cd0c7731ca35bdcf8d8ab68d8ce2cd189d5d57db4695ac192c6436c394960f90a534a63590358 + checksum: 10c0/bf59b84fb8f8324e7e1a80474b6509db5e6277cb44dc45c9fec0e7d2fbf97d5ecc6d19f0bc1bf4162ae9936bc552ad7e249e62ffff800aa4ea0690b10e605ecd languageName: node linkType: hard -"@storybook/core-common@npm:8.1.3": - version: 8.1.3 - resolution: "@storybook/core-common@npm:8.1.3" +"@storybook/core-common@npm:8.1.4": + version: 8.1.4 + resolution: "@storybook/core-common@npm:8.1.4" dependencies: - "@storybook/core-events": "npm:8.1.3" - "@storybook/csf-tools": "npm:8.1.3" - "@storybook/node-logger": "npm:8.1.3" - "@storybook/types": "npm:8.1.3" + "@storybook/core-events": "npm:8.1.4" + "@storybook/csf-tools": "npm:8.1.4" + "@storybook/node-logger": "npm:8.1.4" + "@storybook/types": "npm:8.1.4" "@yarnpkg/fslib": "npm:2.10.3" "@yarnpkg/libzip": "npm:2.3.0" chalk: "npm:^4.1.0" @@ -2857,42 +2878,42 @@ __metadata: peerDependenciesMeta: prettier: optional: true - checksum: 10c0/7ed8fa4385a27b3426d5995e592a4b3701103752b037954b66417f1f15e0bf76247e1abb86ec21d98c80ec27052a03fe7f5a7712093cbd6195f9991673173537 + checksum: 10c0/5ca26dec5b07979202b3c5eb35218786746f4fc4e41f00ed8167a4e73660d428ef83d4f24f671774022202fddf65f9e61d637b8d52dbff4c82a0b50e48d09818 languageName: node linkType: hard -"@storybook/core-events@npm:8.1.3": - version: 8.1.3 - resolution: "@storybook/core-events@npm:8.1.3" +"@storybook/core-events@npm:8.1.4": + version: 8.1.4 + resolution: "@storybook/core-events@npm:8.1.4" dependencies: "@storybook/csf": "npm:^0.1.7" ts-dedent: "npm:^2.0.0" - checksum: 10c0/d88d199d48b5270bef9dd9bc7fea3f88e09c9d4237b40ecf2bc4fb90ee67f6d9af3a09f1e9bf7e241f2a6271479945f972b6fa9f2dfe99c24cb667f365571c80 + checksum: 10c0/ac4e9180299e1d425638cb0626162bb0b82ecba742e85f7c18becfa104ed774571d322c1c9c2f3e5810a4be22274ebb1fe375c00d4978e6dd677b58029843020 languageName: node linkType: hard -"@storybook/core-server@npm:8.1.3": - version: 8.1.3 - resolution: "@storybook/core-server@npm:8.1.3" +"@storybook/core-server@npm:8.1.4": + version: 8.1.4 + resolution: "@storybook/core-server@npm:8.1.4" dependencies: "@aw-web-design/x-default-browser": "npm:1.4.126" "@babel/core": "npm:^7.24.4" "@babel/parser": "npm:^7.24.4" "@discoveryjs/json-ext": "npm:^0.5.3" - "@storybook/builder-manager": "npm:8.1.3" - "@storybook/channels": "npm:8.1.3" - "@storybook/core-common": "npm:8.1.3" - "@storybook/core-events": "npm:8.1.3" + "@storybook/builder-manager": "npm:8.1.4" + "@storybook/channels": "npm:8.1.4" + "@storybook/core-common": "npm:8.1.4" + "@storybook/core-events": "npm:8.1.4" "@storybook/csf": "npm:^0.1.7" - "@storybook/csf-tools": "npm:8.1.3" + "@storybook/csf-tools": "npm:8.1.4" "@storybook/docs-mdx": "npm:3.1.0-next.0" "@storybook/global": "npm:^5.0.0" - "@storybook/manager": "npm:8.1.3" - "@storybook/manager-api": "npm:8.1.3" - "@storybook/node-logger": "npm:8.1.3" - "@storybook/preview-api": "npm:8.1.3" - "@storybook/telemetry": "npm:8.1.3" - "@storybook/types": "npm:8.1.3" + "@storybook/manager": "npm:8.1.4" + "@storybook/manager-api": "npm:8.1.4" + "@storybook/node-logger": "npm:8.1.4" + "@storybook/preview-api": "npm:8.1.4" + "@storybook/telemetry": "npm:8.1.4" + "@storybook/types": "npm:8.1.4" "@types/detect-port": "npm:^1.3.0" "@types/diff": "npm:^5.0.9" "@types/node": "npm:^18.0.0" @@ -2921,34 +2942,34 @@ __metadata: util-deprecate: "npm:^1.0.2" watchpack: "npm:^2.2.0" ws: "npm:^8.2.3" - checksum: 10c0/44b5d4c43231e0a00cde8f5640c91308525692ccf394a356dd409dd08987c103a4f49529edb110f75530869654f676a4f66ec50d85ba2c71eadf09d876313dfc + checksum: 10c0/7265968c236ef0acb744ba578b580e88127deb715de0334680b2188d9021120d40d7e797b0af8fa11a248a88eb08bc44080f4e4349e5daaf3453c1266afc5c6e languageName: node linkType: hard -"@storybook/csf-plugin@npm:8.1.3": - version: 8.1.3 - resolution: "@storybook/csf-plugin@npm:8.1.3" +"@storybook/csf-plugin@npm:8.1.4": + version: 8.1.4 + resolution: "@storybook/csf-plugin@npm:8.1.4" dependencies: - "@storybook/csf-tools": "npm:8.1.3" + "@storybook/csf-tools": "npm:8.1.4" unplugin: "npm:^1.3.1" - checksum: 10c0/aadabf17a8807ae28b569a8fd8bfeb887160ac15401590841735124244f17b5910b252a556d4f990d4d5e1e7bbe31d961c40b8db661461db0a9f15f4a2fb8323 + checksum: 10c0/f77a24fc09966af2f6aad6820c0fa9559fb652717f0890af9d36d75acfdb5ea91bd69f47a43e7d5bcc6898a4e55dbe86d2eab1eb0a54dcd53944dee8a0d77e73 languageName: node linkType: hard -"@storybook/csf-tools@npm:8.1.3": - version: 8.1.3 - resolution: "@storybook/csf-tools@npm:8.1.3" +"@storybook/csf-tools@npm:8.1.4": + version: 8.1.4 + resolution: "@storybook/csf-tools@npm:8.1.4" dependencies: "@babel/generator": "npm:^7.24.4" "@babel/parser": "npm:^7.24.4" "@babel/traverse": "npm:^7.24.1" "@babel/types": "npm:^7.24.0" "@storybook/csf": "npm:^0.1.7" - "@storybook/types": "npm:8.1.3" + "@storybook/types": "npm:8.1.4" fs-extra: "npm:^11.1.0" recast: "npm:^0.23.5" ts-dedent: "npm:^2.0.0" - checksum: 10c0/6d22e506b81256539f1284d15eeb35a55a7873812a619faac2747bb7208f97c05f4acb932c5b727b60189037996989da9d7cebe587b92a1ad82733d5625a30a1 + checksum: 10c0/0cc817fef5773bbfe229584a3fb2bbc97c97017a28c54eb1acc09eb12fbd2ae421d40afd1dce63d1a2feb38d5fe9524d6c59f5567a31f8ebc4b75cc9cd38a33c languageName: node linkType: hard @@ -2968,19 +2989,19 @@ __metadata: languageName: node linkType: hard -"@storybook/docs-tools@npm:8.1.3": - version: 8.1.3 - resolution: "@storybook/docs-tools@npm:8.1.3" +"@storybook/docs-tools@npm:8.1.4": + version: 8.1.4 + resolution: "@storybook/docs-tools@npm:8.1.4" dependencies: - "@storybook/core-common": "npm:8.1.3" - "@storybook/core-events": "npm:8.1.3" - "@storybook/preview-api": "npm:8.1.3" - "@storybook/types": "npm:8.1.3" + "@storybook/core-common": "npm:8.1.4" + "@storybook/core-events": "npm:8.1.4" + "@storybook/preview-api": "npm:8.1.4" + "@storybook/types": "npm:8.1.4" "@types/doctrine": "npm:^0.0.3" assert: "npm:^2.1.0" doctrine: "npm:^3.0.0" lodash: "npm:^4.17.21" - checksum: 10c0/4c76f9bba14acb382417c1f543a80c9b7e679693d730349ea2cabfe2412af19a29b22ecdf3a9c0169730dce83a281edb48d985833b136e59f544cad5d47157b4 + checksum: 10c0/c73e44cb0a24a49bb6d9a17b087982a717650a6756a4b16fc6a9a427463597e0c89c9edbc61a173c964dce45ad971254366247821d6a1d65ed17671809e69683 languageName: node linkType: hard @@ -3001,68 +3022,68 @@ __metadata: languageName: node linkType: hard -"@storybook/instrumenter@npm:8.1.3": - version: 8.1.3 - resolution: "@storybook/instrumenter@npm:8.1.3" +"@storybook/instrumenter@npm:8.1.4": + version: 8.1.4 + resolution: "@storybook/instrumenter@npm:8.1.4" dependencies: - "@storybook/channels": "npm:8.1.3" - "@storybook/client-logger": "npm:8.1.3" - "@storybook/core-events": "npm:8.1.3" + "@storybook/channels": "npm:8.1.4" + "@storybook/client-logger": "npm:8.1.4" + "@storybook/core-events": "npm:8.1.4" "@storybook/global": "npm:^5.0.0" - "@storybook/preview-api": "npm:8.1.3" + "@storybook/preview-api": "npm:8.1.4" "@vitest/utils": "npm:^1.3.1" util: "npm:^0.12.4" - checksum: 10c0/c16eed9434b750a38a4b5ce44c31f8377472fc1eafb6405cab2cd01c4242e87ae81250cb574b9031f35472e3551a3964501480a774f2a2626f2c49badf5aafa2 + checksum: 10c0/015a311fd73198198fa388386242568adb1c51567646d8c9ad49b41539fb318ae09cd8eba31c0aa6a857ab4ea0bcd67c61564b66e505934884c919d3e565ee73 languageName: node linkType: hard -"@storybook/manager-api@npm:8.1.3": - version: 8.1.3 - resolution: "@storybook/manager-api@npm:8.1.3" +"@storybook/manager-api@npm:8.1.4": + version: 8.1.4 + resolution: "@storybook/manager-api@npm:8.1.4" dependencies: - "@storybook/channels": "npm:8.1.3" - "@storybook/client-logger": "npm:8.1.3" - "@storybook/core-events": "npm:8.1.3" + "@storybook/channels": "npm:8.1.4" + "@storybook/client-logger": "npm:8.1.4" + "@storybook/core-events": "npm:8.1.4" "@storybook/csf": "npm:^0.1.7" "@storybook/global": "npm:^5.0.0" "@storybook/icons": "npm:^1.2.5" - "@storybook/router": "npm:8.1.3" - "@storybook/theming": "npm:8.1.3" - "@storybook/types": "npm:8.1.3" + "@storybook/router": "npm:8.1.4" + "@storybook/theming": "npm:8.1.4" + "@storybook/types": "npm:8.1.4" dequal: "npm:^2.0.2" lodash: "npm:^4.17.21" memoizerific: "npm:^1.11.3" store2: "npm:^2.14.2" telejson: "npm:^7.2.0" ts-dedent: "npm:^2.0.0" - checksum: 10c0/9a58068c7718df491afda2474bddf6791b36fd8e031454b5c2bd15fce0ee9fc24209ed6964e1df8866bf35655d8900b1a141d29ade3f4b513affe6a8372d3175 + checksum: 10c0/b12d6048d0401a2169d97485b76c9916fb10339ee7b29186ddd029a9f645c220e9c27579f50e7342260711b4bc043f5766d3745ca745983a4b264a4602fcfd4d languageName: node linkType: hard -"@storybook/manager@npm:8.1.3": - version: 8.1.3 - resolution: "@storybook/manager@npm:8.1.3" - checksum: 10c0/31f57583632b97be4c2cb351a9d44dc830a7a0fe45b6c3dfbbfd48c9f801d751ee8035a8f38cf22aa6a3c887e51400f2ba4806458b9f1d06ed48830023921861 +"@storybook/manager@npm:8.1.4": + version: 8.1.4 + resolution: "@storybook/manager@npm:8.1.4" + checksum: 10c0/14e52132f4ef29866e9fbab87a2a582b653a954d3e24e6c7b1e9021c3be67d6116270b46221b3e76c77a7b03db2e7f5d507c7e37f02741d5c3f6895187d00799 languageName: node linkType: hard -"@storybook/node-logger@npm:8.1.3": - version: 8.1.3 - resolution: "@storybook/node-logger@npm:8.1.3" - checksum: 10c0/3a485439d22be32ee0b27e37d3b556d9a1aaf4a161e17f057d820026dabbe8534a66d5b3cd91d2ce98229380fdf4439f48e77efd2bc78dab792591a0ebd6176c +"@storybook/node-logger@npm:8.1.4": + version: 8.1.4 + resolution: "@storybook/node-logger@npm:8.1.4" + checksum: 10c0/a2fc14f8c544ff48d596c36c8caf27a437cd6a255016d6c4a446003581eaf618107e1cee75922f0af28cb54df93a31e12c52b1f27f3368ba947c3796fdc15bd0 languageName: node linkType: hard -"@storybook/preview-api@npm:8.1.3": - version: 8.1.3 - resolution: "@storybook/preview-api@npm:8.1.3" +"@storybook/preview-api@npm:8.1.4": + version: 8.1.4 + resolution: "@storybook/preview-api@npm:8.1.4" dependencies: - "@storybook/channels": "npm:8.1.3" - "@storybook/client-logger": "npm:8.1.3" - "@storybook/core-events": "npm:8.1.3" + "@storybook/channels": "npm:8.1.4" + "@storybook/client-logger": "npm:8.1.4" + "@storybook/core-events": "npm:8.1.4" "@storybook/csf": "npm:^0.1.7" "@storybook/global": "npm:^5.0.0" - "@storybook/types": "npm:8.1.3" + "@storybook/types": "npm:8.1.4" "@types/qs": "npm:^6.9.5" dequal: "npm:^2.0.2" lodash: "npm:^4.17.21" @@ -3071,37 +3092,37 @@ __metadata: tiny-invariant: "npm:^1.3.1" ts-dedent: "npm:^2.0.0" util-deprecate: "npm:^1.0.2" - checksum: 10c0/a7af73da453ace0975f9f37237930f5bbbdff7e54ee9f5339b9d4a8f647047c9fa1c4c2030fecd6c6f2084625ff1c80aab649b95df08b6b35a663c9e6046a1de + checksum: 10c0/aeeffec81325affd5578dcf7e2c5d3371c4876c75af871b5216eb757d88d7c2cbbe929a3c4a83b9a0444e12f61ba14cf53c38aad29170aa1fdefcc0870524e94 languageName: node linkType: hard -"@storybook/preview@npm:8.1.3": - version: 8.1.3 - resolution: "@storybook/preview@npm:8.1.3" - checksum: 10c0/91755337cb4e483885780b941beaeeb53500fadab40ea702b8902dbb84c108ae736f3a16d0cf5fce0060d2594ca46105dd24e64be827e7826414a55363b785d8 +"@storybook/preview@npm:8.1.4": + version: 8.1.4 + resolution: "@storybook/preview@npm:8.1.4" + checksum: 10c0/e7383348c16eb030e6a8b4a929a65f2b8a4244d1dd7e55a42ce767183a941fba92f84b702660e5734de013a3aed75ff2011174f8156e54eb116c1caaa8e730f7 languageName: node linkType: hard -"@storybook/react-dom-shim@npm:8.1.3": - version: 8.1.3 - resolution: "@storybook/react-dom-shim@npm:8.1.3" +"@storybook/react-dom-shim@npm:8.1.4": + version: 8.1.4 + resolution: "@storybook/react-dom-shim@npm:8.1.4" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - checksum: 10c0/66fbab37ed2ab60361d5c25b083fa8f3c3f9482adb0f8720576bc8fa85acb2f97cbc295dbe72808afd5c9e519728873221ba8748cc4bbae014de4cfeb3fb2f5f + checksum: 10c0/00541404a966406fe8c44f696cba89111d24df2106afcedab7a069bb24504cee2387e94464eda786016c0851025d85944acd500cf70c6a893ac27c150a4bb0b1 languageName: node linkType: hard -"@storybook/react-vite@npm:^8.0.10": - version: 8.1.3 - resolution: "@storybook/react-vite@npm:8.1.3" +"@storybook/react-vite@npm:^8.1.4": + version: 8.1.4 + resolution: "@storybook/react-vite@npm:8.1.4" dependencies: "@joshwooding/vite-plugin-react-docgen-typescript": "npm:0.3.1" "@rollup/pluginutils": "npm:^5.0.2" - "@storybook/builder-vite": "npm:8.1.3" - "@storybook/node-logger": "npm:8.1.3" - "@storybook/react": "npm:8.1.3" - "@storybook/types": "npm:8.1.3" + "@storybook/builder-vite": "npm:8.1.4" + "@storybook/node-logger": "npm:8.1.4" + "@storybook/react": "npm:8.1.4" + "@storybook/types": "npm:8.1.4" find-up: "npm:^5.0.0" magic-string: "npm:^0.30.0" react-docgen: "npm:^7.0.0" @@ -3111,20 +3132,20 @@ __metadata: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta vite: ^4.0.0 || ^5.0.0 - checksum: 10c0/f59f1a6227859a4532f9f7cb85d71d188bf73bf60b98cbddda1fdd593a2bc1ffbbf68b434f2fb62bf949d3400ccd10e7d0167e3462e271ee5a47d373b6d696f8 + checksum: 10c0/fd0abbf7d61333f11a48a4b9979151d0871097ba9539e6effa7562b00be9c7f86c74a9484cea9fea0eaedd29477a7632635c589d9c82084eb7d700ae642d85e1 languageName: node linkType: hard -"@storybook/react@npm:8.1.3, @storybook/react@npm:^8.0.10": - version: 8.1.3 - resolution: "@storybook/react@npm:8.1.3" +"@storybook/react@npm:8.1.4, @storybook/react@npm:^8.1.4": + version: 8.1.4 + resolution: "@storybook/react@npm:8.1.4" dependencies: - "@storybook/client-logger": "npm:8.1.3" - "@storybook/docs-tools": "npm:8.1.3" + "@storybook/client-logger": "npm:8.1.4" + "@storybook/docs-tools": "npm:8.1.4" "@storybook/global": "npm:^5.0.0" - "@storybook/preview-api": "npm:8.1.3" - "@storybook/react-dom-shim": "npm:8.1.3" - "@storybook/types": "npm:8.1.3" + "@storybook/preview-api": "npm:8.1.4" + "@storybook/react-dom-shim": "npm:8.1.4" + "@storybook/types": "npm:8.1.4" "@types/escodegen": "npm:^0.0.6" "@types/estree": "npm:^0.0.51" "@types/node": "npm:^18.0.0" @@ -3147,61 +3168,61 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10c0/f01ce7ceca99157edbdac1d6ec306f0a5a2036bcf9a13b295333aab0e789bfa9cc8272956aef8b9a74a15de3fd2e1445d3f953604aad7a84e4bdc09a305bf913 + checksum: 10c0/9cf5554539e791935d25a3a07189863dcea7f108aee7e73556cd2a49481218f54af75cf2fdf321aaa254903b7a0b84598ebacc3ae55e2036bed82a68f982790a languageName: node linkType: hard -"@storybook/router@npm:8.1.3": - version: 8.1.3 - resolution: "@storybook/router@npm:8.1.3" +"@storybook/router@npm:8.1.4": + version: 8.1.4 + resolution: "@storybook/router@npm:8.1.4" dependencies: - "@storybook/client-logger": "npm:8.1.3" + "@storybook/client-logger": "npm:8.1.4" memoizerific: "npm:^1.11.3" qs: "npm:^6.10.0" - checksum: 10c0/f3fc0db53472923c616cdf172d079c5c0cf3e447fbc2d1dafcde2056f5ae314842b715512fede0e7b9f04a7ebea8498842688ffa3d14b177fe3f76c1f632acb5 + checksum: 10c0/8844f83382ae7c4d38712c907f1d505680366b84787e5b7496a5cda1e238ea746cd002234bbae1135a921ab8812b2b3f0c489875219073439799598e1d800be9 languageName: node linkType: hard -"@storybook/telemetry@npm:8.1.3": - version: 8.1.3 - resolution: "@storybook/telemetry@npm:8.1.3" +"@storybook/telemetry@npm:8.1.4": + version: 8.1.4 + resolution: "@storybook/telemetry@npm:8.1.4" dependencies: - "@storybook/client-logger": "npm:8.1.3" - "@storybook/core-common": "npm:8.1.3" - "@storybook/csf-tools": "npm:8.1.3" + "@storybook/client-logger": "npm:8.1.4" + "@storybook/core-common": "npm:8.1.4" + "@storybook/csf-tools": "npm:8.1.4" chalk: "npm:^4.1.0" detect-package-manager: "npm:^2.0.1" fetch-retry: "npm:^5.0.2" fs-extra: "npm:^11.1.0" read-pkg-up: "npm:^7.0.1" - checksum: 10c0/a5f9606b9d344d4ba6507b838043dcf86cbf4a7fd484ecda2a459e39ce8484ed2699c936032d36b79b6b406515e1b072144c73bdc0ed891e5d13d5ebd19fbc93 + checksum: 10c0/face946361fbedcd68188be0872ef28206b7a397a14fb1492b1bfe2ab37ea7c8f50952d92ba5823b7f35c82cca51e8720dab27f84323eb8570f6144dd2549e49 languageName: node linkType: hard -"@storybook/test@npm:8.1.3, @storybook/test@npm:^8.0.10": - version: 8.1.3 - resolution: "@storybook/test@npm:8.1.3" +"@storybook/test@npm:8.1.4, @storybook/test@npm:^8.1.4": + version: 8.1.4 + resolution: "@storybook/test@npm:8.1.4" dependencies: - "@storybook/client-logger": "npm:8.1.3" - "@storybook/core-events": "npm:8.1.3" - "@storybook/instrumenter": "npm:8.1.3" - "@storybook/preview-api": "npm:8.1.3" + "@storybook/client-logger": "npm:8.1.4" + "@storybook/core-events": "npm:8.1.4" + "@storybook/instrumenter": "npm:8.1.4" + "@storybook/preview-api": "npm:8.1.4" "@testing-library/dom": "npm:^9.3.4" "@testing-library/jest-dom": "npm:^6.4.2" "@testing-library/user-event": "npm:^14.5.2" "@vitest/expect": "npm:1.3.1" "@vitest/spy": "npm:^1.3.1" util: "npm:^0.12.4" - checksum: 10c0/3f71223456a63fc50fa3294430588375281af0e3ba3890f5156c004d1bccc9632967ab79b4810c0a84479b49a683e9a8caf66790471f13c2ef084de495aec215 + checksum: 10c0/4e5834519d12b28254818b70262e801113b6b1d8efeb1a29eeb74e2bbcb53605789d46bf1ce484c18fca285186a7ca85dbf3d21cfc40bf0a31f1079a3351dec9 languageName: node linkType: hard -"@storybook/theming@npm:8.1.3": - version: 8.1.3 - resolution: "@storybook/theming@npm:8.1.3" +"@storybook/theming@npm:8.1.4": + version: 8.1.4 + resolution: "@storybook/theming@npm:8.1.4" dependencies: "@emotion/use-insertion-effect-with-fallbacks": "npm:^1.0.1" - "@storybook/client-logger": "npm:8.1.3" + "@storybook/client-logger": "npm:8.1.4" "@storybook/global": "npm:^5.0.0" memoizerific: "npm:^1.11.3" peerDependencies: @@ -3212,18 +3233,18 @@ __metadata: optional: true react-dom: optional: true - checksum: 10c0/d85211d1e89d40fc1cf2efda01ecdc2786e3750c6a9bfb52f43a59d71d0aa107e220fd9ba5c3bbb1aa3b7e9208598dce5346acc7a595f9fc6db6c2afcd01caa3 + checksum: 10c0/a559fb441abdb6c7dee3c73a87eac1f8cbe8e8673ff935b17570169f8dda5656f550d353f4f04796a24d6262fa1873e87f7bf821b93c761ad213a944a1aea0e8 languageName: node linkType: hard -"@storybook/types@npm:8.1.3": - version: 8.1.3 - resolution: "@storybook/types@npm:8.1.3" +"@storybook/types@npm:8.1.4": + version: 8.1.4 + resolution: "@storybook/types@npm:8.1.4" dependencies: - "@storybook/channels": "npm:8.1.3" + "@storybook/channels": "npm:8.1.4" "@types/express": "npm:^4.7.0" file-system-cache: "npm:2.3.0" - checksum: 10c0/1db2ab1bf629ca3a7237eb4d55b82a0d3cc4d649b30f5c85b88d2cdef44195b1a8fa220fcc1b3024d6b0f24bfbc3e6eda104f6f8df4cde80ad2809590008729a + checksum: 10c0/2d13226158d200f9a563ce1cf09c3168a8572baf02efb82ab47d51d9958f601305fab9052e9579a42c7edef2490feca494c8562b04797594a96ff8ab8369b8fc languageName: node linkType: hard @@ -4707,9 +4728,9 @@ __metadata: languageName: node linkType: hard -"chromatic@npm:^11.3.2": - version: 11.4.0 - resolution: "chromatic@npm:11.4.0" +"chromatic@npm:^11.4.0": + version: 11.4.1 + resolution: "chromatic@npm:11.4.1" peerDependencies: "@chromatic-com/cypress": ^0.*.* || ^1.0.0 "@chromatic-com/playwright": ^0.*.* || ^1.0.0 @@ -4722,7 +4743,7 @@ __metadata: chroma: dist/bin.js chromatic: dist/bin.js chromatic-cli: dist/bin.js - checksum: 10c0/97ef0cfcc267f7da9946a8069345c31b8491fab329186f78cf5fce6b6d811d2105d1285c7b0b6ba76c2255682b720527f14070550d9c8cd17be26d00c3bf4ea3 + checksum: 10c0/50a913706145aaf1eb4e1f2d3f9f00c5d06b61fe3775c5b69099d24192bbde4fa14d53c48fbc90bc070811974fa524e799ca272b7cf0c7ecd1532df56e5453b5 languageName: node linkType: hard @@ -5575,6 +5596,17 @@ __metadata: languageName: node linkType: hard +"dnd-core@npm:^16.0.1": + version: 16.0.1 + resolution: "dnd-core@npm:16.0.1" + dependencies: + "@react-dnd/asap": "npm:^5.0.1" + "@react-dnd/invariant": "npm:^4.0.1" + redux: "npm:^4.2.0" + checksum: 10c0/6b852c576c88b0a42e618efb37e046334f5e9914b8d38ad139933dd9595b6caf2a484953a6301094d23119c17479549553d71e92fd77fa37318122ea1e579f65 + languageName: node + linkType: hard + "doctrine@npm:^3.0.0": version: 3.0.0 resolution: "doctrine@npm:3.0.0" @@ -6854,6 +6886,15 @@ __metadata: languageName: node linkType: hard +"hoist-non-react-statics@npm:^3.3.2": + version: 3.3.2 + resolution: "hoist-non-react-statics@npm:3.3.2" + dependencies: + react-is: "npm:^16.7.0" + checksum: 10c0/fe0889169e845d738b59b64badf5e55fa3cf20454f9203d1eb088df322d49d4318df774828e789898dcb280e8a5521bb59b3203385662ca5e9218a6ca5820e74 + languageName: node + linkType: hard + "hosted-git-info@npm:^2.1.4": version: 2.8.9 resolution: "hosted-git-info@npm:2.8.9" @@ -9754,6 +9795,40 @@ __metadata: languageName: node linkType: hard +"react-dnd-html5-backend@npm:^16.0.1": + version: 16.0.1 + resolution: "react-dnd-html5-backend@npm:16.0.1" + dependencies: + dnd-core: "npm:^16.0.1" + checksum: 10c0/6e4b632a11e20211d71f5f3bedadf13ecec2fa73372fde388619838294b1375f15b717d1ce128e12c872ff7b15c32d26761d2026b33c14fc55e4fd5477c15289 + languageName: node + linkType: hard + +"react-dnd@npm:^16.0.1": + version: 16.0.1 + resolution: "react-dnd@npm:16.0.1" + dependencies: + "@react-dnd/invariant": "npm:^4.0.1" + "@react-dnd/shallowequal": "npm:^4.0.1" + dnd-core: "npm:^16.0.1" + fast-deep-equal: "npm:^3.1.3" + hoist-non-react-statics: "npm:^3.3.2" + peerDependencies: + "@types/hoist-non-react-statics": ">= 3.3.1" + "@types/node": ">= 12" + "@types/react": ">= 16" + react: ">= 16.14" + peerDependenciesMeta: + "@types/hoist-non-react-statics": + optional: true + "@types/node": + optional: true + "@types/react": + optional: true + checksum: 10c0/d069435750f0d6653cfa2b951cac8abb3583fb144ff134a20176608877d9c5964c63384ebbacaa0fdeef819b592a103de0d8e06f3b742311d64a029ffed0baa3 + languageName: node + linkType: hard + "react-docgen-typescript@npm:^2.2.2": version: 2.2.2 resolution: "react-docgen-typescript@npm:2.2.2" @@ -9814,7 +9889,7 @@ __metadata: languageName: node linkType: hard -"react-is@npm:^16.13.1": +"react-is@npm:^16.13.1, react-is@npm:^16.7.0": version: 16.13.1 resolution: "react-is@npm:16.13.1" checksum: 10c0/33977da7a5f1a287936a0c85639fec6ca74f4f15ef1e59a6bc20338fc73dc69555381e211f7a3529b8150a1f71e4225525b41b60b52965bda53ce7d47377ada1 @@ -10005,6 +10080,15 @@ __metadata: languageName: node linkType: hard +"redux@npm:^4.2.0": + version: 4.2.1 + resolution: "redux@npm:4.2.1" + dependencies: + "@babel/runtime": "npm:^7.9.2" + checksum: 10c0/136d98b3d5dbed1cd6279c8c18a6a74c416db98b8a432a46836bdd668475de6279a2d4fd9d1363f63904e00f0678a8a3e7fa532c897163340baf1e71bb42c742 + languageName: node + linkType: hard + "regenerate-unicode-properties@npm:^10.1.0": version: 10.1.1 resolution: "regenerate-unicode-properties@npm:10.1.1" @@ -10694,15 +10778,15 @@ __metadata: languageName: node linkType: hard -"storybook@npm:^8.0.10": - version: 8.1.3 - resolution: "storybook@npm:8.1.3" +"storybook@npm:^8.1.4": + version: 8.1.4 + resolution: "storybook@npm:8.1.4" dependencies: - "@storybook/cli": "npm:8.1.3" + "@storybook/cli": "npm:8.1.4" bin: sb: ./index.js storybook: ./index.js - checksum: 10c0/85ef7f0e4574211ff921b8a18c4fb92abca4fa876e379f20ef4cd5ad14a0b9c4173d56c55013838b538cfab8f0d5201871326851c5698444545b8bebc3574a82 + checksum: 10c0/ef1f645dd71afc857ed169edca26b6d0e90ff9998b36cc751108eba7ed0d2f426885d3fb929e96057d9116b61c0b9892bcb86bca279e99749cca8c1be5c75651 languageName: node linkType: hard @@ -10969,18 +11053,19 @@ __metadata: version: 0.0.0-use.local resolution: "thenextwave@workspace:." dependencies: - "@chromatic-com/storybook": "npm:^1.3.3" + "@chromatic-com/storybook": "npm:^1.5.0" "@eslint/js": "npm:^9.2.0" "@monaco-editor/loader": "npm:^1.4.0" "@monaco-editor/react": "npm:^4.6.0" "@observablehq/plot": "npm:^0.6.14" - "@storybook/addon-essentials": "npm:^8.0.10" - "@storybook/addon-interactions": "npm:^8.0.10" - "@storybook/addon-links": "npm:^8.0.10" - "@storybook/blocks": "npm:^8.0.10" - "@storybook/react": "npm:^8.0.10" - "@storybook/react-vite": "npm:^8.0.10" - "@storybook/test": "npm:^8.0.10" + "@storybook/addon-essentials": "npm:^8.1.4" + "@storybook/addon-interactions": "npm:^8.1.4" + "@storybook/addon-links": "npm:^8.1.4" + "@storybook/blocks": "npm:^8.1.4" + "@storybook/builder-vite": "npm:^8.1.4" + "@storybook/react": "npm:^8.1.4" + "@storybook/react-vite": "npm:^8.1.4" + "@storybook/test": "npm:^8.1.4" "@tanstack/react-table": "npm:^8.17.3" "@types/node": "npm:^20.12.12" "@types/react": "npm:^18.3.2" @@ -11002,11 +11087,13 @@ __metadata: prettier-plugin-jsdoc: "npm:^1.3.0" prettier-plugin-organize-imports: "npm:^3.2.4" react: "npm:^18.3.1" + react-dnd: "npm:^16.0.1" + react-dnd-html5-backend: "npm:^16.0.1" react-dom: "npm:^18.3.1" react-markdown: "npm:^9.0.1" remark-gfm: "npm:^4.0.0" rxjs: "npm:^7.8.1" - storybook: "npm:^8.0.10" + storybook: "npm:^8.1.4" ts-node: "npm:^10.9.2" tslib: "npm:^2.6.2" typescript: "npm:^5.4.5"