@web-noise/core 0.0.11-fix2 → 0.0.12-fix

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +0,0 @@
1
- {"mappings":"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","sources":["packages/core/src/constants.ts","packages/core/src/hooks/useAudioNode.ts","packages/core/src/hooks/useNode.ts","packages/core/src/theme.ts","packages/core/src/hooks/useTheme.ts","packages/core/src/components/EditableLabel.tsx","packages/core/src/components/Modal.tsx","packages/core/src/components/NodeInfoModal.tsx","packages/core/src/components/Node/index.tsx","packages/core/src/types.ts","packages/core/src/helpers/generateNodeId.ts","packages/core/src/store/nodesStore.ts","packages/core/src/store/history/index.ts","packages/core/src/store/audioPatch/compareGraphs.ts","packages/core/src/store/audioPatch/index.ts","packages/core/src/helpers/projectFile.ts","packages/core/src/store/projectStore.ts","packages/core/src/store/index.ts","packages/core/src/styles.ts","packages/core/src/components/contextMenu/styles.tsx","packages/core/src/components/contextMenu/EdgeContextMenu.tsx","packages/core/src/components/AddNode/Plugins.tsx","packages/core/src/components/AddNode/Filters.tsx","packages/core/src/components/AddNode/index.tsx","packages/core/src/components/UploadPatch.tsx","packages/core/src/components/UploadProject.tsx","packages/core/src/lib/hooks/useWorker.ts","packages/core/src/lib/hooks/useMessageChannel.ts","packages/core/src/lib/helpers.ts","packages/core/src/lib/index.ts","packages/core/src/components/UploadAudio.tsx","packages/core/src/components/contextMenu/EditorContextMenu.tsx","packages/core/src/components/contextMenu/NodeContextMenu.tsx","packages/core/src/components/ControlPanel/styles.tsx","packages/core/src/components/ControlPanel/ControlPanelItem.tsx","packages/core/src/components/ControlPanel/index.tsx","packages/core/src/components/Help/HelpModal.tsx","packages/core/src/components/Help/index.tsx","packages/core/src/components/ResumeContext.tsx","packages/core/src/components/ToggleMinimap.tsx","packages/core/src/components/Wire.tsx","packages/core/src/components/Editor/index.tsx","packages/core/src/components/App.tsx","packages/core/index.ts"],"sourcesContent":["export const DRAG_HANDLE_CLASS = \"web-noise-drag-handle\";\nexport const DRAG_HANDLE_SELECTOR = `.${DRAG_HANDLE_CLASS}`;\n\nexport const CONTROL_PANEL_GRID_CONFIG = {\n rowHeight: 10,\n cols: 4,\n};\n\nexport enum PortType {\n Gate = \"gate\",\n Number = \"number\",\n Audio = \"audio\",\n Any = \"any\",\n}\n","import { AudioNodeState } from \"@web-noise/patch\";\nimport useStore from \"../store\";\nimport type { WNAudioNode } from \"../types\";\n\nconst useAudioNode = <T = WNAudioNode>(id: string) => {\n const patch = useStore(({ patch }) => patch);\n return patch.audioNodes.get(id) as AudioNodeState<T> | undefined;\n};\n\nexport default useAudioNode;\n","import { useCallback } from \"react\";\nimport useStore from \"../store\";\nimport { WNNodeData } from \"../types\";\n\nconst useNode = (id: string) => {\n const updateNodeData = useStore(({ updateNodeData }) => updateNodeData);\n\n const updateNodeValues = useCallback(\n (values: WNNodeData[\"values\"]) => updateNodeData(id, { values }),\n [id, updateNodeData]\n );\n const updateNodeConfig = useCallback(\n (config: WNNodeData[\"config\"]) => updateNodeData(id, { config }),\n [id, updateNodeData]\n );\n const updateNodeLabel = useCallback(\n (label: string) => updateNodeData(id, { label }),\n [id, updateNodeData]\n );\n\n return {\n updateNodeValues,\n updateNodeConfig,\n updateNodeLabel,\n };\n};\n\nexport default useNode;\n","const LEVA_COLOR_ACCENT2_BLUE = \"#007bff\";\nconst COLOR_GREEN_PRIMARY = \"#14df42\";\nconst COLOR_WHITE_PRIMARY = \"#ffffff\";\n\nconst colors = <const>{\n elevation1: \"#292d39\", // bg color of the root panel (main title bar)\n elevation2: \"#181c20\", // bg color of the rows (main panel color)\n elevation3: \"#373c4b\", // bg color of the inputs\n accent1: \"#0066dc\",\n accent2: LEVA_COLOR_ACCENT2_BLUE,\n accent3: \"#3c93ff\",\n highlight1: \"#535760\",\n highlight2: \"#8c92a4\",\n highlight3: \"#fefefe\",\n vivid1: COLOR_GREEN_PRIMARY,\n whitePrimary: COLOR_WHITE_PRIMARY,\n error: \"#db5353\",\n};\n\nconst zIndex = <const>{\n modal: 9998,\n controlPanel: 9999,\n resumeContextLayout: 10003,\n};\n\nconst theme = {\n colors,\n zIndex,\n};\n\nexport type Theme = typeof theme;\n\nexport default theme;\n","import { useTheme as useEmotionTheme } from \"@emotion/react\";\nimport type { Theme } from '../theme';\n\nconst useTheme = () => {\n return useEmotionTheme() as Theme;\n}\n\nexport default useTheme;\n","import styled from \"@emotion/styled\";\nimport React, { useCallback, useEffect, useRef, useState } from \"react\";\n\nexport const TitleBarLabel = styled.input`\n width: 100%;\n background: none;\n border: none;\n text-align: center;\n color: var(--leva-colors-highlight1);\n font-family: var(--leva-fonts-mono);\n font-size: var(--leva-fontSizes-root);\n cursor: inherit;\n text-overflow: ellipsis;\n outline: none;\n\n &:focus {\n box-shadow: 0 0 0 green var(--leva-colors-accent2);\n }\n &:focus-within {\n box-shadow: 0 0 0 green var(--leva-colors-accent2);\n }\n &:focus-vissible {\n box-shadow: 0 0 0 green var(--leva-colors-accent2);\n }\n &:not([readonly]):focus {\n color: #fff;\n appearance: none;\n cursor: auto;\n background-color: var(--leva-colors-elevation2);\n padding: 0.3rem;\n border-radius: 0.2rem;\n }\n`;\n\ninterface EditableLabelProps {\n onChange: (value: string) => void;\n value?: string;\n className?: string;\n}\n\nconst EditableLabel = ({\n onChange,\n value = \"\",\n className,\n}: EditableLabelProps) => {\n const labelInputRef = useRef<HTMLInputElement>(null);\n\n const [labelEditMode, setLabelEditMode] = useState(false);\n const editNodeLabel = useCallback(\n (event: React.MouseEvent) => {\n //@ts-ignore\n event.target?.select();\n setLabelEditMode(true);\n },\n [setLabelEditMode],\n );\n\n const exitEditMode = useCallback(() => {\n window.getSelection()?.collapseToEnd();\n setLabelEditMode(false);\n }, [setLabelEditMode]);\n\n const saveNodeLabel = useCallback(() => {\n exitEditMode();\n if (labelInputRef.current) {\n onChange(labelInputRef.current.value);\n }\n }, [labelInputRef, exitEditMode, onChange]);\n\n const cancelNodeLabelEdit = useCallback(() => {\n exitEditMode();\n if (labelInputRef.current && value) {\n labelInputRef.current.value = value;\n }\n }, [labelInputRef, exitEditMode, value]);\n\n useEffect(() => {\n if (!labelInputRef.current) {\n return;\n }\n labelInputRef.current.value = value;\n }, [value, labelInputRef]);\n\n return (\n <TitleBarLabel\n ref={labelInputRef}\n type=\"text\"\n readOnly={!labelEditMode}\n onDoubleClick={(event) => editNodeLabel(event)}\n onBlur={saveNodeLabel}\n onKeyDown={(event) => {\n switch (event.key) {\n case \"Escape\":\n cancelNodeLabelEdit();\n break;\n case \"Enter\":\n saveNodeLabel();\n break;\n }\n }}\n className={className}\n />\n );\n};\n\nexport default EditableLabel;\n","import styled from \"@emotion/styled\";\nimport { type ReactNode, useEffect } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { MdClose as CloseIcon } from \"react-icons/md\";\nimport useTheme from \"../hooks/useTheme\";\nimport { Theme } from \"../theme\";\n\nconst ModalOuter = styled.div<{ theme: Theme }>`\n position: fixed;\n z-index: ${({ theme }) => theme.zIndex.modal};\n width: 100%;\n height: 100%;\n top: 0;\n left: 0;\n background: ${({ theme }) => theme.colors.elevation3}cc;\n display: flex;\n align-items: center;\n justify-content: center;\n`;\n\nconst ModalInner = styled.div<{ theme: Theme }>`\n background: ${({ theme }) => theme.colors.elevation2};\n box-shadow: 1px 1px 1px 1px ${({ theme }) => theme.colors.elevation1};\n color: white;\n width: 70%;\n height: 80%;\n overflow-y: scroll;\n position: relative;\n`;\n\nconst ModalCloser = styled(CloseIcon)<{ theme: Theme }>`\n position: absolute;\n top: 0.2rem;\n right: 0.2rem;\n cursor: pointer;\n`;\n\ninterface ModalProps {\n onClose?: () => void;\n children: ReactNode;\n}\n\nexport const Modal = ({ children, onClose, ...props }: ModalProps) => {\n const theme = useTheme();\n\n useEffect(() => {\n const escHandler = (event: KeyboardEvent) => {\n if (event.key === \"Escape\") {\n onClose?.();\n }\n };\n document.addEventListener(\"keydown\", escHandler);\n return () => {\n document.removeEventListener(\"keydown\", escHandler);\n };\n }, [onClose]);\n\n return createPortal(\n <ModalOuter theme={theme} onClick={onClose}>\n <ModalInner\n {...props}\n onClick={(e) => {\n e.stopPropagation();\n }}\n theme={theme}\n >\n {children}\n <ModalCloser theme={theme} onClick={onClose} />\n </ModalInner>\n </ModalOuter>,\n document.body,\n );\n};\n\nexport default Modal;\n","import styled from \"@emotion/styled\";\nimport Modal from \"./Modal\";\nimport { marked } from \"marked\";\nimport useStore from \"../store\";\nimport { withTheme } from \"@emotion/react\";\nimport { useMemo } from \"react\";\nimport { WNAudioNode } from \"../types\";\nimport { Theme } from \"../theme\";\n\ninterface NodeInfoModalProps {\n isOpen: boolean;\n onClose: () => void;\n type: string;\n node: WNAudioNode;\n}\n\nconst NodeInfoWrapper = withTheme(styled.div<{ theme: Theme }>`\n height: 100%;\n width: 100%;\n overflow: scroll;\n padding: 0.6rem;\n box-sizing: border-box;\n\n hr {\n border: none;\n border-bottom: 1px solid ${({ theme }) => theme.colors.elevation3};\n }\n\n code {\n background-color: ${({ theme }) => theme.colors.elevation3};\n color: ${({ theme }) => theme.colors.highlight3};\n font-family:\n source-code-pro, Menlo, Monaco, Consolas, \"Courier New\", monospace;\n }\n\n pre {\n background-color: ${({ theme }) => theme.colors.elevation3};\n padding: 0.2rem 0.3rem;\n border-radius: 1px;\n }\n\n a {\n color: ${({ theme }) => theme.colors.accent1};\n }\n`);\n\nconst useNodeManifest = (type: string) => {\n const data = useStore((store) => store.nodesConfiguration[type]);\n return data;\n};\n\nconst NodeInfoModal = ({\n isOpen,\n onClose,\n type: nodeType,\n node,\n}: NodeInfoModalProps) => {\n const { info, portsDescription } = useNodeManifest(nodeType);\n\n const portsInfo = useMemo(() => {\n const parts: string[] = [];\n\n const inputPorts = node.inputs;\n if (inputPorts) {\n parts.push(`## Inputs`);\n for (const portName in inputPorts) {\n const port = inputPorts[portName];\n parts.push(`### *${portName}*`);\n\n const description = portsDescription?.inputs?.[portName];\n\n if (description) {\n parts.push(description);\n }\n\n if (Array.isArray(port.type)) {\n parts.push(`**Types**: \\`${port.type.join(\", \")}\\``);\n } else {\n parts.push(`**Type**: \\`${port.type || \"unknown\"}\\``);\n }\n\n if (port.range) {\n parts.push(`**Range**: \\`[${port.range[0]}, ${port.range[1]}]\\``);\n }\n\n if (port.defaultValue !== undefined) {\n parts.push(`**Default**: \\`${port.defaultValue}\\``);\n }\n }\n }\n\n const outputPorts = node.outputs;\n if (outputPorts) {\n parts.push(`## Outputs`);\n for (const portName in outputPorts) {\n const port = outputPorts[portName];\n parts.push(`### *${portName}*`);\n\n const description = portsDescription?.outputs?.[portName];\n\n if (description) {\n parts.push(description);\n }\n\n if (Array.isArray(port.type)) {\n parts.push(`**Types**: \\`${port.type.join(\", \")}\\``);\n } else {\n parts.push(`**Type**: \\`${port.type || \"unknown\"}\\``);\n }\n\n if (port.range) {\n parts.push(`**Range**: \\`[${port.range[0]}, ${port.range[1]}]\\``);\n }\n\n if (port.defaultValue !== undefined) {\n parts.push(`**Default**: \\`${port.defaultValue}\\``);\n }\n }\n }\n return parts.join(\"\\n\\n\");\n }, [node, portsDescription]);\n\n const combinedInfo = (info || \"\") + \"\\n\\n\" + portsInfo;\n\n return isOpen ? (\n <Modal onClose={onClose}>\n <NodeInfoWrapper\n dangerouslySetInnerHTML={{ __html: marked(combinedInfo || \"\") }}\n />\n </Modal>\n ) : null;\n};\n\nexport default NodeInfoModal;\n","import styled from \"@emotion/styled\";\nimport { withTheme } from \"@emotion/react\";\nimport { Resizable } from \"re-resizable\";\nimport { ComponentProps, useMemo, useState } from \"react\";\nimport {\n MdSettings as SettingsIcon,\n MdInfoOutline as InfoIcon,\n} from \"react-icons/md\";\nimport { Handle, HandleProps, NodeProps, Position } from \"reactflow\";\nimport { DRAG_HANDLE_CLASS, PortType } from \"../../constants\";\nimport useAudioNode from \"../../hooks/useAudioNode\";\nimport useNode from \"../../hooks/useNode\";\nimport useTheme from \"../../hooks/useTheme\";\nimport useStore from \"../../store\";\nimport { Theme } from \"../../theme\";\nimport { AudioPort, WNNodeData } from \"../../types\";\nimport EditableLabel from \"../EditableLabel\";\nimport NodeInfoModal from \"../NodeInfoModal\";\n\nconst NodeWrapper = styled.div`\n background-color: var(--leva-colors-elevation1);\n`;\n\nconst NodeLoaderWrapper = styled(NodeWrapper)`\n padding: 2rem 5rem;\n`;\n\nconst NodeErrorWrapper = styled(NodeWrapper)`\n padding: 1rem 2rem;\n`;\n\nconst SettingsIconWrapper = styled(SettingsIcon)`\n font-size: 1.2rem;\n opacity: 0.4;\n width: 1rem;\n &:hover {\n opacity: 1;\n cursor: pointer;\n }\n`;\n\nconst InfoIconWrapper = styled(InfoIcon)`\n font-size: 1.2rem;\n opacity: 0.4;\n width: 1rem;\n &:hover {\n opacity: 1;\n cursor: pointer;\n }\n`;\n\nconst Section = styled.div`\n position: relative;\n font-family: var(--leva-fonts-mono);\n font-size: var(--leva-fontSizes-root);\n background-color: var(--leva-colors-elevation1);\n`;\n\nexport const TitleBarInner = styled(Section)`\n display: flex;\n height: var(--leva-sizes-titleBarHeight);\n touch-action: none;\n align-items: center;\n justify-content: center;\n flex: 1 1 0%;\n color: var(--leva-colors-highlight1);\n padding: 0 0.4rem;\n gap: 0.3rem;\n`;\n\nexport const PortsPanel = styled(Section)<{ theme: Theme }>(\n ({ theme }) => `\n display: grid;\n grid-template-areas: \"inputs outputs\";\n background: ${theme.colors.elevation2};\n border-bottom: 1px solid ${theme.colors.elevation1};\n color: ${theme.colors.highlight3};\n font-size: 0.6rem;\n`,\n);\n\nexport const InputPorts = styled.div`\n grid-area: inputs;\n text-align: left;\n`;\n\nexport const OutputPorts = styled.div`\n grid-area: outputs;\n text-align: right;\n`;\n\nexport const Port = styled.div`\n position: relative;\n padding: 5px 10px;\n`;\n\nconst portColors = {\n [PortType.Audio]: \"#4ade80\", // vibrant green\n [PortType.Gate]: \"#c084fc\", // rich purple\n [PortType.Number]: \"#38bdf8\", // bright blue\n [PortType.Any]: \"#ffffff\", // white\n};\n\nconst StyledHandle = withTheme(styled(Handle, {\n shouldForwardProp: (prop) => prop !== \"portType\",\n})<{ portType?: AudioPort[\"type\"]; theme: Theme }>`\n --port-color: ${(props) => {\n if (!props.portType) return props.theme.colors.highlight2;\n if (Array.isArray(props.portType)) {\n const colors = props.portType.map((type) => portColors[type]);\n return `linear-gradient(0.25turn, ${colors.join(\", \")});`;\n } else {\n return portColors[props.portType];\n }\n }};\n border-color: var(--port-color);\n background: var(--port-color);\n box-shadow: 0px 0px 0px 1px ${({ theme }) => theme.colors.elevation2} inset;\n`);\n\nconst StyledInputHandle = withTheme(styled(StyledHandle)`\n left: -3px;\n`);\n\nexport const InputHandle = ({\n ...props\n}: Partial<HandleProps & ComponentProps<typeof StyledInputHandle>>) => (\n <StyledInputHandle {...props} type=\"target\" position={Position.Left} />\n);\n\nexport const StyledOutputHandle = withTheme(styled(StyledHandle)`\n right: -3px;\n`);\n\nexport const OutputHandle = (\n props: Partial<HandleProps & ComponentProps<typeof StyledOutputHandle>>,\n) => <StyledOutputHandle {...props} type=\"source\" position={Position.Right} />;\n\nexport type WNNodeProps<T = Record<string, unknown>> = NodeProps<\n T & WNNodeData\n>;\n\nexport interface TitleBarProps {\n className?: string;\n [key: string]: unknown;\n}\n\nexport const TitleBar = ({ className, ...props }: TitleBarProps) => (\n <TitleBarInner\n {...props}\n className={[className, DRAG_HANDLE_CLASS].join(\" \")}\n />\n);\n\nexport interface WNNodeParameters extends NodeProps {\n children?: any;\n}\n\nconst useNodeManifest = (type: string) => {\n const data = useStore((store) => store.nodesConfiguration[type]);\n\n return data;\n};\n\nconst useConfigNode = (type: string) => {\n const { configNode: ConfigNode } = useNodeManifest(type);\n\n return {\n ConfigNode,\n };\n};\n\nexport const WNNode = (props: WNNodeParameters) => {\n const { id, children, selected, ...rest } = props;\n const theme = useTheme();\n const getNode = useStore(({ getNode }) => getNode);\n const nodesConfiguration = useStore((store) => store.nodesConfiguration);\n\n const [isInfoModalShown, setIsInfoModalShown] = useState(false);\n\n const { info } = useNodeManifest(props.type);\n\n const isResizeable = useMemo(\n () => nodesConfiguration[props.type].resizable ?? false,\n [nodesConfiguration, props.type],\n );\n\n const { updateNodeLabel, updateNodeConfig } = useNode(id);\n const { data } = getNode(id) || {};\n const audioNode = useAudioNode(id);\n const { ConfigNode } = useConfigNode(rest.type);\n\n const [configMode, setShowConfigMode] = useState(false);\n\n if (!audioNode) {\n return (\n <NodeLoaderWrapper className={DRAG_HANDLE_CLASS}>\n can't find audio node\n </NodeLoaderWrapper>\n );\n }\n\n if (audioNode.loading) {\n return (\n <NodeLoaderWrapper className={DRAG_HANDLE_CLASS}>\n loading\n </NodeLoaderWrapper>\n );\n }\n\n if (audioNode.error) {\n return (\n <NodeErrorWrapper className={DRAG_HANDLE_CLASS}>\n error: {audioNode.error.toString()}\n </NodeErrorWrapper>\n );\n }\n\n if (!audioNode.node) {\n return (\n <NodeLoaderWrapper className={DRAG_HANDLE_CLASS}>\n can't find audio node\n </NodeLoaderWrapper>\n );\n }\n\n const size = data?.config?.size as\n | { width: number; height: number }\n | undefined;\n\n const {\n node: { inputs, outputs },\n } = audioNode;\n\n return (\n <NodeWrapper>\n <TitleBar>\n {info && (\n <InfoIconWrapper onClickCapture={() => setIsInfoModalShown(true)} />\n )}\n <EditableLabel\n value={data?.label ?? \"No Name\"}\n onChange={updateNodeLabel}\n />\n {ConfigNode && (\n <SettingsIconWrapper\n onClickCapture={() => setShowConfigMode((isShown) => !isShown)}\n />\n )}\n </TitleBar>\n <PortsPanel theme={theme}>\n <InputPorts>\n {inputs\n ? Object.keys(inputs).map((key, index) => (\n <Port key={index}>\n <InputHandle id={key} portType={inputs[key].type} />\n <span>{key}</span>\n </Port>\n ))\n : null}\n </InputPorts>\n <OutputPorts>\n {outputs\n ? Object.keys(outputs).map((key, index) => (\n <Port key={index}>\n <OutputHandle id={key} portType={outputs[key].type} />\n <span>{key}</span>\n </Port>\n ))\n : null}\n </OutputPorts>\n </PortsPanel>\n {ConfigNode && configMode && selected ? (\n <ConfigNode {...props} />\n ) : isResizeable ? (\n <Resizable\n size={size}\n minWidth={180}\n minHeight={30}\n enable={{\n bottom: true,\n bottomRight: true,\n right: true,\n }}\n onResizeStop={(e, direction, ref, d) => {\n const newSize = size\n ? {\n width: size.width + d.width,\n height: size.height + d.height,\n }\n : ref.getBoundingClientRect();\n updateNodeConfig({\n ...data?.config,\n size: newSize,\n });\n }}\n >\n {children}\n </Resizable>\n ) : (\n children\n )}\n <NodeInfoModal\n isOpen={isInfoModalShown}\n type={props.type}\n onClose={() => setIsInfoModalShown(false)}\n node={audioNode.node}\n />\n </NodeWrapper>\n );\n};\n","import { Edge, Node, Viewport } from \"reactflow\";\nimport { WNNodeProps } from \"./components/Node\";\nimport { PortType } from \"./constants\";\n\nexport type AudioNodeChannel = [AudioNode, number];\n\nexport interface AudioPort {\n aliases?: string[];\n type?: PortType | PortType[];\n range?: [number, number];\n defaultValue?: number;\n mono?: boolean;\n}\n\nexport interface InputPort extends AudioPort {\n port: AudioNode | AudioNodeChannel | AudioParam;\n}\n\nexport interface OutputPort extends AudioPort {\n port: AudioNode | AudioNodeChannel;\n}\n\nexport interface WNAudioNode extends Record<string, any> {\n inputs?: Record<string, InputPort | never>;\n outputs?: Record<string, OutputPort | never>;\n destroy?: () => void;\n setValues?: (values?: any) => void;\n}\n\nexport type CreateWNAudioNode<T = WNAudioNode> = (\n audioContext: AudioContext,\n data?: WNNodeData,\n) => T | Promise<T>;\n\nexport type NodeDefaultConfig = {\n size?: { width: number; height: number };\n};\n\nexport interface WNNodeData<\n Values = Record<string, unknown>,\n Config = Record<string, unknown>,\n> {\n label: string;\n values?: Values;\n config?: Config & NodeDefaultConfig;\n}\n\nexport type WNNode = Node<WNNodeData>;\nexport type WNEdge = Edge;\n\nexport interface GraphState {\n nodes: WNNode[];\n edges: WNEdge[];\n}\n\nexport type ControlPanelNodes = Array<{\n id: WNNode[\"id\"];\n width?: number;\n height?: number;\n x?: number;\n y?: number;\n}>;\n\nexport interface ControlPanelState {\n show: boolean;\n nodes: ControlPanelNodes;\n size: { width: number; height: number };\n}\n\nexport interface EditorStoreState extends GraphState {\n controlPanel: ControlPanelState;\n}\n\nexport interface EditorState extends EditorStoreState {\n viewport: Viewport;\n}\n\nexport interface EditorFile {\n id: string;\n name?: string;\n type: \"patch\" | never;\n file: EditorState;\n}\n\nexport interface AudioFile {\n id: string;\n name?: string;\n type: \"audio\";\n file: string;\n}\n\nexport type ProjectFile = EditorFile | AudioFile;\n\nexport interface Project {\n files: Array<ProjectFile>;\n}\n\nexport interface ControlPanelNodeProps {\n node: WNNode;\n audioNode?: WNAudioNode | null;\n updateNodeValues?: (param: any) => void;\n}\n\n// export type ControlPanelNode = (props: ControlPanelNodeProps) => any;\nexport type ControlPanelNode = any;\nexport type ConfigNode = (props: WNNodeProps<WNNodeData>) => any;\n\nexport interface PluginComponent {\n id?: string;\n type: string;\n node: any;\n audioNode: CreateWNAudioNode | false;\n controlPanelNode?: ControlPanelNode;\n configNode?: ConfigNode;\n defaultConfig?: any;\n resizable?: boolean;\n description?: string;\n portsDescription?: {\n inputs?: Record<string, string>;\n outputs?: Record<string, string>;\n };\n name?: string;\n info?: string;\n tags?: string[];\n}\n\nexport interface PluginConfig {\n id?: string;\n components: Array<PluginComponent>;\n name?: string;\n description?: string;\n}\n","import { WNNode } from \"../types\";\n\nconst generateNodeId = (node?: Partial<WNNode>): WNNode[\"id\"] => {\n const random = +new Date() + Math.floor(Math.random() * 1000);\n if (!node?.type) {\n return random.toString();\n }\n return `${node.type}-${random}`;\n};\n\nexport default generateNodeId;\n","import {\n addEdge,\n applyEdgeChanges,\n applyNodeChanges,\n OnConnect,\n OnEdgesChange,\n OnNodesChange,\n NodeTypes,\n} from \"reactflow\";\nimport { StateCreator } from \"zustand\";\nimport { DRAG_HANDLE_SELECTOR } from \"../constants\";\nimport type { WNNode, WNNodeData, WNEdge, GraphState } from \"../types\";\n\n//@TODO: rename to NodesStore\nexport interface NodesState {\n nodes: WNNode[];\n edges: WNEdge[];\n onNodesChange: OnNodesChange;\n onEdgesChange: OnEdgesChange;\n onConnect: OnConnect;\n addNode: (node: WNNode) => void;\n setNodes: (nodes: WNNode[]) => void;\n setEdges: (edges: WNEdge[]) => void;\n setNodesAndEdges: (elements: GraphState) => void;\n getNodesAndEdges: () => GraphState;\n clearElements: () => void;\n getNode: (id: string) => WNNode | null;\n updateNodeData: (id: string, data: Partial<WNNodeData>) => void;\n nodeTypes: NodeTypes;\n setNodeTypes: (nodeTypes: NodeTypes) => void;\n}\n\nconst nodesStateCreator: StateCreator<NodesState> = (set, get) => ({\n nodes: [],\n edges: [],\n onNodesChange: (changes) => {\n set(({ nodes }) => ({\n nodes: applyNodeChanges(changes, nodes).map((node) => ({\n dragHandle: DRAG_HANDLE_SELECTOR,\n ...node,\n })),\n }));\n },\n onEdgesChange: (changes) => {\n set(({ edges }) => ({\n edges: applyEdgeChanges(changes, edges),\n }));\n },\n onConnect: (connection) => {\n set(({ edges }) => ({\n edges: addEdge(connection, edges),\n }));\n },\n addNode: (node) => {\n set(({ nodes }) => ({\n nodes: nodes.concat(node),\n }));\n },\n setNodes: (nodes) => {\n set({\n nodes,\n });\n },\n setEdges: (edges) => {\n set({\n edges,\n });\n },\n setNodesAndEdges: ({ nodes, edges }) => {\n set({\n nodes,\n edges,\n });\n },\n getNodesAndEdges: () => {\n const { nodes, edges } = get();\n return { nodes, edges };\n },\n clearElements: () => {\n set({\n nodes: [],\n edges: [],\n });\n },\n getNode: (id) => {\n const { nodes } = get();\n const node = nodes.find((node) => node.id === id);\n return node || null;\n },\n updateNodeData: (id, data) => {\n set(({ nodes }) => {\n return {\n nodes: nodes.map((node) => {\n if (node.id === id) {\n return {\n ...node,\n data: {\n ...node.data,\n ...data,\n },\n };\n }\n\n return node;\n }),\n };\n });\n },\n nodeTypes: {},\n setNodeTypes: (nodeTypes) => set({ nodeTypes }),\n});\n\nexport default nodesStateCreator;\n","import type { StateCreator } from \"zustand\";\nimport type { StoreState } from \"../\";\nimport * as jsondiffpatch from \"jsondiffpatch\";\nimport type { Delta, FilterContext } from \"jsondiffpatch\";\n\nexport interface HistoryState {\n history: {\n maxHistoryLength: number;\n buffer: Array<Delta>;\n pointer: number;\n skipCollect: boolean;\n push: (changes: Delta) => void;\n back: () => void;\n forward: () => void;\n clear: () => void;\n };\n}\n\nconst cloneObject = <T = unknown>(input: T): T => {\n return JSON.parse(JSON.stringify(input));\n};\n\nexport const historyStateCreator: StateCreator<HistoryState> = (set, get) => ({\n history: {\n maxHistoryLength: 5,\n buffer: [],\n pointer: 0,\n skipCollect: false,\n push: (changes: Delta) => {\n const { history } = get();\n const { maxHistoryLength, skipCollect } = history;\n\n if (skipCollect) {\n set({\n history: {\n ...history,\n skipCollect: false,\n },\n });\n return;\n }\n\n set(({ history }) => {\n if (!history) {\n return {};\n }\n const { buffer, pointer } = history;\n\n const newBuffer = buffer.slice(\n Math.max(pointer - maxHistoryLength + 1, 0),\n pointer,\n );\n\n return {\n history: {\n ...history,\n buffer: [...newBuffer, changes],\n pointer: Math.min(pointer + 1, maxHistoryLength),\n },\n };\n });\n },\n back: () => {\n const { nodes, edges, controlPanel, history } = get() as StoreState;\n const { buffer, pointer } = history;\n\n const patchData = buffer[pointer - 1];\n if (!patchData) {\n return;\n }\n\n const reversedPatchData = jsondiffpatch.reverse(patchData);\n if (!reversedPatchData) {\n return;\n }\n\n const updates = cloneObject({\n nodes,\n edges,\n controlPanel,\n });\n\n const patch = jsondiffpatch.patch(updates, reversedPatchData);\n\n set({\n ...patch,\n history: {\n ...history,\n pointer: pointer - 1,\n skipCollect: true,\n },\n });\n },\n forward: () => {\n const { nodes, edges, controlPanel, history } = get() as StoreState;\n const { buffer, pointer } = history;\n\n const patchData = buffer[pointer];\n if (!patchData) {\n return;\n }\n\n const updates = cloneObject({\n nodes,\n edges,\n controlPanel,\n });\n\n const patch = jsondiffpatch.patch(updates, patchData);\n\n set({\n ...patch,\n history: {\n ...history,\n pointer: pointer + 1,\n skipCollect: true,\n },\n });\n },\n // @TODO: remove this method and store history per file\n clear: () => {\n const { history } = get();\n set({\n history: {\n ...history,\n buffer: [],\n pointer: 0,\n skipCollect: true,\n },\n });\n },\n },\n});\n\nconst COLLECT_CHANGES_DEBOUNCE_TIME = 500;\n\nexport const createChangesCollector = (set: any, get: () => StoreState) => {\n const jsondiffpatchInstance = jsondiffpatch.create({\n propertyFilter: (name: string, context: FilterContext) => {\n //@TODO: rework this function, find better solution\n if (context.parent?.parent?.childName === \"controlPanel\") {\n return true;\n }\n if (\n // @ts-ignore\n [\"data\", \"position\", \"controlPanel\"].includes(context.parent?.childName)\n ) {\n return true;\n }\n return [\n \"controlPanel\",\n \"size\",\n \"edges\",\n\n \"nodes\",\n \"data\",\n \"label\",\n \"config\",\n \"values\",\n\n \"position\",\n \"x\",\n \"y\",\n ].includes(name);\n },\n });\n let oldState: StoreState | null = get();\n let timer: any;\n\n return (state: StoreState, prevState: StoreState) => {\n if (state.currentFileIndex !== prevState.currentFileIndex) {\n get().history.clear();\n }\n clearTimeout(timer);\n if (!oldState) {\n oldState = prevState;\n }\n timer = setTimeout(() => {\n const changes = jsondiffpatchInstance.diff(oldState, state);\n oldState = null;\n\n if (changes) {\n get().history.push(changes);\n }\n }, COLLECT_CHANGES_DEBOUNCE_TIME);\n };\n};\n\ntype StoreStateCreator = StateCreator<StoreState>;\n\nconst history =\n (config: StoreStateCreator): StoreStateCreator =>\n (set, get, api) => {\n const collectChanges = createChangesCollector(set, get);\n api.subscribe(collectChanges);\n return config((...args) => set(...args), get, api);\n };\n\nexport default history;\n","import { WNEdge, WNNode } from \"../../types\";\n\nexport type GraphItems = Array<WNNode | WNEdge>;\n\nexport interface GraphComparisonResult {\n added: GraphItems;\n removed: GraphItems;\n}\n\nexport const compareGraphs = (\n left: GraphItems,\n right: GraphItems,\n): GraphComparisonResult => {\n const setLeft = new Set(left.map((item) => item.id));\n const setRight = new Set(right.map((item) => item.id));\n\n const added = right.filter((item) => !setLeft.has(item.id));\n const removed = left.filter((item) => !setRight.has(item.id));\n\n return { added, removed };\n};\n","import { createPatch, Patch } from \"@web-noise/patch\";\nimport type { StateCreator } from \"zustand\";\nimport type { StoreState } from \"../\";\nimport { compareGraphs } from \"./compareGraphs\";\n\ntype StoreStateCreator = StateCreator<StoreState>;\nexport interface AudioPatchState {\n patch: Patch;\n nodesState: Record<string, any>;\n}\n\nexport const audioPatchStateCreator: StateCreator<AudioPatchState> = (\n set,\n get,\n) => ({\n patch: createPatch(),\n nodesState: {},\n});\n\nconst audioPatch =\n (config: StoreStateCreator): StoreStateCreator =>\n (set, get, api) => {\n api.subscribe(async (state, prevState) => {});\n\n const promises = new Set<Promise<unknown>>();\n\n let currentState = {\n ...get(),\n nodes: [],\n edges: [],\n };\n\n return config(\n async (...args) => {\n const oldState = get();\n const [storeChanges] = args;\n\n //@ts-ignore\n const newState = {\n ...currentState,\n ...(typeof storeChanges === \"function\"\n ? storeChanges({ ...currentState })\n : storeChanges),\n };\n\n const nodeChanges = compareGraphs(currentState.nodes, newState.nodes);\n const edgeChanges = compareGraphs(currentState.edges, newState.edges);\n\n //@ts-ignore\n currentState = newState;\n\n const newNodes = nodeChanges.added;\n const newEdges = edgeChanges.added;\n const removedEdges = edgeChanges.removed;\n const removedNodes = nodeChanges.removed;\n\n const { patch } = oldState;\n\n if (newNodes.length) {\n const promise = patch.registerAudioNodes(\n //@ts-ignore\n newNodes,\n );\n promises.add(promise);\n await promise;\n promises.delete(promise);\n }\n\n if (!(newEdges.length || removedEdges.length || removedNodes.length)) {\n set(...args);\n return;\n }\n\n if (promises.size) {\n try {\n await Promise.all([...promises.values()]);\n } catch (e) {\n console.log(\"some error\", e);\n }\n }\n\n if (newEdges.length) {\n patch.registerAudioConnections(\n //@ts-ignore\n newEdges,\n );\n }\n\n if (removedEdges.length) {\n //@ts-ignore\n patch.unregisterAudioConnections(removedEdges);\n }\n\n if (removedNodes.length) {\n //@ts-ignore\n patch.unregisterAudioNodes(removedNodes);\n }\n\n set(...args);\n },\n get,\n api,\n );\n };\n\nexport default audioPatch;\n","import type { AudioFile, EditorFile, ProjectFile } from \"../types\";\n\nexport const isPatch = (file: ProjectFile): file is EditorFile =>\n !(\"type\" in file) || file.type === \"patch\";\n\nexport const isAudio = (file: ProjectFile): file is AudioFile =>\n file.type === \"audio\";\n","import { StateCreator } from \"zustand\";\nimport type { EditorState, Project, ProjectFile } from \"../types\";\nimport type { StoreState } from \"./\";\nimport { isAudio } from \"../helpers/projectFile\";\n\nexport interface ProjectState {\n project: Project;\n setProject: (project: Project) => void;\n getProject: () => Project;\n\n pullEditorChanges: () => void;\n syncEditorWithCurrentFile: () => void;\n\n //@TODO: move inside project\n currentFileIndex: number;\n setCurrentFileIndex: (index: number) => void;\n\n updateFileContent: (fileIndex: number, file: ProjectFile) => void;\n updateFileName: (fileIndex: number, fileName: string) => void;\n addFile: (file: ProjectFile, name?: string) => void;\n deleteFile: (fileIndex: number) => void;\n}\n\nconst projectStateCreator: StateCreator<ProjectState> = (set, get) => ({\n project: { files: [] },\n setProject(project) {\n set({ project, currentFileIndex: 0 });\n },\n getProject() {\n return get().project;\n },\n\n pullEditorChanges() {\n const { getEditorState, currentFileIndex, updateFileContent, project } =\n get() as StoreState;\n const currentFile = project.files[currentFileIndex];\n if (isAudio(currentFile)) {\n return;\n }\n updateFileContent(currentFileIndex, {\n ...currentFile,\n file: getEditorState(),\n });\n },\n\n syncEditorWithCurrentFile: () => {\n const { currentFileIndex, setEditorState, project } = get() as StoreState;\n const currentFile = project.files[currentFileIndex];\n if (currentFile.type === \"audio\") {\n console.log(\"audio file. skipping\");\n return;\n }\n setEditorState(currentFile.file);\n },\n\n currentFileIndex: 0,\n setCurrentFileIndex(newFileIndex) {\n const { currentFileIndex } = get();\n if (newFileIndex === currentFileIndex) {\n return;\n }\n set({ currentFileIndex: newFileIndex });\n },\n\n updateFileContent(index, file) {\n const { project } = get();\n set({\n project: {\n ...project,\n files: project.files.map((f, i) => {\n if (i === index) {\n return {\n // @TODO check again if merging is really needed here\n ...f,\n ...file,\n };\n }\n return f;\n }),\n },\n });\n },\n updateFileName(index, fileName) {\n const { project } = get();\n set({\n project: {\n ...project,\n files: project.files.map((f, i) => {\n if (i === index) {\n return {\n ...f,\n name: fileName,\n };\n }\n return f;\n }),\n },\n });\n },\n addFile(file) {\n const { project } = get();\n const files = [...project.files, file];\n set({\n project: {\n ...project,\n files,\n },\n });\n },\n deleteFile: (fileIndex) => {\n const { project, currentFileIndex } = get();\n\n set({\n project: {\n ...project,\n files: project.files.filter((_, index) => fileIndex !== index),\n },\n });\n\n if (fileIndex <= currentFileIndex) {\n set({ currentFileIndex: currentFileIndex - 1 });\n }\n },\n});\n\nexport default projectStateCreator;\n","import {\n addEdge,\n getConnectedEdges,\n NodeTypes,\n OnConnect,\n Viewport,\n} from \"reactflow\";\nimport { create, StateCreator } from \"zustand\";\nimport { setAudioNodeTypes, AudioNodeTypes } from \"@web-noise/patch\";\nimport { CONTROL_PANEL_GRID_CONFIG } from \"../constants\";\nimport generateNodeId from \"../helpers/generateNodeId\";\nimport {\n ControlPanelNode,\n ControlPanelNodes,\n PluginComponent,\n PluginConfig,\n WNEdge,\n WNNode,\n GraphState,\n EditorState,\n ControlPanelState,\n EditorStoreState,\n} from \"../types\";\nimport nodesStateCreator, { NodesState } from \"./nodesStore\";\nimport history, { historyStateCreator, HistoryState } from \"./history\";\nimport audioPatch, {\n AudioPatchState,\n audioPatchStateCreator,\n} from \"./audioPatch\";\nimport projectStateCreator, { ProjectState } from \"./projectStore\";\n\nexport type { AudioNodeTypes, NodesState, GraphState };\n\ninterface EditorConfig {\n showMinimap: boolean;\n}\n\ntype NodesConfiguration = Record<string, PluginComponent>;\n\nexport type StoreState = NodesState &\n HistoryState &\n ProjectState &\n AudioPatchState & {\n setGraph: (elements: { nodes: WNNode[]; edges: WNEdge[] }) => Promise<void>;\n clearGraph: () => void;\n createNode: (node: WNNode) => void;\n createNodes: (node: WNNode[]) => Promise<void>;\n removeNode: (node: WNNode) => void;\n removeNodes: (nodes: WNNode[]) => void;\n removeEdges: (nodes: WNEdge[]) => void;\n onConnect: OnConnect;\n createEdges: (edge: WNEdge[]) => void;\n onEdgesDelete: (edges: WNEdge[]) => void;\n onNodesDelete: (nodes: WNNode[]) => Promise<void>;\n plugins: Array<PluginConfig>;\n setPlugins: (plugins: Array<PluginConfig>) => void;\n nodesConfiguration: NodesConfiguration;\n config: EditorConfig;\n setConfig: (config: Partial<EditorConfig>) => void;\n getEditorState: () => EditorState;\n setEditorState: (state: EditorState) => Promise<void>;\n isHelpShown: boolean;\n toggleHelp: () => void;\n copyBuffer: { nodes: WNNode[]; edges: WNEdge[] };\n copy: (elements: { nodes: WNNode[]; edges: WNEdge[] }) => void;\n copySelectedItems: () => void;\n pasteBuffer: (x: number, y: number) => void;\n /* move to control panel store */\n getControlPanelNode: (node: WNNode) => ControlPanelNode | null;\n controlPanel: ControlPanelState;\n setControlPanelNodes: (nodes: ControlPanelNodes) => void;\n showControlPanel: () => void;\n hideControlPanel: () => void;\n setControlPanelSize: (width: { width: number; height: number }) => void;\n addNodeToControlPanel: (node: WNNode) => void;\n removeNodeFromControlPanel: (node: WNNode) => void;\n removeNodesFromControlPanel: (nodes: WNNode[]) => void;\n /* / move to control panel store */\n viewport: Viewport;\n setViewport: (viewport: Viewport) => void;\n };\n\nexport const stateCreator: StateCreator<StoreState> = (...args) => {\n const [set, get] = args;\n return {\n ...nodesStateCreator(...args),\n ...historyStateCreator(...args),\n ...audioPatchStateCreator(...args),\n ...projectStateCreator(...args),\n\n setGraph: async ({ nodes, edges }) => {\n const {\n patch,\n createNodes,\n createEdges,\n setNodesAndEdges,\n nodes: activeNodes,\n edges: activeEdges,\n } = get();\n setNodesAndEdges({ nodes: [], edges: [] });\n\n await createNodes(nodes);\n createEdges(edges);\n },\n clearGraph: () => {\n const { setGraph } = get();\n setGraph({ nodes: [], edges: [] });\n },\n createNodes: async (nodes) => {\n const { createNode } = get();\n await Promise.all(nodes.map((node) => createNode(node)));\n },\n createNode: (nodeData) => {\n const { addNode, nodesConfiguration } = get();\n\n const { type, id, data } = nodeData;\n\n if (typeof type === \"undefined\") {\n throw new Error(`node type is not defined for node: ${id}`);\n }\n\n const node = {\n ...nodeData,\n data: {\n ...data,\n config: {\n ...nodesConfiguration[type]?.defaultConfig,\n ...data?.config,\n },\n },\n };\n\n addNode(node);\n },\n removeNode: (node) => get().removeNodes([node]),\n removeNodes: (nodes) => {\n const {\n edges,\n nodes: currentNodes,\n onNodesDelete,\n removeEdges,\n removeNodesFromControlPanel,\n } = get();\n const parentNodeIds = nodes.map(({ id }) => id);\n const children = currentNodes.filter(\n ({ parentNode }) => parentNode && parentNodeIds.includes(parentNode),\n );\n const resultingNodes = [...nodes, ...children];\n removeNodesFromControlPanel(resultingNodes);\n const connectedEdges = getConnectedEdges(resultingNodes, edges);\n removeEdges(connectedEdges);\n onNodesDelete(resultingNodes);\n const nodeIds = resultingNodes.map(({ id }) => id);\n set({\n nodes: currentNodes.filter(({ id }) => !nodeIds.includes(id)),\n });\n },\n removeEdges: (edges) => {\n const { edges: currentEdges, onEdgesDelete } = get();\n const edgeIds = edges.map(({ id }) => id);\n onEdgesDelete(edges);\n set({\n edges: currentEdges.filter(({ id }) => !edgeIds.includes(id)),\n });\n },\n createEdges: (newEdges) => {\n const { patch, edges, setEdges } = get();\n setEdges(newEdges);\n },\n onConnect: async (connection) => {\n const { edges, createEdges } = get();\n const newEdges = addEdge(connection, edges);\n createEdges(newEdges);\n },\n onEdgesDelete: (edges) => {\n const { patch } = get();\n },\n onNodesDelete: async (nodes) => {\n const { removeNodesFromControlPanel, patch } = get();\n removeNodesFromControlPanel(nodes);\n },\n plugins: [],\n setPlugins: async (plugins) => {\n const { setNodeTypes } = get();\n set({ plugins });\n\n const nodesConf: NodesConfiguration = plugins.reduce((acc, plugin) => {\n return {\n ...acc,\n ...plugin.components.reduce(\n (subAcc, item) => ({\n ...subAcc,\n [item.type]: item,\n }),\n {},\n ),\n };\n }, {});\n\n const nodeTypes: NodeTypes = Object.keys(nodesConf).reduce(\n (acc, type) => {\n return {\n ...acc,\n [type]: nodesConf[type].node,\n };\n },\n {},\n );\n\n const audioNodeTypes: AudioNodeTypes = Object.keys(nodesConf).reduce(\n (acc, type) => {\n return {\n ...acc,\n [type]: nodesConf[type].audioNode,\n };\n },\n {},\n );\n\n setAudioNodeTypes(audioNodeTypes);\n setNodeTypes(nodeTypes);\n\n set(({ nodesConfiguration }) => ({\n nodesConfiguration: { ...nodesConfiguration, ...nodesConf },\n }));\n },\n nodesConfiguration: {},\n config: { showMinimap: false },\n setConfig: (changes) => {\n set(({ config }) => ({ config: { ...config, ...changes } }));\n },\n getEditorState: () => {\n const { getNodesAndEdges, controlPanel, viewport } = get();\n return {\n ...getNodesAndEdges(),\n controlPanel,\n viewport,\n };\n },\n setEditorState: async ({ nodes, edges, controlPanel, viewport }) => {\n const { setGraph } = get();\n await setGraph({ nodes, edges });\n // @TODO: remove this line once audio patch initialisation is reworked\n await new Promise((r) => setTimeout(r, 1000));\n set({\n controlPanel,\n viewport,\n });\n },\n isHelpShown: false,\n toggleHelp: () => {\n const { isHelpShown: showHelp } = get();\n set({ isHelpShown: !showHelp });\n },\n copyBuffer: { nodes: [], edges: [] },\n copy: (elements) => {\n set({ copyBuffer: elements });\n },\n copySelectedItems: () => {\n const { nodes: currentNodes, edges: currentEdges, copy } = get();\n const nodes = currentNodes.filter(({ selected }) => selected);\n const edges = currentEdges.filter(({ selected }) => selected);\n if (!nodes.length) {\n return;\n }\n copy({ nodes, edges });\n },\n pasteBuffer: (x = 0, y = 0) => {\n const { copyBuffer, createNodes, setEdges, nodes, edges } = get();\n const { nodes: nodesToCopy, edges: edgesToCopy } = copyBuffer;\n\n if (!nodesToCopy.length) {\n return;\n }\n\n set({\n nodes: nodes.map((node) => ({ ...node, selected: false })),\n });\n\n const topLeftNode = nodesToCopy.reduce((acc, node) => {\n if (!acc) {\n return node;\n }\n if (\n node.position.x < acc.position.x &&\n node.position.y < acc.position.y\n ) {\n return node;\n }\n return acc;\n });\n\n const xDelta = topLeftNode.position.x - x;\n const yDelta = topLeftNode.position.y - y;\n\n const { nodes: newNodes, mapping } = nodesToCopy.reduce(\n (acc, node) => {\n const newNodeId = generateNodeId(node);\n return {\n nodes: [\n ...acc.nodes,\n {\n ...node,\n id: newNodeId,\n position: {\n x: node.position.x - xDelta,\n y: node.position.y - yDelta,\n },\n selected: true,\n },\n ],\n mapping: {\n ...acc.mapping,\n [node.id]: newNodeId,\n },\n };\n },\n { nodes: [], mapping: {} } as {\n nodes: Array<WNNode>;\n mapping: Record<WNNode[\"id\"], WNNode[\"id\"]>;\n },\n );\n createNodes(newNodes);\n\n const newEdges = edgesToCopy.map((edge) => {\n const source = mapping[edge.source] || edge.source;\n const target = mapping[edge.target] || edge.target;\n return {\n ...edge,\n id: edge.id.replace(edge.source, source).replace(edge.target, target),\n source,\n target,\n selected: true,\n };\n });\n setEdges([\n ...edges.map((edge) => ({ ...edge, selected: false })),\n ...newEdges,\n ]);\n },\n getControlPanelNode: (node) => {\n const { nodesConfiguration } = get();\n const { type } = node;\n if (!type) {\n return null;\n }\n const controlPanelNode = nodesConfiguration[type]?.controlPanelNode;\n if (!controlPanelNode) {\n console.error(`could not find node for type ${type}`);\n return null;\n }\n return controlPanelNode;\n },\n controlPanel: {\n show: true,\n nodes: [],\n size: {\n width: 200,\n height: 100,\n },\n },\n showControlPanel: () =>\n set(({ controlPanel }) => ({\n controlPanel: { ...controlPanel, show: true },\n })),\n hideControlPanel: () =>\n set(({ controlPanel }) => ({\n controlPanel: { ...controlPanel, show: false },\n })),\n addNodeToControlPanel: (node) => {\n const { nodesConfiguration } = get();\n const defaultConfig = node.type\n ? nodesConfiguration[node.type]?.defaultConfig\n : {};\n const { height } = defaultConfig?.size || {};\n const newNode = {\n id: node.id,\n ...(height\n ? { height: height / CONTROL_PANEL_GRID_CONFIG.rowHeight }\n : {}),\n };\n set(({ controlPanel }) => ({\n controlPanel: {\n ...controlPanel,\n nodes: [...controlPanel.nodes, newNode],\n },\n }));\n },\n removeNodeFromControlPanel: (node) =>\n get().removeNodesFromControlPanel([node]),\n removeNodesFromControlPanel: (nodes) => {\n const nodeIds = nodes.map(({ id }) => id);\n set(({ controlPanel }) => {\n const nodes = controlPanel.nodes.filter(\n ({ id }) => !nodeIds.includes(id),\n );\n return {\n controlPanel: {\n ...controlPanel,\n nodes,\n },\n };\n });\n },\n setControlPanelNodes: (nodes) => {\n set(({ controlPanel }) => {\n return {\n controlPanel: {\n ...controlPanel,\n nodes,\n },\n };\n });\n },\n setControlPanelSize: (size) => {\n set(({ controlPanel }) => {\n return {\n controlPanel: {\n ...controlPanel,\n size,\n },\n };\n });\n },\n\n viewport: { x: 0, y: 0, zoom: 1 },\n setViewport: (viewport) => set({ viewport }),\n };\n};\n\nconst useStore = create<StoreState>(audioPatch(history(stateCreator)));\n\nexport default useStore;\n","import { injectGlobal } from \"@emotion/css\";\nimport theme from \"./theme\";\n\ninjectGlobal`\n .react-flow {\n .react-flow__pane {\n /* background: rgb(106 106 106); */\n /* background: \"white\"; */\n // background: #292d39;\n background: ${theme.colors.elevation3};\n }\n\n .react-flow__background {\n /* background: #efefef; */\n stroke: white;\n }\n\n .react-flow__node-default {\n background: #292d39;\n color: white;\n border: none;\n /* background: transparent; */\n }\n\n .react-flow__node {\n padding: 0;\n width: auto;\n }\n\n .react-flow__edge-path {\n stroke: ${theme.colors.accent2};\n }\n\n .react-flow__node.selected {\n border: 1px solid ${theme.colors.accent2};\n box-shadow: 0 0 0 0.5px #${theme.colors.accent2};\n }\n\n .react-flow__node-default.selected, .react-flow__node-default.selected:hover {\n box-shadow: 0 0 0 0.5px #${theme.colors.accent2};\n }\n\n /* .react-flow__minimap-mask {\n fill: ${theme.colors.elevation1}\n }\n\n .react-flow__minimap-node {\n fill:${theme.colors.accent2}\n } */\n }\n\n`;\n","import styled from \"@emotion/styled\";\nimport { Item, Menu } from \"react-contexify\";\nimport \"react-contexify/dist/ReactContexify.css\";\nimport { Theme } from \"../../theme\";\n\n\nexport const ItemWrapper = styled(Item)``;\n\nexport const MenuWrapper = styled(Menu)<{ colors: Theme[\"colors\"] }>`\n background: ${({ colors }) => colors.elevation2};\n padding: 0;\n border-radius: 0;\n\n .react-contexify__item__content {\n color: ${({ colors }) => colors.whitePrimary};\n }\n\n .react-contexify__separator {\n background-color: ${({ colors }) => colors.elevation1};\n margin: 0;\n }\n`;\n","import { useCallback } from \"react\";\nimport { useContextMenu } from \"react-contexify\";\nimport \"react-contexify/dist/ReactContexify.css\";\nimport useTheme from \"../../hooks/useTheme\";\nimport useStore from \"../../store\";\nimport { ItemWrapper, MenuWrapper } from \"./styles\";\n\nexport const MENU_ID = \"editor-edge-menu\";\n\nexport const useEdgeContextMenu = () => {\n const { show } = useContextMenu({\n id: MENU_ID,\n });\n\n const onContextMenu = useCallback(\n (event: React.MouseEvent<Element, MouseEvent>, edge: unknown) => {\n event.stopPropagation();\n show(event, { props: { edge } });\n },\n [show],\n );\n\n return { onContextMenu };\n};\n\nconst EdgeContextMenu = () => {\n const theme = useTheme();\n\n const removeEdges = useStore((store) => store.removeEdges);\n\n return (\n <>\n <MenuWrapper id={MENU_ID} animation={false} colors={theme.colors}>\n <ItemWrapper onClick={(event) => removeEdges([event.props.edge])}>\n Delete Edge (DEL)\n </ItemWrapper>\n </MenuWrapper>\n </>\n );\n};\n\nexport default EdgeContextMenu;\n","import styled from \"@emotion/styled\";\nimport { useMemo } from \"react\";\nimport useStore from \"../../store\";\nimport { Theme } from \"../../theme\";\nimport { PluginComponent } from \"../../types\";\nimport { FiltersState } from \"./Filters\";\nimport { withTheme } from \"@emotion/react\";\n\nconst PluginsWrapper = withTheme(styled.div<{ theme: Theme }>`\n width: 100%;\n`);\n\nconst PluginWrapper = withTheme(styled.div<{ theme: Theme }>`\n padding: 1rem;\n display: flex;\n flex-direction: column;\n gap: 1rem;\n`);\n\nconst NodesList = withTheme(styled.ul<{ theme: Theme }>`\n list-style: none;\n margin: 0;\n padding: 0;\n padding-bottom: 10px;\n columns: 2;\n\n li {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 4px;\n overflow: hidden;\n border: 1px solid ${({ theme }) => theme.colors.elevation3};\n border-radius: 4px;\n margin-bottom: 0.5rem;\n &:hover {\n border-color: ${({ theme }) => theme.colors.accent2};\n cursor: pointer;\n }\n }\n`);\n\nconst NodeTitle = withTheme(styled.div<{ theme: Theme }>`\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n`);\n\nconst NodeDescription = withTheme(styled.div<{ theme: Theme }>`\n color: ${({ theme }) => theme.colors.highlight2};\n font-size: 12px;\n`);\n\nexport const TagsList = withTheme(styled.div<{ theme: Theme }>`\n display: flex;\n gap: 0.2rem;\n flex-wrap: wrap;\n`);\n\nexport const PluginTag = withTheme(styled.span<{\n theme: Theme;\n isActive?: boolean;\n}>`\n cursor: pointer;\n font-size: 10px;\n background: ${({ theme, isActive }) =>\n isActive ? theme.colors.highlight1 : theme.colors.elevation3};\n border-radius: 2px;\n padding: 0.1rem 0.2rem;\n border: 1px solid;\n border-color: ${({ theme }) => theme.colors.elevation2};\n &:hover {\n border-color: ${({ theme }) => theme.colors.accent2};\n }\n`);\n\nconst PluginHeader = withTheme(styled.div<{ theme: Theme }>``);\n\nconst PluginTitle = withTheme(styled.div<{ theme: Theme }>`\n font-size: 1.1rem;\n padding: 0.25rem 0;\n color: ${({ theme }) => theme.colors.highlight3};\n`);\n\nconst PluginDescription = withTheme(styled.div<{ theme: Theme }>`\n color: ${({ theme }) => theme.colors.highlight2};\n font-size: 12px;\n`);\n\ninterface PluginsProps {\n filters: FiltersState;\n onComponentClick: (component: PluginComponent) => void;\n onTagClick: (tag: string) => void;\n}\n\nconst Plugins = ({\n onComponentClick,\n filters: { plugin, search = \"\", tags },\n onTagClick,\n}: PluginsProps) => {\n const plugins = useStore(({ plugins }) => plugins);\n\n const pluginsGroup = useMemo(() => {\n if (!plugin) {\n return plugins;\n }\n return plugins.filter(({ name }) => name === plugin);\n }, [plugins, plugin]);\n\n const filteredPlugins = useMemo(() => {\n if (!search && !tags?.length) {\n return pluginsGroup;\n }\n const filteredByTags = pluginsGroup.map((plugin) => ({\n ...plugin,\n components: tags?.length\n ? plugin.components.filter((component) =>\n tags?.every((tag) => component.tags?.includes(tag)),\n )\n : plugin.components,\n }));\n return filteredByTags.map((plugin) => ({\n ...plugin,\n components: plugin.components.filter(\n ({ type, name }) =>\n type.toLocaleLowerCase().includes(search.toLocaleLowerCase()) ||\n name?.toLocaleLowerCase().includes(search.toLocaleLowerCase()),\n ),\n }));\n }, [pluginsGroup, search, tags]);\n\n return (\n <PluginsWrapper>\n {filteredPlugins.map(({ name, description, components }, index) => {\n if (!components.length) {\n return null;\n }\n return (\n <PluginWrapper key={index}>\n <PluginHeader>\n <PluginTitle>{name}</PluginTitle>\n <PluginDescription>{description}</PluginDescription>\n </PluginHeader>\n <NodesList>\n {components\n .sort((a, b) =>\n a.type.toLowerCase() > b.type.toLowerCase() ? 1 : -1,\n )\n .map((component, idx) => (\n <li onClick={() => onComponentClick(component)} key={idx}>\n <NodeTitle>{component.name || component.type}</NodeTitle>\n {component.description && (\n <NodeDescription>{component.description}</NodeDescription>\n )}\n <TagsList>\n {component.tags?.map((tag, tagIdx) => (\n <PluginTag\n isActive={tags?.includes(tag)}\n onClickCapture={(event) => {\n event.stopPropagation();\n onTagClick(tag);\n }}\n key={tagIdx}\n >\n {tag}\n </PluginTag>\n ))}\n </TagsList>\n </li>\n ))}\n </NodesList>\n </PluginWrapper>\n );\n })}\n </PluginsWrapper>\n );\n};\n\nexport default Plugins;\n","import styled from \"@emotion/styled\";\nimport { withTheme } from \"@emotion/react\";\nimport { useEffect, useRef } from \"react\";\nimport useTheme from \"../../hooks/useTheme\";\nimport useStore from \"../../store\";\nimport { Theme } from \"../../theme\";\nimport { PluginTag, TagsList } from \"./Plugins\";\n\nconst InputWrapper = styled.div`\n display: flex;\n position: relative;\n`;\n\nconst InputInner = styled.input<{ theme: Theme }>`\n padding-right: 2rem;\n padding-left: 0.3rem;\n width: 100%;\n appearance: textfield;\n font-size: inherit;\n background: none;\n border: none;\n color: var(--leva-colors-highlight1);\n font-family: var(--leva-fonts-mono);\n cursor: inherit;\n text-overflow: ellipsis;\n outline: none;\n appearance: textfield;\n cursor: auto;\n background-color: ${({ theme }) => theme.colors.elevation3};\n border-radius: 0.1rem;\n height: 2rem;\n color: ${({ theme }) => theme.colors.highlight2};\n\n &:focus,\n &:hover {\n box-shadow: 0 0 0 var(--leva-borderWidths-focus)\n ${({ theme }) => theme.colors.accent2};\n color: ${({ theme }) => theme.colors.whitePrimary};\n }\n &::-webkit-inner-spin-button,\n &::-webkit-outer-spin-button {\n -webkit-appearance: none;\n margin-right: 1rem;\n }\n`;\n\nconst FiltersWrapper = styled.div<{ theme: Theme }>`\n display: flex;\n flex-direction: column;\n padding: 0.6rem;\n gap: 0.6rem;\n border-right: 1px solid ${({ theme }) => theme.colors.elevation3};\n min-width: 14rem;\n`;\n\nconst PluginName = styled.label<{ theme: Theme }>`\n user-select: none;\n input {\n display: none;\n }\n input:checked + span {\n color: ${({ theme }) => theme.colors.accent2};\n }\n &:hover {\n color: ${({ theme }) => theme.colors.accent3};\n cursor: pointer;\n }\n`;\n\nconst StyledPluginTag = withTheme(styled(PluginTag)<{ theme: Theme }>`\n font-size: 12px;\n padding: 0.2rem 0.4rem;\n &:hover {\n }\n &::after {\n content: \"×\";\n margin-left: 0.4rem;\n }\n`);\n\nexport interface FiltersState {\n search?: string;\n plugin?: string | null;\n tags?: string[];\n}\n\ninterface FiltersProps {\n value: FiltersState;\n onChange: (filters: FiltersState) => void;\n}\n\nconst Filters = ({ onChange, value }: FiltersProps) => {\n const theme = useTheme();\n const plugins = useStore(({ plugins }) => plugins);\n\n const inputRef = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n if (!inputRef.current) {\n return;\n }\n inputRef.current.focus();\n }, [inputRef]);\n\n return (\n <FiltersWrapper theme={theme}>\n <InputWrapper>\n <InputInner\n ref={inputRef}\n theme={theme}\n value={value.search || \"\"}\n placeholder=\"search...\"\n onChange={(event) =>\n onChange({ ...value, search: event.target.value })\n }\n />\n </InputWrapper>\n <TagsList>\n {value.tags?.map((tag, index) => (\n <StyledPluginTag\n key={index}\n isActive\n onClick={() => {\n const newTags = value.tags?.filter((t) => t !== tag) || [];\n onChange({ ...value, tags: newTags });\n }}\n >\n {tag}\n </StyledPluginTag>\n ))}\n </TagsList>\n {plugins.map(({ name, components }, index) => {\n if (!name) {\n return null;\n }\n return (\n <PluginName theme={theme} key={index}>\n <input\n type=\"checkbox\"\n name=\"plugin\"\n value={name}\n checked={name === value.plugin}\n onChange={() => {\n onChange({\n ...value,\n plugin: name === value.plugin ? null : name,\n });\n }}\n />\n <span>{name}</span>\n </PluginName>\n );\n })}\n </FiltersWrapper>\n );\n};\n\nexport default Filters;\n","import styled from \"@emotion/styled\";\nimport { MouseEvent, useCallback, useState } from \"react\";\nimport { Position, useReactFlow } from \"reactflow\";\nimport useTheme from \"../../hooks/useTheme\";\nimport useStore from \"../../store\";\nimport { Theme } from \"../../theme\";\nimport { PluginComponent } from \"../../types\";\nimport Modal from \"../Modal\";\nimport Filters, { FiltersState } from \"./Filters\";\nimport Plugins from \"./Plugins\";\n\ninterface AddNodeProps {\n isOpen: boolean;\n closeMenu: () => void;\n mousePosition: MousePosition;\n}\n\ntype MousePosition = {\n x: number;\n y: number;\n};\n\nconst AddNodeWrapper = styled.div<{ theme: Theme }>`\n height: 100%;\n width: 100%;\n display: flex;\n gap: 1rem;\n`;\n\nconst PluginsPanel = styled.div<{ theme: Theme }>`\n flex-grow: 1;\n height: 100%;\n overflow-y: scroll;\n`;\n\nconst AddNode = ({ isOpen, closeMenu, mousePosition }: AddNodeProps) => {\n const theme = useTheme();\n const { screenToFlowPosition } = useReactFlow();\n\n const { createNode } = useStore(({ createNode }) => ({\n createNode,\n }));\n\n const [filtersState, setFiltersState] = useState<FiltersState>({});\n\n const onComponentClick = useCallback(\n ({ type }: PluginComponent) => {\n const { x, y } = screenToFlowPosition(mousePosition);\n const newNode = {\n //@TODO: generate node id in `createNode` function\n id: `${type}-${+new Date()}`,\n type,\n data: { label: type },\n position: {\n x,\n y,\n },\n targetPosition: Position.Left,\n sourcePosition: Position.Right,\n };\n createNode(newNode);\n closeMenu();\n },\n [mousePosition, screenToFlowPosition, createNode, closeMenu, mousePosition],\n );\n\n return isOpen ? (\n <Modal\n onClose={() => {\n closeMenu();\n setFiltersState({});\n }}\n >\n <AddNodeWrapper theme={theme}>\n <Filters onChange={setFiltersState} value={filtersState} />\n <PluginsPanel theme={theme}>\n <Plugins\n filters={filtersState}\n onTagClick={(tag) => {\n setFiltersState((state) => ({\n ...state,\n tags: state.tags?.includes(tag)\n ? state.tags.filter((t) => t !== tag)\n : [...(state.tags || []), tag],\n }));\n }}\n onComponentClick={(component) => {\n onComponentClick(component);\n setFiltersState({});\n }}\n />\n </PluginsPanel>\n </AddNodeWrapper>\n </Modal>\n ) : null;\n};\n\nexport default AddNode;\n","import styled from \"@emotion/styled\";\nimport { MouseEvent, useCallback, useState, useRef, ChangeEvent } from \"react\";\nimport { FileDrop } from \"react-file-drop\";\nimport { FaFileUpload } from \"react-icons/fa\";\nimport useTheme from \"../hooks/useTheme\";\nimport useStore from \"../store\";\nimport { Theme } from \"../theme\";\nimport Modal from \"./Modal\";\n\ninterface UploadPatchProps {\n isOpen: boolean;\n closeMenu: () => void;\n}\n\nconst UploadPatchWrapper = styled.div<{ theme: Theme }>`\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n width: 100%;\n\n .drop-zone-wrapper {\n width: 80%;\n height: 80%;\n }\n\n .drop-zone {\n height: 100%;\n width: 100%;\n border-style: dashed;\n border-width: 2px;\n border-color: ${({ theme }) => theme.colors.highlight2};\n opacity: 0.5;\n cursor: pointer;\n }\n\n .drop-zone:hover,\n .drop-zone-drag-over {\n opacity: 1;\n }\n`;\n\nconst DropZoneInner = styled.div<{ theme: Theme }>`\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: center;\n height: 100%;\n width: 100%;\n`;\n\nconst FileUploadIcon = styled(FaFileUpload)`\n width: 100%;\n height: 8rem;\n`;\n\nconst FileUploadMessage = styled.div`\n font-size: 2rem;\n`;\n\nconst UploadPatch = ({ isOpen, closeMenu }: UploadPatchProps) => {\n const theme = useTheme();\n\n const setGraph = useStore(({ setGraph }) => setGraph);\n const setEditorState = useStore((store) => store.setEditorState);\n\n const fileInputRef = useRef<HTMLInputElement>(null);\n\n const uploadFile = useCallback(\n (files: FileList | null) => {\n const [file] = files || [];\n file\n .text()\n .then(JSON.parse)\n .then((editorState) => {\n setEditorState(editorState);\n closeMenu();\n })\n .catch(console.error);\n },\n [setGraph, closeMenu],\n );\n\n const onTargetClick = () => {\n fileInputRef.current?.click();\n };\n\n return isOpen ? (\n <Modal onClose={closeMenu}>\n <UploadPatchWrapper theme={theme}>\n <input\n onChange={({ target: { files } }) => uploadFile(files)}\n ref={fileInputRef}\n type=\"file\"\n className=\"hidden\"\n style={{ display: \"none\" }}\n accept=\".json\"\n />\n <FileDrop\n className=\"drop-zone-wrapper\"\n targetClassName=\"drop-zone\"\n draggingOverTargetClassName=\"drop-zone-drag-over\"\n onTargetClick={onTargetClick}\n onDrop={(files) => uploadFile(files)}\n >\n <DropZoneInner theme={theme}>\n <FileUploadIcon />\n <FileUploadMessage>click or drop file here</FileUploadMessage>\n </DropZoneInner>\n </FileDrop>\n </UploadPatchWrapper>\n </Modal>\n ) : null;\n};\n\nexport default UploadPatch;\n","import styled from \"@emotion/styled\";\nimport { useCallback, useRef } from \"react\";\nimport { FileDrop } from \"react-file-drop\";\nimport { FaFileUpload } from \"react-icons/fa\";\nimport useTheme from \"../hooks/useTheme\";\nimport useStore from \"../store\";\nimport { Theme } from \"../theme\";\nimport Modal from \"./Modal\";\n\ninterface UploadProjectProps {\n isOpen: boolean;\n closeMenu: () => void;\n}\n\nconst UploadProjectWrapper = styled.div<{ theme: Theme }>`\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n width: 100%;\n\n .drop-zone-wrapper {\n width: 80%;\n height: 80%;\n }\n\n .drop-zone {\n height: 100%;\n width: 100%;\n border-style: dashed;\n border-width: 2px;\n border-color: ${({ theme }) => theme.colors.highlight2};\n opacity: 0.5;\n cursor: pointer;\n }\n\n .drop-zone:hover,\n .drop-zone-drag-over {\n opacity: 1;\n }\n`;\n\nconst DropZoneInner = styled.div<{ theme: Theme }>`\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: center;\n height: 100%;\n width: 100%;\n`;\n\nconst FileUploadIcon = styled(FaFileUpload)`\n width: 100%;\n height: 8rem;\n`;\n\nconst FileUploadMessage = styled.div`\n font-size: 2rem;\n`;\n\n// @TODO: unify with upload file\nconst UploadProject = ({ isOpen, closeMenu }: UploadProjectProps) => {\n const theme = useTheme();\n\n const setProject = useStore((store) => store.setProject);\n\n const fileInputRef = useRef<HTMLInputElement>(null);\n\n const uploadFile = useCallback(\n (files: FileList | null) => {\n const [file] = files || [];\n file\n .text()\n .then(JSON.parse)\n .then((projectState) => {\n setProject(projectState);\n closeMenu();\n })\n .catch(console.error);\n },\n [setProject, closeMenu],\n );\n\n const onTargetClick = () => {\n fileInputRef.current?.click();\n };\n\n return isOpen ? (\n <Modal onClose={closeMenu}>\n <UploadProjectWrapper theme={theme}>\n <input\n onChange={({ target: { files } }) => uploadFile(files)}\n ref={fileInputRef}\n type=\"file\"\n className=\"hidden\"\n style={{ display: \"none\" }}\n accept=\".json\"\n />\n <FileDrop\n className=\"drop-zone-wrapper\"\n targetClassName=\"drop-zone\"\n draggingOverTargetClassName=\"drop-zone-drag-over\"\n onTargetClick={onTargetClick}\n onDrop={(files) => uploadFile(files)}\n >\n <DropZoneInner theme={theme}>\n <FileUploadIcon />\n <FileUploadMessage>click or drop file here</FileUploadMessage>\n </DropZoneInner>\n </FileDrop>\n </UploadProjectWrapper>\n </Modal>\n ) : null;\n};\n\nexport default UploadProject;\n","import { useEffect, useState } from \"react\";\n\nexport const useWorker = (url: string | URL) => {\n const [worker, setWorker] = useState<Worker | null>(null);\n\n useEffect(() => {\n const newWorker = new Worker(url);\n setWorker(newWorker);\n return () => {\n newWorker?.terminate();\n setWorker(null);\n };\n }, []);\n\n return worker;\n};\n\nexport default useWorker;\n","import { useEffect, useState } from \"react\";\n\nexport const useMessageChannel = () => {\n const [channel, setChannel] = useState<MessageChannel | null>(null);\n useEffect(() => {\n const newChannel = new MessageChannel();\n newChannel.port2.start();\n setChannel(newChannel);\n return () => {\n setChannel(null);\n newChannel.port2.close();\n };\n }, []);\n return channel;\n};\n\nexport default useMessageChannel;\n","export const setParameterValue = (\n param: AudioParam,\n value: number | undefined,\n audioContext: AudioContext,\n): void => {\n if (typeof value === \"undefined\") {\n return;\n }\n param.setValueAtTime(value, audioContext.currentTime);\n};\n\nexport const fileToBase64 = (file: File): Promise<string> => {\n return new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.readAsDataURL(file);\n reader.onload = () => resolve(reader.result?.toString() || \"\");\n reader.onerror = (error) => reject(error);\n });\n};\n","export { useWorker } from \"./hooks/useWorker\";\nexport { useMessageChannel } from \"./hooks/useMessageChannel\";\nexport * from \"./helpers\";\n","import styled from \"@emotion/styled\";\nimport { MouseEvent, useCallback, useState, useRef, ChangeEvent } from \"react\";\nimport { FileDrop } from \"react-file-drop\";\nimport { FaFileUpload } from \"react-icons/fa\";\nimport useTheme from \"../hooks/useTheme\";\nimport useStore from \"../store\";\nimport { Theme } from \"../theme\";\nimport Modal from \"./Modal\";\nimport { fileToBase64 } from \"../lib\";\n\ninterface UploadAudioProps {\n isOpen: boolean;\n closeMenu: () => void;\n}\n\nconst UploadAudioWrapper = styled.div<{ theme: Theme }>`\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n width: 100%;\n\n .drop-zone-wrapper {\n width: 80%;\n height: 80%;\n }\n\n .drop-zone {\n height: 100%;\n width: 100%;\n border-style: dashed;\n border-width: 2px;\n border-color: ${({ theme }) => theme.colors.highlight2};\n opacity: 0.5;\n cursor: pointer;\n }\n\n .drop-zone:hover,\n .drop-zone-drag-over {\n opacity: 1;\n }\n`;\n\nconst DropZoneInner = styled.div<{ theme: Theme }>`\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: center;\n height: 100%;\n width: 100%;\n`;\n\nconst FileUploadIcon = styled(FaFileUpload)`\n width: 100%;\n height: 8rem;\n`;\n\nconst FileUploadMessage = styled.div`\n font-size: 2rem;\n`;\n\nconst UploadAudio = ({ isOpen, closeMenu }: UploadAudioProps) => {\n const theme = useTheme();\n\n const setGraph = useStore(({ setGraph }) => setGraph);\n const setEditorState = useStore((store) => store.setEditorState);\n const addFile = useStore((store) => store.addFile);\n\n const fileInputRef = useRef<HTMLInputElement>(null);\n\n const uploadFile = useCallback(\n async (files: FileList | null) => {\n const [file] = files || [];\n const base64 = await fileToBase64(file);\n addFile({\n type: \"audio\",\n // @TODO: use nanoid here\n id: `audio-file-${+new Date()}`,\n name: file.name,\n file: base64,\n });\n closeMenu();\n },\n [addFile, closeMenu],\n );\n\n const onTargetClick = () => {\n fileInputRef.current?.click();\n };\n\n return isOpen ? (\n <Modal onClose={closeMenu}>\n <UploadAudioWrapper theme={theme}>\n <input\n onChange={({ target: { files } }) => uploadFile(files)}\n ref={fileInputRef}\n type=\"file\"\n className=\"hidden\"\n style={{ display: \"none\" }}\n accept=\".wav,.mp3,.ogg\"\n />\n <FileDrop\n className=\"drop-zone-wrapper\"\n targetClassName=\"drop-zone\"\n draggingOverTargetClassName=\"drop-zone-drag-over\"\n onTargetClick={onTargetClick}\n onDrop={(files) => uploadFile(files)}\n >\n <DropZoneInner theme={theme}>\n <FileUploadIcon />\n <FileUploadMessage>click or drop file here</FileUploadMessage>\n </DropZoneInner>\n </FileDrop>\n </UploadAudioWrapper>\n </Modal>\n ) : null;\n};\n\nexport default UploadAudio;\n","import downloadFile from \"js-file-download\";\nimport {\n type ReactNode,\n useCallback,\n useEffect,\n useMemo,\n useState,\n} from \"react\";\nimport { Separator, useContextMenu } from \"react-contexify\";\nimport \"react-contexify/dist/ReactContexify.css\";\nimport { useReactFlow, type XYPosition } from \"reactflow\";\nimport hotkeys from \"hotkeys-js\";\nimport useTheme from \"../../hooks/useTheme\";\nimport useStore from \"../../store\";\nimport AddNode from \"../AddNode\";\nimport UploadPatch from \"../UploadPatch\";\nimport UploadProject from \"../UploadProject\";\nimport { ItemWrapper, MenuWrapper } from \"./styles\";\nimport UploadAudio from \"../UploadAudio\";\n\ntype MousePosition = {\n x: number;\n y: number;\n};\n\nexport const MENU_ID = \"editor-menu\";\n\nexport const useEditorContextMenu = () => {\n const { show } = useContextMenu({\n id: MENU_ID,\n });\n\n return { onContextMenu: show };\n};\n\nconst EditorContextMenu = ({\n editorContextMenu = [],\n}: {\n editorContextMenu?: Array<ReactNode>;\n}) => {\n const theme = useTheme();\n\n const [mousePosition, setMousePosition] = useState<MousePosition>({\n x: 0,\n y: 0,\n });\n const [showAddNode, setShowAddNode] = useState(false);\n const [showUploadPatch, setShowUploadPatch] = useState(false);\n const [showUploadProject, setShowUploadProject] = useState(false);\n const [showUploadAudio, setShowUploadAudio] = useState(false);\n\n const addNodeHandler = useCallback(\n (x: number, y: number) => {\n setMousePosition({ x, y });\n setShowAddNode(true);\n },\n [setShowAddNode],\n );\n\n const pasteBuffer = useStore((store) => store.pasteBuffer);\n const { screenToFlowPosition } = useReactFlow();\n const pasteBufferHandler = useCallback(\n (mousePosition: XYPosition) => {\n const { x, y } = screenToFlowPosition(mousePosition);\n pasteBuffer(x, y);\n },\n [setShowAddNode, screenToFlowPosition],\n );\n\n const clearGraph = useStore(({ clearGraph }) => clearGraph);\n\n const getEditorState = useStore((store) => store.getEditorState);\n const getProject = useStore((store) => store.getProject);\n\n const deleteAllHandler = useCallback(() => {\n clearGraph();\n }, [clearGraph]);\n\n const toggleHelp = useStore((store) => store.toggleHelp);\n\n const historyBack = useStore((store) => store.history.back);\n const historyForward = useStore((store) => store.history.forward);\n const historyPointer = useStore((store) => store.history.pointer);\n const historyBuffer = useStore((store) => store.history.buffer);\n\n const copySelectedItems = useStore((store) => store.copySelectedItems);\n const nodes = useStore((store) => store.nodes);\n const selectedNodes = useMemo(\n () => nodes.filter(({ selected }) => selected),\n [nodes],\n );\n const currentCopyBuffer = useStore((store) => store.copyBuffer);\n\n const reactFlowInstance = useReactFlow();\n\n const downloadPatchHandler = useCallback(() => {\n const fileName = \"web-noise-patch.json\";\n const editorState = getEditorState();\n const data = {\n ...editorState,\n };\n downloadFile(JSON.stringify(data, null, 2), fileName);\n }, [getEditorState, reactFlowInstance]);\n\n const downloadProjectHandler = useCallback(() => {\n const fileName = \"web-noise-project.json\";\n const projectState = getProject();\n const data = {\n ...projectState,\n };\n downloadFile(JSON.stringify(data, null, 2), fileName);\n }, [getEditorState, reactFlowInstance]);\n\n useEffect(() => {\n hotkeys(\"command+shift+a\", () => {\n addNodeHandler(200, 50);\n return false;\n });\n //@TODO: find more elegant way to handle ?\n hotkeys(\"shift+/\", () => {\n toggleHelp();\n return false;\n });\n hotkeys(\"command+z\", () => {\n historyBack();\n return false;\n });\n hotkeys(\"command+shift+z\", () => {\n historyForward();\n return false;\n });\n hotkeys(\"command+c\", () => {\n copySelectedItems();\n return false;\n });\n hotkeys(\"command+v\", () => {\n pasteBufferHandler({ x: 200, y: 50 });\n return false;\n });\n return () => {\n hotkeys.unbind();\n };\n }, [addNodeHandler, pasteBufferHandler]);\n\n return (\n <>\n <AddNode\n isOpen={showAddNode}\n closeMenu={() => setShowAddNode(false)}\n mousePosition={mousePosition}\n />\n <UploadPatch\n isOpen={showUploadPatch}\n closeMenu={() => setShowUploadPatch(false)}\n />\n <UploadProject\n isOpen={showUploadProject}\n closeMenu={() => setShowUploadProject(false)}\n />\n <UploadAudio\n isOpen={showUploadAudio}\n closeMenu={() => setShowUploadAudio(false)}\n />\n <MenuWrapper id={MENU_ID} animation={false} colors={theme.colors}>\n <ItemWrapper\n onClick={({ triggerEvent: { clientX, clientY } }) =>\n addNodeHandler(clientX, clientY)\n }\n >\n Add Node (⌘+⇧+A)\n </ItemWrapper>\n <Separator />\n <ItemWrapper onClick={deleteAllHandler}>Delete All</ItemWrapper>\n <Separator />\n <ItemWrapper onClick={downloadPatchHandler}>Download patch</ItemWrapper>\n <ItemWrapper onClick={() => setShowUploadPatch(true)}>\n Upload patch\n </ItemWrapper>\n <Separator />\n <ItemWrapper onClick={downloadProjectHandler}>\n Download project\n </ItemWrapper>\n <ItemWrapper onClick={() => setShowUploadProject(true)}>\n Upload project\n </ItemWrapper>\n <Separator />\n <ItemWrapper onClick={() => setShowUploadAudio(true)}>\n Upload Audio File\n </ItemWrapper>\n <Separator />\n <ItemWrapper disabled={historyPointer === 0} onClick={historyBack}>\n Undo (⌘+z)\n </ItemWrapper>\n <ItemWrapper\n disabled={historyPointer === historyBuffer.length}\n onClick={historyForward}\n >\n Redo (⌘+⇧+Z)\n </ItemWrapper>\n <Separator />\n <ItemWrapper\n disabled={!selectedNodes.length}\n onClick={copySelectedItems}\n >\n Copy Selected (⌘+c)\n </ItemWrapper>\n <ItemWrapper\n disabled={!currentCopyBuffer.nodes.length}\n onClick={({ triggerEvent: { clientX: x, clientY: y } }) =>\n pasteBufferHandler({ x, y })\n }\n >\n Paste (⌘+v)\n </ItemWrapper>\n <Separator />\n {editorContextMenu.map((item, index) => (\n <ItemWrapper key={index}>{item}</ItemWrapper>\n ))}\n <Separator />\n <ItemWrapper onClick={toggleHelp}>Help (⇧+?)</ItemWrapper>\n </MenuWrapper>\n </>\n );\n};\n\nexport default EditorContextMenu;\n","import { useCallback } from \"react\";\nimport { useContextMenu, PredicateParams } from \"react-contexify\";\nimport \"react-contexify/dist/ReactContexify.css\";\nimport { WNNode } from \"../../types\";\nimport useTheme from \"../../hooks/useTheme\";\nimport useStore from \"../../store\";\nimport { ItemWrapper, MenuWrapper } from \"./styles\";\n\nexport const MENU_ID = \"editor-node-menu\";\n\nexport const useNodeContextMenu = () => {\n const { show } = useContextMenu({\n id: MENU_ID,\n });\n\n const onContextMenu = useCallback(\n (event: React.MouseEvent<Element, MouseEvent>, node: unknown) => {\n event.stopPropagation();\n show(event, { props: { node } });\n },\n [],\n );\n\n return { onContextMenu };\n};\n\nconst NodeContextMenu = (args: unknown) => {\n const theme = useTheme();\n\n const removeNodes = useStore((store) => store.removeNodes);\n const addNodeToControlPanel = useStore(\n (store) => store.addNodeToControlPanel,\n );\n const removeNodeFromControlPanel = useStore(\n (store) => store.removeNodeFromControlPanel,\n );\n const copy = useStore((store) => store.copy);\n const nodesConfiguration = useStore((store) => store.nodesConfiguration);\n const controlPanelNodes = useStore((store) => store.controlPanel.nodes);\n\n const isOnControlPanel = useCallback(\n ({ props }: PredicateParams<{ node: WNNode }>) => {\n if (!props?.node.type) {\n return false;\n }\n return !!controlPanelNodes.find(({ id }) => id === props.node.id);\n },\n [controlPanelNodes],\n );\n\n const hasControlPanelNode = useCallback(\n ({ props }: PredicateParams<{ node: WNNode }>) => {\n if (!props?.node.type) {\n return false;\n }\n const nodeConfiguration = nodesConfiguration[props.node.type];\n return !!nodeConfiguration?.controlPanelNode;\n },\n [nodesConfiguration],\n );\n\n return (\n <>\n <MenuWrapper id={MENU_ID} animation={false} colors={theme.colors}>\n <ItemWrapper onClick={(event) => removeNodes([event.props.node])}>\n Delete Node (DEL)\n </ItemWrapper>\n <ItemWrapper\n hidden={(...args) =>\n !hasControlPanelNode(...args) || isOnControlPanel(...args)\n }\n onClick={(event) => addNodeToControlPanel(event.props.node)}\n >\n Add To Control Panel\n </ItemWrapper>\n <ItemWrapper\n hidden={(...args) =>\n !hasControlPanelNode(...args) || !isOnControlPanel(...args)\n }\n onClick={(event) => removeNodeFromControlPanel(event.props.node)}\n >\n Remove From Control Panel\n </ItemWrapper>\n <ItemWrapper\n onClick={(event) => copy({ nodes: [event.props.node], edges: [] })}\n >\n Copy (⌘+c)\n </ItemWrapper>\n </MenuWrapper>\n </>\n );\n};\n\nexport default NodeContextMenu;\n","import styled from \"@emotion/styled\";\nimport \"react-grid-layout/css/styles.css\";\nimport { Theme } from \"../../theme\";\nimport { TitleBar } from \"../Node\";\n\nexport const PanelTitle = styled.div`\n width: 100%;\n padding: 0.4rem 0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n`;\n\nexport const TitleBarWrapper = styled(TitleBar)<{ theme: Theme }>`\n display: flex;\n gap: 0.1rem;\n padding: 0 0.4rem;\n justify-content: start;\n font-size: 0.6rem;\n height: auto;\n min-width: 0;\n`;\n\nexport const IconsBar = styled.div`\n display: flex;\n align-items: center;\n height: 70%;\n width: auto;\n gap: 0.4rem;\n`;\n\nexport const IconWrapper = styled.span<{ theme: Theme }>`\n width: auto;\n height: 100%;\n cursor: pointer;\n svg {\n width: auto;\n height: 100%;\n }\n &:hover {\n color: ${({ theme }) => theme.colors.highlight2};\n cursor: pointer;\n }\n`;\n","import { useMemo } from \"react\";\n\nimport styled from \"@emotion/styled\";\nimport \"react-grid-layout/css/styles.css\";\nimport { MdDragHandle as DragIcon } from \"react-icons/md\";\nimport { MdClose as CloseIcon } from \"react-icons/md\";\nimport useAudioNode from \"../../hooks/useAudioNode\";\nimport useNode from \"../../hooks/useNode\";\nimport useTheme from \"../../hooks/useTheme\";\nimport useStore from \"../../store\";\nimport { Theme } from \"../../theme\";\nimport { WNNode, ControlPanelNodeProps } from \"../../types\";\nimport { IconsBar, IconWrapper, PanelTitle, TitleBarWrapper } from \"./styles\";\n\nconst ControlPanelNodeWrapper = styled.div<{ theme: Theme }>`\n height: 100%;\n display: grid;\n grid-template-rows: auto 1fr;\n`;\n\nconst PanelNode = (props: ControlPanelNodeProps) => {\n const { node } = props;\n\n const getControlPanelNode = useStore((store) => store.getControlPanelNode);\n\n const ControlPanelNode = useMemo(() => getControlPanelNode(node), [node]);\n\n if (!ControlPanelNode) {\n return null;\n }\n\n return <ControlPanelNode {...props} />;\n};\n\ninterface ControlPanelItemProps {\n node: WNNode;\n showControls: boolean;\n onDelete: (node: WNNode) => void;\n}\n\nconst ControlPanelItem = (props: ControlPanelItemProps) => {\n const { node, showControls, onDelete } = props;\n const theme = useTheme();\n\n const { id } = node;\n const { node: audioNode } = useAudioNode(id) || {};\n const { updateNodeValues } = useNode(id);\n\n return (\n <ControlPanelNodeWrapper theme={theme}>\n <TitleBarWrapper theme={theme}>\n <PanelTitle>{node.data.label}</PanelTitle>\n {showControls && (\n <IconsBar>\n <IconWrapper theme={theme}>\n <DragIcon className=\"grid-item-handle\" />\n </IconWrapper>\n <IconWrapper theme={theme}>\n <CloseIcon onClick={() => onDelete(node)} />\n </IconWrapper>\n </IconsBar>\n )}\n </TitleBarWrapper>\n <PanelNode\n node={node}\n audioNode={audioNode}\n updateNodeValues={updateNodeValues}\n />\n </ControlPanelNodeWrapper>\n );\n};\n\nexport default ControlPanelItem;\n","import styled from \"@emotion/styled\";\nimport { Resizable } from \"re-resizable\";\nimport { useMemo, useRef, useState } from \"react\";\nimport GridLayout from \"react-grid-layout\";\nimport \"react-grid-layout/css/styles.css\";\nimport {\n AiFillLock as LockIcon,\n AiFillUnlock as UnlockIcon,\n} from \"react-icons/ai\";\nimport { MdClose as CloseIcon } from \"react-icons/md\";\nimport { RxDashboard as ControlPanelIcon } from \"react-icons/rx\";\nimport Drawer from \"react-modern-drawer\";\nimport \"react-modern-drawer/dist/index.css\";\nimport { CONTROL_PANEL_GRID_CONFIG } from \"../../constants\";\nimport useTheme from \"../../hooks/useTheme\";\nimport useStore from \"../../store\";\nimport { Theme } from \"../../theme\";\nimport ControlPanelItem from \"./ControlPanelItem\";\nimport { IconsBar, IconWrapper, PanelTitle, TitleBarWrapper } from \"./styles\";\n\nconst ControlPanelIconWrapper = styled.div<{ theme: Theme }>`\n position: fixed;\n z-index: 5;\n box-shadow: 0px 1px 2px ${({ theme }) => theme.colors.elevation2};\n left: 1rem;\n top: 4rem;\n width: 2rem;\n height: 2rem;\n border-radius: 50%;\n overflow: hidden;\n display: flex;\n align-items: center;\n justify-content: center;\n background: ${({ theme }) => theme.colors.elevation1};\n color: ${({ theme }) => theme.colors.highlight1};\n\n :hover {\n color: ${({ theme }) => theme.colors.highlight2};\n cursor: pointer;\n }\n`;\n\nconst ControlPanelIconsBar = styled(IconsBar)`\n height: 80%;\n`;\n\nconst CloseIconWrapper = styled(IconWrapper)`\n font-size: 1rem;\n display: flex;\n align-items: center;\n`;\n\nconst ControlPanelHeader = styled(TitleBarWrapper)<{ theme: Theme }>`\n grid-template-columns: 1fr auto;\n border-bottom: 1px solid ${({ theme }) => theme.colors.elevation3};\n font-size: 0.7rem;\n`;\n\nconst ControlPanelTitle = styled(PanelTitle)`\n text-align: center;\n`;\n\nconst ControlPanelWrapper = styled.div`\n height: 100%;\n width: 100%;\n padding: 0.3rem 0.4rem;\n box-sizing: border-box;\n`;\n\nconst ControlPanelBody = styled.div<{ theme: Theme }>`\n height: auto;\n padding: 0;\n max-height: 95vh;\n overflow-y: scroll;\n border: 1px solid ${({ theme }) => theme.colors.elevation3};\n`;\n\nconst ControlPanelSettings = styled.div`\n padding: 1rem 0;\n font-family: var(--leva-fonts-mono);\n font-size: 0.8rem;\n`;\n\nconst LockGridWrapper = styled.div<{ theme: Theme }>`\n display: flex;\n align-items: center;\n gap: 0.5rem;\n cursor: pointer;\n color: ${({ theme }) => theme.colors.highlight1};\n &:hover {\n color: ${({ theme }) => theme.colors.highlight2};\n }\n`;\n\nconst GridResizeHandle = styled.div<{ theme: Theme }>`\n position: absolute;\n height: 1rem;\n top: 0;\n bottom: 0;\n margin: auto;\n left: 0.5rem;\n border-right: 1px solid ${({ theme }) => theme.colors.whitePrimary};\n`;\n\nconst ControlPanelItemWrapper = styled.div<{ theme: Theme }>`\n box-sizing: border-box;\n overflow: hidden;\n .react-resizable-handle:after {\n border-color: ${({ theme }) => theme.colors.whitePrimary};\n border-width: 1px;\n }\n`;\n\nconst ControlPanel = () => {\n const theme = useTheme();\n const nodeRef = useRef(null);\n const nodes = useStore((store) => store.nodes);\n const {\n show,\n nodes: controlPanelNodes,\n size,\n } = useStore((store) => store.controlPanel);\n\n const { width = 200, height } = size;\n\n const filteredNodes = useMemo(() => {\n const nodeIds = controlPanelNodes.map(({ id }) => id);\n return nodes.filter(({ id }) => nodeIds.includes(id));\n }, [nodes, controlPanelNodes]);\n\n const showControlPanel = useStore((store) => store.showControlPanel);\n const hideControlPanel = useStore((store) => store.hideControlPanel);\n const setControlPanelNodes = useStore((store) => store.setControlPanelNodes);\n const setControlPanelSize = useStore((store) => store.setControlPanelSize);\n const removeNodeFromControlPanel = useStore(\n (store) => store.removeNodeFromControlPanel,\n );\n\n const [isGridLocked, setGridLocked] = useState(true);\n\n const layout = useMemo(() => {\n const fallbackY = controlPanelNodes.reduce(\n (acc, { height = CONTROL_PANEL_GRID_CONFIG.rowHeight, x, y = 0 }) => {\n const Y = y + height;\n return Y > acc ? Y : acc;\n },\n 0,\n );\n return controlPanelNodes.map(({ id: i, width, height, x, y }) => {\n return {\n i,\n w: width || CONTROL_PANEL_GRID_CONFIG.cols,\n h: height || 6,\n x: x ?? 0,\n y: y ?? fallbackY,\n };\n });\n }, [controlPanelNodes]);\n\n if (!filteredNodes.length) {\n return null;\n }\n\n return (\n <>\n <ControlPanelIconWrapper\n theme={theme}\n ref={nodeRef}\n onClick={showControlPanel}\n >\n <ControlPanelIcon />\n </ControlPanelIconWrapper>\n <Drawer\n open={show}\n onClose={hideControlPanel}\n direction=\"left\"\n className=\"\"\n size=\"auto\"\n enableOverlay={false}\n style={{\n background: theme.colors.elevation1,\n position: \"absolute\",\n }}\n >\n <ControlPanelHeader theme={theme}>\n <ControlPanelTitle>Control Panel</ControlPanelTitle>\n <ControlPanelIconsBar>\n <CloseIconWrapper onClick={hideControlPanel} theme={theme}>\n <CloseIcon />\n </CloseIconWrapper>\n </ControlPanelIconsBar>\n </ControlPanelHeader>\n\n <ControlPanelWrapper>\n <ControlPanelSettings>\n {isGridLocked ? (\n <LockGridWrapper\n theme={theme}\n onClick={() => setGridLocked(false)}\n >\n <LockIcon />\n Unlock grid\n </LockGridWrapper>\n ) : (\n <LockGridWrapper\n theme={theme}\n onClick={() => setGridLocked(true)}\n >\n <UnlockIcon />\n Lock grid\n </LockGridWrapper>\n )}\n </ControlPanelSettings>\n <Resizable\n enable={{\n top: false,\n right: !isGridLocked,\n bottom: false,\n left: false,\n topRight: false,\n bottomRight: false,\n bottomLeft: false,\n topLeft: false,\n }}\n handleComponent={{\n right: <GridResizeHandle theme={theme} />,\n }}\n minWidth={120}\n size={{ width, height: \"auto\" }}\n onResizeStop={(e, direction, ref, d) => {\n setControlPanelSize({\n width: width + d.width,\n height: height + d.height,\n });\n }}\n >\n <ControlPanelBody theme={theme}>\n {/* @ts-ignore */}\n <GridLayout\n layout={layout}\n className=\"layout\"\n cols={CONTROL_PANEL_GRID_CONFIG.cols}\n rowHeight={CONTROL_PANEL_GRID_CONFIG.rowHeight}\n width={width}\n margin={[0, 0]}\n isResizable={!isGridLocked}\n draggableHandle=\".grid-item-handle\"\n onLayoutChange={(nodes) => {\n requestAnimationFrame(() => {\n setControlPanelNodes(\n nodes.map(({ i, w, h, x, y }) => ({\n id: i,\n width: w,\n height: h,\n x,\n y,\n })),\n );\n });\n }}\n >\n {filteredNodes.map((node) => {\n return (\n <ControlPanelItemWrapper key={node.id} theme={theme}>\n <ControlPanelItem\n node={node}\n showControls={!isGridLocked}\n onDelete={removeNodeFromControlPanel}\n />\n </ControlPanelItemWrapper>\n );\n })}\n </GridLayout>\n </ControlPanelBody>\n </Resizable>\n </ControlPanelWrapper>\n </Drawer>\n </>\n );\n};\n\nexport default ControlPanel;\n","import { withTheme } from \"@emotion/react\";\nimport styled from \"@emotion/styled\";\n// @ts-ignore\nimport { marked } from \"marked\";\nimport useStore from \"../../store\";\nimport { Theme } from \"../../theme\";\nimport Modal from \"../Modal\";\n\n//@ts-ignore\nimport HELP from \"bundle-text:./HELP.md\";\n\nconst MdPreview = withTheme(styled.div<{ theme: Theme }>`\n font-family: var(--leva-fonts-mono);\n font-size: 0.7rem;\n box-sizing: border-box;\n height: 100%;\n width: 100%;\n overflow: scroll;\n color: ${({ theme }) => theme.colors.whitePrimary};\n padding: 0 0.5rem;\n\n code {\n color: ${({ theme }) => theme.colors.accent3};\n filter: hue-rotate(180deg);\n }\n`);\n\nconst ModalContent = withTheme(styled.div<{ theme: Theme }>`\n padding: 1rem;\n height: 100%;\n width: 100%;\n box-sizing: border-box;\n overflow: hidden;\n`);\n\nconst HelpModal = () => {\n const isHelpShown = useStore((store) => store.isHelpShown);\n const toggleHelp = useStore((store) => store.toggleHelp);\n\n if (!isHelpShown) {\n return null;\n }\n\n return (\n <Modal\n onClose={() => {\n toggleHelp();\n }}\n >\n <ModalContent>\n <MdPreview\n dangerouslySetInnerHTML={{ __html: marked(HELP) }}\n onWheelCapture={(event) => event.stopPropagation()}\n ></MdPreview>\n </ModalContent>\n </Modal>\n );\n};\n\nexport default HelpModal;\n","import { FaQuestion } from \"react-icons/fa\";\nimport { ControlButton } from \"reactflow\";\nimport useStore from \"../../store\";\nimport HelpModal from './HelpModal';\n\n\nconst HelpButton = () => {\n const toggleHelp = useStore((store) => store.toggleHelp);\n\n return (\n <>\n <ControlButton onClick={toggleHelp}>\n <FaQuestion />\n </ControlButton>\n </>\n );\n};\n\nexport { HelpModal, HelpButton };\n\n","import styled from \"@emotion/styled\";\nimport { useState } from \"react\";\nimport { FaVolumeOff as IconUnmute } from \"react-icons/fa\";\nimport useStore from \"../store\";\nimport useTheme from \"../hooks/useTheme\";\nimport { Theme } from \"../theme\";\n\n\nconst Layout = styled.div<{ theme: Theme }>`\n position: fixed;\n z-index: ${({ theme }) => theme.zIndex.resumeContextLayout};\n width: 100%;\n height: 100%;\n top: 0;\n left: 0;\n background: rgb(24 28 32 / 90%);\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: center;\n color: ${({ theme }) => theme.colors.whitePrimary};\n cursor: pointer;\n`;\n\nconst Row = styled.div`\n display: flex;\n width: 100%;\n align-items: center;\n justify-content: center;\n`;\n\nconst Message = styled.div<{ theme: Theme }>`\n font-family: var(--leva-fonts-mono);\n font-size: 2rem;\n`;\n\nconst Icon = styled(IconUnmute)`\n width: 7rem;\n height: 7rem;\n`;\n\nconst ResumeContext = () => {\n const theme = useTheme();\n const patch = useStore(({ patch }) => patch);\n const audioContext = patch.audioContext;\n const [isContextResumed, setIsContextResumed] = useState<boolean>(\n audioContext.state === \"running\",\n );\n\n if (isContextResumed) {\n return null;\n }\n\n return (\n <Layout\n theme={theme}\n onClick={() => {\n audioContext.resume();\n setIsContextResumed(true);\n }}\n >\n <Row>\n <Message theme={theme}>Click anywhere to resume audio context</Message>\n </Row>\n <Row>\n <Icon />\n </Row>\n </Layout>\n );\n};\n\nexport default ResumeContext;\n","import { FaMap, FaRegMap } from \"react-icons/fa\";\nimport { ControlButton } from \"reactflow\";\nimport useStore from \"../store\";\n\nconst ToggleMinimap = () => {\n const setConfig = useStore(({ setConfig }) => setConfig);\n const { showMinimap } = useStore(({ config }) => config);\n\n return (\n <ControlButton onClick={() => setConfig({ showMinimap: !showMinimap })}>\n {showMinimap ? <FaMap /> : <FaRegMap />}\n </ControlButton>\n );\n};\n\nexport default ToggleMinimap;\n","import { useEffect, useMemo } from \"react\";\nimport { EdgeProps, getBezierPath, getConnectedEdges } from \"reactflow\";\nimport useTheme from \"../hooks/useTheme\";\nimport useStore from \"../store\";\n\nconst Wire = ({\n id,\n sourceX,\n sourceY,\n targetX,\n targetY,\n sourcePosition,\n targetPosition,\n style = {},\n data,\n markerStart,\n markerEnd,\n source,\n target,\n sourceHandleId,\n targetHandleId,\n selected,\n}: EdgeProps) => {\n const theme = useTheme();\n const getNode = useStore(({ getNode }) => getNode);\n const sourceNode = getNode(source);\n const targetNode = getNode(target);\n const isConnectedToSelected = sourceNode?.selected || targetNode?.selected;\n useEffect(() => {\n if (!sourceHandleId || !targetHandleId) {\n return;\n }\n console.log(`connected ${source} to ${target}`);\n return () => {\n console.log(`disconnected ${source} from ${target}`);\n };\n }, [source, sourceHandleId, target, targetHandleId]);\n\n const [edgePath] = getBezierPath({\n targetX,\n targetY,\n targetPosition,\n sourceX,\n sourceY,\n sourcePosition,\n });\n\n return (\n <>\n <path\n id={id}\n style={{\n ...style,\n stroke: selected\n ? theme.colors.accent2\n : isConnectedToSelected\n ? theme.colors.highlight3\n : theme.colors.highlight2,\n }}\n className=\"react-flow__edge-path Wire\"\n d={edgePath}\n markerEnd={markerEnd}\n />\n <path\n style={{\n ...style,\n strokeWidth: 8,\n color: \"transparent\",\n opacity: 0,\n cursor: \"pointer\",\n }}\n d={edgePath}\n markerEnd={markerEnd}\n />\n </>\n );\n};\n\nexport default Wire;\n","import {\n type ReactNode,\n useCallback,\n useEffect,\n useMemo,\n useState,\n} from \"react\";\nimport ReactFlow, {\n Background,\n BackgroundVariant,\n Controls,\n MiniMap,\n ReactFlowInstance,\n ReactFlowProvider,\n useOnViewportChange,\n} from \"reactflow\";\nimport \"reactflow/dist/style.css\";\nimport useStore from \"../../store\";\nimport type { EditorState, PluginConfig } from \"../../types\";\nimport EdgeContextMenu, {\n useEdgeContextMenu,\n} from \"../contextMenu/EdgeContextMenu\";\nimport EditorContextMenu, {\n useEditorContextMenu,\n} from \"../contextMenu/EditorContextMenu\";\nimport NodeContextMenu, {\n useNodeContextMenu,\n} from \"../contextMenu/NodeContextMenu\";\nimport ControlPanel from \"../ControlPanel\";\nimport { HelpButton, HelpModal } from \"../Help\";\nimport ResumeContext from \"../ResumeContext\";\nimport ToggleMinimap from \"../ToggleMinimap\";\nimport Wire from \"../Wire\";\n\nconst onNodeDragStop = (_event: any, node: any) =>\n console.log(\"drag stop\", node);\nconst onNodeClick = (_event: any, element: any) =>\n console.log(\"click\", element);\n\nconst snapGrid: [number, number] = [20, 20];\n\ninterface EditorProps {\n editorState?: EditorState;\n plugins?: Array<PluginConfig>;\n editorContextMenu?: Array<ReactNode>;\n onChange?: ({ nodes, edges, controlPanel }: EditorState) => void;\n}\n\nexport const EditorPane = ({\n editorState,\n plugins = [],\n editorContextMenu = [],\n onChange = () => {},\n ...props\n}: EditorProps) => {\n const edgeTypes = useMemo(\n () => ({\n wire: Wire,\n }),\n [],\n );\n\n const {\n nodes,\n edges,\n controlPanel,\n onNodesChange,\n onNodesDelete,\n onEdgesChange,\n onEdgesDelete,\n onConnect,\n setPlugins,\n setViewport,\n viewport,\n } = useStore();\n\n const editorConfig = useStore(({ config }) => config);\n\n const nodeTypes = useStore(({ nodeTypes }) => nodeTypes);\n\n useEffect(() => {\n setPlugins(plugins);\n }, [plugins]);\n\n const [reactflowInstance, setReactflowInstance] =\n useState<ReactFlowInstance | null>(null);\n\n useEffect(() => {\n if (!reactflowInstance) {\n return;\n }\n onChange({\n nodes,\n edges,\n controlPanel,\n viewport,\n });\n }, [nodes, edges, controlPanel, viewport]);\n\n const onInit = useCallback(\n (rfi: ReactFlowInstance) => {\n if (!reactflowInstance) {\n setReactflowInstance(rfi);\n console.log(\"flow loaded:\", rfi);\n }\n },\n [reactflowInstance],\n );\n\n const { onContextMenu: onEditorContextMenu } = useEditorContextMenu();\n const { onContextMenu: onNodeContextMenu } = useNodeContextMenu();\n const { onContextMenu: onEdgeContextMenu } = useEdgeContextMenu();\n\n useEffect(() => {\n if (!viewport) {\n return;\n }\n reactflowInstance?.setViewport(viewport);\n }, [viewport, reactflowInstance]);\n\n useOnViewportChange({\n onEnd: setViewport,\n });\n\n return (\n <ReactFlow\n nodes={nodes}\n edges={edges}\n onNodesChange={onNodesChange}\n onNodesDelete={onNodesDelete}\n onEdgesChange={onEdgesChange}\n onConnect={onConnect}\n onNodeDragStop={onNodeDragStop}\n onEdgesDelete={onEdgesDelete}\n onInit={onInit}\n onNodeClick={onNodeClick}\n onContextMenu={onEditorContextMenu}\n onNodeContextMenu={onNodeContextMenu}\n onEdgeContextMenu={onEdgeContextMenu}\n nodeTypes={nodeTypes}\n edgeTypes={edgeTypes}\n snapGrid={snapGrid}\n defaultViewport={editorState?.viewport}\n defaultEdgeOptions={{ type: \"wire\" }}\n snapToGrid\n fitView\n disableKeyboardA11y\n >\n <Background variant={BackgroundVariant.Dots} gap={12} />\n {editorConfig.showMinimap ? <MiniMap /> : null}\n\n <Controls\n style={{\n right: \"1rem\",\n left: \"initial\",\n bottom: \"40%\",\n top: \"initial\",\n }}\n showInteractive={false}\n >\n <ToggleMinimap />\n <HelpButton />\n </Controls>\n\n <ResumeContext />\n <ControlPanel />\n <HelpModal />\n <EditorContextMenu editorContextMenu={editorContextMenu} />\n <NodeContextMenu />\n <EdgeContextMenu />\n </ReactFlow>\n );\n};\n\nexport const Editor = (props: EditorProps) => (\n <ReactFlowProvider>\n <EditorPane {...props} />\n </ReactFlowProvider>\n);\n\nexport default Editor;\n","import { css, Global, ThemeProvider, withTheme } from \"@emotion/react\";\nimport styled from \"@emotion/styled\";\nimport { nanoid } from \"nanoid\";\nimport { ComponentProps, ReactNode, useEffect, useMemo, useState } from \"react\";\nimport { FaPlus as IconAdd } from \"react-icons/fa6\";\nimport { MdClose } from \"react-icons/md\";\nimport { registerFetcher } from \"@web-noise/fetch\";\nimport useStore from \"../store\";\nimport \"../styles\";\nimport defaultTheme, { Theme } from \"../theme\";\nimport type {\n EditorFile,\n EditorState,\n PluginConfig,\n Project,\n ProjectFile,\n} from \"../types\";\nimport { Editor } from \"./Editor\";\nimport EditableLabel from \"./EditableLabel\";\nimport { isAudio, isPatch } from \"../helpers/projectFile\";\nimport { fileToBase64 } from \"../lib\";\n\n// @TODO: move default state to editor\nexport const EDITOR_DEFAULTS = {\n nodes: [],\n edges: [],\n controlPanel: {\n nodes: [],\n show: false,\n size: {\n width: 200,\n height: 100,\n },\n },\n viewport: { x: 0, y: 0, zoom: 1.5 },\n};\n\nexport const AppWrapper = withTheme(styled.div<{ theme: Theme }>`\n display: flex;\n flex-direction: column;\n height: 100%;\n width: 100%;\n`);\n\nexport const FileUploadLayout = withTheme(styled.div<{ theme: Theme }>`\n position: fixed;\n height: 100%;\n width: 100%;\n background: rgba(0, 0, 0, 0.7);\n color: white;\n display: flex;\n justify-content: center;\n align-items: center;\n font-size: 2rem;\n`);\n\nexport const EditorContainerWrapper = withTheme(styled.div<{ theme: Theme }>`\n height: 100%;\n width: 100%;\n display: flex;\n position: relative;\n`);\n\nexport const AudioTabWrapper = withTheme(styled.div<{ theme: Theme }>`\n display: flex;\n flex: 1;\n align-items: center;\n justify-content: center;\n background: ${({ theme }) => theme.colors.elevation3};\n`);\n\nexport const EditorLoadingOverlay = withTheme(styled.div<{\n theme: Theme;\n show: boolean;\n}>`\n background: ${({ theme }) => theme.colors.elevation2};\n opacity: 0.7;\n position: absolute;\n width: 100%;\n height: 100%;\n left: 0;\n top: 0;\n z-index: 1;\n font-family: var(--leva-fonts-mono);\n color: ${({ theme }) => theme.colors.whitePrimary};\n display: ${({ show }) => (show ? \"flex\" : \"none\")};\n align-items: center;\n justify-content: center;\n font-size: 6rem;\n`);\n\ntype EditorContainerProps = AppProps & {\n file: ProjectFile;\n};\n\nexport const EditorContainer = (props: EditorContainerProps) => {\n const pullEditorChanges = useStore((store) => store.pullEditorChanges);\n const currentFileIndex = useStore((store) => store.currentFileIndex);\n\n const [showLoader, setShowLoader] = useState(true);\n\n useEffect(() => {\n setShowLoader(true);\n setTimeout(() => {\n setShowLoader(false);\n }, 1600);\n }, [currentFileIndex]);\n\n const { file } = props;\n if (!file) return null;\n\n if (file.type === \"audio\") {\n return (\n <AudioTabWrapper>\n <audio src={file.file} controls />\n </AudioTabWrapper>\n );\n }\n\n return (\n <>\n <Editor\n {...props}\n onChange={(state) => {\n pullEditorChanges();\n }}\n editorState={(file as EditorFile).file || EDITOR_DEFAULTS}\n />\n\n <EditorLoadingOverlay show={showLoader}>Loading...</EditorLoadingOverlay>\n </>\n );\n};\n\nexport const TabsContainer = withTheme(styled.div<{ theme: Theme }>`\n height: 2rem;\n display: flex;\n align-items: center;\n background: ${({ theme }) => theme.colors.elevation2};\n`);\n\nexport const Tab = withTheme(styled.div<{ theme: Theme; active?: boolean }>`\n display: flex;\n align-items: center;\n cursor: pointer;\n height: 100%;\n box-sizing: border-box;\n padding: 0.3rem 0.4rem;\n\n border-right: 1px solid ${({ theme }) => theme.colors.elevation1};\n\n input {\n color: ${({ theme, active }) =>\n active ? theme.colors.whitePrimary : theme.colors.highlight1};\n\n &:not([readonly]):focus {\n background-color: ${({ theme }) => theme.colors.elevation1};\n }\n }\n`);\n\nexport const TabIconWrapper = withTheme(styled.div<{ theme: Theme }>`\n display: flex;\n align-items: center;\n height: 100%;\n padding: 0 0.5rem;\n cursor: pointer;\n\n color: ${({ theme }) => theme.colors.highlight1};\n &:hover {\n color: ${({ theme }) => theme.colors.whitePrimary};\n }\n`);\n\nexport const AddFileIcon = withTheme(styled(IconAdd)<{ theme: Theme }>`\n height: 40%;\n width: auto;\n`);\n\nexport const CloseIcon = withTheme(styled(MdClose)<{ theme: Theme }>`\n height: 70%;\n width: auto;\n cursor: pointer;\n color: ${({ theme }) => theme.colors.highlight1};\n &:hover {\n color: ${({ theme }) => theme.colors.whitePrimary};\n }\n`);\n\nconst generateId = (): string => {\n return nanoid();\n};\n\nconst generateEmptyFile = (): ProjectFile => ({\n file: EDITOR_DEFAULTS,\n name: \"Unnamed\",\n type: \"patch\",\n id: generateId(),\n});\n\ninterface AppProps {\n projectState?: Project;\n plugins?: Array<PluginConfig>;\n editorContextMenu?: Array<ReactNode>;\n onChange?: ({ nodes, edges, controlPanel }: EditorState) => void;\n theme?: Theme;\n}\n\nexport const App = ({ ...props }: AppProps) => {\n const { projectState, theme } = props;\n const currentFileIndex = useStore((store) => store.currentFileIndex);\n const currentFile = useStore(\n (store) => store.project.files[store.currentFileIndex],\n );\n const setCurrentFileIndex = useStore((store) => store.setCurrentFileIndex);\n\n const project = useStore((store) => store.project);\n const setProject = useStore((store) => store.setProject);\n const getProject = useStore((store) => store.getProject);\n const updateFileName = useStore((store) => store.updateFileName);\n\n const addFile = useStore((store) => store.addFile);\n const deleteFile = useStore((store) => store.deleteFile);\n const syncEditorWithCurrentFile = useStore(\n (store) => store.syncEditorWithCurrentFile,\n );\n\n const setEditorState = useStore((store) => store.setEditorState);\n const pullEditorChanges = useStore((store) => store.pullEditorChanges);\n\n const [isDragging, setIsDragging] = useState(false);\n\n const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {\n e.preventDefault();\n e.stopPropagation();\n setIsDragging(false);\n };\n\n const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {\n e.preventDefault();\n e.stopPropagation();\n setIsDragging(true);\n };\n\n const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {\n e.preventDefault();\n e.stopPropagation();\n\n setIsDragging(false);\n\n const files = Array.from(e.dataTransfer.files);\n files.forEach(async (file) => {\n if (file.type === \"application/json\") {\n const fileData = JSON.parse(await file.text());\n if (fileData.files && fileData.files.length) {\n if (\n !window.confirm(\n \"This action will replace your current project. Continue?\",\n )\n ) {\n return;\n }\n setProject(fileData);\n setCurrentFileIndex(0);\n syncEditorWithCurrentFile();\n return;\n }\n const emptyFile = generateEmptyFile();\n const newGraphState = {\n ...emptyFile,\n file: {\n ...fileData,\n controlPanel: {\n ...EDITOR_DEFAULTS.controlPanel,\n ...fileData.controlPanel,\n },\n },\n name: file.name,\n };\n addFile(newGraphState, file.name);\n return;\n }\n if (file.type.match(/^audio\\//)) {\n const base64 = await fileToBase64(file);\n addFile({\n type: \"audio\",\n // @TODO: use nanoid here\n id: `audio-file-${+new Date()}`,\n name: file.name,\n file: base64,\n });\n return;\n }\n console.error(\"Unsupported file type\", file);\n });\n };\n\n useEffect(() => {\n setProject(\n projectState || {\n files: [generateEmptyFile()],\n },\n );\n const file = projectState?.files[0];\n file?.file && file?.type !== \"audio\" && setEditorState(file.file);\n }, [projectState]);\n\n // EXPERIMENTAL CODE\n useEffect(() => {\n const fetcher: typeof fetch = async (...args) => {\n const request = new Request(...args);\n const files = getProject().files;\n const index = request.url.replace(\"project://\", \"\");\n const file = files.find(({ id }) => id === index);\n\n if (!file) {\n return new Response(`File not found: ${request.url}`, { status: 404 });\n }\n\n if (isPatch(file)) {\n return new Response(JSON.stringify(file.file ?? null));\n }\n\n if (isAudio(file)) {\n return fetch(file.file);\n }\n\n return new Response(null);\n };\n registerFetcher(\"project://*\", fetcher);\n return () => {\n //unregister here\n };\n }, [getProject]);\n\n useEffect(() => {\n syncEditorWithCurrentFile();\n }, [currentFileIndex, syncEditorWithCurrentFile]);\n\n return (\n <ThemeProvider theme={theme || defaultTheme}>\n <Global\n styles={css`\n :root {\n --leva-colors-elevation1: #292d39;\n --leva-colors-elevation2: #181c20;\n --leva-colors-elevation3: #373c4b;\n --leva-colors-accent1: #0066dc;\n --leva-colors-accent2: #007bff;\n --leva-colors-accent3: #3c93ff;\n --leva-colors-highlight1: #535760;\n --leva-colors-highlight2: #8c92a4;\n --leva-colors-highlight3: #fefefe;\n --leva-colors-vivid1: #ffcc00;\n --leva-colors-folderWidgetColor: var(--leva-colors-highlight2);\n --leva-colors-folderTextColor: var(--leva-colors-highlight3);\n --leva-colors-toolTipBackground: var(--leva-colors-highlight3);\n --leva-colors-toolTipText: var(--leva-colors-elevation2);\n --leva-radii-xs: 2px;\n --leva-radii-sm: 3px;\n --leva-radii-lg: 10px;\n --leva-space-xs: 3px;\n --leva-space-sm: 6px;\n --leva-space-md: 10px;\n --leva-space-rowGap: 7px;\n --leva-space-colGap: 7px;\n --leva-fonts-mono:\n ui-monospace, SFMono-Regular, Menlo, \"Roboto Mono\", monospace;\n --leva-fonts-sans: system-ui, sans-serif;\n --leva-fontSizes-root: 11px;\n --leva-fontSizes-toolTip: var(--leva-fontSizes-root);\n --leva-sizes-rootWidth: 280px;\n --leva-sizes-controlWidth: 160px;\n --leva-sizes-numberInputMinWidth: 38px;\n --leva-sizes-scrubberWidth: 8px;\n --leva-sizes-scrubberHeight: 16px;\n --leva-sizes-rowHeight: 24px;\n --leva-sizes-folderTitleHeight: 20px;\n --leva-sizes-checkboxSize: 16px;\n --leva-sizes-joystickWidth: 100px;\n --leva-sizes-joystickHeight: 100px;\n --leva-sizes-colorPickerWidth: var(--leva-sizes-controlWidth);\n --leva-sizes-colorPickerHeight: 100px;\n --leva-sizes-imagePreviewWidth: var(--leva-sizes-controlWidth);\n --leva-sizes-imagePreviewHeight: 100px;\n --leva-sizes-monitorHeight: 60px;\n --leva-sizes-titleBarHeight: 39px;\n --leva-shadows-level1: 0 0 9px 0 #00000088;\n --leva-shadows-level2: 0 4px 14px #00000033;\n --leva-borderWidths-root: 0px;\n --leva-borderWidths-input: 1px;\n --leva-borderWidths-focus: 1px;\n --leva-borderWidths-hover: 1px;\n --leva-borderWidths-active: 1px;\n --leva-borderWidths-folder: 1px;\n --leva-fontWeights-label: normal;\n --leva-fontWeights-folder: normal;\n --leva-fontWeights-button: normal;\n }\n `}\n />\n <AppWrapper\n onDragOver={handleDragOver}\n onDragLeave={handleDragLeave}\n onDrop={handleDrop}\n >\n <TabsContainer>\n {project.files.map((file, index) => (\n <Tab\n onClick={() => {\n setCurrentFileIndex(index);\n }}\n key={index}\n active={index === currentFileIndex}\n >\n <EditableLabel\n onChange={(val) => updateFileName(index, val)}\n value={file.name || \"Unnamed\"}\n />\n <CloseIcon\n onClick={(event) => {\n event.stopPropagation();\n if (\n !window.confirm(\"Do you really want to delete this file?\")\n ) {\n return;\n }\n deleteFile(index);\n }}\n />\n </Tab>\n ))}\n <TabIconWrapper\n onClick={() => {\n addFile(generateEmptyFile());\n setCurrentFileIndex(project.files.length);\n }}\n >\n <AddFileIcon />\n </TabIconWrapper>\n </TabsContainer>\n <EditorContainerWrapper>\n <EditorContainer file={currentFile!} {...props} />\n </EditorContainerWrapper>\n {isDragging && (\n <FileUploadLayout>\n Drop file(s) to upload to the project\n </FileUploadLayout>\n )}\n </AppWrapper>\n </ThemeProvider>\n );\n};\n\nexport default App;\n","export { default as Editor, EDITOR_DEFAULTS } from \"./src/components/App\";\nexport { default as Wire } from \"./src/components/Wire\";\nexport {\n WNNode,\n TitleBar,\n type WNNodeProps,\n PortsPanel,\n OutputPorts,\n OutputHandle,\n InputPorts,\n InputHandle,\n Port,\n} from \"./src/components/Node\";\nexport { default as Modal } from \"./src/components/Modal\";\nexport { default as EditableLabel } from \"./src/components/EditableLabel\";\nexport { default as useAudioNode } from \"./src/hooks/useAudioNode\";\nexport { default as useNode } from \"./src/hooks/useNode\";\nexport { default as useTheme } from \"./src/hooks/useTheme\";\nexport { default as useStore } from \"./src/store\";\nexport { default as theme } from \"./src/theme\";\nexport { isAudio, isPatch } from \"./src/helpers/projectFile\";\n\nexport { PortType } from \"./src/constants\";\n\nexport type {\n WNAudioNode,\n CreateWNAudioNode,\n ControlPanelNodeProps,\n PluginConfig,\n PluginComponent,\n ControlPanelNode,\n WNNodeData,\n WNNode as TWNNode,\n InputPort,\n OutputPort,\n EditorState,\n Project,\n WNEdge as TWNEdge,\n EditorStoreState,\n} from \"./src/types\";\n\nexport type { Theme } from \"./src/theme\";\n"],"names":[],"version":3,"file":"types.d.ts.map"}