diff --git a/src/assets/add-state.svg b/src/assets/add-state.svg new file mode 100644 index 0000000..b8129b1 --- /dev/null +++ b/src/assets/add-state.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/add-transition.svg b/src/assets/add-transition.svg new file mode 100644 index 0000000..13ecd04 --- /dev/null +++ b/src/assets/add-transition.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/delete.svg b/src/assets/delete.svg new file mode 100644 index 0000000..7d8e030 --- /dev/null +++ b/src/assets/delete.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/dropdown-down-arrow-icon.svg b/src/assets/dropdown-down-arrow-icon.svg new file mode 100644 index 0000000..f825c62 --- /dev/null +++ b/src/assets/dropdown-down-arrow-icon.svg @@ -0,0 +1,16 @@ + + + 259A5ED8-21C3-470F-A7B8-71B40BEE1D8B + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/dropdown-up-arrow-icon.svg b/src/assets/dropdown-up-arrow-icon.svg new file mode 100644 index 0000000..bce0fdb --- /dev/null +++ b/src/assets/dropdown-up-arrow-icon.svg @@ -0,0 +1,16 @@ + + + 1D35DC97-62D9-43A0-AE02-E8BBB25D8EBE + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/fit-view.svg b/src/assets/fit-view.svg new file mode 100644 index 0000000..25a1c8a --- /dev/null +++ b/src/assets/fit-view.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/home.svg b/src/assets/home.svg new file mode 100644 index 0000000..35b5107 --- /dev/null +++ b/src/assets/home.svg @@ -0,0 +1,20 @@ + + + 7A5EE42E-DAC5-4ED3-8280-0C0B118B96CA + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/pause.svg b/src/assets/pause.svg new file mode 100644 index 0000000..8f33890 --- /dev/null +++ b/src/assets/pause.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/assets/play.svg b/src/assets/play.svg new file mode 100644 index 0000000..9db7303 --- /dev/null +++ b/src/assets/play.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/assets/plus.svg b/src/assets/plus.svg new file mode 100644 index 0000000..92067cc --- /dev/null +++ b/src/assets/plus.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/recenter.svg b/src/assets/recenter.svg new file mode 100644 index 0000000..dc5106f --- /dev/null +++ b/src/assets/recenter.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/reset.svg b/src/assets/reset.svg new file mode 100644 index 0000000..e570d0e --- /dev/null +++ b/src/assets/reset.svg @@ -0,0 +1,20 @@ + + + 01E0CD2E-DB5B-4479-A8A9-A455633C30D9 + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/select.svg b/src/assets/select.svg new file mode 100644 index 0000000..e53b9ba --- /dev/null +++ b/src/assets/select.svg @@ -0,0 +1,20 @@ + + + 04F55683-D1CF-4755-827A-53A232744239 + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/step.svg b/src/assets/step.svg new file mode 100644 index 0000000..07a0c13 --- /dev/null +++ b/src/assets/step.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/assets/text.svg b/src/assets/text.svg new file mode 100644 index 0000000..2f5e358 --- /dev/null +++ b/src/assets/text.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/components/app.scss b/src/components/app.scss index 1310b28..e241817 100644 --- a/src/components/app.scss +++ b/src/components/app.scss @@ -87,6 +87,25 @@ flex: 1; flex-grow: 0; gap: 10px; + background-color: #dddddd7f; + border-radius: 3px; + padding: 5px; + + button { + width: fit-content; + padding: 3px 5px; + border-radius: 3px; + border: solid 1px #177991; + background-color: #fff; + font-weight: 500; + display: flex; + align-items: center; + gap: 3px; + + &:hover { + background-color: rgba(114, 191, 202, 0.12); + } + } .generate { display: flex; @@ -102,15 +121,22 @@ label { display: block; white-space: nowrap; + font-weight: bold; } select { padding: 2px; - width: 125px; + width: 100%; + border-radius: 1.5px; + border: solid 1px #177991; + background-color: #fff; } input { padding: 2px; width: 55px; + border-radius: 1.5px; + border: solid 1px #177991; + background-color: #fff; } } @@ -123,10 +149,6 @@ .buttons { display: flex; gap: 5px; - - button { - width: fit-content; - } } } @@ -134,14 +156,15 @@ display: flex; flex-direction: column; flex: 1; - gap: 10px; + gap: 5px; .output { display: flex; flex-direction: column; // gap: 10px; flex: 1; - border: 1px solid #000; + border: 1px solid #979797; + background-color: white; position: relative; .inner-output { @@ -163,6 +186,19 @@ &.expanded { flex-direction: column; + + .firstItem { + display: flex; + flex-direction: row; + + .collapse { + flex-grow: 1; + text-align: right; + width: 10px; + height: 10px; + margin-top: -2px; + } + } } &.collapsed { flex-direction: row; @@ -170,6 +206,9 @@ .expand { flex-grow: 1; text-align: right; + width: 10px; + height: 10px; + margin-top: -2px; } } } @@ -181,7 +220,9 @@ } .buttons { - + button { + padding: 8px; + } } } } diff --git a/src/components/app.tsx b/src/components/app.tsx index 55462e5..ceab954 100644 --- a/src/components/app.tsx +++ b/src/components/app.tsx @@ -3,10 +3,16 @@ import { clsx } from "clsx"; import { useCODAP } from "../hooks/use-codap"; import { useGraph } from "../hooks/use-graph"; -import { Graph } from "./graph"; import { useGenerator } from "../hooks/use-generator"; import { Edge, Node } from "../type"; import { Drawing } from "./drawing"; +import { Dataset } from "./dataset"; + +import StepIcon from "../assets/step.svg"; +import PlayIcon from "../assets/play.svg"; +import PauseIcon from "../assets/pause.svg"; +import DropdownUpArrowIcon from "../assets/dropdown-up-arrow-icon.svg"; +import DropdownDownArrowIcon from "../assets/dropdown-down-arrow-icon.svg"; import "./app.scss"; @@ -34,7 +40,10 @@ const SequenceOutputHeader = ({ group }: { group: SequenceGroup }) => { if (expanded) { return (
-
Starting State: {startingState}
+
+
Starting State: {startingState}
+ +
Max Length: {lengthLimit}
Delimiter: {delimiter}
@@ -48,7 +57,7 @@ const SequenceOutputHeader = ({ group }: { group: SequenceGroup }) => { {lengthLimit} / {delimiter} - + ); }; @@ -266,6 +275,7 @@ export const App = () => { const uiForGenerate = () => { const disabled = graphEmpty(); const playLabel = generationMode === "playing" ? "Pause" : (generationMode === "paused" ? "Resume" : "Play"); + const PlayOrPauseIcon = generationMode === "playing" ? PauseIcon : PlayIcon; const onPlayClick = generationMode === "playing" ? handlePause : (generationMode === "paused" ? handleResume : handlePlay); @@ -303,18 +313,26 @@ export const App = () => { /> + +
+ +
TBD
+
+
@@ -427,8 +445,7 @@ export const App = () => { setSelectedNodeId={setSelectedNodeId} /> : - { highlightAllNextNodes={highlightAllNextNodes} selectedNodeId={selectedNodeId} animating={animating} - allowDragging={true && !animating} - autoArrange={true} setSelectedNodeId={setSelectedNodeId} /> } diff --git a/src/components/dataset.scss b/src/components/dataset.scss new file mode 100644 index 0000000..634d3a8 --- /dev/null +++ b/src/components/dataset.scss @@ -0,0 +1,5 @@ +.dataset { + display: flex; + flex-direction: row; + flex: 1; +} \ No newline at end of file diff --git a/src/components/dataset.tsx b/src/components/dataset.tsx new file mode 100644 index 0000000..3b8b11e --- /dev/null +++ b/src/components/dataset.tsx @@ -0,0 +1,47 @@ +import React from "react"; +import { Tool, Toolbar } from "./toolbar"; +import { Graph } from "./graph"; +import { Node, Edge, GraphData } from "../type"; + +import "./dataset.scss"; + +interface Props { + highlightNode?: Node, + highlightLoopOnNode?: Node, + highlightEdge?: Edge, + highlightAllNextNodes: boolean; + graph: GraphData; + selectedNodeId?: string; + animating: boolean; + setSelectedNodeId: (id?: string, skipToggle?: boolean) => void; +} + +const tools: Tool[] = ["select","fitView","recenter","reset","home"]; + +export const Dataset = (props: Props) => { + const {highlightNode, highlightLoopOnNode, highlightEdge, highlightAllNextNodes, + graph, setSelectedNodeId, selectedNodeId, animating} = props; + + const handleToolSelected = (tool: Tool) => { + // TBD + }; + + return ( +
+ + +
+ ); +}; diff --git a/src/components/drawing.scss b/src/components/drawing.scss index c62178f..3ce7fbf 100644 --- a/src/components/drawing.scss +++ b/src/components/drawing.scss @@ -3,29 +3,6 @@ flex-direction: row; flex: 1; - .sidebar { - display: flex; - flex-direction: column; - gap: 5px; - padding: 5px; - background-color: #ddd; - - button { - width: 30px; - height: 30px; - padding: 2px; - - &.selected { - background-color: #999; - - svg path { - stroke: white; - fill: white; - } - } - } - } - .dragIcon { position: absolute; width: 30px; diff --git a/src/components/drawing.tsx b/src/components/drawing.tsx index b47df40..956408d 100644 --- a/src/components/drawing.tsx +++ b/src/components/drawing.tsx @@ -1,5 +1,4 @@ -import React, { useCallback, useEffect, useRef, useState } from "react"; -import { clsx } from "clsx"; +import React, { useCallback, useEffect, useState } from "react"; import { nanoid } from "nanoid"; import { DrawingMode, Graph, Point, RubberBand } from "./graph"; @@ -7,14 +6,12 @@ import { Edge, GraphData, Node } from "../type"; import { DragIcon } from "./drawing/drag-icon"; import { NodeModal } from "./drawing/node-modal"; - -import SelectIcon from "../assets/select-icon.svg"; -import AddNodeIcon from "../assets/add-node-icon.svg"; -import AddEdgeIcon from "../assets/add-edge-icon.svg"; -import DeleteIcon from "../assets/delete-icon.svg"; +import { Tool, Toolbar } from "./toolbar"; import "./drawing.scss"; +const tools: Tool[] = ["select","addNode","addEdge","addText","delete","fitView","recenter","reset","home"]; +const drawingTools: Tool[] = ["select", "addNode", "addEdge", "delete"]; interface Props { highlightNode?: Node, @@ -43,12 +40,11 @@ export const Drawing = (props: Props) => { } }, [drawingMode, _setSelectedNodeId]); - const sidebarRef = useRef(null); - const translateToGraphPoint = (e: MouseEvent|React.MouseEvent): Point => { + // the offsets were determined visually to put the state centered on the mouse return { - x: e.clientX - (sidebarRef?.current?.clientWidth || 0), - y: e.clientY - (sidebarRef?.current?.clientTop || 0) + x: e.clientX - 50, + y: e.clientY - 12, }; }; @@ -82,24 +78,25 @@ export const Drawing = (props: Props) => { return () => window.removeEventListener("keydown", listenForEscape); }, [drawingMode, setDrawingMode, clearSelections]); + const handleToolSelected = (tool: Tool) => { + if (drawingTools.includes(tool)) { + setDrawingMode(tool as DrawingMode); + clearSelections(); + } + }; + + /* + + keep for now until ok is given to delete + const handleSetSelectMode = useCallback(() => { setDrawingMode("select"); clearSelections(); }, [setDrawingMode, clearSelections]); - const handleSetAddNodeMode = useCallback(() => { - setDrawingMode("addNode"); - clearSelections(); - }, [setDrawingMode, clearSelections]); - const handleSetAddEdgeMode = useCallback(() => { - setDrawingMode("addEdge"); - clearSelections(); - }, [setDrawingMode, clearSelections]); - const handleSetDeleteMode = useCallback(() => { - setDrawingMode("delete"); - clearSelections(); - }, [setDrawingMode, clearSelections]); + */ const addNode = useCallback(({x, y}: {x: number, y: number}) => { + console.log("ADD NODE"); setGraph(prev => { const id = nanoid(); const label = `State ${prev.nodes.length + 1}`; @@ -127,26 +124,18 @@ export const Drawing = (props: Props) => { }, [setGraph]); const handleClicked = useCallback((e: React.MouseEvent) => { + console.log("HANDLE CLICKED"); if (drawingMode === "addNode") { addNode(translateToGraphPoint(e)); - handleSetSelectMode(); + // handleSetSelectMode(); } else if (drawingMode === "addEdge") { const onSVGBackground = ((e.target as HTMLElement)?.tagName || "").toLowerCase() === "svg"; if (onSVGBackground) { clearSelections(); - handleSetSelectMode(); + // handleSetSelectMode(); } } - }, [drawingMode, addNode, handleSetSelectMode, clearSelections]); - - // allow nodes to be "dragged" from the toolbar to the canvas - const handleMouseUp = useCallback((e: React.MouseEvent) => { - if (drawingMode === "addNode") { - handleClicked(e); - e.preventDefault(); - e.stopPropagation(); - } - }, [drawingMode, handleClicked]); + }, [drawingMode, addNode/*, handleSetSelectMode */, clearSelections]); const handleNodeClicked = useCallback((id: string, onLoop?: boolean) => { const node = getNode(id); @@ -159,7 +148,9 @@ export const Drawing = (props: Props) => { setFirstEdgeNode(node); } else { addEdge({from: firstEdgeNode.id, to: node.id}); - handleSetSelectMode(); + setFirstEdgeNode(undefined); + setRubberBand(undefined); + // handleSetSelectMode(); } } @@ -188,9 +179,9 @@ export const Drawing = (props: Props) => { edges }; }); - handleSetSelectMode(); + // handleSetSelectMode(); } - }, [addEdge, drawingMode, getNode, firstEdgeNode, setFirstEdgeNode, setGraph, handleSetSelectMode]); + }, [addEdge, drawingMode, getNode, firstEdgeNode, setFirstEdgeNode, setGraph/*, handleSetSelectMode */]); const handleNodeDoubleClicked = useCallback((id: string) => { if (drawingMode === "select") { @@ -198,9 +189,11 @@ export const Drawing = (props: Props) => { } if (drawingMode === "addEdge") { addEdge({from: id, to: id}); - handleSetSelectMode(); + setFirstEdgeNode(undefined); + setRubberBand(undefined); + // handleSetSelectMode(); } - }, [drawingMode, addEdge, handleSetSelectMode, getNode]); + }, [drawingMode, addEdge/*, handleSetSelectMode */, getNode]); const handleEdgeClicked = useCallback(({from, to}: {from: string, to: string}) => { if (drawingMode === "delete") { @@ -214,9 +207,9 @@ export const Drawing = (props: Props) => { return prev; } }); - handleSetSelectMode(); + // handleSetSelectMode(); } - }, [setGraph, drawingMode, handleSetSelectMode]); + }, [setGraph, drawingMode/*, handleSetSelectMode */]); const handleDragStop = useCallback((id: string, {x, y}: Point) => { setGraph(prev => { @@ -251,36 +244,7 @@ export const Drawing = (props: Props) => { return (
-
- - - - -
+ { selectedNodeId={selectedNodeId} animating={animating} onClick={handleClicked} - onMouseUp={handleMouseUp} onNodeClick={handleNodeClicked} onNodeDoubleClick={handleNodeDoubleClicked} onEdgeClick={handleEdgeClicked} diff --git a/src/components/graph.tsx b/src/components/graph.tsx index 8b01366..4ef256e 100644 --- a/src/components/graph.tsx +++ b/src/components/graph.tsx @@ -54,7 +54,6 @@ type Props = { selectedNodeId?: string; animating: boolean; onClick?: (e: React.MouseEvent) => void; - onMouseUp?: (e: React.MouseEvent) => void; onNodeClick?: (id: string, onLoop?: boolean) => void; onNodeDoubleClick?: (id: string) => void; onEdgeClick?: (options: {from: string, to: string}) => void; @@ -236,7 +235,7 @@ const lineDashArray = (edge: D3Edge) => edge.value ? "" : "4"; export const Graph = (props: Props) => { const {graph, highlightNode, highlightLoopOnNode, highlightEdge, highlightAllNextNodes, allowDragging, autoArrange, mode, rubberBand, drawingMode, - onClick, onMouseUp, onNodeClick, onNodeDoubleClick, onEdgeClick, onDragStop, + onClick, onNodeClick, onNodeDoubleClick, onEdgeClick, onDragStop, selectedNodeId, setSelectedNodeId, animating} = props; const svgRef = useRef(null); const wrapperRef = useRef(null); @@ -760,16 +759,10 @@ export const Graph = (props: Props) => { } }, [autoArrange, onClick]); - const handleMouseUp = useCallback((e: React.MouseEvent) => { - if (!autoArrange && onMouseUp) { - onMouseUp(e); - } - }, [autoArrange, onMouseUp]); - const viewBox = mode === "drawing" ? `0 0 ${width} ${height}` : `${-width / 2} ${-height / 2} ${width} ${height}`; return ( -
+
= { + select: "Select", + addNode: "Add State", + addEdge: "Add Transition", + addText: "Create From Text", + delete: "Delete", + fitView: "Fit View", + recenter: "Recenter View", + reset: "Reset", + home: "Back to Main", +}; + +const toolIcons: Record = { + select: SelectIcon, + addNode: AddStateIcon, + addEdge: AddTransitionIcon, + addText: TextIcon, + delete: DeleteIcon, + fitView: FitViewIcon, + recenter: RecenterIcon, + reset: ResetIcon, + home: HomeIcon, +}; + +interface ToolbarButtonProps { + tool: Tool + selectedTool: Tool; + onClick: (tool: Tool) => void; +} + +interface ToolbarProps { + tools: Tool[] + onToolSelected: (tool: Tool) => void; +} + +export const ToolbarButton = ({tool, selectedTool, onClick}: ToolbarButtonProps) => { + const handleClick = () => onClick(tool); + const selected = toggleableTools.includes(tool) && tool === selectedTool; + const notImplemented = notImplementedTools.includes(tool); + const title = `${toolTitles[tool]}${notImplemented ? " (NOT YET IMPLEMENTED)" : ""}`; + const ToolIcon = toolIcons[tool]; + + return ( + + ); +}; + +export const Toolbar = ({tools, onToolSelected}: ToolbarProps) => { + const [selectedTool, setSelectedTool] = useState("select"); + + const handleToolSelected = (tool: Tool) => { + if (toggleableTools.includes(tool)) { + setSelectedTool(tool); + } + onToolSelected(tool); + }; + + const topTools = tools.filter(tool => !nonTopTools.includes(tool)); + const bottomTools = tools.filter(tool => nonTopTools.includes(tool)); + + return ( +
+
+ {topTools.map(tool => ( + ) + )} +
+
+ {bottomTools.map(tool => ( + ) + )} +
+
+ ); +};