diff --git a/.changeset/foo-bar.md b/.changeset/foo-bar.md new file mode 100644 index 000000000..aac0e7355 --- /dev/null +++ b/.changeset/foo-bar.md @@ -0,0 +1,5 @@ +--- +"@tokens-studio/graph-editor": major +--- + +Editor now accepts a `nodeLoader` which handles loading nodes instead of a static lookup diff --git a/.changeset/giant-steaks-jump.md b/.changeset/giant-steaks-jump.md new file mode 100644 index 000000000..39fecd6e2 --- /dev/null +++ b/.changeset/giant-steaks-jump.md @@ -0,0 +1,6 @@ +--- +"@tokens-studio/graph-editor": major +--- + +Removed `initialGraph` from the editor. This can now be passed through with the Frame.graph option instead + diff --git a/.changeset/light-avocados-hammer.md b/.changeset/light-avocados-hammer.md new file mode 100644 index 000000000..f7c33e073 --- /dev/null +++ b/.changeset/light-avocados-hammer.md @@ -0,0 +1,5 @@ +--- +"@tokens-studio/graph-engine": major +--- + +The engine now allows for async node loading during deserialization using a nodeLoader instead of a static lookup diff --git a/.changeset/wild-cars-kiss.md b/.changeset/wild-cars-kiss.md new file mode 100644 index 000000000..529b3c565 --- /dev/null +++ b/.changeset/wild-cars-kiss.md @@ -0,0 +1,16 @@ +--- +"@tokens-studio/graph-editor": major +--- + +Removes the External Loader from the editor as this will likely be scoped per Frame + +You should instead attach your external loader directly to the graph you created when creating a frame + +```ts +const graph = new Graph(); + +graph.externaloLoader = myExternalLoader; + +const Frame = new Frame({ graph , ...etc}) + +``` diff --git a/packages/graph-editor/package.json b/packages/graph-editor/package.json index 4779ca9de..b03dcdf7a 100644 --- a/packages/graph-editor/package.json +++ b/packages/graph-editor/package.json @@ -34,6 +34,7 @@ "format": "npm run format:eslint && npm run format:prettier", "format:eslint": "eslint . --fix", "format:prettier": "prettier \"**/*.{tsx,ts,js,md,json}\" --write", + "test": "vitest run", "test:e2e": "cypress run", "test:e2e:manual": "cypress open", "lint": "npm run lint:prettier && npm run lint:eslint", @@ -121,6 +122,7 @@ "cypress": "^13.9.0", "cypress-react-selector": "^3.0.0", "glob": "^11.0.0", + "happy-dom": "^16.3.0", "postcss": "^8.4.47", "postcss-cli": "^11.0.0", "postcss-css-variables": "^0.19.0", diff --git a/packages/graph-editor/readme.md b/packages/graph-editor/readme.md index 274985faa..41dd56bac 100644 --- a/packages/graph-editor/readme.md +++ b/packages/graph-editor/readme.md @@ -83,3 +83,7 @@ In the example above, we use the useRef hook to create a reference to the Editor ## Development We need to force the cypress react selector to use a version of `resq` that supports 18.2.0. This is done through resolutions and by manually adding it as a dev dependency + +## Notes + +This project uses [CSS modules](https://github.com/css-modules/css-modules) to handle encapsulating css. We distribute this in the dist with imports to `*.module.css` files still remaining without transformation to the css files. We assume that the implementing system will handle this transformation and export of the mangled css classnames if needed. diff --git a/packages/graph-editor/src/components/commandPalette/index.tsx b/packages/graph-editor/src/components/commandPalette/index.tsx index d44657282..fa629029d 100644 --- a/packages/graph-editor/src/components/commandPalette/index.tsx +++ b/packages/graph-editor/src/components/commandPalette/index.tsx @@ -13,18 +13,19 @@ import { observer } from 'mobx-react-lite'; import { showNodesCmdPaletteSelector } from '@/redux/selectors/ui.js'; import { useDispatch, useSelector } from 'react-redux'; import { useSelectAddedNodes } from '@/hooks/useSelectAddedNodes.js'; -import React from 'react'; +import React, { useCallback } from 'react'; import Search from '@tokens-studio/icons/Search.js'; import styles from './index.module.css'; export interface ICommandMenu { items: DropPanelStore; - handleSelectNewNodeType: (node: NodeRequest) => + handleSelectNewNodeType: (node: NodeRequest) => Promise< | { graphNode: Node; flowNode: ReactFlowNode; } - | undefined; + | undefined + >; } const CommandItem = observer( @@ -80,7 +81,10 @@ const CommandMenuGroup = observer( }, ); -const CommandMenu = ({ items, handleSelectNewNodeType }: ICommandMenu) => { +export const CommandMenu = ({ + items, + handleSelectNewNodeType, +}: ICommandMenu) => { const showNodesCmdPalette = useSelector(showNodesCmdPaletteSelector); const dispatch = useDispatch(); const cursorPositionRef = React.useRef<{ x: number; y: number }>({ @@ -91,8 +95,8 @@ const CommandMenu = ({ items, handleSelectNewNodeType }: ICommandMenu) => { const reactflow = useReactFlow(); const selectAddedNodes = useSelectAddedNodes(); - const handleSelectItem = (item) => { - const newNode = handleSelectNewNodeType({ + const handleSelectItem = async (item) => { + const newNode = await handleSelectNewNodeType({ position: reactflow.screenToFlowPosition(cursorPositionRef.current), ...item, }); @@ -132,13 +136,16 @@ const CommandMenu = ({ items, handleSelectNewNodeType }: ICommandMenu) => { }, [dispatch.ui, showNodesCmdPalette]); // Close the menu when Escape key is pressed inside the input - const handleKeyDown = (e) => { - if (e.key === 'Escape') { - e.preventDefault(); + const handleKeyDown = useCallback( + (e) => { + if (e.key === 'Escape') { + e.preventDefault(); - dispatch.ui.setShowNodesCmdPalette(false); - } - }; + dispatch.ui.setShowNodesCmdPalette(false); + } + }, + [dispatch.ui], + ); return ( ); } - -export { CommandMenu }; diff --git a/packages/graph-editor/src/components/contextMenus/paneContextMenu.tsx b/packages/graph-editor/src/components/contextMenus/paneContextMenu.tsx index b64d011a8..f828b14f5 100644 --- a/packages/graph-editor/src/components/contextMenus/paneContextMenu.tsx +++ b/packages/graph-editor/src/components/contextMenus/paneContextMenu.tsx @@ -1,13 +1,12 @@ import { ContextMenuItem } from './ContextMenuStyles.js'; import { Menu, Separator } from 'react-contexify'; import { clear } from '../../editor/actions/clear.js'; -import { showGrid, snapGrid } from '@/redux/selectors/settings.js'; import { useAction } from '@/editor/actions/provider.js'; import { useAutoLayout } from '../../editor/hooks/useAutolayout.js'; import { useDispatch } from '@/hooks/index.js'; +import { useFrame } from '@/system/frame/hook.js'; import { useLocalGraph } from '@/context/graph.js'; import { useReactFlow } from 'reactflow'; -import { useSelector } from 'react-redux'; import React, { useCallback } from 'react'; export interface IPaneContextMenu { @@ -17,8 +16,8 @@ export interface IPaneContextMenu { export const PaneContextMenu = ({ id }: IPaneContextMenu) => { const reactFlowInstance = useReactFlow(); - const showGridValue = useSelector(showGrid); - const snapGridValue = useSelector(snapGrid); + + const frame = useFrame(); const dispatch = useDispatch(); const graph = useLocalGraph(); const createNode = useAction('createNode'); @@ -51,12 +50,12 @@ export const PaneContextMenu = ({ id }: IPaneContextMenu) => { }, [reactFlowInstance]); const setShowGrid = useCallback(() => { - dispatch.settings.setShowGrid(!showGridValue); - }, [dispatch.settings, showGridValue]); + frame.settings.setShowGrid(!frame.settings.showGrid); + }, [frame.settings]); const setSnapGrid = useCallback(() => { - dispatch.settings.setSnapGrid(!snapGridValue); - }, [dispatch.settings, snapGridValue]); + frame.settings.setSnapGrid(!frame.settings.snapGrid); + }, [frame.settings]); const clearCallback = useCallback(() => { clear(reactFlowInstance, graph); @@ -70,7 +69,7 @@ export const PaneContextMenu = ({ id }: IPaneContextMenu) => { Apply Layout - {showGridValue ? 'Hide' : 'Show'} Grid + {frame.settings.showGrid ? 'Hide' : 'Show'} Grid Recenter diff --git a/packages/graph-editor/src/components/contextMenus/selectionContextMenu.tsx b/packages/graph-editor/src/components/contextMenus/selectionContextMenu.tsx index ee764bfa9..a1a09569e 100644 --- a/packages/graph-editor/src/components/contextMenus/selectionContextMenu.tsx +++ b/packages/graph-editor/src/components/contextMenus/selectionContextMenu.tsx @@ -74,7 +74,7 @@ export const SelectionContextMenu = ({ id, nodes }: INodeContextMenuProps) => { }); }, [nodes, reactFlowInstance, selectedNodeIds, store]); - const onCreateSubgraph = useCallback(() => { + const onCreateSubgraph = useCallback(async () => { //We need to work out which nodes do not have parents in the selection const lookup = new Set(selectedNodeIds); @@ -96,7 +96,7 @@ export const SelectionContextMenu = ({ id, nodes }: INodeContextMenuProps) => { y: position.y / selectedNodes.length, }; - const nodes = createNode({ + const nodes = await createNode({ type: 'studio.tokens.generic.subgraph', position: finalPosition, }); diff --git a/packages/graph-editor/src/components/controls/array.tsx b/packages/graph-editor/src/components/controls/array.tsx index 7fba5ac2c..47ad0b9d7 100644 --- a/packages/graph-editor/src/components/controls/array.tsx +++ b/packages/graph-editor/src/components/controls/array.tsx @@ -21,10 +21,8 @@ import { import { ColorPickerPopover } from '../colorPicker/index.js'; import { IField } from './interface.js'; import { JSONTree } from 'react-json-tree'; -import { delayedUpdateSelector } from '@/redux/selectors/index.js'; import { observer } from 'mobx-react-lite'; import { toJS } from 'mobx'; -import { useSelector } from 'react-redux'; import FloppyDisk from '@tokens-studio/icons/FloppyDisk.js'; import Minus from '@tokens-studio/icons/Minus.js'; import Plus from '@tokens-studio/icons/Plus.js'; @@ -40,7 +38,7 @@ const NEW_ITEM_DEFAULTS = { }, }; -export const ArrayField = observer(({ port, readOnly }: IField) => { +export const ArrayField = observer(({ port, readOnly, settings }: IField) => { const [value, setValue] = React.useState(port.value); const [selectItemsType, setSelectItemsType] = React.useState( port.type.items.$id, @@ -49,7 +47,6 @@ export const ArrayField = observer(({ port, readOnly }: IField) => { const [autofocusIndex, setAutofocusIndex] = React.useState( null, ); - const useDelayed = useSelector(delayedUpdateSelector); React.useEffect(() => { setValue(port.value); @@ -82,13 +79,13 @@ export const ArrayField = observer(({ port, readOnly }: IField) => { setValue(newValueArray); - if (useDelayed) { + if (settings.delayedUpdate) { return; } (port as Input).setValue(newValueArray); }, - [port, useDelayed, value], + [port, settings.delayedUpdate, value], ); const onColorChange = useCallback( @@ -106,12 +103,12 @@ export const ArrayField = observer(({ port, readOnly }: IField) => { setAutofocusIndex(newValueArray.length - 1); - if (useDelayed) { + if (settings.delayedUpdate) { return; } (port as Input).setValue(newValueArray); - }, [itemsType, port, useDelayed, value]); + }, [itemsType, port, settings.delayedUpdate, value]); const removeItem = useCallback( (index: number) => { @@ -121,13 +118,13 @@ export const ArrayField = observer(({ port, readOnly }: IField) => { setValue(newValueArray); setAutofocusIndex(null); - if (useDelayed) { + if (settings.delayedUpdate) { return; } (port as Input).setValue(newValueArray); }, - [port, useDelayed, value], + [port, settings.delayedUpdate, value], ); const itemList = React.useMemo(() => { @@ -255,7 +252,7 @@ export const ArrayField = observer(({ port, readOnly }: IField) => { onClick={addItem} /> - {useDelayed && ( + {settings.delayedUpdate && ( } diff --git a/packages/graph-editor/src/components/controls/color.tsx b/packages/graph-editor/src/components/controls/color.tsx index 20f4815de..4e51cc16c 100644 --- a/packages/graph-editor/src/components/controls/color.tsx +++ b/packages/graph-editor/src/components/controls/color.tsx @@ -2,15 +2,12 @@ import { ColorPickerPopover } from '../colorPicker/index.js'; import { IField } from './interface.js'; import { IconButton, Stack, Text } from '@tokens-studio/ui'; import { Input, hexToColor, toColor, toHex } from '@tokens-studio/graph-engine'; -import { delayedUpdateSelector } from '@/redux/selectors/index.js'; import { observer } from 'mobx-react-lite'; -import { useSelector } from 'react-redux'; import FloppyDisk from '@tokens-studio/icons/FloppyDisk.js'; import React, { useCallback } from 'react'; import styles from './color.module.css'; -export const ColorField = observer(({ port, readOnly }: IField) => { - const useDelayed = useSelector(delayedUpdateSelector); +export const ColorField = observer(({ port, readOnly, settings }: IField) => { const [val, setVal] = React.useState(''); React.useEffect(() => { @@ -33,14 +30,14 @@ export const ColorField = observer(({ port, readOnly }: IField) => { col = e.target.value; } setVal(col); - if (useDelayed) { + if (settings.delayedUpdate) { return; } //We need to convert from hex (port as Input).setValue(hexToColor(col)); }, - [port, useDelayed], + [port, settings.delayedUpdate], ); if (readOnly) { @@ -62,7 +59,7 @@ export const ColorField = observer(({ port, readOnly }: IField) => { {val} - {useDelayed && ( + {settings.delayedUpdate && ( } onClick={() => (port as Input).setValue(val)} diff --git a/packages/graph-editor/src/components/controls/curve.tsx b/packages/graph-editor/src/components/controls/curve.tsx index fc96a719b..b3bce2a9a 100644 --- a/packages/graph-editor/src/components/controls/curve.tsx +++ b/packages/graph-editor/src/components/controls/curve.tsx @@ -4,21 +4,23 @@ import { Input } from '@tokens-studio/graph-engine'; import { observer } from 'mobx-react-lite'; import React from 'react'; -export const CurveField = observer(({ port, readOnly }: IField) => { - const onChange = (index: number, value: number[]) => { - if (!readOnly) { - const points = [...port.value.curves[0].points]; - points[index] = value; - (port as Input).setValue({ - curves: [ - { - points, - }, - ], - }); - } - }; - return ( - - ); -}); +export const CurveField = observer( + ({ port, readOnly }: Omit) => { + const onChange = (index: number, value: number[]) => { + if (!readOnly) { + const points = [...port.value.curves[0].points]; + points[index] = value; + (port as Input).setValue({ + curves: [ + { + points, + }, + ], + }); + } + }; + return ( + + ); + }, +); diff --git a/packages/graph-editor/src/components/controls/interface.tsx b/packages/graph-editor/src/components/controls/interface.tsx index 6cb097cee..d05d7682d 100644 --- a/packages/graph-editor/src/components/controls/interface.tsx +++ b/packages/graph-editor/src/components/controls/interface.tsx @@ -1,5 +1,7 @@ import { Port } from '@tokens-studio/graph-engine'; +import { SystemSettings } from '@/system/frame/settings.js'; export interface IField { port: Port; readOnly?: boolean; + settings: SystemSettings; } diff --git a/packages/graph-editor/src/components/controls/numeric.tsx b/packages/graph-editor/src/components/controls/numeric.tsx index 6bb608bd9..a51012629 100644 --- a/packages/graph-editor/src/components/controls/numeric.tsx +++ b/packages/graph-editor/src/components/controls/numeric.tsx @@ -1,18 +1,16 @@ import { IField } from './interface.js'; import { IconButton, Stack, TextInput } from '@tokens-studio/ui'; import { Input } from '@tokens-studio/graph-engine'; -import { delayedUpdateSelector } from '@/redux/selectors/index.js'; import { observer } from 'mobx-react-lite'; -import { useSelector } from 'react-redux'; + import FloppyDisk from '@tokens-studio/icons/FloppyDisk.js'; import React, { useCallback } from 'react'; -export const NumericField = observer(({ port, readOnly }: IField) => { +export const NumericField = observer(({ port, readOnly, settings }: IField) => { const [intermediate, setIntermediate] = React.useState( undefined, ); const [hadErr, setHadErr] = React.useState(false); - const useDelayed = useSelector(delayedUpdateSelector); const [val, setVal] = React.useState(port.value); React.useEffect(() => { @@ -24,7 +22,7 @@ export const NumericField = observer(({ port, readOnly }: IField) => { if (!readOnly) { const number = Number.parseFloat(e.target.value); if (!Number.isNaN(number)) { - if (!useDelayed) { + if (!settings.delayedUpdate) { (port as Input).setValue(number); } else { setVal(number); @@ -37,7 +35,7 @@ export const NumericField = observer(({ port, readOnly }: IField) => { setIntermediate(e.target.value); } }, - [port, readOnly, useDelayed], + [readOnly, settings.delayedUpdate, port], ); return ( @@ -49,7 +47,7 @@ export const NumericField = observer(({ port, readOnly }: IField) => { onChange={onChange} disabled={readOnly} /> - {useDelayed && ( + {settings.delayedUpdate && ( } onClick={() => (port as Input).setValue(val)} diff --git a/packages/graph-editor/src/components/controls/slider.tsx b/packages/graph-editor/src/components/controls/slider.tsx index a7ee58a4d..1509f67fe 100644 --- a/packages/graph-editor/src/components/controls/slider.tsx +++ b/packages/graph-editor/src/components/controls/slider.tsx @@ -2,18 +2,16 @@ import { IField } from './interface.js'; import { IconButton, Stack, Text } from '@tokens-studio/ui'; import { Input } from '@tokens-studio/graph-engine'; import { Slider } from '../slider/index.js'; -import { delayedUpdateSelector } from '@/redux/selectors/index.js'; + import { observer } from 'mobx-react-lite'; -import { useSelector } from 'react-redux'; import FloppyDisk from '@tokens-studio/icons/FloppyDisk.js'; import React, { useCallback } from 'react'; -export const SliderField = observer(({ port, readOnly }: IField) => { +export const SliderField = observer(({ port, readOnly, settings }: IField) => { const min = port.type.minimum || 0; const max = port.type.maximum || 1; const step = port.type.multipleOf || (max - min) / 100; - const useDelayed = useSelector(delayedUpdateSelector); const [val, setVal] = React.useState(port.value); React.useEffect(() => { @@ -23,14 +21,14 @@ export const SliderField = observer(({ port, readOnly }: IField) => { const onChange = useCallback( (value: number[]) => { if (!readOnly) { - if (!useDelayed) { + if (!settings.delayedUpdate) { (port as Input).setValue(value[0]); } else { setVal(value[0]); } } }, - [port, readOnly, useDelayed], + [port, readOnly, settings.delayedUpdate], ); return ( @@ -43,7 +41,7 @@ export const SliderField = observer(({ port, readOnly }: IField) => { onValueChange={onChange} /> {val} - {useDelayed && ( + {settings.delayedUpdate && ( } onClick={() => (port as Input).setValue(val)} diff --git a/packages/graph-editor/src/components/controls/string.tsx b/packages/graph-editor/src/components/controls/string.tsx index 438b53bdc..41640f5b6 100644 --- a/packages/graph-editor/src/components/controls/string.tsx +++ b/packages/graph-editor/src/components/controls/string.tsx @@ -1,14 +1,11 @@ import { IField } from './interface.js'; import { IconButton, Stack, Text, TextInput } from '@tokens-studio/ui'; import { Input } from '@tokens-studio/graph-engine'; -import { delayedUpdateSelector } from '@/redux/selectors/index.js'; import { observer } from 'mobx-react-lite'; -import { useSelector } from 'react-redux'; import FloppyDisk from '@tokens-studio/icons/FloppyDisk.js'; import React, { useEffect } from 'react'; -export const Textfield = observer(({ port, readOnly }: IField) => { - const useDelayed = useSelector(delayedUpdateSelector); +export const Textfield = observer(({ port, readOnly, settings }: IField) => { const [val, setVal] = React.useState(port.value); useEffect(() => { @@ -18,7 +15,7 @@ export const Textfield = observer(({ port, readOnly }: IField) => { const onChange = (e: React.ChangeEvent) => { const str = e.target.value; setVal(str); - if (useDelayed) { + if (settings.delayedUpdate) { return; } @@ -32,7 +29,7 @@ export const Textfield = observer(({ port, readOnly }: IField) => { return ( - {useDelayed && ( + {settings.delayedUpdate && ( } onClick={() => (port as Input).setValue(val)} diff --git a/packages/graph-editor/src/components/controls/text.tsx b/packages/graph-editor/src/components/controls/text.tsx index 33b5ad6c8..e64df340c 100644 --- a/packages/graph-editor/src/components/controls/text.tsx +++ b/packages/graph-editor/src/components/controls/text.tsx @@ -1,14 +1,12 @@ import { IField } from './interface.js'; import { IconButton, Textarea as UITextarea } from '@tokens-studio/ui'; import { Input } from '@tokens-studio/graph-engine'; -import { delayedUpdateSelector } from '@/redux/selectors/index.js'; import { observer } from 'mobx-react-lite'; -import { useSelector } from 'react-redux'; + import FloppyDisk from '@tokens-studio/icons/FloppyDisk.js'; import React, { useEffect } from 'react'; -export const TextArea = observer(({ port, readOnly }: IField) => { - const useDelayed = useSelector(delayedUpdateSelector); +export const TextArea = observer(({ settings, port, readOnly }: IField) => { const [val, setVal] = React.useState(port.value); useEffect(() => { @@ -17,7 +15,7 @@ export const TextArea = observer(({ port, readOnly }: IField) => { const onChange = (str: string) => { setVal(str); - if (useDelayed) { + if (settings.delayedUpdate) { return; } @@ -31,7 +29,7 @@ export const TextArea = observer(({ port, readOnly }: IField) => { return ( <> - {useDelayed && ( + {settings.delayedUpdate && ( } onClick={() => (port as Input).setValue(val)} diff --git a/packages/graph-editor/src/components/dialogs/findDialog.tsx b/packages/graph-editor/src/components/dialogs/findDialog.tsx index a16e4d58a..07fad3b88 100644 --- a/packages/graph-editor/src/components/dialogs/findDialog.tsx +++ b/packages/graph-editor/src/components/dialogs/findDialog.tsx @@ -1,10 +1,8 @@ import { Button, Dialog, IconButton, Text, TextInput } from '@tokens-studio/ui'; import { title as annotatedTitle } from '@/annotations/index.js'; -import { - graphEditorSelector, - showSearchSelector, -} from '@/redux/selectors/index.js'; -import { useDispatch, useGraph } from '@/hooks/index.js'; +import { graphEditorSelector } from '@/redux/selectors/index.js'; +import { useFrame } from '@/system/frame/hook.js'; +import { useGraph } from '@/hooks/index.js'; import { useSelector } from 'react-redux'; import React from 'react'; import Xmark from '@tokens-studio/icons/Xmark.js'; @@ -13,12 +11,11 @@ export const FindDialog = () => { const [id, setId] = React.useState(''); const [title, setTitle] = React.useState(''); const localGraph = useGraph(); - const dispatch = useDispatch(); const graph = useSelector(graphEditorSelector); - const open = useSelector(showSearchSelector); + const frame = useFrame(); const setOpen = (value: boolean) => { - dispatch.settings.setShowSearch(value); + frame.settings.setShowSearch(value); }; const onClick = () => { @@ -65,7 +62,7 @@ export const FindDialog = () => { }; return ( - + diff --git a/packages/graph-editor/src/components/flow/edges/edge.tsx b/packages/graph-editor/src/components/flow/edges/edge.tsx index e86b056e8..879169763 100644 --- a/packages/graph-editor/src/components/flow/edges/edge.tsx +++ b/packages/graph-editor/src/components/flow/edges/edge.tsx @@ -1,14 +1,14 @@ -import { EdgeType } from '../../../redux/models/settings.js'; +import { EdgeType, SystemSettings } from '@/system/frame/settings.js'; import { Port } from '@tokens-studio/graph-engine'; -import { edgeType as edgeTypeSelector } from '../../../redux/selectors/settings.js'; import { getBetterBezierPath } from './offsetBezier.js'; import { getSimpleBezierPath, getSmoothStepPath, getStraightPath, } from 'reactflow'; +import { observer } from 'mobx-react-lite'; +import { useFrame } from '@/system/frame/hook.js'; import { useLocalGraph } from '@/context/graph.js'; -import { useSelector } from 'react-redux'; import React from 'react'; import colors from '@/tokens/colors.js'; @@ -26,89 +26,112 @@ const extractColor = (port: Port) => { return { color, backgroundColor }; }; -export default function CustomEdge({ - id, - sourceX, - sourceY, - targetX, - targetY, - sourcePosition, - targetPosition, - style = {}, - data, - markerEnd, -}) { - const edgeType = useSelector(edgeTypeSelector); - const graph = useLocalGraph(); - - const edge = graph.getEdge(id); - let col = undefined; - - if (edge) { - const sourceNode = graph.getNode(edge?.source); - - const sourcePort = sourceNode?.outputs[edge?.sourceHandle]; - - if (sourcePort) { - const { backgroundColor } = extractColor(sourcePort as Port); - col = backgroundColor; - } - } +export default (props) => { + const frame = useFrame(); + return ; +}; - let edgeFn; - switch (edgeType) { - case EdgeType.bezier: - edgeFn = getBetterBezierPath; - break; - case EdgeType.simpleBezier: - edgeFn = getSimpleBezierPath; - break; - case EdgeType.smoothStep: - edgeFn = getSmoothStepPath; - break; - case EdgeType.straight: - edgeFn = getStraightPath; - break; - default: - edgeFn = getBetterBezierPath; - } +export interface ICustomEdge { + settings: SystemSettings; + id: string; + sourceX: number; + sourceY: number; + targetX: number; + targetY: number; + sourcePosition: object; + targetPosition: object; + style: object; + data?: { + text: string; + }; + markerEnd: string; +} - const [edgePath] = edgeFn({ +export const CustomEdgeInner = observer( + ({ + settings, + id, sourceX, sourceY, - sourcePosition, targetX, targetY, + sourcePosition, targetPosition, - }); + style = {}, + data, + markerEnd, + }: ICustomEdge) => { + const graph = useLocalGraph(); - return ( - <> - - - - - - {data?.text} - - - - ); -} + const edge = graph.getEdge(id); + let col = undefined; + + if (edge) { + const sourceNode = graph.getNode(edge?.source); + + const sourcePort = sourceNode?.outputs[edge?.sourceHandle]; + + if (sourcePort) { + const { backgroundColor } = extractColor(sourcePort as Port); + col = backgroundColor; + } + } + + let edgeFn; + switch (settings.edgeType) { + case EdgeType.bezier: + edgeFn = getBetterBezierPath; + break; + case EdgeType.simpleBezier: + edgeFn = getSimpleBezierPath; + break; + case EdgeType.smoothStep: + edgeFn = getSmoothStepPath; + break; + case EdgeType.straight: + edgeFn = getStraightPath; + break; + default: + edgeFn = getBetterBezierPath; + } + + const [edgePath] = edgeFn({ + sourceX, + sourceY, + sourcePosition, + targetX, + targetY, + targetPosition, + }); + + return ( + <> + + + + + + {data?.text} + + + + ); + }, +); diff --git a/packages/graph-editor/src/components/flow/nodes/passthroughNode.tsx b/packages/graph-editor/src/components/flow/nodes/passthroughNode.tsx index e456a6501..ea83df2dd 100644 --- a/packages/graph-editor/src/components/flow/nodes/passthroughNode.tsx +++ b/packages/graph-editor/src/components/flow/nodes/passthroughNode.tsx @@ -2,21 +2,20 @@ import { Handle } from '../handles.js'; import { HandleContainer } from '../handles.js'; import { Stack } from '@tokens-studio/ui'; import { extractType, extractTypeIcon } from '../wrapper/nodeV2.js'; -import { icons } from '@/registry/icon.js'; +import { useFrame } from '@/system/frame/hook.js'; import { useLocalGraph } from '@/context/graph.js'; -import { useSelector } from 'react-redux'; import React from 'react'; export const PassthroughNode = (args) => { const { id } = args; const graph = useLocalGraph(); const node = graph.getNode(id); - const iconTypeRegistry = useSelector(icons); + const frame = useFrame(); if (!node) return null; const port = node.inputs.value; - const typeCol = extractTypeIcon(port, iconTypeRegistry); + const typeCol = extractTypeIcon(port, frame.icons); const type = extractType(port.type); return ( diff --git a/packages/graph-editor/src/components/flow/wrapper/nodeV2.tsx b/packages/graph-editor/src/components/flow/wrapper/nodeV2.tsx index 3cf7ee226..0d7934c13 100644 --- a/packages/graph-editor/src/components/flow/wrapper/nodeV2.tsx +++ b/packages/graph-editor/src/components/flow/wrapper/nodeV2.tsx @@ -17,16 +17,11 @@ import { ErrorBoundaryContent } from '@/components/ErrorBoundaryContent.js'; import { Node as GraphNode } from '@tokens-studio/graph-engine'; import { Handle, HandleContainer, useHandle } from '../handles.js'; import { Stack, Text } from '@tokens-studio/ui'; -import { icons, nodeSpecifics } from '@/redux/selectors/registry.js'; -import { - inlineTypes, - inlineValues, - showTimings, -} from '@/redux/selectors/settings.js'; +import { SystemSettings } from '@/system/frame/settings.js'; import { observer } from 'mobx-react-lite'; import { title } from '@/annotations/index.js'; +import { useFrame } from '@/system/frame/hook.js'; import { useLocalGraph } from '@/context/graph.js'; -import { useSelector } from 'react-redux'; import React from 'react'; import clsx from 'clsx'; import colors from '@/tokens/colors.js'; @@ -57,6 +52,7 @@ export const NodeV2 = (args) => { const { id } = args; const graph = useLocalGraph(); const node = graph.getNode(id); + const frame = useFrame(); if (!node) { return
Node not found
; @@ -64,7 +60,11 @@ export const NodeV2 = (args) => { return ( }> - + ); }; @@ -77,10 +77,40 @@ export interface INodeWrap { subtitle?: string; icon?: React.ReactNode; node: GraphNode; + settings: SystemSettings; +} + +export interface ISpecificWrapper { + specifics: Record< + string, + React.FC<{ + node: GraphNode; + }> + >; + node: GraphNode; } -const NodeWrap = observer(({ node, icon }: INodeWrap) => { - const showTimingsValue = useSelector(showTimings); - const specifics = useSelector(nodeSpecifics); + +export const SpecificWrapper = observer( + ({ specifics, node }: ISpecificWrapper) => { + const Specific = specifics[node.factory.type]; + + if (!Specific) { + return null; + } + return ( + + + + ); + }, +); + +const NodeWrap = observer(({ settings, node, icon }: INodeWrap) => { + const frame = useFrame(); //Check if the input allows for dynamic inputs const isInput = !!( @@ -88,8 +118,6 @@ const NodeWrap = observer(({ node, icon }: INodeWrap) => { node.annotations[annotatedDynamicInputs] ); - const Specific = specifics[node.factory.type]; - return ( {
- {Specific && ( - - - - )} +
- {showTimingsValue && ( + {settings.showTimings && (
{node.lastExecutedDuration}ms @@ -137,18 +157,25 @@ const NodeWrap = observer(({ node, icon }: INodeWrap) => { ); }); + export interface IPortArray { ports: Record; hideNames?: boolean; } export const PortArray = observer(({ ports, hideNames }: IPortArray) => { + const frame = useFrame(); const entries = Object.values(ports).sort(); return ( <> {entries .filter((x) => x.visible != false || x.isConnected) .map((input) => ( - + ))} ); @@ -313,12 +340,17 @@ const getValuePreview = (value, type) => { : valuePreview; }; +export interface IInputHandle { + port: Port; + hideName?: boolean; + inlineTypes: boolean; + inlineValues: boolean; +} + const InputHandle = observer( - ({ port, hideName }: { port: Port; hideName?: boolean }) => { - const inlineTypesValue = useSelector(inlineTypes); - const iconTypeRegistry = useSelector(icons); - const inlineValuesValue = useSelector(inlineValues); - const typeCol = extractTypeIcon(port, iconTypeRegistry); + ({ port, hideName, inlineTypes, inlineValues }: IInputHandle) => { + const frame = useFrame(); + const typeCol = extractTypeIcon(port, frame.icons); const input = port as unknown as Input; const type = extractType(port.type); const handleInformation = useHandle(); @@ -334,7 +366,7 @@ const InputHandle = observer( variadic > {!hideName && {port.name} + } - {inlineTypesValue && } + {inlineTypes && } {port._edges.map((edge, i) => { return ( @@ -354,7 +386,7 @@ const InputHandle = observer( flexDirection: 'row', }} > - {inlineValuesValue && ( + {inlineValues && ( )} - {inlineTypesValue && } + {inlineTypes && } ); })} @@ -401,7 +433,7 @@ const InputHandle = observer( > {input.name} - {inlineValuesValue && ( + {inlineValues && ( )} - {inlineTypesValue && } + {inlineTypes && } ); }, diff --git a/packages/graph-editor/src/components/hotKeys/index.tsx b/packages/graph-editor/src/components/hotKeys/index.tsx index 7fbb3e2d6..882c32ccb 100644 --- a/packages/graph-editor/src/components/hotKeys/index.tsx +++ b/packages/graph-editor/src/components/hotKeys/index.tsx @@ -1,20 +1,14 @@ import { HotKeys as HotKeysComp } from 'react-hotkeys'; import { SerializedNode } from '@/types/serializedNode.js'; import { annotatedDeleteable } from '@tokens-studio/graph-engine'; -import { - inlineTypes, - inlineValues, - showGrid, - snapGrid, -} from '@/redux/selectors/settings.js'; import { savedViewports } from '@/annotations/index.js'; import { useAction } from '@/editor/actions/provider.js'; import { useAutoLayout } from '@/editor/hooks/useAutolayout.js'; import { useDispatch } from '@/hooks/useDispatch.js'; +import { useFrame } from '@/system/frame/hook.js'; import { useLocalGraph } from '@/hooks/index.js'; import { useMemo } from 'react'; import { useReactFlow } from 'reactflow'; -import { useSelector } from 'react-redux'; import { useToast } from '@/hooks/useToast.js'; import React from 'react'; import copy from 'copy-to-clipboard'; @@ -79,10 +73,7 @@ export const getViewports = (graph) => { }; export const useHotkeys = () => { - const showGridValue = useSelector(showGrid); - const snapGridValue = useSelector(snapGrid); - const inlineTypesValue = useSelector(inlineTypes); - const inlineValuesValue = useSelector(inlineValues); + const frame = useFrame(); const duplicateNodes = useAction('duplicateNodes'); const deleteNode = useAction('deleteNode'); const copyNodes = useAction('copyNodes'); @@ -232,20 +223,20 @@ export const useHotkeys = () => { }, TOGGLE_GRID: (event) => { event.preventDefault(); - dispatch.settings.setShowGrid(!showGridValue); + frame.settings.setShowGrid(!frame.settings.showGrid); }, FIND: (event) => { event.preventDefault(); dispatch.settings.setShowSearch(true); }, TOGGLE_SNAP_GRID: () => { - dispatch.settings.setSnapGrid(!snapGridValue); + frame.settings.setSnapGrid(!frame.settings.snapGrid); }, TOGGLE_TYPES: () => { - dispatch.settings.setInlineTypes(!inlineTypesValue); + frame.settings.setInlineTypes(!frame.settings.inlineTypes); }, TOGGLE_VALUES: () => { - dispatch.settings.setInlineValues(!inlineValuesValue); + dispatch.settings.setInlineValues(!frame.settings.inlineValues); }, }), @@ -257,10 +248,7 @@ export const useHotkeys = () => { graph, layout, reactFlowInstance, - showGridValue, - snapGridValue, - inlineTypesValue, - inlineValuesValue, + frame.settings, trigger, ], ); diff --git a/packages/graph-editor/src/components/panels/dropPanel/DragItem.tsx b/packages/graph-editor/src/components/panels/dropPanel/DragItem.tsx index e77c5896c..f61f31ae3 100644 --- a/packages/graph-editor/src/components/panels/dropPanel/DragItem.tsx +++ b/packages/graph-editor/src/components/panels/dropPanel/DragItem.tsx @@ -1,4 +1,5 @@ import { GrabberIcon } from '@/components/icons/GrabberIcon.js'; +import { NodeEntry } from './NodeEntry.js'; import { NodeHoverCard } from '@/components/NodeHoverCard.js'; import React, { useCallback } from 'react'; import styles from './DragItem.module.css'; @@ -6,8 +7,7 @@ import styles from './DragItem.module.css'; type DragItemProps = { data?: unknown; type: string; - children: React.ReactNode; - title?: string; + title: string; description?: string; docs?: string; icon?: React.ReactNode | string; @@ -20,7 +20,6 @@ export const DragItem = ({ description, icon, docs, - children, ...rest }: DragItemProps) => { const [isDragging, setIsDragging] = React.useState(false); @@ -62,7 +61,7 @@ export const DragItem = ({ - {children} +
diff --git a/packages/graph-editor/src/components/panels/dropPanel/NodeEntry.tsx b/packages/graph-editor/src/components/panels/dropPanel/NodeEntry.tsx index 23118af8e..cbfdc0f5e 100644 --- a/packages/graph-editor/src/components/panels/dropPanel/NodeEntry.tsx +++ b/packages/graph-editor/src/components/panels/dropPanel/NodeEntry.tsx @@ -1,39 +1,18 @@ -import { Text } from '@tokens-studio/ui'; +import { Text } from '@tokens-studio/ui/Text.js'; import React from 'react'; +import styles from './nodeEntry.module.css'; -export const NodeEntry = ({ - icon, - text, -}: { +export interface INodeEntry { icon?: React.ReactNode | string; text: string; -}) => { +} + +export const NodeEntry = ({ icon, text }: INodeEntry) => { return ( <> - {icon && ( -
- {icon} -
- )} + {icon &&
{icon}
} - + {text} diff --git a/packages/graph-editor/src/components/panels/dropPanel/dropPanel.module.css b/packages/graph-editor/src/components/panels/dropPanel/dropPanel.module.css index 0adbdf244..f6574c606 100644 --- a/packages/graph-editor/src/components/panels/dropPanel/dropPanel.module.css +++ b/packages/graph-editor/src/components/panels/dropPanel/dropPanel.module.css @@ -7,6 +7,19 @@ flex-direction: column; } +.vertical { + padding-top: var(--component-spacing-xs); + width: 100%; + flex: 1; + overflow: auto; + box-sizing: border-box; +} + +.search { + padding: 0 var(--component-spacing-md); + padding-top: var(--component-spacing-lg) +} + .accordion { width: 100%; display: flex; @@ -17,10 +30,12 @@ border: 0; } } + .accordionItem { border: 0; } + .accordionTrigger { display: flex; align-items: center; diff --git a/packages/graph-editor/src/components/panels/dropPanel/dropPanel.tsx b/packages/graph-editor/src/components/panels/dropPanel/dropPanel.tsx index 3377567e0..b5b71f924 100644 --- a/packages/graph-editor/src/components/panels/dropPanel/dropPanel.tsx +++ b/packages/graph-editor/src/components/panels/dropPanel/dropPanel.tsx @@ -1,10 +1,8 @@ import { Accordion, Stack, TextInput } from '@tokens-studio/ui'; import { DragItem } from './DragItem.js'; import { DropPanelStore } from './data.js'; -import { NodeEntry } from './NodeEntry.js'; import { observer } from 'mobx-react-lite'; -import { panelItemsSelector } from '@/redux/selectors/registry.js'; -import { useSelector } from 'react-redux'; +import { useFrame } from '@/system/frame/hook.js'; import NavArrowRight from '@tokens-studio/icons/NavArrowRight.js'; import React, { useState } from 'react'; import styles from './dropPanel.module.css'; @@ -24,8 +22,8 @@ export interface IDropPanel { } export const DropPanel = () => { - const data = useSelector(panelItemsSelector); - return ; + const frame = useFrame(); + return ; }; export const DropPanelInner = observer(({ data }: IDropPanel) => { @@ -43,25 +41,8 @@ export const DropPanelInner = observer(({ data }: IDropPanel) => { return (
- - + + @@ -73,14 +54,13 @@ export const DropPanelInner = observer(({ data }: IDropPanel) => { .map((item) => ( - - + /> )); if (filteredValues.length === 0) { diff --git a/packages/graph-editor/src/components/panels/dropPanel/index.ts b/packages/graph-editor/src/components/panels/dropPanel/index.ts index c0c286a75..14952d6c2 100644 --- a/packages/graph-editor/src/components/panels/dropPanel/index.ts +++ b/packages/graph-editor/src/components/panels/dropPanel/index.ts @@ -1,2 +1,3 @@ export * from './dropPanel.js'; export * from './data.js'; +export * from './DragItem.js'; diff --git a/packages/graph-editor/src/components/panels/dropPanel/nodeEntry.module.css b/packages/graph-editor/src/components/panels/dropPanel/nodeEntry.module.css new file mode 100644 index 000000000..1a0742e93 --- /dev/null +++ b/packages/graph-editor/src/components/panels/dropPanel/nodeEntry.module.css @@ -0,0 +1,15 @@ +.icon { + color: var(--color-neutral-canvas-default-fg-subtle); + width: var(--size-100); + height: var(--size-100); + display: flex; + align-items: center; + justify-content: center; + font: var(--font-body-sm); +} + +.title { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} \ No newline at end of file diff --git a/packages/graph-editor/src/components/panels/inputs/dynamicInputs.tsx b/packages/graph-editor/src/components/panels/inputs/dynamicInputs.tsx index e8cc5e78d..c7ab04606 100644 --- a/packages/graph-editor/src/components/panels/inputs/dynamicInputs.tsx +++ b/packages/graph-editor/src/components/panels/inputs/dynamicInputs.tsx @@ -1,7 +1,6 @@ import { Button, Checkbox, - Heading, Label, Select, Stack, @@ -77,7 +76,6 @@ export const DynamicInputs = observer(({ node }: { node: Node }) => { return ( - Add Input - - - - {selectedNode.factory.title} - - - -
- {dynamicInputs && } + + {dynamicInputs && } - {SpecificInput ? : null} - - {/* The purpose of the key is to invalidate the port panel if the selected node changes */} - - -
+ {SpecificInput ? : null} + + {/* The purpose of the key is to invalidate the port panel if the selected node changes */} + -
+
); } diff --git a/packages/graph-editor/src/components/panels/legend/index.tsx b/packages/graph-editor/src/components/panels/legend/index.tsx index 2fed1030f..b27d1159f 100644 --- a/packages/graph-editor/src/components/panels/legend/index.tsx +++ b/packages/graph-editor/src/components/panels/legend/index.tsx @@ -1,12 +1,11 @@ import { Stack, Text } from '@tokens-studio/ui'; -import { icons } from '@/redux/selectors/registry.js'; -import { useSelector } from 'react-redux'; +import { useFrame } from '@/system/frame/hook.js'; import React, { useMemo } from 'react'; import colors from '@/tokens/colors.js'; export const Legend = () => { - const iconsRegistry = useSelector(icons); - return ; + const frame = useFrame(); + return ; }; export interface ILegendInner { diff --git a/packages/graph-editor/src/components/panels/output/index.tsx b/packages/graph-editor/src/components/panels/output/index.tsx index 8621fbf08..1256baf77 100644 --- a/packages/graph-editor/src/components/panels/output/index.tsx +++ b/packages/graph-editor/src/components/panels/output/index.tsx @@ -1,11 +1,12 @@ -import { Heading, Stack } from '@tokens-studio/ui'; import { Node } from '@tokens-studio/graph-engine'; import { PortPanel } from '@/components/portPanel/index.js'; +import { Stack } from '@tokens-studio/ui/Stack.js'; import { currentNode } from '@/redux/selectors/graph.js'; import { observer } from 'mobx-react-lite'; import { useGraph } from '@/hooks/useGraph.js'; import { useSelector } from 'react-redux'; import React, { useMemo } from 'react'; +import styles from './styles.module.css'; export function OutputSheet() { const graph = useGraph(); @@ -23,43 +24,10 @@ export function OutputSheet() { */ const OutputSheetObserver = observer(({ node }: { node: Node }) => { return ( -
- - - - {node.factory.title} - - - -
- - - -
+ + + -
+ ); }); diff --git a/packages/graph-editor/src/components/panels/output/styles.module.css b/packages/graph-editor/src/components/panels/output/styles.module.css new file mode 100644 index 000000000..61e2800ca --- /dev/null +++ b/packages/graph-editor/src/components/panels/output/styles.module.css @@ -0,0 +1,10 @@ +.outer { + height: 100%; + flex: 1; + padding: var(--component-spacing-md); +} + +.inner { + padding-top: var(--component-spacing-md); + padding-bottom: var(--component-spacing-md); +} \ No newline at end of file diff --git a/packages/graph-editor/src/components/panels/settings/index.tsx b/packages/graph-editor/src/components/panels/settings/index.tsx index 5b7659377..04b14d41a 100644 --- a/packages/graph-editor/src/components/panels/settings/index.tsx +++ b/packages/graph-editor/src/components/panels/settings/index.tsx @@ -1,17 +1,9 @@ import { Checkbox, Label, Select, Stack, Text } from '@tokens-studio/ui'; -import { EdgeType, LayoutType } from '@/redux/models/settings.js'; -import { - connectOnClickSelector, - delayedUpdateSelector, - edgeType, - inlineTypes, - inlineValues, - layoutType, - showMinimapSelector, - showTimings, -} from '@/redux/selectors/settings.js'; +import { EdgeType, LayoutType, SystemSettings } from '@/system/frame/settings.js'; import { contextMenuSelector } from '@/redux/selectors/ui.js'; +import { observer } from 'mobx-react-lite'; import { useDispatch } from '@/hooks/useDispatch.js'; +import { useFrame } from '@/system/frame/hook.js'; import { useSelector } from 'react-redux'; import React from 'react'; @@ -19,173 +11,174 @@ const EdgeValues = Object.values(EdgeType); const LayoutValues = Object.values(LayoutType); export const Settings = () => { - const edgeTypeValue = useSelector(edgeType); - const layoutTypeValue = useSelector(layoutType); - const showTimingsValue = useSelector(showTimings); - const inlineTypesValue = useSelector(inlineTypes); - const inlineValuesValue = useSelector(inlineValues); - const delayedUpdateValue = useSelector(delayedUpdateSelector); - const connectOnClick = useSelector(connectOnClickSelector); - const contextMenus = useSelector(contextMenuSelector); - const showMinimap = useSelector(showMinimapSelector); - const dispatch = useDispatch(); + const frame = useFrame(); - return ( -
- - - - dispatch.settings.setInlineTypes(Boolean(checked)) - } - checked={inlineTypesValue} - /> - - - - Adds additional labels to help differentiate types for colorblind - users. - - - - - - dispatch.settings.setInlineValues(Boolean(checked)) - } - checked={inlineValuesValue} - /> - - - - Shows values directly on the node. Useful for debugging but can be - cluttered. - - - - - - dispatch.settings.setDelayedUpdate(Boolean(checked)) - } - checked={delayedUpdateValue} - /> - - - - Forces a user to click save to update port. - + return ; +}; + +export const SettingsInner = observer( + ({ settings }: { settings: SystemSettings }) => { + const contextMenus = useSelector(contextMenuSelector); + const dispatch = useDispatch(); + + return ( +
+ + + + settings.setInlineTypes(Boolean(checked)) + } + checked={settings.inlineTypes} + /> + + + + Adds additional labels to help differentiate types for + colorblind users. + + - - - - dispatch.settings.setConnectOnClick(Boolean(checked)) - } - checked={connectOnClick} - /> - - - - Allows you to quick connect nodes by clicking on the 2 port. - + + + settings.setInlineValues(Boolean(checked)) + } + checked={settings.inlineValues} + /> + + + + Shows values directly on the node. Useful for debugging but can + be cluttered. + + - - - - dispatch.settings.setShowTimings(Boolean(checked)) - } - checked={showTimingsValue} - /> - - - - Shows how long it takes for a node to process. - + + + settings.setDelayedUpdate(Boolean(checked)) + } + checked={settings.delayedUpdate} + /> + + + + Forces a user to click save to update port. + + - - - - dispatch.settings.setShowMinimap(Boolean(checked)) - } - checked={showMinimap} - /> - - - - Shows the minimap in the graph editing area - + + + settings.setConnectOnClick(Boolean(checked)) + } + checked={settings.connectOnClick} + /> + + + + Allows you to quick connect nodes by clicking on the 2 port. + + - - - - dispatch.ui.setContextMenus(Boolean(checked)) - } - checked={contextMenus} - /> - - - - Provides right click context menus. - + + + settings.setShowTimings(Boolean(checked)) + } + checked={settings.showTimings} + /> + + + + Shows how long it takes for a node to process. + + - - -
- + checked={settings.showMinimap} + /> + + + + Shows the minimap in the graph editing area + +
-
- + checked={contextMenus} + /> + + + + Provides right click context menus. + + +
+ + +
+ +
+ +
+ +
-
-
- ); -}; +
+ ); + }, +); diff --git a/packages/graph-editor/src/components/panels/unified/index.tsx b/packages/graph-editor/src/components/panels/unified/index.tsx new file mode 100644 index 000000000..a740cff91 --- /dev/null +++ b/packages/graph-editor/src/components/panels/unified/index.tsx @@ -0,0 +1,59 @@ +import { + Accordion, + Heading, + IconButton, + Separator, + Stack, +} from '@tokens-studio/ui'; +import { InfoCircle } from 'iconoir-react'; +import { Inputsheet } from '../inputs/index.js'; +import { OutputSheet } from '../output/index.js'; +import { currentNode } from '@/redux/selectors/graph.js'; +import { useGraph } from '@/hooks/useGraph.js'; +import { useSelector } from 'react-redux'; +import React, { useMemo } from 'react'; +import styles from './styles.module.css'; + +export function UnifiedSheet() { + const graph = useGraph(); + const nodeID = useSelector(currentNode); + const selectedNode = useMemo(() => graph?.getNode(nodeID), [graph, nodeID]); + + if (!selectedNode) { + return <>; + } + + return ( +
+ + + + {selectedNode.factory.title} + } + /> + + + + + Inputs + + + + + + + + Outputs + + + + + + + +
+ ); +} diff --git a/packages/graph-editor/src/components/panels/unified/styles.module.css b/packages/graph-editor/src/components/panels/unified/styles.module.css new file mode 100644 index 000000000..1569b3c66 --- /dev/null +++ b/packages/graph-editor/src/components/panels/unified/styles.module.css @@ -0,0 +1,14 @@ +.container { + height: 100%; + width: 100%; + flex: 1; + display: flex; + overflow: auto; + flex-direction: column; +} + +.inner { + height: 100%; + flex: 1; + padding: var(--component-spacing-md); +} \ No newline at end of file diff --git a/packages/graph-editor/src/components/portPanel/index.tsx b/packages/graph-editor/src/components/portPanel/index.tsx index 4fcc84818..aa5b78ee9 100644 --- a/packages/graph-editor/src/components/portPanel/index.tsx +++ b/packages/graph-editor/src/components/portPanel/index.tsx @@ -6,21 +6,19 @@ import { Tooltip, } from '@tokens-studio/ui'; import { Port as GraphPort } from '@tokens-studio/graph-engine'; -import { Input } from '@tokens-studio/graph-engine'; -import { observer } from 'mobx-react-lite'; -import { useSelector } from 'react-redux'; -import React, { useCallback, useMemo } from 'react'; - import { IField } from '@/components/controls/interface.js'; import { InlineTypeLabel } from '@/components/flow/index.js'; -import { controls } from '@/redux/selectors/registry.js'; +import { Input } from '@tokens-studio/graph-engine'; import { deletable, hidden, resetable } from '@/annotations/index.js'; +import { observer } from 'mobx-react-lite'; +import { useFrame } from '@/system/frame/hook.js'; import { useGraph } from '@/hooks/useGraph.js'; import Download from '@tokens-studio/icons/Download.js'; import Eye from '@tokens-studio/icons/Eye.js'; import EyeClosed from '@tokens-studio/icons/EyeClosed.js'; import MoreVert from '@tokens-studio/icons/MoreVert.js'; import Puzzle from '@tokens-studio/icons/Puzzle.js'; +import React, { useCallback, useMemo } from 'react'; import Undo from '@tokens-studio/icons/Undo.js'; import Xmark from '@tokens-studio/icons/Xmark.js'; import copy from 'copy-to-clipboard'; @@ -30,6 +28,11 @@ export interface IPortPanel { readOnly?: boolean; } +export interface IPort { + port: GraphPort; + readOnly?: boolean; +} + export const PortPanel = observer(({ ports, readOnly }: IPortPanel) => { const entries = Object.values(ports).sort(); @@ -44,22 +47,24 @@ export const PortPanel = observer(({ ports, readOnly }: IPortPanel) => { ); }); -export const Port = observer(({ port, readOnly: isReadOnly }: IField) => { +export const Port = observer(({ port, readOnly: isReadOnly }: IPort) => { const readOnly = isReadOnly || port.isConnected; - const controlSelector = useSelector(controls); + const frame = useFrame(); const graph = useGraph(); const isInput = 'studio.tokens.generic.input' === port.node.factory.type; const isDynamicInput = Boolean(port.annotations[deletable]); const resettable = Boolean(port.annotations[resetable]); const inner = useMemo(() => { - const field = controlSelector.find((x) => x.matcher(port, { readOnly })); + const field = frame.controls.find((x) => x.matcher(port, { readOnly })); const Component = field?.component as React.FC; - return ; + return ( + + ); //We use an explicit dependency on the type // eslint-disable-next-line react-hooks/exhaustive-deps - }, [controlSelector, port, readOnly, port.type]); + }, [frame.controls, port, readOnly, port.type]); const onClick = useCallback(() => { port.setVisible(!port.visible); diff --git a/packages/graph-editor/src/components/toolbar/dropdowns/add.tsx b/packages/graph-editor/src/components/toolbar/dropdowns/add.tsx index 8fb9b1ac9..57cfac7d9 100644 --- a/packages/graph-editor/src/components/toolbar/dropdowns/add.tsx +++ b/packages/graph-editor/src/components/toolbar/dropdowns/add.tsx @@ -1,16 +1,15 @@ import { Button, DropdownMenu, Stack, Tooltip } from '@tokens-studio/ui'; -import { panelItemsSelector } from '@/redux/selectors/index.js'; import { useAction } from '@/editor/actions/provider.js'; import { useDispatch } from '@/hooks/index.js'; +import { useFrame } from '@/system/frame/hook.js'; import { useReactFlow } from 'reactflow'; import { useSelectAddedNodes } from '@/hooks/useSelectAddedNodes.js'; -import { useSelector } from 'react-redux'; import NavArrowRight from '@tokens-studio/icons/NavArrowRight.js'; import Plus from '@tokens-studio/icons/Plus.js'; import React, { useCallback } from 'react'; export const AddDropdown = () => { - const data = useSelector(panelItemsSelector); + const frame = useFrame(); const createNode = useAction('createNode'); const reactFlowInstance = useReactFlow(); const dispatch = useDispatch(); @@ -37,7 +36,7 @@ export const AddDropdown = () => { }, [dispatch.ui]); const addNode = useCallback( - (type: string) => { + async (type: string) => { const newNode = { type, position: reactFlowInstance.screenToFlowPosition( @@ -45,7 +44,7 @@ export const AddDropdown = () => { ), }; - const node = createNode(newNode); + const node = await createNode(newNode); if (node) { selectAddedNodes([node.flowNode]); @@ -55,7 +54,7 @@ export const AddDropdown = () => { ); const nodes = React.useMemo(() => { - return data.groups.map((group) => { + return frame.panelItems.groups.map((group) => { return ( @@ -82,7 +81,7 @@ export const AddDropdown = () => { ); }); - }, [data.groups, addNode]); + }, [frame.panelItems.groups, addNode]); return ( diff --git a/packages/graph-editor/src/components/toolbar/toolbar.tsx b/packages/graph-editor/src/components/toolbar/toolbar.tsx index fce1acc44..76cf0b74b 100644 --- a/packages/graph-editor/src/components/toolbar/toolbar.tsx +++ b/packages/graph-editor/src/components/toolbar/toolbar.tsx @@ -1,12 +1,13 @@ import * as Toolbar from '@radix-ui/react-toolbar'; -import { ToolBarButtonsSelector } from '@/redux/selectors/index.js'; -import { useSelector } from 'react-redux'; +import { useFrame } from '@/system/frame/hook.js'; import React from 'react'; import styles from './toolbar.module.css'; export const GraphToolbar = () => { - const toolbarButtons = useSelector(ToolBarButtonsSelector); - return {toolbarButtons}; + const frame = useFrame(); + return ( + {frame.toolbarButtons} + ); }; export const ToolbarSeparator = ({ className = '', ...props }) => ( diff --git a/packages/graph-editor/src/context/ExternalDataContext.tsx b/packages/graph-editor/src/context/ExternalDataContext.tsx deleted file mode 100644 index 773bcb971..000000000 --- a/packages/graph-editor/src/context/ExternalDataContext.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React, { createContext, useContext, useMemo } from 'react'; - -interface ExternalSetData { - tokens: Record[]; -} - -export interface EditorExternalSet { - name: string; - identifier: string; -} - -type ExternalDataContextType = { - tokenSets?: EditorExternalSet[]; - loadSetTokens: (identifier: string) => Promise; -}; - -const ExternalDataContext = createContext({ - tokenSets: undefined, - loadSetTokens: async () => ({ tokens: [] }), -}); - -function ExternalDataContextProvider({ - children, - tokenSets, - loadSetTokens, -}: { - children: React.ReactNode; - tokenSets?: EditorExternalSet[]; - loadSetTokens: (identifier: string) => Promise; -}) { - const providerValue = useMemo( - () => ({ tokenSets, loadSetTokens }), - [tokenSets, loadSetTokens], - ); - - return ( - - {children} - - ); -} - -function useExternalData() { - const context = useContext(ExternalDataContext); - - if (context === undefined) { - console.error( - 'useExternalData must be used within a ExternalDataContextProvider', - ); - } - return context; -} - -export { ExternalDataContextProvider, useExternalData }; diff --git a/packages/graph-editor/src/context/ExternalLoaderContext.tsx b/packages/graph-editor/src/context/ExternalLoaderContext.tsx deleted file mode 100644 index 1c01083e2..000000000 --- a/packages/graph-editor/src/context/ExternalLoaderContext.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { ExternalLoader } from '@tokens-studio/graph-engine'; -import React, { createContext, useContext } from 'react'; - -const ExternalLoaderContext = createContext( - undefined, -); - -function ExternalLoaderProvider({ - children, - externalLoader, -}: { - children: React.ReactNode; - externalLoader?: ExternalLoader; -}) { - return ( - - {children} - - ); -} - -function useExternalLoader() { - const context = useContext(ExternalLoaderContext); - - return context; -} - -export { ExternalLoaderProvider, useExternalLoader }; diff --git a/packages/graph-editor/src/context/index.ts b/packages/graph-editor/src/context/index.ts deleted file mode 100644 index 36973211f..000000000 --- a/packages/graph-editor/src/context/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './ExternalDataContext.js'; diff --git a/packages/graph-editor/src/data/version.ts b/packages/graph-editor/src/data/version.ts index 1a2aa36dc..0fad64366 100644 --- a/packages/graph-editor/src/data/version.ts +++ b/packages/graph-editor/src/data/version.ts @@ -1 +1 @@ -export const version = '4.3.11'; +export const version = '4.3.12'; diff --git a/packages/graph-editor/src/editor/actions/copyNodes.tsx b/packages/graph-editor/src/editor/actions/copyNodes.tsx index f8bbdc220..bd0a04ea4 100644 --- a/packages/graph-editor/src/editor/actions/copyNodes.tsx +++ b/packages/graph-editor/src/editor/actions/copyNodes.tsx @@ -5,7 +5,7 @@ import { v4 as uuidv4 } from 'uuid'; export const copyNodeAction = ( reactFlowInstance: ReactFlowInstance, graph, - nodeLookup, + nodeLoader, ) => { return async (nodes: SerializedNode[]) => { const { addNodes } = await nodes.reduce( @@ -19,7 +19,7 @@ export const copyNodeAction = ( ...node.engine, id: newID, }, - nodeLookup, + nodeLoader, ); graph.addNode(newGraphNode); diff --git a/packages/graph-editor/src/editor/actions/createNode.tsx b/packages/graph-editor/src/editor/actions/createNode.tsx index e3d762220..1ab5cb285 100644 --- a/packages/graph-editor/src/editor/actions/createNode.tsx +++ b/packages/graph-editor/src/editor/actions/createNode.tsx @@ -1,5 +1,5 @@ import { Dispatch } from '@/redux/store.js'; -import { Graph, Node, NodeFactory } from '@tokens-studio/graph-engine'; +import { Graph, Node, NodeLoader } from '@tokens-studio/graph-engine'; import { ReactFlowInstance, Node as ReactFlowNode } from 'reactflow'; export type NodeRequest = { @@ -11,7 +11,7 @@ export type NodeRequest = { export interface ICreateNode { reactFlowInstance: ReactFlowInstance; graph: Graph; - nodeLookup: Record; + nodeLoader: NodeLoader; iconLookup: Record; /** * If a customized node would be created in the editor, it would be created using this UI lookup. @@ -25,13 +25,13 @@ export interface ICreateNode { export const createNode = ({ reactFlowInstance, graph, - nodeLookup, + nodeLoader, iconLookup, customUI, dropPanelPosition, dispatch, }: ICreateNode) => { - return (nodeRequest: NodeRequest) => { + return async (nodeRequest: NodeRequest) => { const position = nodeRequest.position || { x: dropPanelPosition.x, y: dropPanelPosition.y, @@ -59,12 +59,13 @@ export const createNode = ({ } //Lookup the node type - const Factory = nodeLookup[nodeRequest.type]; + const Factory = await nodeLoader(nodeRequest.type); //Generate the new node - const node = new Factory({ + const node = await new Factory({ graph: graph, }); + graph.addNode(node); const finalPos = position || { x: 0, y: 0 }; diff --git a/packages/graph-editor/src/editor/actions/duplicate.ts b/packages/graph-editor/src/editor/actions/duplicate.ts index 6e19a0163..a1a4669d3 100644 --- a/packages/graph-editor/src/editor/actions/duplicate.ts +++ b/packages/graph-editor/src/editor/actions/duplicate.ts @@ -1,16 +1,10 @@ import { Edge, Node, ReactFlowInstance } from 'reactflow'; -import { - Graph, - NodeFactory, - Port, - annotatedSingleton, -} from '@tokens-studio/graph-engine'; +import { Graph, Port, annotatedSingleton } from '@tokens-studio/graph-engine'; import { v4 as uuidv4 } from 'uuid'; export interface IDuplicate { reactFlowInstance: ReactFlowInstance; graph: Graph; - nodeLookup: Record; } /** diff --git a/packages/graph-editor/src/editor/actions/provider.tsx b/packages/graph-editor/src/editor/actions/provider.tsx index de86a28b9..d145bf9d4 100644 --- a/packages/graph-editor/src/editor/actions/provider.tsx +++ b/packages/graph-editor/src/editor/actions/provider.tsx @@ -5,12 +5,13 @@ import React from 'react'; import type { NodeRequest } from './createNode.js'; export type Actions = { - createNode: (nodeRequest: NodeRequest) => + createNode: (nodeRequest: NodeRequest) => Promise< | undefined | { graphNode: Node; flowNode: FlowNode; - }; + } + >; deleteNode: (nodeId: string) => void; copyNodes: (nodes: SerializedNode[]) => void; duplicateNodes: (nodeIds: string[]) => void; diff --git a/packages/graph-editor/src/editor/editorTypes.ts b/packages/graph-editor/src/editor/editorTypes.ts index c613b6ae3..7b410ab99 100644 --- a/packages/graph-editor/src/editor/editorTypes.ts +++ b/packages/graph-editor/src/editor/editorTypes.ts @@ -1,22 +1,22 @@ import { - CapabilityFactory, - ExternalLoader, Graph, Node as GraphNode, SchemaObject, SerializedGraph, } from '@tokens-studio/graph-engine'; -import { Control } from '../types/controls.js'; -import { DropPanelStore } from '@/components/panels/dropPanel/index.js'; + import { Edge, Node, ReactFlowInstance } from 'reactflow'; import { Menu } from '@/components/menubar/data.js'; -import type { LayoutBase, TabBase, TabData } from 'rc-dock'; +import { System } from '@/system/index.js'; +import type { LayoutBase } from 'rc-dock'; export interface EditorProps { id?: string; - tabLoader?: (tab: TabBase) => TabData | undefined; - + /** + * The system to use for the editor + */ + system: System; /** * A lookup of the custom node types to display in the editor. * Not populating this will result in the default items being displayed. @@ -31,10 +31,6 @@ export interface EditorProps { emptyContent?: React.ReactNode; children?: React.ReactNode; onOutputChange?: (output: Record) => void; - /** - * An external loader to use for loading the graphs or node data - */ - externalLoader?: ExternalLoader; /** * Whether or not to show the menu */ @@ -44,31 +40,6 @@ export interface EditorProps { * A custom menu to display in the editor. */ menuItems?: Menu; - - /** - * Capabilities to load into the graphs. Each factory is loaded into each graph individually - */ - capabilities?: CapabilityFactory[]; - /** - * Items to display in the drop panel. - * Not populating this will result in the default items being displayed. - */ - panelItems: DropPanelStore; - /** - * Customize the controls that are displayed in the editor - */ - controls?: Control[]; - - /** - * A lookup of the custom node ui types to display in the editor. - */ - customNodeUI?: Record; - - /** - * Additional specifics to display in the editor for custom types - */ - specifics?: Record>; - /** * An initial layout to use */ @@ -79,21 +50,11 @@ export interface EditorProps { */ schemas?: SchemaObject[]; - /** - * Additional icons to display in the editor for custom types - */ - icons?: Record; - /** - * Additional buttons to display in the toolbar - */ - toolbarButtons?: React.ReactElement; - /** * Additional colors to display in the editor for custom types */ typeColors?: Record; - initialGraph?: Graph; } export interface GraphEditorProps { @@ -111,7 +72,6 @@ export interface GraphEditorProps { */ nodeTypes?: Record; children?: React.ReactNode; - initialGraph?: SerializedGraph; } export type ImperativeEditorRef = { diff --git a/packages/graph-editor/src/editor/graph.tsx b/packages/graph-editor/src/editor/graph.tsx index 000dc8cdc..50e4bbe9d 100644 --- a/packages/graph-editor/src/editor/graph.tsx +++ b/packages/graph-editor/src/editor/graph.tsx @@ -55,19 +55,8 @@ import { NodeV2 } from '@/components/index.js'; import { PaneContextMenu } from '../components/contextMenus/paneContextMenu.js'; import { PassthroughNode } from '@/components/flow/nodes/passthroughNode.js'; import { SelectionContextMenu } from '@/components/contextMenus/selectionContextMenu.js'; -import { - capabilitiesSelector, - nodeTypesSelector, - panelItemsSelector, -} from '@/redux/selectors/registry.js'; import { clear } from './actions/clear.js'; import { connectNodes } from './actions/connect.js'; -import { - connectOnClickSelector, - showGrid, - showMinimapSelector, - snapGrid, -} from '@/redux/selectors/settings.js'; import { contextMenuSelector } from '@/redux/selectors/ui.js'; import { copyNodeAction } from './actions/copyNodes.js'; import { currentPanelIdSelector } from '@/redux/selectors/graph.js'; @@ -83,7 +72,7 @@ import { } from '@/annotations/index.js'; import { duplicateNodes } from './actions/duplicate.js'; import { useContextMenu } from 'react-contexify'; -import { useExternalLoader } from '@/context/ExternalLoaderContext.js'; +import { useFrame } from '@/system/frame/hook.js'; import { useSelectAddedNodes } from '@/hooks/useSelectAddedNodes.js'; import { useSelector } from 'react-redux'; import { useSetCurrentNode } from '@/hooks/useSetCurrentNode.js'; @@ -112,47 +101,30 @@ export const EditorApp = React.forwardRef< ImperativeEditorRef, GraphEditorProps >((props: GraphEditorProps, ref) => { - const panelItems = useSelector(panelItemsSelector); - const fullNodeLookup = useSelector(nodeTypesSelector); - const { id, customNodeUI = {}, children } = props; + const frame = useFrame(); + + const { id, children } = props; - const externalLoader = useExternalLoader(); - const showMinimap = useSelector(showMinimapSelector); - const capabilities = useSelector(capabilitiesSelector); const contextMenus = useSelector(contextMenuSelector); - const connectOnClick = useSelector(connectOnClickSelector); const reactFlowWrapper = useRef(null); const reactFlowInstance = useReactFlow(); const dispatch = useDispatch(); const { getIntersectingNodes } = reactFlowInstance; const store = useStoreApi(); - const initialGraph: Graph = useMemo(() => { - //Set defaults - const graph = new Graph(); - graph.annotations[title] = 'Untitled Graph'; - graph.annotations[description] = ''; - return graph; - }, []); - const [graph, setTheGraph] = useState(initialGraph); - - const showGridValue = useSelector(showGrid); - const snapGridValue = useSelector(snapGrid); + + const [graph, setTheGraph] = useState(frame.graph); + const internalRef = useRef(null); const activeGraphId = useSelector(currentPanelIdSelector); - //Update the external loader - useEffect(() => { - graph.externalLoader = externalLoader; - }, [graph, externalLoader]); - const iconLookup = useMemo(() => { - return panelItems.groups.reduce((acc, group) => { + return frame.panelItems.groups.reduce((acc, group) => { group.items.forEach((item) => { acc[item.type] = item.icon || group.icon; }); return acc; }, {}); - }, [panelItems]); + }, [frame.panelItems.groups]); const refProxy = useCallback( (v) => { @@ -173,7 +145,7 @@ export const EditorApp = React.forwardRef< //Attach sideeffect listeners useEffect(() => { - capabilities.forEach((factory) => graph.registerCapability(factory)); + frame.capabilities.forEach((factory) => graph.registerCapability(factory)); graph.onFinalize('serialize', (serialized) => { const nodes = reactFlowInstance.getNodes(); @@ -370,7 +342,7 @@ export const EditorApp = React.forwardRef< // Create flow node types here, instead of the global scope to ensure that custom nodes added by the user are available in nodeTypes const fullNodeTypesRef = useRef({ - ...customNodeUI, + ...frame.customNodeUI, GenericNode: NodeV2, [PASSTHROUGH]: PassthroughNode, [EditorNodeTypes.GROUP]: groupNode, @@ -381,12 +353,12 @@ export const EditorApp = React.forwardRef< //Turn it into an O(1) lookup object return Object.fromEntries( Object.entries({ - ...customNodeUI, + ...frame.customNodeUI, [NOTE]: NOTE, 'studio.tokens.generic.preview': 'studio.tokens.generic.preview', }).map(([k]) => [k, k]), ); - }, [customNodeUI]); + }, [frame.customNodeUI]); const handleDeleteNode = useMemo(() => { return deleteNode(graph, dispatch, reactFlowInstance); @@ -397,7 +369,7 @@ export const EditorApp = React.forwardRef< createNode({ reactFlowInstance, graph, - nodeLookup: fullNodeLookup, + nodeLoader: frame.nodeLoader, iconLookup, customUI: customNodeMap, dropPanelPosition, @@ -406,7 +378,7 @@ export const EditorApp = React.forwardRef< [ reactFlowInstance, graph, - fullNodeLookup, + frame.nodeLoader, iconLookup, customNodeMap, dropPanelPosition, @@ -429,7 +401,7 @@ export const EditorApp = React.forwardRef< }, loadRaw: async (serializedGraph) => { if (internalRef.current) { - await graph.deserialize(serializedGraph, fullNodeLookup); + await graph.deserialize(serializedGraph, frame.nodeLoader); internalRef?.current.load(graph); } }, @@ -510,7 +482,7 @@ export const EditorApp = React.forwardRef< [ reactFlowInstance, graph, - fullNodeLookup, + frame.nodeLoader, setNodes, setEdges, dispatch.graph, @@ -520,12 +492,6 @@ export const EditorApp = React.forwardRef< useSetCurrentNode(); - useEffect(() => { - if (props.initialGraph) { - internalRef.current?.loadRaw(props.initialGraph); - } - }, []); - const onConnect = useMemo( () => connectNodes({ graph, setEdges, dispatch }), [dispatch, graph, setEdges], @@ -589,15 +555,16 @@ export const EditorApp = React.forwardRef< ); const onEdgeDblClick = useCallback( - (event, clickedEdge) => { + async (event, clickedEdge) => { event.stopPropagation(); const position = reactFlowInstance?.screenToFlowPosition({ x: event.clientX, y: event.clientY, }); + const PassthroughFactory = await frame.nodeLoader(PASSTHROUGH); - const newNode = new fullNodeLookup[PASSTHROUGH]({ + const newNode = new PassthroughFactory({ graph, }); @@ -657,7 +624,7 @@ export const EditorApp = React.forwardRef< return [...filtered, newEdge, newEdge2]; }); }, - [fullNodeLookup, graph, reactFlowInstance, setEdges, setNodes], + [reactFlowInstance, frame, graph, setNodes, setEdges], ); const onNodeDrag = useCallback( @@ -698,10 +665,9 @@ export const EditorApp = React.forwardRef< const duplicateNodesAction = duplicateNodes({ graph, reactFlowInstance, - nodeLookup: fullNodeLookup, }); - const copyNodes = copyNodeAction(reactFlowInstance, graph, fullNodeLookup); + const copyNodes = copyNodeAction(reactFlowInstance, graph, frame.nodeLoader); const selectAddedNodes = useSelectAddedNodes(); const onDrop = useCallback( @@ -721,12 +687,15 @@ export const EditorApp = React.forwardRef< }); //Some of the nodes might be invalid, so remember to filter them out - const newNodes = positionUpdated - .map((nodeRequest) => handleSelectNewNodeType(nodeRequest)) - .filter((x) => !!x) - .map((x) => x?.flowNode ?? ({} as Node)); + const newNodes = await Promise.all( + positionUpdated.map((nodeRequest) => + handleSelectNewNodeType(nodeRequest), + ), + ); - selectAddedNodes(newNodes); + selectAddedNodes( + newNodes.filter((x) => !!x).map((x) => x?.flowNode ?? ({} as Node)), + ); }, [handleSelectNewNodeType, reactFlowInstance], ); @@ -762,10 +731,10 @@ export const EditorApp = React.forwardRef< onEdgeDoubleClick={onEdgeDblClick} onEdgesDelete={onEdgesDeleted} edges={edges} - connectOnClick={connectOnClick} + connectOnClick={frame.settings.connectOnClick} elevateNodesOnSelect={true} onNodeDragStop={onNodeDragStop} - snapToGrid={snapGridValue} + snapToGrid={frame.settings.snapGrid} edgeTypes={edgeTypes} nodeTypes={fullNodeTypesRef.current} snapGrid={snapGridCoords} @@ -798,7 +767,7 @@ export const EditorApp = React.forwardRef< maxZoom={Infinity} proOptions={proOptions} > - {showGridValue && ( + {frame.settings.showGrid && ( )} @@ -820,7 +789,7 @@ export const EditorApp = React.forwardRef< - {showMinimap && } + {frame.settings.showMinimap && } { + const system = useFrame(); const dagreAutoLayout = useDagreLayout(); - const layoutType = useSelector(layoutTypeSelector); - return useCallback(() => { - switch (layoutType) { + switch (system.settings.layoutType) { case LayoutType.dagre: dagreAutoLayout(); break; } - }, [dagreAutoLayout, layoutType]); + }, [dagreAutoLayout, system.settings.layoutType]); }; diff --git a/packages/graph-editor/src/editor/index.tsx b/packages/graph-editor/src/editor/index.tsx index 13275df10..99ea807a8 100644 --- a/packages/graph-editor/src/editor/index.tsx +++ b/packages/graph-editor/src/editor/index.tsx @@ -2,10 +2,8 @@ import { EditorProps, ImperativeEditorRef } from './editorTypes.js'; import { LayoutController } from './layoutController.js'; import { ReduxProvider } from '../redux/index.js'; import { ToastProvider } from '@/hooks/useToast.js'; -import { defaultControls } from '@/registry/control.js'; -import { nodeLookup as defaultNodeLookup } from '@tokens-studio/graph-engine'; -import { defaultPanelGroupsFactory } from '@/components/index.js'; -import { defaultSpecifics } from '@/registry/specifics.js'; + +import { SystemContext } from '@/system/hook.js'; import React from 'react'; /** @@ -14,33 +12,16 @@ import React from 'react'; */ export const Editor = React.forwardRef( (props: EditorProps, ref) => { - const { - panelItems = defaultPanelGroupsFactory(), - capabilities, - - toolbarButtons, - schemas, - nodeTypes = defaultNodeLookup, - controls = [...defaultControls], - specifics = defaultSpecifics, - icons, - } = props; + const { schemas } = props; // Note that the provider exists around the layout controller so that the layout controller can register itself during mount return ( - - - + + + + + ); }, diff --git a/packages/graph-editor/src/editor/layout/data.tsx b/packages/graph-editor/src/editor/layout/data.tsx new file mode 100644 index 000000000..4f9d972fd --- /dev/null +++ b/packages/graph-editor/src/editor/layout/data.tsx @@ -0,0 +1,166 @@ +import { DropPanel } from '@/components/panels/dropPanel/dropPanel.js'; +import { ErrorBoundary } from 'react-error-boundary'; +import { ErrorBoundaryContent } from '@/components/ErrorBoundaryContent.js'; +import { GraphEditor } from '../graphEditor.js'; +import { Inputsheet } from '@/components/panels/inputs/index.js'; +import { LayoutData, TabBase, TabData } from 'rc-dock'; +import { MAIN_GRAPH_ID } from '@/constants.js'; +import { OutputSheet } from '@/components/panels/output/index.js'; +import React from 'react'; + +export const layoutDataFactory = (): LayoutData => { + return { + dockbox: { + mode: 'vertical', + children: [ + { + mode: 'horizontal', + children: [ + { + size: 2, + mode: 'vertical', + children: [ + { + mode: 'horizontal', + children: [ + { + size: 3, + mode: 'vertical', + children: [ + { + tabs: [ + { + id: 'dropPanel', + title: '', + content: <>, + }, + { + id: 'previewNodesPanel', + title: '', + content: <>, + }, + ], + }, + ], + }, + { + size: 17, + mode: 'vertical', + children: [ + { + id: 'graphs', + size: 700, + group: 'graph', + panelLock: { panelStyle: 'graph' }, + tabs: [ + { + closable: true, + cached: true, + id: MAIN_GRAPH_ID, + group: 'graph', + title: 'Graph', + content: <>, + }, + ], + }, + ], + }, + + { + size: 4, + mode: 'vertical', + children: [ + { + size: 12, + tabs: [ + { + id: 'input', + title: '', + content: <>, + }, + ], + }, + { + size: 12, + tabs: [ + { + id: 'outputs', + title: '', + content: <>, + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + }; +}; + +export const layoutLoader = + (props, ref) => + (tab: TabBase): TabData => { + const { id } = tab; + switch (id) { + case MAIN_GRAPH_ID: + return { + closable: true, + cached: true, + id: MAIN_GRAPH_ID, + group: 'graph', + title: 'Graph', + content: ( + }> + + + ), + }; + case 'input': + return { + closable: true, + cached: true, + group: 'popout', + id: 'input', + title: 'Inputs', + content: ( + }> + + + ), + }; + case 'outputs': + return { + closable: true, + cached: true, + group: 'popout', + id: 'outputs', + title: 'Outputs', + content: ( + }> + + + ), + }; + + case 'dropPanel': + return { + group: 'popout', + id: 'dropPanel', + title: 'Nodes', + content: ( + }> + + + ), + closable: true, + }; + default: + return tab as TabData; + } + }; diff --git a/packages/graph-editor/src/editor/layout/groups.tsx b/packages/graph-editor/src/editor/layout/groups.tsx new file mode 100644 index 000000000..378d6dee2 --- /dev/null +++ b/packages/graph-editor/src/editor/layout/groups.tsx @@ -0,0 +1,78 @@ +import { IconButton } from '@tokens-studio/ui/IconButton.js'; +import { Stack } from '@tokens-studio/ui/Stack.js'; +import { TabGroup } from 'rc-dock'; +import ArrowUpRight from '@tokens-studio/icons/ArrowUpRight.js'; +import Maximize from '@tokens-studio/icons/Maximize.js'; +import React from 'react'; +import Reduce from '@tokens-studio/icons/Reduce.js'; +import Xmark from '@tokens-studio/icons/Xmark.js'; + +const DockButton = (rest) => { + return ; +}; + +export const groups: Record = { + popout: { + animated: true, + floatable: true, + + panelExtra: (panelData, context) => { + const buttons: React.ReactElement[] = []; + if (panelData?.parent?.mode !== 'window') { + const maxxed = panelData?.parent?.mode === 'maximize'; + buttons.push( + : } + onClick={() => context.dockMove(panelData, null, 'maximize')} + >, + ); + buttons.push( + } + onClick={() => context.dockMove(panelData, null, 'new-window')} + >, + ); + } + buttons.push( + } + onClick={() => context.dockMove(panelData, null, 'remove')} + >, + ); + return {buttons}; + }, + }, + /** + * Note that the graph has a huge issue when ran in a popout window, as such we disable it for now + */ + graph: { + animated: true, + floatable: true, + panelExtra: (panelData, context) => { + const buttons: React.ReactElement[] = []; + if (panelData?.parent?.mode !== 'window') { + const maxxed = panelData?.parent?.mode === 'maximize'; + buttons.push( + : } + onClick={() => context.dockMove(panelData, null, 'maximize')} + >, + ); + } + + return {buttons}; + }, + }, +}; diff --git a/packages/graph-editor/src/editor/layout/utils.ts b/packages/graph-editor/src/editor/layout/utils.ts new file mode 100644 index 000000000..0ac3069cf --- /dev/null +++ b/packages/graph-editor/src/editor/layout/utils.ts @@ -0,0 +1,45 @@ +import { BoxBase, LayoutBase, PanelBase } from 'rc-dock'; + +export function recurseFindGraphPanel( + base: BoxBase | PanelBase, +): PanelBase | null { + if (base.id === 'graphs') { + return base as PanelBase; + } + //Check if it has children + if ((base as BoxBase).children) { + for (const child of (base as BoxBase).children) { + const found = recurseFindGraphPanel(child); + if (found) { + return found; + } + } + } + return null; +} + +export function findGraphPanel(layout: LayoutBase): PanelBase | null { + //We need to recursively search for the graph panel + // It is most likely in the dockbox + const dockbox = recurseFindGraphPanel(layout.dockbox); + if (dockbox) { + return dockbox; + } + if (layout.floatbox) { + const floatBox = recurseFindGraphPanel(layout.floatbox); + if (floatBox) { + return floatBox; + } + } else if (layout.maxbox) { + const tab = recurseFindGraphPanel(layout.maxbox); + if (tab) { + return tab; + } + } else if (layout.windowbox) { + const tab = recurseFindGraphPanel(layout.windowbox); + if (tab) { + return tab; + } + } + return null; +} diff --git a/packages/graph-editor/src/editor/layoutController.tsx b/packages/graph-editor/src/editor/layoutController.tsx index 30d7a5a6d..906a40d43 100644 --- a/packages/graph-editor/src/editor/layoutController.tsx +++ b/packages/graph-editor/src/editor/layoutController.tsx @@ -12,18 +12,18 @@ import { DropPanel } from '@/components/panels/dropPanel/dropPanel.js'; import { EditorProps, ImperativeEditorRef } from './editorTypes.js'; import { ErrorBoundary } from 'react-error-boundary'; import { ErrorBoundaryContent } from '@/components/ErrorBoundaryContent.js'; -import { ExternalLoaderProvider } from '@/context/ExternalLoaderContext.js'; import { FindDialog } from '@/components/dialogs/findDialog.js'; +import { FrameContext } from '@/system/frame/hook.js'; import { GraphEditor } from './graphEditor.js'; import { IconButton, Stack, Tooltip } from '@tokens-studio/ui'; -import { Inputsheet } from '@/components/panels/inputs/index.js'; import { MAIN_GRAPH_ID } from '@/constants.js'; import { MenuBar, defaultMenuDataFactory } from '@/components/menubar/index.js'; -import { OutputSheet } from '@/components/panels/output/index.js'; +import { UnifiedSheet } from '@/components/panels/unified/index.js'; import { dockerSelector } from '@/redux/selectors/refs.js'; import { useDispatch } from '@/hooks/useDispatch.js'; import { useRegisterRef } from '@/hooks/useRegisterRef.js'; import { useSelector } from 'react-redux'; +import { useSystem } from '@/system/hook.js'; import Maximize from '@tokens-studio/icons/Maximize.js'; import React, { MutableRefObject, useCallback, useEffect } from 'react'; import Reduce from '@tokens-studio/icons/Reduce.js'; @@ -200,15 +200,7 @@ const defaultLayout: LayoutBase = { size: 12, tabs: [ { - id: 'input', - }, - ], - }, - { - size: 12, - tabs: [ - { - id: 'outputs', + id: 'unifiedPorts', }, ], }, @@ -225,20 +217,20 @@ const defaultLayout: LayoutBase = { }; const layoutLoader = (tab: TabBase, props, ref): TabData => { - const { id, ...rest } = tab; + const { id } = tab; switch (id) { case 'graphs': return { - id: 'graphs', + ...tab, //@ts-expect-error size: 700, group: 'graph', panelLock: { panelStyle: 'graph' }, - ...rest, }; case MAIN_GRAPH_ID: return { + ...tab, closable: true, cached: true, group: 'graph', @@ -250,37 +242,23 @@ const layoutLoader = (tab: TabBase, props, ref): TabData => { ), }; - case 'input': - return { - closable: true, - cached: true, - group: 'popout', - id: 'input', - title: 'Inputs', - content: ( - }> - - - ), - }; - case 'outputs': + case 'unifiedPorts': return { + ...tab, closable: true, cached: true, group: 'popout', - id: 'outputs', - title: 'Outputs', + title: 'Ports', content: ( }> - + ), }; - case 'dropPanel': return { + ...tab, group: 'popout', - id: 'dropPanel', title: 'Nodes', content: ( }> @@ -300,12 +278,12 @@ export const LayoutController = React.forwardRef< EditorProps >((props: EditorProps, ref) => { const { - tabLoader, - externalLoader, initialLayout, menuItems = defaultMenuDataFactory(), } = props; + const system = useSystem(); + const registerDocker = useRegisterRef('docker'); const dispatch = useDispatch(); @@ -313,13 +291,13 @@ export const LayoutController = React.forwardRef< const loadTab = useCallback( (tab): TabData => { - const loaded = tabLoader?.(tab); + const loaded = system.tabLoader?.(tab); if (!loaded) { return layoutLoader(tab, props, ref); } return loaded; }, - [tabLoader, props, ref], + [system, props, ref], ); useEffect(() => { @@ -330,7 +308,7 @@ export const LayoutController = React.forwardRef< const onLayoutChange = (newLayout: LayoutBase) => { //We need to find the graph tab container in the newlayout - const graphContainer = findGraphPanel(newLayout); + const graphContainer = findGraphPanel(newLayout) if (graphContainer?.activeId) { //Get the active Id to find the currently selected graph @@ -339,7 +317,7 @@ export const LayoutController = React.forwardRef< }; return ( - + - + ); }); LayoutController.displayName = 'LayoutController'; diff --git a/packages/graph-editor/src/hooks/useOpenPanel.tsx b/packages/graph-editor/src/hooks/useOpenPanel.tsx index d1a9b3d53..06f6b4201 100644 --- a/packages/graph-editor/src/hooks/useOpenPanel.tsx +++ b/packages/graph-editor/src/hooks/useOpenPanel.tsx @@ -5,7 +5,7 @@ import React, { MutableRefObject, useCallback } from 'react'; import type { DockLayout } from 'rc-dock'; export type PanelFactory = { - group: string; + group: string | undefined; id: string; title: React.ReactElement | string; content: React.ReactElement; diff --git a/packages/graph-editor/src/index.tsx b/packages/graph-editor/src/index.tsx index b508c6315..5a56a4dc9 100644 --- a/packages/graph-editor/src/index.tsx +++ b/packages/graph-editor/src/index.tsx @@ -3,7 +3,6 @@ export * from './utils/index.js'; export * from './types/index.js'; export * from './redux/index.js'; export * from './redux/selectors/index.js'; -export * from './context/index.js'; export * from './editor/graph.js'; export * from './editor/index.js'; export * from './editor/editorTypes.js'; @@ -19,4 +18,6 @@ export * from './registry/control.js'; export * from './registry/specifics.js'; export * from './registry/toolbar.js'; export * from './types/index.js'; -export { DropPanelStore } from './components/panels/dropPanel/data.js'; + +export * from './system/index.js'; +export * from './system/frame/index.js'; diff --git a/packages/graph-editor/src/redux/index.tsx b/packages/graph-editor/src/redux/index.tsx index 8a824681e..d0185779a 100644 --- a/packages/graph-editor/src/redux/index.tsx +++ b/packages/graph-editor/src/redux/index.tsx @@ -4,20 +4,14 @@ import React, { useEffect } from 'react'; export const ReduxProvider = ({ children, - nodeTypes, - panelItems, - capabilities, - icons, schemas, - controls, - specifics, - toolbarButtons, + // toolbarButtons, }) => { - useEffect(() => { - if (toolbarButtons) { - store.dispatch.registry.setToolbarButtons(toolbarButtons); - } - }, [toolbarButtons]); + // useEffect(() => { + // if (toolbarButtons) { + // store.dispatch.registry.setToolbarButtons(toolbarButtons); + // } + // }, [toolbarButtons]); useEffect(() => { if (schemas) { @@ -25,29 +19,5 @@ export const ReduxProvider = ({ } }, [schemas]); - useEffect(() => { - store.dispatch.registry.registerIcons(icons || {}); - }, [icons]); - - useEffect(() => { - store.dispatch.registry.setControls(controls); - }, [controls]); - - useEffect(() => { - store.dispatch.registry.setNodeTypes(nodeTypes); - }, [nodeTypes]); - - useEffect(() => { - store.dispatch.registry.setSpecifics(specifics); - }, [specifics]); - - useEffect(() => { - store.dispatch.registry.setPanelItems(panelItems); - }, [panelItems]); - - useEffect(() => { - store.dispatch.registry.setCapabilities(capabilities || []); - }, [capabilities]); - return {children}; }; diff --git a/packages/graph-editor/src/redux/models/index.ts b/packages/graph-editor/src/redux/models/index.ts index 0afb0f6b2..b8d86b01d 100644 --- a/packages/graph-editor/src/redux/models/index.ts +++ b/packages/graph-editor/src/redux/models/index.ts @@ -3,12 +3,10 @@ import { RootModel } from './root.js'; import { graphState } from './graph.js'; import { refState } from './refs.js'; import { registryState } from './registry.js'; -import { settingsState } from './settings.js'; import { uiState } from './ui.js'; export const models: RootModel = { graph: graphState, - settings: settingsState, ui: uiState, refs: refState, registry: registryState, diff --git a/packages/graph-editor/src/redux/models/registry.ts b/packages/graph-editor/src/redux/models/registry.ts index 742830230..50a97aa59 100644 --- a/packages/graph-editor/src/redux/models/registry.ts +++ b/packages/graph-editor/src/redux/models/registry.ts @@ -1,46 +1,21 @@ import { AllSchemas, - CapabilityFactory, Node, SchemaObject, } from '@tokens-studio/graph-engine'; -import { Control } from '@/types/controls.js'; -import { DefaultToolbarButtons } from '@/registry/toolbar.js'; -import { - DropPanelStore, - defaultPanelGroupsFactory, -} from '@/components/panels/dropPanel/index.js'; -import { ReactElement } from 'react'; import { RootModel } from './root.js'; import { createModel } from '@rematch/core'; -import { defaultControls } from '@/registry/control.js'; -import { defaultSpecifics } from '@/registry/specifics.js'; -import { icons } from '@/registry/icon.js'; + import { inputControls } from '@/registry/inputControls.js'; export interface RegistryState { - //Additional specific controls for nodes. Appended to the end of the default controls - nodeSpecifics: Record>; - icons: Record; inputControls: Record>; - controls: Control[]; - nodeTypes: Record; - panelItems: DropPanelStore; - capabilities: CapabilityFactory[]; - toolbarButtons: ReactElement[]; schemas: SchemaObject[]; } export const registryState = createModel()({ state: { - nodeSpecifics: defaultSpecifics, - icons: icons(), inputControls: { ...inputControls }, - controls: [...(defaultControls as Control[])], - panelItems: defaultPanelGroupsFactory(), - nodeTypes: {}, - capabilities: [], - toolbarButtons: DefaultToolbarButtons(), schemas: AllSchemas, } as RegistryState, reducers: { @@ -50,65 +25,5 @@ export const registryState = createModel()({ schemas, }; }, - setToolbarButtons(state, toolbarButtons: ReactElement[]) { - return { - ...state, - toolbarButtons, - }; - }, - setCapabilities(state, payload: CapabilityFactory[]) { - return { - ...state, - capabilities: payload, - }; - }, - setNodeTypes: (state, payload: Record) => { - return { - ...state, - nodeTypes: payload, - }; - }, - setSpecifics: ( - state, - payload: Record>, - ) => { - return { - ...state, - nodeSpecifics: payload, - }; - }, - setControls(state, payload: Control[]) { - return { - ...state, - controls: payload, - }; - }, - registerIcons(state, payload: Record) { - return { - ...state, - icons: { - ...state.icons, - ...payload, - }, - }; - }, - registerInputControl( - state, - payload: { key: string; value: React.FC<{ node: Node }> }, - ) { - return { - ...state, - inputControls: { - ...state.inputControls, - [payload.key]: payload.value, - }, - }; - }, - setPanelItems(state, payload: DropPanelStore) { - return { - ...state, - panelItems: payload, - }; - }, }, }); diff --git a/packages/graph-editor/src/redux/models/root.ts b/packages/graph-editor/src/redux/models/root.ts index df2bbe604..e586803e0 100644 --- a/packages/graph-editor/src/redux/models/root.ts +++ b/packages/graph-editor/src/redux/models/root.ts @@ -2,11 +2,9 @@ import { Models } from '@rematch/core'; import { graphState } from './graph.js'; import { refState } from './refs.js'; import { registryState } from './registry.js'; -import { settingsState } from './settings.js'; import { uiState } from './ui.js'; export interface RootModel extends Models { - settings: typeof settingsState; ui: typeof uiState; graph: typeof graphState; refs: typeof refState; diff --git a/packages/graph-editor/src/redux/models/settings.ts b/packages/graph-editor/src/redux/models/settings.ts deleted file mode 100644 index b9ff22b05..000000000 --- a/packages/graph-editor/src/redux/models/settings.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { RootModel } from './root.js'; -import { createModel } from '@rematch/core'; - -export enum EdgeType { - bezier = 'Bezier', - smoothStep = 'Smooth step', - straight = 'Straight', - simpleBezier = 'Simple Bezier', -} -export enum LayoutType { - dagre = 'Dagre', - elkForce = 'Elk - Force', - elkRect = 'Elk - Rect', - elkLayered = 'Elk - Layered', - elkStress = 'Elk - Stress', -} - -export interface SettingsState { - edgeType: EdgeType; - layoutType: LayoutType; - debugMode: boolean; - showTimings: boolean; - showMinimap: boolean; - showGrid: boolean; - showSearch: boolean; - /** - * Whether to delay the update of a node when a value is changed - */ - delayedUpdate: boolean; - /** - * Whether to show the types inline with the nodes - */ - inlineTypes: boolean; - /** - * Whether to show the values inline with the nodes - */ - inlineValues: boolean; - connectOnClick: boolean; - snapGrid: boolean; -} - -export const settingsState = createModel()({ - state: { - edgeType: EdgeType.bezier, - layoutType: LayoutType.dagre, - showGrid: true, - connectOnClick: true, - showTimings: false, - showSearch: false, - inlineTypes: false, - inlineValues: false, - snapGrid: false, - debugMode: false, - showMinimap: false, - delayedUpdate: false, - } as SettingsState, - reducers: { - setConnectOnClick(state, connectOnClick: boolean) { - return { - ...state, - connectOnClick, - }; - }, - setShowSearch(state, showSearch: boolean) { - return { - ...state, - showSearch, - }; - }, - setShowMinimap(state, showMinimap: boolean) { - return { - ...state, - showMinimap, - }; - }, - - setDelayedUpdate(state, delayedUpdate: boolean) { - return { - ...state, - delayedUpdate, - }; - }, - - setSnapGrid(state, snapGrid: boolean) { - return { - ...state, - snapGrid, - }; - }, - setShowGrid(state, showGrid: boolean) { - return { - ...state, - showGrid, - }; - }, - setDebugMode(state, debugMode: boolean) { - return { - ...state, - debugMode, - }; - }, - setInlineTypes(state, inlineTypes: boolean) { - return { - ...state, - inlineTypes, - }; - }, - setInlineValues(state, inlineValues: boolean) { - return { - ...state, - inlineValues, - }; - }, - setEdgeType(state, edgeType: EdgeType) { - return { - ...state, - edgeType, - }; - }, - setShowTimings(state, showTimings: boolean) { - return { - ...state, - showTimings, - }; - }, - setLayoutType(state, layoutType: LayoutType) { - return { - ...state, - layoutType, - }; - }, - }, -}); diff --git a/packages/graph-editor/src/redux/selectors/index.ts b/packages/graph-editor/src/redux/selectors/index.ts index 9365f451f..40ee7fda4 100644 --- a/packages/graph-editor/src/redux/selectors/index.ts +++ b/packages/graph-editor/src/redux/selectors/index.ts @@ -2,5 +2,4 @@ export * from './graph.js'; export * from './refs.js'; export * from './roots.js'; export * from './registry.js'; -export * from './settings.js'; export * from './ui.js'; diff --git a/packages/graph-editor/src/redux/selectors/registry.ts b/packages/graph-editor/src/redux/selectors/registry.ts index ed9e71f27..5e2db078a 100644 --- a/packages/graph-editor/src/redux/selectors/registry.ts +++ b/packages/graph-editor/src/redux/selectors/registry.ts @@ -1,37 +1,11 @@ import { createSelector } from 'reselect'; import { registry } from './roots.js'; -export const icons = createSelector(registry, (state) => state.icons); export const inputControls = createSelector( registry, (state) => state.inputControls, ); -export const controls = createSelector(registry, (state) => state.controls); -export const nodeSpecifics = createSelector( - registry, - (state) => state.nodeSpecifics, -); - -export const panelItemsSelector = createSelector( - registry, - (state) => state.panelItems, -); - -export const capabilitiesSelector = createSelector( - registry, - (state) => state.capabilities, -); - -export const nodeTypesSelector = createSelector( - registry, - (state) => state.nodeTypes, -); - -export const ToolBarButtonsSelector = createSelector( - registry, - (state) => state.toolbarButtons, -); export const SchemaSelector = createSelector( registry, diff --git a/packages/graph-editor/src/redux/selectors/roots.ts b/packages/graph-editor/src/redux/selectors/roots.ts index abcf184a6..dbc75ccff 100644 --- a/packages/graph-editor/src/redux/selectors/roots.ts +++ b/packages/graph-editor/src/redux/selectors/roots.ts @@ -1,5 +1,4 @@ import { RootState } from '../store.js'; -export const settings = (state: RootState) => state.settings; export const ui = (state: RootState) => state.ui; export const graph = (state: RootState) => state.graph; export const refs = (state: RootState) => state.refs; diff --git a/packages/graph-editor/src/redux/selectors/settings.ts b/packages/graph-editor/src/redux/selectors/settings.ts deleted file mode 100644 index f1685b829..000000000 --- a/packages/graph-editor/src/redux/selectors/settings.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { createSelector } from 'reselect'; -import { settings } from './roots.js'; - -export const edgeType = createSelector(settings, (state) => state.edgeType); - -export const layoutType = createSelector(settings, (state) => state.layoutType); - -export const debugMode = createSelector(settings, (state) => state.debugMode); -export const inlineTypes = createSelector( - settings, - (state) => state.inlineTypes, -); -export const inlineValues = createSelector( - settings, - (state) => state.inlineValues, -); - -export const showGrid = createSelector(settings, (state) => state.showGrid); - -export const snapGrid = createSelector(settings, (state) => state.snapGrid); -export const showTimings = createSelector( - settings, - (state) => state.showTimings, -); - -export const showMinimapSelector = createSelector( - settings, - (state) => state.showMinimap, -); - -export const delayedUpdateSelector = createSelector( - settings, - (state) => state.delayedUpdate, -); -export const showSearchSelector = createSelector( - settings, - (state) => state.showSearch, -); -export const connectOnClickSelector = createSelector( - settings, - (state) => state.connectOnClick, -); diff --git a/packages/graph-editor/src/redux/store.tsx b/packages/graph-editor/src/redux/store.tsx index 3ec8c5195..fef9bacef 100644 --- a/packages/graph-editor/src/redux/store.tsx +++ b/packages/graph-editor/src/redux/store.tsx @@ -3,7 +3,6 @@ import { RefState } from './models/refs.js'; import { RegistryState } from './models/registry.js'; import { RematchDispatch, init } from '@rematch/core'; import { RootModel, models } from './models/index.js'; -import { SettingsState } from './models/settings.js'; import { UIState } from './models/ui.js'; export const store = init({ @@ -20,7 +19,6 @@ export const store = init({ export type Dispatch = RematchDispatch; export type RootState = { graph: GraphState; - settings: SettingsState; ui: UIState; refs: RefState; registry: RegistryState; diff --git a/packages/graph-editor/src/registry/icon.tsx b/packages/graph-editor/src/registry/icon.tsx index aa2014ad7..bb5b2a4b1 100644 --- a/packages/graph-editor/src/registry/icon.tsx +++ b/packages/graph-editor/src/registry/icon.tsx @@ -20,7 +20,7 @@ import Text from '@tokens-studio/icons/Text.js'; * Default icons for the graph editor * These icons are used to represent the different types of custom types in the graph editor */ -export const icons = () => +export const iconsFactory = () => ({ [COLOR]: , [CURVE]: , diff --git a/packages/graph-editor/src/system/frame/hook.tsx b/packages/graph-editor/src/system/frame/hook.tsx new file mode 100644 index 000000000..39c4786bc --- /dev/null +++ b/packages/graph-editor/src/system/frame/hook.tsx @@ -0,0 +1,8 @@ +import { Frame } from './index.js'; +import { createContext, useContext } from 'react'; + +export const FrameContext = createContext(undefined); + +export const useFrame = (): Frame => { + return useContext(FrameContext)!; +}; diff --git a/packages/graph-editor/src/system/frame/index.tsx b/packages/graph-editor/src/system/frame/index.tsx new file mode 100644 index 000000000..afed16062 --- /dev/null +++ b/packages/graph-editor/src/system/frame/index.tsx @@ -0,0 +1,104 @@ +import { + CapabilityFactory, + Graph, + Node, + NodeLoader, +} from '@tokens-studio/graph-engine'; +import { Control } from '@/types/controls.js'; +import { DefaultToolbarButtons } from '@/registry/toolbar.js'; +import { DropPanelStore } from '@/components/index.js'; +import { SystemSettings } from './settings.js'; +import { defaultSpecifics } from '@/registry/specifics.js'; +import { iconsFactory } from '@/registry/icon.js'; +import { makeAutoObservable } from 'mobx'; +import React from 'react'; + + +export interface IFrame { + /** + * The underlying graph to use for the frame + */ + graph :Graph; + /** + * Items to display in the drop panel. + * Not populating this will result in the default items being displayed. + */ + panelItems?: DropPanelStore; + /** + * Customize the controls that are displayed in the editor + */ + controls?: Control[]; + /** + * Additional specifics to display in the editor for custom types + */ + specifics?: Record< + string, + React.FC<{ + node: Node; + }> + >; + /** + * The loader for nodes to display in the editor + */ + nodeLoader: NodeLoader; + + /** + * Capabilities to load into the graphs. Each factory is loaded into each graph individually. + */ + capabilities?: CapabilityFactory[]; + /** + * A lookup of the custom node ui types to display in the editor. + */ + customNodeUI?: Record; + /** + * An icon lookup to be used for legends, etc + */ + icons?: Record; + + settings?: SystemSettings; + + /** + * Additional buttons to display in the toolbar + */ + toolbarButtons?: React.ReactElement[]; +} + +export class Frame { + specifics!: Record< + string, + React.FC<{ + node: Node; + }> + >; + nodeLoader!: NodeLoader; + graph!: Graph; + panelItems!: DropPanelStore; + capabilities!: CapabilityFactory[]; + customNodeUI!: Record; + controls!: Control[]; + settings!: SystemSettings; + icons!: Record; + toolbarButtons!: React.ReactElement[]; + + constructor(config: IFrame) { + const defaultConfig: Partial