Compare commits
13 Commits
bec33211c7
...
7c8741ef36
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c8741ef36 | |||
| cb89557998 | |||
| b7aa1079d4 | |||
| ee05c3f5d6 | |||
| 5feb2ea228 | |||
| 49a6e5b4a3 | |||
| d690c94369 | |||
| 97f6ead03b | |||
| 17739de7bd | |||
| 98e095c10d | |||
| 71e81fefd4 | |||
| e24467a074 | |||
| 4ebaee4827 |
455
package-lock.json
generated
455
package-lock.json
generated
@@ -8,17 +8,20 @@
|
|||||||
"name": "reactflow-shadcn-app",
|
"name": "reactflow-shadcn-app",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@monaco-editor/react": "^4.7.0",
|
"@codemirror/lang-javascript": "^6.2.2",
|
||||||
|
"@codemirror/lang-yaml": "^6.0.2",
|
||||||
"@radix-ui/react-context-menu": "^2.2.16",
|
"@radix-ui/react-context-menu": "^2.2.16",
|
||||||
"@radix-ui/react-menubar": "^1.1.16",
|
"@radix-ui/react-menubar": "^1.1.16",
|
||||||
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
|
"@uiw/react-codemirror": "^4.25.7",
|
||||||
"@xyflow/react": "^12.10.1",
|
"@xyflow/react": "^12.10.1",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"lucide-react": "^0.577.0",
|
"lucide-react": "^0.577.0",
|
||||||
"monaco-editor": "^0.53.0",
|
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"reactflow": "^11.0.0",
|
"reactflow": "^11.0.0",
|
||||||
@@ -490,6 +493,15 @@
|
|||||||
"@babel/core": "^7.0.0-0"
|
"@babel/core": "^7.0.0-0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/runtime": {
|
||||||
|
"version": "7.28.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
|
||||||
|
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/template": {
|
"node_modules/@babel/template": {
|
||||||
"version": "7.28.6",
|
"version": "7.28.6",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
||||||
@@ -538,6 +550,129 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@codemirror/autocomplete": {
|
||||||
|
"version": "6.20.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.1.tgz",
|
||||||
|
"integrity": "sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/language": "^6.0.0",
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.17.0",
|
||||||
|
"@lezer/common": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/commands": {
|
||||||
|
"version": "6.10.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.2.tgz",
|
||||||
|
"integrity": "sha512-vvX1fsih9HledO1c9zdotZYUZnE4xV0m6i3m25s5DIfXofuprk6cRcLUZvSk3CASUbwjQX21tOGbkY2BH8TpnQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/language": "^6.0.0",
|
||||||
|
"@codemirror/state": "^6.4.0",
|
||||||
|
"@codemirror/view": "^6.27.0",
|
||||||
|
"@lezer/common": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/lang-javascript": {
|
||||||
|
"version": "6.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.5.tgz",
|
||||||
|
"integrity": "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/autocomplete": "^6.0.0",
|
||||||
|
"@codemirror/language": "^6.6.0",
|
||||||
|
"@codemirror/lint": "^6.0.0",
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.17.0",
|
||||||
|
"@lezer/common": "^1.0.0",
|
||||||
|
"@lezer/javascript": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/lang-yaml": {
|
||||||
|
"version": "6.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/lang-yaml/-/lang-yaml-6.1.2.tgz",
|
||||||
|
"integrity": "sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/autocomplete": "^6.0.0",
|
||||||
|
"@codemirror/language": "^6.0.0",
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@lezer/common": "^1.2.0",
|
||||||
|
"@lezer/highlight": "^1.2.0",
|
||||||
|
"@lezer/lr": "^1.0.0",
|
||||||
|
"@lezer/yaml": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/language": {
|
||||||
|
"version": "6.12.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.2.tgz",
|
||||||
|
"integrity": "sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.23.0",
|
||||||
|
"@lezer/common": "^1.5.0",
|
||||||
|
"@lezer/highlight": "^1.0.0",
|
||||||
|
"@lezer/lr": "^1.0.0",
|
||||||
|
"style-mod": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/lint": {
|
||||||
|
"version": "6.9.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.5.tgz",
|
||||||
|
"integrity": "sha512-GElsbU9G7QT9xXhpUg1zWGmftA/7jamh+7+ydKRuT0ORpWS3wOSP0yT1FOlIZa7mIJjpVPipErsyvVqB9cfTFA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.35.0",
|
||||||
|
"crelt": "^1.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/search": {
|
||||||
|
"version": "6.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.6.0.tgz",
|
||||||
|
"integrity": "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.37.0",
|
||||||
|
"crelt": "^1.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/state": {
|
||||||
|
"version": "6.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.4.tgz",
|
||||||
|
"integrity": "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@marijn/find-cluster-break": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/theme-one-dark": {
|
||||||
|
"version": "6.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz",
|
||||||
|
"integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/language": "^6.0.0",
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.0.0",
|
||||||
|
"@lezer/highlight": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/view": {
|
||||||
|
"version": "6.39.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.16.tgz",
|
||||||
|
"integrity": "sha512-m6S22fFpKtOWhq8HuhzsI1WzUP/hB9THbDj0Tl5KX4gbO6Y91hwBl7Yky33NdvB6IffuRFiBxf1R8kJMyXmA4Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/state": "^6.5.0",
|
||||||
|
"crelt": "^1.0.6",
|
||||||
|
"style-mod": "^4.1.0",
|
||||||
|
"w3c-keyname": "^2.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@dotenvx/dotenvx": {
|
"node_modules/@dotenvx/dotenvx": {
|
||||||
"version": "1.53.0",
|
"version": "1.53.0",
|
||||||
"resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.53.0.tgz",
|
"resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.53.0.tgz",
|
||||||
@@ -1320,6 +1455,58 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@lezer/common": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@lezer/highlight": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@lezer/common": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@lezer/javascript": {
|
||||||
|
"version": "1.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz",
|
||||||
|
"integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@lezer/common": "^1.2.0",
|
||||||
|
"@lezer/highlight": "^1.1.3",
|
||||||
|
"@lezer/lr": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@lezer/lr": {
|
||||||
|
"version": "1.4.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.8.tgz",
|
||||||
|
"integrity": "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@lezer/common": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@lezer/yaml": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lezer/yaml/-/yaml-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@lezer/common": "^1.2.0",
|
||||||
|
"@lezer/highlight": "^1.0.0",
|
||||||
|
"@lezer/lr": "^1.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@marijn/find-cluster-break": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@modelcontextprotocol/sdk": {
|
"node_modules/@modelcontextprotocol/sdk": {
|
||||||
"version": "1.27.1",
|
"version": "1.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.27.1.tgz",
|
||||||
@@ -1361,29 +1548,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@monaco-editor/loader": {
|
|
||||||
"version": "1.7.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz",
|
|
||||||
"integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"state-local": "^1.0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@monaco-editor/react": {
|
|
||||||
"version": "4.7.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz",
|
|
||||||
"integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@monaco-editor/loader": "^1.5.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"monaco-editor": ">= 0.25.0 < 1",
|
|
||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
|
||||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@mswjs/interceptors": {
|
"node_modules/@mswjs/interceptors": {
|
||||||
"version": "0.41.3",
|
"version": "0.41.3",
|
||||||
"resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.41.3.tgz",
|
"resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.41.3.tgz",
|
||||||
@@ -1504,6 +1668,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/number": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/primitive": {
|
"node_modules/@radix-ui/primitive": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
||||||
@@ -1977,6 +2147,67 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-select": {
|
||||||
|
"version": "2.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz",
|
||||||
|
"integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/number": "1.1.1",
|
||||||
|
"@radix-ui/primitive": "1.1.3",
|
||||||
|
"@radix-ui/react-collection": "1.1.7",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-direction": "1.1.1",
|
||||||
|
"@radix-ui/react-dismissable-layer": "1.1.11",
|
||||||
|
"@radix-ui/react-focus-guards": "1.1.3",
|
||||||
|
"@radix-ui/react-focus-scope": "1.1.7",
|
||||||
|
"@radix-ui/react-id": "1.1.1",
|
||||||
|
"@radix-ui/react-popper": "1.2.8",
|
||||||
|
"@radix-ui/react-portal": "1.1.9",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-slot": "1.2.3",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1",
|
||||||
|
"@radix-ui/react-use-previous": "1.1.1",
|
||||||
|
"@radix-ui/react-visually-hidden": "1.2.3",
|
||||||
|
"aria-hidden": "^1.2.4",
|
||||||
|
"react-remove-scroll": "^2.6.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-separator": {
|
"node_modules/@radix-ui/react-separator": {
|
||||||
"version": "1.1.8",
|
"version": "1.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz",
|
||||||
@@ -2041,6 +2272,35 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-switch": {
|
||||||
|
"version": "1.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz",
|
||||||
|
"integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.3",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||||
|
"@radix-ui/react-use-previous": "1.1.1",
|
||||||
|
"@radix-ui/react-use-size": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-use-callback-ref": {
|
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||||
@@ -2126,6 +2386,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-previous": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-use-rect": {
|
"node_modules/@radix-ui/react-use-rect": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
|
||||||
@@ -2162,6 +2437,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-visually-hidden": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": "2.1.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/rect": {
|
"node_modules/@radix-ui/rect": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
|
||||||
@@ -3022,12 +3320,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/trusted-types": {
|
|
||||||
"version": "1.0.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-1.0.6.tgz",
|
|
||||||
"integrity": "sha512-230RC8sFeHoT6sSUlRO6a8cAnclO06eeiq1QDfiv2FGCLWFvvERWgwIQD4FWqD9A69BN7Lzee4OXwoMVnnsWDw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@types/validate-npm-package-name": {
|
"node_modules/@types/validate-npm-package-name": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/validate-npm-package-name/-/validate-npm-package-name-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/validate-npm-package-name/-/validate-npm-package-name-4.0.2.tgz",
|
||||||
@@ -3035,6 +3327,59 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@uiw/codemirror-extensions-basic-setup": {
|
||||||
|
"version": "4.25.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.7.tgz",
|
||||||
|
"integrity": "sha512-tPV/AGjF4yM22D5mnyH7EuYBkWO05wF5Y4x3lmQJo6LuHmhjh0RQsVDjqeIgNOkXT3UO9OdkL4dzxw465/JZVg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/autocomplete": "^6.0.0",
|
||||||
|
"@codemirror/commands": "^6.0.0",
|
||||||
|
"@codemirror/language": "^6.0.0",
|
||||||
|
"@codemirror/lint": "^6.0.0",
|
||||||
|
"@codemirror/search": "^6.0.0",
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://jaywcjlove.github.io/#/sponsor"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@codemirror/autocomplete": ">=6.0.0",
|
||||||
|
"@codemirror/commands": ">=6.0.0",
|
||||||
|
"@codemirror/language": ">=6.0.0",
|
||||||
|
"@codemirror/lint": ">=6.0.0",
|
||||||
|
"@codemirror/search": ">=6.0.0",
|
||||||
|
"@codemirror/state": ">=6.0.0",
|
||||||
|
"@codemirror/view": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@uiw/react-codemirror": {
|
||||||
|
"version": "4.25.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.25.7.tgz",
|
||||||
|
"integrity": "sha512-s/EbEe0dFANWEgfLbfdIrrOGv0R7M1XhkKG3ShroBeH6uP9pVNQy81YHOLRCSVcytTp9zAWRNfXR/+XxZTvV7w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.18.6",
|
||||||
|
"@codemirror/commands": "^6.1.0",
|
||||||
|
"@codemirror/state": "^6.1.1",
|
||||||
|
"@codemirror/theme-one-dark": "^6.0.0",
|
||||||
|
"@uiw/codemirror-extensions-basic-setup": "4.25.7",
|
||||||
|
"codemirror": "^6.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://jaywcjlove.github.io/#/sponsor"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@babel/runtime": ">=7.11.0",
|
||||||
|
"@codemirror/state": ">=6.0.0",
|
||||||
|
"@codemirror/theme-one-dark": ">=6.0.0",
|
||||||
|
"@codemirror/view": ">=6.0.0",
|
||||||
|
"codemirror": ">=6.0.0",
|
||||||
|
"react": ">=17.0.0",
|
||||||
|
"react-dom": ">=17.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vitejs/plugin-react": {
|
"node_modules/@vitejs/plugin-react": {
|
||||||
"version": "5.1.4",
|
"version": "5.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.4.tgz",
|
||||||
@@ -3695,6 +4040,21 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/codemirror": {
|
||||||
|
"version": "6.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
|
||||||
|
"integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/autocomplete": "^6.0.0",
|
||||||
|
"@codemirror/commands": "^6.0.0",
|
||||||
|
"@codemirror/language": "^6.0.0",
|
||||||
|
"@codemirror/lint": "^6.0.0",
|
||||||
|
"@codemirror/search": "^6.0.0",
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
@@ -3821,6 +4181,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/crelt": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
||||||
|
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@@ -5569,15 +5935,6 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/monaco-editor": {
|
|
||||||
"version": "0.53.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.53.0.tgz",
|
|
||||||
"integrity": "sha512-0WNThgC6CMWNXXBxTbaYYcunj08iB5rnx4/G56UOPeL9UVIUGGHA1GR0EWIh9Ebabj7NpCRawQ5b0hfN1jQmYQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/trusted-types": "^1.0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
@@ -6978,12 +7335,6 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/state-local": {
|
|
||||||
"version": "1.0.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
|
|
||||||
"integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/statuses": {
|
"node_modules/statuses": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||||
@@ -7089,6 +7440,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/style-mod": {
|
||||||
|
"version": "4.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
|
||||||
|
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/sucrase": {
|
"node_modules/sucrase": {
|
||||||
"version": "3.35.1",
|
"version": "3.35.1",
|
||||||
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
|
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
|
||||||
@@ -7652,6 +8009,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/w3c-keyname": {
|
||||||
|
"version": "2.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||||
|
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/web-streams-polyfill": {
|
"node_modules/web-streams-polyfill": {
|
||||||
"version": "3.3.3",
|
"version": "3.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
||||||
|
|||||||
@@ -8,17 +8,18 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@monaco-editor/react": "^4.7.0",
|
"@codemirror/lang-javascript": "^6.2.2",
|
||||||
|
"@uiw/react-codemirror": "^4.25.7",
|
||||||
"@radix-ui/react-context-menu": "^2.2.16",
|
"@radix-ui/react-context-menu": "^2.2.16",
|
||||||
"@radix-ui/react-menubar": "^1.1.16",
|
"@radix-ui/react-menubar": "^1.1.16",
|
||||||
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
"@xyflow/react": "^12.10.1",
|
"@xyflow/react": "^12.10.1",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"js-yaml": "^4.1.0",
|
|
||||||
"lucide-react": "^0.577.0",
|
"lucide-react": "^0.577.0",
|
||||||
"monaco-editor": "^0.53.0",
|
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"reactflow": "^11.0.0",
|
"reactflow": "^11.0.0",
|
||||||
@@ -26,7 +27,6 @@
|
|||||||
"tailwindcss-animate": "^1.0.7"
|
"tailwindcss-animate": "^1.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/js-yaml": "^4.0.9",
|
|
||||||
"@types/node": "^25.3.3",
|
"@types/node": "^25.3.3",
|
||||||
"@types/react": "^18.0.0",
|
"@types/react": "^18.0.0",
|
||||||
"@types/react-dom": "^18.0.0",
|
"@types/react-dom": "^18.0.0",
|
||||||
|
|||||||
73
src/App.tsx
73
src/App.tsx
@@ -1,22 +1,24 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactFlow, {
|
import {
|
||||||
|
ReactFlow,
|
||||||
|
ReactFlowProvider,
|
||||||
Controls,
|
Controls,
|
||||||
Background,
|
Background,
|
||||||
MiniMap,
|
MiniMap,
|
||||||
addEdge,
|
addEdge,
|
||||||
applyEdgeChanges,
|
|
||||||
applyNodeChanges,
|
|
||||||
useNodesState,
|
useNodesState,
|
||||||
useEdgesState,
|
useEdgesState,
|
||||||
Node,
|
type Node,
|
||||||
Edge,
|
type Edge,
|
||||||
Connection,
|
type Connection,
|
||||||
NodeChange,
|
type ColorMode,
|
||||||
EdgeChange,
|
} from '@xyflow/react'
|
||||||
} from 'reactflow'
|
|
||||||
import ConfigNode from './components/graph/ConfigNode'
|
import ConfigNode from './components/graph/ConfigNode'
|
||||||
|
import FunctionNode from './components/graph/FunctionNode'
|
||||||
import RenderingNode from './components/graph/RenderingNode'
|
import RenderingNode from './components/graph/RenderingNode'
|
||||||
|
import VariableNode from './components/graph/VariableNode'
|
||||||
import FlowContext from './lib/flowContext'
|
import FlowContext from './lib/flowContext'
|
||||||
|
import { useTheme } from './lib/themeContext'
|
||||||
import {
|
import {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
ContextMenuContent,
|
ContextMenuContent,
|
||||||
@@ -25,14 +27,22 @@ import {
|
|||||||
ContextMenuLabel,
|
ContextMenuLabel,
|
||||||
ContextMenuTrigger,
|
ContextMenuTrigger,
|
||||||
} from "@/components/ui/context-menu"
|
} from "@/components/ui/context-menu"
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Moon, Sun } from 'lucide-react'
|
||||||
// Generate short unique node ids when missing
|
// Generate short unique node ids when missing
|
||||||
const genId = () => `node_${Math.random().toString(36).slice(2, 9)}`
|
const genId = () => `node_${Math.random().toString(36).slice(2, 9)}`
|
||||||
|
|
||||||
|
const SNAP_GRID: [number, number] = [15, 15]
|
||||||
|
const snapToGrid = (x: number, y: number): { x: number; y: number } => ({
|
||||||
|
x: Math.round(x / SNAP_GRID[0]) * SNAP_GRID[0],
|
||||||
|
y: Math.round(y / SNAP_GRID[1]) * SNAP_GRID[1],
|
||||||
|
})
|
||||||
|
|
||||||
const initialNodes: Node[] = [
|
const initialNodes: Node[] = [
|
||||||
{
|
{
|
||||||
id: 'config-1',
|
id: 'config-1',
|
||||||
position: { x: 50, y: 50 },
|
position: { x: 50, y: 50 },
|
||||||
data: { yaml: "# example:\nmessage: Hello from config" },
|
data: { plantuml: '@startuml\nactor User\nparticipant "Render" as R\nUser -> R : config\n@enduml\n' },
|
||||||
type: 'config',
|
type: 'config',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -46,6 +56,7 @@ const initialNodes: Node[] = [
|
|||||||
const initialEdges: Edge[] = [{ id: 'e-config-render', source: 'config-1', target: 'render-1' }]
|
const initialEdges: Edge[] = [{ id: 'e-config-render', source: 'config-1', target: 'render-1' }]
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
const { theme, toggleTheme } = useTheme()
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes)
|
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes)
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges)
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges)
|
||||||
const [rfInstance, setRfInstance] = React.useState<any | null>(null)
|
const [rfInstance, setRfInstance] = React.useState<any | null>(null)
|
||||||
@@ -55,7 +66,7 @@ export default function App() {
|
|||||||
const [contextTarget, setContextTarget] = React.useState<null | { type: 'canvas' | 'node'; nodeId?: string; clientX: number; clientY: number }>(null)
|
const [contextTarget, setContextTarget] = React.useState<null | { type: 'canvas' | 'node'; nodeId?: string; clientX: number; clientY: number }>(null)
|
||||||
|
|
||||||
const nodeTypes = React.useMemo(
|
const nodeTypes = React.useMemo(
|
||||||
() => ({ config: ConfigNode, render: RenderingNode }),
|
() => ({ config: ConfigNode, render: RenderingNode, variable: VariableNode, function: FunctionNode }),
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -102,13 +113,26 @@ export default function App() {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
// ignore and use raw point
|
// ignore and use raw point
|
||||||
}
|
}
|
||||||
|
position = snapToGrid(position.x, position.y)
|
||||||
|
|
||||||
const id = genId()
|
const id = genId()
|
||||||
|
const typeMap: Record<string, Node['type']> = {
|
||||||
|
config: 'config',
|
||||||
|
render: 'render',
|
||||||
|
variable: 'variable',
|
||||||
|
function: 'function',
|
||||||
|
}
|
||||||
|
const dataMap: Record<string, any> = {
|
||||||
|
config: { plantuml: '@startuml\n\n@enduml\n', title: `config-${id}` },
|
||||||
|
render: {},
|
||||||
|
variable: { value: '', valueType: 'string' },
|
||||||
|
function: { body: '// args[0], args[1], ...\nreturn args[0];' },
|
||||||
|
}
|
||||||
const newNode: Node = {
|
const newNode: Node = {
|
||||||
id,
|
id,
|
||||||
type: type === 'config' ? 'config' : 'render',
|
type: typeMap[type] ?? 'config',
|
||||||
position: { x: position.x, y: position.y },
|
position: { x: position.x, y: position.y },
|
||||||
data: type === 'config' ? { yaml: '# Enter YAML here\n', title: `config-${id}` } : {},
|
data: dataMap[type] ?? {},
|
||||||
}
|
}
|
||||||
setNodes((nds) => nds.concat(newNode))
|
setNodes((nds) => nds.concat(newNode))
|
||||||
lastClickRef.current = null
|
lastClickRef.current = null
|
||||||
@@ -126,24 +150,39 @@ export default function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="reactflow-wrapper" ref={wrapperRef} onContextMenu={onCanvasContextMenu} style={{ position: 'relative' }}>
|
<div className="reactflow-wrapper" ref={wrapperRef} onContextMenu={onCanvasContextMenu} style={{ position: 'relative' }}>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="absolute top-3 right-3 z-10 nodrag nopan"
|
||||||
|
onClick={toggleTheme}
|
||||||
|
title={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
|
||||||
|
aria-label={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
|
||||||
|
>
|
||||||
|
{theme === 'dark' ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
|
||||||
|
</Button>
|
||||||
<FlowContext.Provider value={{ nodes, setNodes, edges, setEdges }}>
|
<FlowContext.Provider value={{ nodes, setNodes, edges, setEdges }}>
|
||||||
<ContextMenu>
|
<ContextMenu>
|
||||||
<ContextMenuTrigger asChild>
|
<ContextMenuTrigger asChild>
|
||||||
<div style={{ width: '100%', height: '100%' }}>
|
<div style={{ width: '100%', height: '100%' }}>
|
||||||
<ReactFlow
|
<ReactFlowProvider initialNodes={nodes} initialEdges={edges} fitView>
|
||||||
|
<ReactFlow
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
edges={edges}
|
edges={edges}
|
||||||
onNodesChange={onNodesChange}
|
onNodesChange={onNodesChange}
|
||||||
onEdgesChange={onEdgesChange}
|
onEdgesChange={onEdgesChange}
|
||||||
onConnect={onConnect}
|
onConnect={onConnect}
|
||||||
nodeTypes={nodeTypes}
|
nodeTypes={nodeTypes}
|
||||||
|
colorMode={theme as ColorMode}
|
||||||
|
snapToGrid
|
||||||
|
snapGrid={SNAP_GRID}
|
||||||
fitView
|
fitView
|
||||||
onInit={onInit}
|
onInit={onInit}
|
||||||
>
|
>
|
||||||
<Background />
|
<Background variant="dots" gap={20} />
|
||||||
<Controls />ß
|
<Controls />
|
||||||
<MiniMap />
|
<MiniMap />
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
|
</ReactFlowProvider>
|
||||||
</div>
|
</div>
|
||||||
</ContextMenuTrigger>
|
</ContextMenuTrigger>
|
||||||
|
|
||||||
@@ -156,6 +195,8 @@ export default function App() {
|
|||||||
<ContextMenuLabel>New Node</ContextMenuLabel>
|
<ContextMenuLabel>New Node</ContextMenuLabel>
|
||||||
<ContextMenuItem onSelect={() => createNode('config')}>Config</ContextMenuItem>
|
<ContextMenuItem onSelect={() => createNode('config')}>Config</ContextMenuItem>
|
||||||
<ContextMenuItem onSelect={() => createNode('render')}>Renderer</ContextMenuItem>
|
<ContextMenuItem onSelect={() => createNode('render')}>Renderer</ContextMenuItem>
|
||||||
|
<ContextMenuItem onSelect={() => createNode('variable')}>Variable</ContextMenuItem>
|
||||||
|
<ContextMenuItem onSelect={() => createNode('function')}>Function</ContextMenuItem>
|
||||||
</ContextMenuGroup>
|
</ContextMenuGroup>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
24
src/components/graph/BaseHandle.tsx
Normal file
24
src/components/graph/BaseHandle.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import type { ComponentProps } from "react";
|
||||||
|
import { Handle, type HandleProps } from "@xyflow/react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export type BaseHandleProps = HandleProps;
|
||||||
|
|
||||||
|
export function BaseHandle({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: ComponentProps<typeof Handle>) {
|
||||||
|
return (
|
||||||
|
<Handle
|
||||||
|
{...props}
|
||||||
|
className={cn(
|
||||||
|
"dark:border-secondary dark:bg-secondary h-[11px] w-[11px] rounded-full border border-slate-300 bg-slate-100 transition",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Handle>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import React, { memo, useCallback, useContext, useEffect, useRef, useState } from 'react'
|
import React, { memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { Handle, Position } from 'reactflow'
|
import CodeMirror from '@uiw/react-codemirror'
|
||||||
import Editor, { loader } from '@monaco-editor/react'
|
|
||||||
import FlowContext from '../../lib/flowContext'
|
import FlowContext from '../../lib/flowContext'
|
||||||
|
import { plantumlLanguage } from '../../lib/plantumlLanguage'
|
||||||
|
import { useTheme } from '../../lib/themeContext'
|
||||||
import {
|
import {
|
||||||
BaseNode,
|
BaseNode,
|
||||||
BaseNodeContent,
|
BaseNodeContent,
|
||||||
@@ -9,22 +10,18 @@ import {
|
|||||||
BaseNodeHeader,
|
BaseNodeHeader,
|
||||||
BaseNodeHeaderTitle,
|
BaseNodeHeaderTitle,
|
||||||
} from './BaseNode'
|
} from './BaseNode'
|
||||||
import { GitBranch, Pencil } from 'lucide-react'
|
import { GitBranchPlus, ScrollText } from 'lucide-react'
|
||||||
import {
|
import {
|
||||||
Menubar,
|
Menubar,
|
||||||
MenubarContent,
|
MenubarContent,
|
||||||
MenubarItem,
|
MenubarItem,
|
||||||
MenubarLabel,
|
|
||||||
MenubarMenu,
|
MenubarMenu,
|
||||||
MenubarSeparator,
|
|
||||||
MenubarSub,
|
MenubarSub,
|
||||||
MenubarSubContent,
|
MenubarSubContent,
|
||||||
MenubarSubTrigger,
|
MenubarSubTrigger,
|
||||||
MenubarTrigger,
|
MenubarTrigger,
|
||||||
} from '../ui/menubar'
|
} from '../ui/menubar'
|
||||||
|
import { InputHandle, OutputHandle } from './NodeHandles'
|
||||||
// Ensure Monaco can load its web workers under Vite by pointing to a CDN
|
|
||||||
loader.config({ paths: { vs: 'https://unpkg.com/monaco-editor@latest/min/vs' } })
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
id: string
|
id: string
|
||||||
@@ -32,105 +29,110 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ConfigNode = memo(function ConfigNode({ id, data }: Props) {
|
export const ConfigNode = memo(function ConfigNode({ id, data }: Props) {
|
||||||
const [value, setValue] = useState<string>(data?.yaml ?? '# Enter YAML here\n')
|
const [value, setValue] = useState<string>(data?.plantuml ?? '@startuml\n\n@enduml\n')
|
||||||
|
const { theme } = useTheme()
|
||||||
const ctx = useContext(FlowContext)
|
const ctx = useContext(FlowContext)
|
||||||
const setNodes = ctx?.setNodes
|
const setNodes = ctx?.setNodes
|
||||||
const storedYaml = ctx?.nodes?.find((n: any) => n.id === id)?.data?.yaml ?? ''
|
const storedPlantuml = ctx?.nodes?.find((n: any) => n.id === id)?.data?.plantuml ?? ''
|
||||||
|
|
||||||
const editorRef = useRef<any>(null)
|
const editorRef = useRef<unknown>(null)
|
||||||
const monacoRef = useRef<any>(null)
|
|
||||||
|
|
||||||
const incomingEdges = ctx?.edges?.filter((e: any) => e.target === id) ?? []
|
const incomingEdges = ctx?.edges?.filter((e: any) => e.target === id) ?? []
|
||||||
const incomingIds = incomingEdges.map((e: any) => e.source).sort()
|
const incomingIds = incomingEdges.map((e: any) => e.source).sort()
|
||||||
const connectedConfigNodes = (ctx?.nodes ?? []).filter((n: any) => incomingIds.includes(n.id) && n.type === 'config')
|
const connectedConfigNodes = (ctx?.nodes ?? []).filter((n: any) => incomingIds.includes(n.id) && n.type === 'config')
|
||||||
|
const connectedVariableNodes = (ctx?.nodes ?? []).filter((n: any) => incomingIds.includes(n.id) && n.type === 'variable')
|
||||||
|
const connectedFunctionNodes = (ctx?.nodes ?? []).filter((n: any) => incomingIds.includes(n.id) && n.type === 'function')
|
||||||
|
const hasDependencies = connectedConfigNodes.length > 0 || connectedVariableNodes.length > 0 || connectedFunctionNodes.length > 0
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (setNodes) {
|
if (setNodes) {
|
||||||
setNodes((nds: any[]) => nds.map((n) => (n.id === id ? { ...n, data: { ...n.data, yaml: value } } : n)))
|
setNodes((nds: any[]) => nds.map((n) => (n.id === id ? { ...n, data: { ...n.data, plantuml: value } } : n)))
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(val?: string) => {
|
(val: string) => {
|
||||||
const v = val ?? ''
|
setValue(val)
|
||||||
setValue(v)
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.debug('ConfigNode:onChange', id, v.substring(0, 60))
|
|
||||||
if (setNodes) {
|
if (setNodes) {
|
||||||
setNodes((nds: any[]) => nds.map((n) => (n.id === id ? { ...n, data: { ...n.data, yaml: v } } : n)))
|
setNodes((nds: any[]) => nds.map((n) => (n.id === id ? { ...n, data: { ...n.data, plantuml: val } } : n)))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[id, setNodes]
|
[id, setNodes]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleEditorMount = useCallback((editor: any, monaco: any) => {
|
const insertAt = useCallback(
|
||||||
editorRef.current = editor
|
(insertText: string, mode: 'prepend' | 'append' | 'cursor') => {
|
||||||
monacoRef.current = monaco
|
const ref = editorRef.current as { view: { state: { doc: { length: number; toString(): string }; selection: { main: { from: number } } }; dispatch: (arg: { changes: { from: number; to: number; insert: string } }) => void } } | null
|
||||||
}, [])
|
if (ref?.view) {
|
||||||
|
const view = ref.view
|
||||||
const insertIncludeFromNode = useCallback(
|
const doc = view.state.doc
|
||||||
(sourceNode: any, mode: 'prepend' | 'append' | 'cursor') => {
|
const len = doc.length
|
||||||
const ref = `${sourceNode.id}.yaml`
|
let from: number
|
||||||
const includeText = `!include ${ref}\n`
|
|
||||||
const editor = editorRef.current
|
|
||||||
const monaco = monacoRef.current
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (editor && monaco) {
|
|
||||||
const model = editor.getModel()
|
|
||||||
const lineCount = model.getLineCount()
|
|
||||||
const selection = editor.getSelection()
|
|
||||||
|
|
||||||
let range
|
|
||||||
if (mode === 'prepend') {
|
|
||||||
range = new monaco.Range(1, 1, 1, 1)
|
|
||||||
} else if (mode === 'append') {
|
|
||||||
range = new monaco.Range(lineCount + 1, 1, lineCount + 1, 1)
|
|
||||||
} else if (selection) {
|
|
||||||
range = new monaco.Range(selection.startLineNumber, selection.startColumn, selection.startLineNumber, selection.startColumn)
|
|
||||||
} else {
|
|
||||||
range = new monaco.Range(lineCount + 1, 1, lineCount + 1, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
editor.executeEdits('insert-include', [{ range, text: includeText, forceMoveMarkers: true }])
|
|
||||||
const newVal = editor.getModel().getValue()
|
|
||||||
onChange(newVal)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode === 'prepend') {
|
if (mode === 'prepend') {
|
||||||
onChange(includeText + value)
|
from = 0
|
||||||
|
} else if (mode === 'append') {
|
||||||
|
from = len
|
||||||
} else {
|
} else {
|
||||||
onChange(value + '\n' + includeText)
|
const main = view.state.selection.main
|
||||||
|
from = main.from
|
||||||
}
|
}
|
||||||
} catch (err) {
|
view.dispatch({ changes: { from, to: from, insert: insertText } })
|
||||||
// eslint-disable-next-line no-console
|
const newVal = view.state.doc.toString()
|
||||||
console.error('Insert include failed', err)
|
onChange(newVal)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (mode === 'prepend') {
|
||||||
|
onChange(insertText + value)
|
||||||
|
} else {
|
||||||
|
onChange(value + insertText)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[onChange, value]
|
[onChange, value]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const insertIncludeFromNode = useCallback(
|
||||||
|
(sourceNode: any, mode: 'prepend' | 'append' | 'cursor') => {
|
||||||
|
const ref = `${sourceNode.id}.puml`
|
||||||
|
insertAt(`!include ${ref}\n`, mode)
|
||||||
|
},
|
||||||
|
[insertAt]
|
||||||
|
)
|
||||||
|
|
||||||
|
const insertVariableReference = useCallback(
|
||||||
|
(sourceNode: any, mode: 'prepend' | 'append' | 'cursor') => {
|
||||||
|
insertAt(`\${${sourceNode.id}}`, mode)
|
||||||
|
},
|
||||||
|
[insertAt]
|
||||||
|
)
|
||||||
|
|
||||||
|
const insertFunctionCall = useCallback(
|
||||||
|
(sourceNode: any, mode: 'prepend' | 'append' | 'cursor') => {
|
||||||
|
insertAt(`\${${sourceNode.id}()}`, mode)
|
||||||
|
},
|
||||||
|
[insertAt]
|
||||||
|
)
|
||||||
|
|
||||||
|
const extensions = useMemo(() => [plantumlLanguage.extension], [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseNode className="w-80">
|
<BaseNode className="w-80">
|
||||||
<BaseNodeHeader className="border-b">
|
<BaseNodeHeader className="border-b">
|
||||||
<Pencil className="size-4" />
|
<ScrollText className="size-4" />
|
||||||
<BaseNodeHeaderTitle>{id}.yml</BaseNodeHeaderTitle>
|
<BaseNodeHeaderTitle>{id}.puml</BaseNodeHeaderTitle>
|
||||||
</BaseNodeHeader>
|
</BaseNodeHeader>
|
||||||
|
|
||||||
<BaseNodeContent>
|
<BaseNodeContent>
|
||||||
{connectedConfigNodes.length > 0 && (
|
{hasDependencies && (
|
||||||
<div className="w-full pb-1">
|
<div className="w-full">
|
||||||
<Menubar className="h-7 bg-muted/60 px-1 py-0">
|
<Menubar className="h-auto bg-none p-1 border-none shadow-none">
|
||||||
<MenubarMenu>
|
<MenubarMenu>
|
||||||
<MenubarTrigger className="px-1.5 py-0 text-xs">
|
<MenubarTrigger className="px-1.5 py-0 text-xs">
|
||||||
<GitBranch className="size-3.5" />
|
<GitBranchPlus className="size-3.5" />
|
||||||
</MenubarTrigger>
|
</MenubarTrigger>
|
||||||
<MenubarContent className="min-w-[12rem]">
|
<MenubarContent className="min-w-[12rem]">
|
||||||
{connectedConfigNodes.map((n: any) => (
|
{connectedConfigNodes.map((n: any) => (
|
||||||
<MenubarSub key={n.id}>
|
<MenubarSub key={`config-${n.id}`}>
|
||||||
<MenubarSubTrigger className="text-xs">
|
<MenubarSubTrigger className="text-xs">
|
||||||
{n.data?.title ?? n.id}
|
{n.data?.title ?? n.id}
|
||||||
</MenubarSubTrigger>
|
</MenubarSubTrigger>
|
||||||
@@ -156,32 +158,63 @@ export const ConfigNode = memo(function ConfigNode({ id, data }: Props) {
|
|||||||
</MenubarSubContent>
|
</MenubarSubContent>
|
||||||
</MenubarSub>
|
</MenubarSub>
|
||||||
))}
|
))}
|
||||||
|
{connectedVariableNodes.map((n: any) => (
|
||||||
|
<MenubarSub key={`var-${n.id}`}>
|
||||||
|
<MenubarSubTrigger className="text-xs">
|
||||||
|
{n.id}
|
||||||
|
</MenubarSubTrigger>
|
||||||
|
<MenubarSubContent>
|
||||||
|
<MenubarItem
|
||||||
|
className="text-xs"
|
||||||
|
onClick={() => insertVariableReference(n, 'cursor')}
|
||||||
|
>
|
||||||
|
Insert at cursor
|
||||||
|
</MenubarItem>
|
||||||
|
</MenubarSubContent>
|
||||||
|
</MenubarSub>
|
||||||
|
))}
|
||||||
|
{connectedFunctionNodes.map((n: any) => (
|
||||||
|
<MenubarSub key={`fn-${n.id}`}>
|
||||||
|
<MenubarSubTrigger className="text-xs">
|
||||||
|
{n.id}
|
||||||
|
</MenubarSubTrigger>
|
||||||
|
<MenubarSubContent>
|
||||||
|
<MenubarItem
|
||||||
|
className="text-xs"
|
||||||
|
onClick={() => insertFunctionCall(n, 'cursor')}
|
||||||
|
>
|
||||||
|
Insert at cursor
|
||||||
|
</MenubarItem>
|
||||||
|
</MenubarSubContent>
|
||||||
|
</MenubarSub>
|
||||||
|
))}
|
||||||
</MenubarContent>
|
</MenubarContent>
|
||||||
</MenubarMenu>
|
</MenubarMenu>
|
||||||
</Menubar>
|
</Menubar>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div style={{ height: 180 }} className="w-full">
|
<div style={{ height: 180 }} className="w-full nodrag nopan overflow-hidden rounded border border-input">
|
||||||
<Editor
|
<CodeMirror
|
||||||
height="100%"
|
// @ts-expect-error ref is { view, state, editor }; package ref type not in our node_modules
|
||||||
defaultLanguage="yaml"
|
ref={editorRef}
|
||||||
value={value}
|
value={value}
|
||||||
theme="vs-light"
|
height="180px"
|
||||||
onMount={handleEditorMount}
|
theme={theme}
|
||||||
|
extensions={extensions}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
options={{ minimap: { enabled: false }, fontSize: 12 }}
|
basicSetup={{ lineNumbers: true, foldGutter: false }}
|
||||||
|
className="text-sm [&_.cm-editor]:outline-none [&_.cm-editor]:cursor-text [&_.cm-gutters]:border-0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</BaseNodeContent>
|
</BaseNodeContent>
|
||||||
|
|
||||||
<BaseNodeFooter>
|
<BaseNodeFooter>
|
||||||
<div className="w-full text-xs text-gray-500">Stored YAML: {storedYaml ? `${storedYaml.length} chars` : 'none'}</div>
|
<div className="w-full text-xs text-muted-foreground">{storedPlantuml ? `${storedPlantuml.length} chars` : 'none'}</div>
|
||||||
</BaseNodeFooter>
|
</BaseNodeFooter>
|
||||||
|
|
||||||
<Handle type="target" position={Position.Left} id="ain" style={{ background: '#F97316', top: 20, width: 12, height: 12, borderRadius: 3 }} />
|
<InputHandle id="ain" />
|
||||||
|
<OutputHandle id="out" />
|
||||||
<Handle type="source" position={Position.Right} id="out" style={{ background: '#10B981', top: 20, width: 12, height: 12, borderRadius: 3 }} />
|
|
||||||
</BaseNode>
|
</BaseNode>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
94
src/components/graph/FunctionNode.tsx
Normal file
94
src/components/graph/FunctionNode.tsx
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import React, { memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import CodeMirror from '@uiw/react-codemirror'
|
||||||
|
import { javascript } from '@codemirror/lang-javascript'
|
||||||
|
import FlowContext from '../../lib/flowContext'
|
||||||
|
import { useTheme } from '../../lib/themeContext'
|
||||||
|
import {
|
||||||
|
BaseNode,
|
||||||
|
BaseNodeContent,
|
||||||
|
BaseNodeFooter,
|
||||||
|
BaseNodeHeader,
|
||||||
|
BaseNodeHeaderTitle,
|
||||||
|
} from './BaseNode'
|
||||||
|
import { InputHandle, OutputHandle } from './NodeHandles'
|
||||||
|
import { Code2 } from 'lucide-react'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: string
|
||||||
|
data: { body?: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_BODY = `// args[0], args[1], ... are the variable values (in order)
|
||||||
|
return args[0];
|
||||||
|
`
|
||||||
|
|
||||||
|
export const FunctionNode = memo(function FunctionNode({ id, data }: Props) {
|
||||||
|
const [value, setValue] = useState<string>(data?.body ?? DEFAULT_BODY)
|
||||||
|
const { theme } = useTheme()
|
||||||
|
const ctx = useContext(FlowContext)
|
||||||
|
const setNodes = ctx?.setNodes
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (setNodes) {
|
||||||
|
setNodes((nds: any[]) =>
|
||||||
|
nds.map((n) => (n.id === id ? { ...n, data: { ...n.data, body: value } } : n))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const onChange = useCallback(
|
||||||
|
(val: string) => {
|
||||||
|
setValue(val)
|
||||||
|
if (setNodes) {
|
||||||
|
setNodes((nds: any[]) =>
|
||||||
|
nds.map((n) => (n.id === id ? { ...n, data: { ...n.data, body: val } } : n))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[id, setNodes]
|
||||||
|
)
|
||||||
|
|
||||||
|
const storedBody = ctx?.nodes?.find((n: any) => n.id === id)?.data?.body ?? ''
|
||||||
|
|
||||||
|
const extensions = useMemo(() => [javascript()], [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseNode className="w-72">
|
||||||
|
<BaseNodeHeader className="border-b">
|
||||||
|
<Code2 className="size-4" />
|
||||||
|
<BaseNodeHeaderTitle>{id}</BaseNodeHeaderTitle>
|
||||||
|
</BaseNodeHeader>
|
||||||
|
|
||||||
|
<BaseNodeContent>
|
||||||
|
<p className="text-[10px] text-muted-foreground mb-1">
|
||||||
|
Call in config: <code className="rounded bg-muted px-0.5">${{id}(var1, var2)}</code>
|
||||||
|
</p>
|
||||||
|
<div style={{ height: 120 }} className="w-full nodrag nopan overflow-hidden rounded border border-input">
|
||||||
|
<CodeMirror
|
||||||
|
value={value}
|
||||||
|
height="120px"
|
||||||
|
theme={theme}
|
||||||
|
extensions={extensions}
|
||||||
|
onChange={onChange}
|
||||||
|
basicSetup={{ lineNumbers: true, foldGutter: false }}
|
||||||
|
className="text-xs [&_.cm-editor]:outline-none [&_.cm-editor]:cursor-text [&_.cm-gutters]:border-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</BaseNodeContent>
|
||||||
|
|
||||||
|
<BaseNodeFooter>
|
||||||
|
<div className="w-full text-xs text-muted-foreground">
|
||||||
|
Body: {storedBody ? `${storedBody.length} chars` : 'none'}
|
||||||
|
</div>
|
||||||
|
</BaseNodeFooter>
|
||||||
|
|
||||||
|
<InputHandle id="in" />
|
||||||
|
<OutputHandle id="out" />
|
||||||
|
</BaseNode>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
FunctionNode.displayName = 'FunctionNode'
|
||||||
|
|
||||||
|
export default FunctionNode
|
||||||
58
src/components/graph/NodeHandles.tsx
Normal file
58
src/components/graph/NodeHandles.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Handle, Position } from '@xyflow/react'
|
||||||
|
import { Circle, CircleDashed } from 'lucide-react'
|
||||||
|
|
||||||
|
type NodeHandleProps = {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InputHandle({ id }: NodeHandleProps) {
|
||||||
|
return (
|
||||||
|
<Handle
|
||||||
|
type="target"
|
||||||
|
position={Position.Left}
|
||||||
|
id={id}
|
||||||
|
style={{
|
||||||
|
top: 20,
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
left: -10,
|
||||||
|
borderRadius: '9999px',
|
||||||
|
background: 'transparent',
|
||||||
|
border: 'none',
|
||||||
|
padding: 0,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CircleDashed className="w-4 h-4 text-muted-foreground pointer-events-none" />
|
||||||
|
</Handle>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function OutputHandle({ id }: NodeHandleProps) {
|
||||||
|
return (
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
id={id}
|
||||||
|
style={{
|
||||||
|
top: 20,
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
right: -10,
|
||||||
|
borderRadius: '9999px',
|
||||||
|
background: 'transparent',
|
||||||
|
border: 'none',
|
||||||
|
padding: 0,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Circle className="w-4 h-4 text-foreground pointer-events-none" />
|
||||||
|
</Handle>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { memo, useContext, useEffect, useMemo, useState } from 'react'
|
import { memo, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { Handle, Position } from 'reactflow'
|
|
||||||
import yaml from 'js-yaml'
|
|
||||||
import FlowContext from '../../lib/flowContext'
|
import FlowContext from '../../lib/flowContext'
|
||||||
|
import { useTheme } from '../../lib/themeContext'
|
||||||
import { Empty, EmptyHeader, EmptyTitle, EmptyDescription, EmptyContent, EmptyMedia } from '../ui/empty'
|
import { Empty, EmptyHeader, EmptyTitle, EmptyDescription, EmptyContent, EmptyMedia } from '../ui/empty'
|
||||||
import {
|
import {
|
||||||
BaseNode,
|
BaseNode,
|
||||||
@@ -10,16 +9,66 @@ import {
|
|||||||
BaseNodeHeader,
|
BaseNodeHeader,
|
||||||
BaseNodeHeaderTitle,
|
BaseNodeHeaderTitle,
|
||||||
} from './BaseNode'
|
} from './BaseNode'
|
||||||
import { Rocket } from 'lucide-react'
|
import { Sparkles } from 'lucide-react'
|
||||||
|
import { InputHandle, OutputHandle } from './NodeHandles'
|
||||||
|
|
||||||
|
// Use relative URL so Vite dev proxy (and optional prod proxy) avoids CORS
|
||||||
|
const KROKI_PLANTUML_SVG = '/api/kroki/plantuml/svg'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
id: string
|
id: string
|
||||||
data?: any
|
data?: any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DARK_SKINPARAMS = `
|
||||||
|
skinparam backgroundColor #1e1e1e
|
||||||
|
skinparam defaultFontColor #e0e0e0
|
||||||
|
skinparam shadowing false
|
||||||
|
skinparam ArrowColor #b0b0b0
|
||||||
|
skinparam ArrowFontColor #e0e0e0
|
||||||
|
skinparam ActivityBackgroundColor #2d2d2d
|
||||||
|
skinparam ActivityBorderColor #6b6b6b
|
||||||
|
skinparam ActivityDiamondBackgroundColor #2d2d2d
|
||||||
|
skinparam ActivityDiamondBorderColor #6b6b6b
|
||||||
|
skinparam SequenceParticipantBackgroundColor #2d2d2d
|
||||||
|
skinparam SequenceParticipantBorderColor #6b6b6b
|
||||||
|
skinparam SequenceLifeLineBorderColor #6b6b6b
|
||||||
|
skinparam SequenceBoxBackgroundColor #252525
|
||||||
|
skinparam SequenceBoxBorderColor #6b6b6b
|
||||||
|
skinparam SequenceActorBackgroundColor #2d2d2d
|
||||||
|
skinparam SequenceActorBorderColor #6b6b6b
|
||||||
|
skinparam ClassBackgroundColor #2d2d2d
|
||||||
|
skinparam ClassBorderColor #6b6b6b
|
||||||
|
skinparam ComponentBackgroundColor #2d2d2d
|
||||||
|
skinparam ComponentBorderColor #6b6b6b
|
||||||
|
skinparam StateBackgroundColor #2d2d2d
|
||||||
|
skinparam StateBorderColor #6b6b6b
|
||||||
|
skinparam partitionBorderColor #6b6b6b
|
||||||
|
skinparam sequence {
|
||||||
|
ArrowColor #b0b0b0
|
||||||
|
LifeLineBorderColor #6b6b6b
|
||||||
|
LifeLineBackgroundColor #2d2d2d
|
||||||
|
ParticipantBorderColor #6b6b6b
|
||||||
|
ActorBorderColor #6b6b6b
|
||||||
|
BoxBorderColor #6b6b6b
|
||||||
|
}
|
||||||
|
skinparam activity {
|
||||||
|
ArrowColor #b0b0b0
|
||||||
|
BorderColor #6b6b6b
|
||||||
|
DiamondBorderColor #6b6b6b
|
||||||
|
}
|
||||||
|
skinparam class {
|
||||||
|
ArrowColor #b0b0b0
|
||||||
|
BorderColor #6b6b6b
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
export const RenderingNode = memo(function RenderingNode({ id }: Props) {
|
export const RenderingNode = memo(function RenderingNode({ id }: Props) {
|
||||||
const [output, setOutput] = useState<string>('')
|
const [svgContent, setSvgContent] = useState<string | null>(null)
|
||||||
const [error, setError] = useState<null | { kind: string; message: string }>(null)
|
const [error, setError] = useState<null | { kind: string; message: string }>(null)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const runIdRef = useRef(0)
|
||||||
|
const { theme } = useTheme()
|
||||||
|
|
||||||
const ctx = useContext(FlowContext)
|
const ctx = useContext(FlowContext)
|
||||||
const nodes = ctx?.nodes ?? []
|
const nodes = ctx?.nodes ?? []
|
||||||
@@ -31,85 +80,193 @@ export const RenderingNode = memo(function RenderingNode({ id }: Props) {
|
|||||||
const incomingIds = useMemo(() => incomingEdges.map((e: any) => e.source).sort(), [incomingEdges])
|
const incomingIds = useMemo(() => incomingEdges.map((e: any) => e.source).sort(), [incomingEdges])
|
||||||
const srcId = incomingIds.length > 0 ? incomingIds[0] : null
|
const srcId = incomingIds.length > 0 ? incomingIds[0] : null
|
||||||
const srcNode = nodes.find((n: any) => n.id === srcId)
|
const srcNode = nodes.find((n: any) => n.id === srcId)
|
||||||
const yamlText = srcNode && srcNode.type === 'config' ? srcNode?.data?.yaml ?? '' : ''
|
const plantumlText = srcNode && srcNode.type === 'config' ? srcNode?.data?.plantuml ?? '' : ''
|
||||||
const srcData = srcNode?.data ?? {}
|
const srcData = srcNode?.data ?? {}
|
||||||
|
|
||||||
useEffect(() => {
|
const configSignature = useMemo(
|
||||||
// debug
|
() =>
|
||||||
// eslint-disable-next-line no-console
|
nodes
|
||||||
console.debug('RenderingNode:selection', {
|
.filter((n: any) => n.type === 'config')
|
||||||
id,
|
.map((n: any) => `${n.id}:${n.data?.title ?? ''}:${n.data?.plantuml ?? ''}`)
|
||||||
incomingIds,
|
.join('|'),
|
||||||
srcNode: srcNode ? { id: srcNode.id, type: srcNode.type, yaml: (srcNode.data?.yaml ?? '').slice(0, 60) } : null,
|
[nodes]
|
||||||
})
|
)
|
||||||
|
|
||||||
if (!yamlText) {
|
const edgesSignature = useMemo(
|
||||||
|
() =>
|
||||||
|
edges
|
||||||
|
.map((e: any) => `${e.source}->${e.target}`)
|
||||||
|
.sort()
|
||||||
|
.join('|'),
|
||||||
|
[edges]
|
||||||
|
)
|
||||||
|
|
||||||
|
const variablesSignature = useMemo(
|
||||||
|
() =>
|
||||||
|
nodes
|
||||||
|
.filter((n: any) => n.type === 'variable')
|
||||||
|
.map((n: any) => `${n.id}:${n.data?.value}`)
|
||||||
|
.join('|'),
|
||||||
|
[nodes]
|
||||||
|
)
|
||||||
|
|
||||||
|
const functionsSignature = useMemo(
|
||||||
|
() =>
|
||||||
|
nodes
|
||||||
|
.filter((n: any) => n.type === 'function')
|
||||||
|
.map((n: any) => `${n.id}:${n.data?.body ?? ''}`)
|
||||||
|
.join('|'),
|
||||||
|
[nodes]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!plantumlText) {
|
||||||
if (incomingIds.length === 0) {
|
if (incomingIds.length === 0) {
|
||||||
setOutput('')
|
setSvgContent(null)
|
||||||
setError(null)
|
setError(null)
|
||||||
|
setLoading(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setOutput('')
|
setSvgContent(null)
|
||||||
setError({ kind: 'no-yaml', message: 'No YAML found on connected configuration node' })
|
setError({ kind: 'no-plantuml', message: 'No PlantUML found on connected configuration node' })
|
||||||
|
setLoading(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
runIdRef.current += 1
|
||||||
const resolveIncludes = (text: string, visited = new Set<string>()): string => {
|
const thisRunId = runIdRef.current
|
||||||
const includeRegex = /^(\s*)!include\s+([^\s]+)\s*$/gm
|
let cancelled = false
|
||||||
return text.replace(includeRegex, (match: string, indent: string, ref: string) => {
|
|
||||||
const refName = ref.endsWith('.yaml') ? ref.slice(0, -5) : ref
|
|
||||||
const refNode = nodes.find((n: any) => n.id === refName || n.data?.title === refName)
|
|
||||||
if (!refNode) throw new Error(`Included node not found: ${ref}`)
|
|
||||||
if (visited.has(refNode.id)) throw new Error(`Circular include detected: ${ref}`)
|
|
||||||
|
|
||||||
const isReachable = (startId: string, targetId: string) => {
|
const run = async () => {
|
||||||
const q: string[] = [startId]
|
setLoading(true)
|
||||||
const seen = new Set<string>([startId])
|
setError(null)
|
||||||
while (q.length) {
|
try {
|
||||||
const cur = q.shift()!
|
const configIdsUsed = new Set<string>()
|
||||||
if (cur === targetId) return true
|
|
||||||
for (const e of edges) {
|
const resolveIncludes = (text: string, visited = new Set<string>()): string => {
|
||||||
if (e.source === cur && !seen.has(e.target)) {
|
const includeRegex = /^(\s*)!include\s+([^\s]+)\s*$/gm
|
||||||
seen.add(e.target)
|
return text.replace(includeRegex, (match: string, indent: string, ref: string) => {
|
||||||
q.push(e.target)
|
const refName = ref.endsWith('.puml') ? ref.slice(0, -5) : ref
|
||||||
|
const refNode = nodes.find((n: any) => n.id === refName || n.data?.title === refName)
|
||||||
|
if (!refNode) throw new Error(`Included node not found: ${ref}`)
|
||||||
|
if (visited.has(refNode.id)) throw new Error(`Circular include detected: ${ref}`)
|
||||||
|
|
||||||
|
const isReachable = (startId: string, targetId: string) => {
|
||||||
|
const q: string[] = [startId]
|
||||||
|
const seen = new Set<string>([startId])
|
||||||
|
while (q.length) {
|
||||||
|
const cur = q.shift()!
|
||||||
|
if (cur === targetId) return true
|
||||||
|
for (const e of edges) {
|
||||||
|
if (e.source === cur && !seen.has(e.target)) {
|
||||||
|
seen.add(e.target)
|
||||||
|
q.push(e.target)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isReachable(refNode.id, id)) throw new Error(`Included node not connected to renderer: ${ref}`)
|
||||||
|
|
||||||
|
visited.add(refNode.id)
|
||||||
|
if (refNode.type === 'config') configIdsUsed.add(refNode.id)
|
||||||
|
const includedRaw = String(refNode.data?.plantuml ?? '')
|
||||||
|
const resolved = resolveIncludes(includedRaw, visited)
|
||||||
|
visited.delete(refNode.id)
|
||||||
|
|
||||||
|
const indented = resolved
|
||||||
|
.split('\n')
|
||||||
|
.map((line: string) => (line === '' ? '' : indent + line))
|
||||||
|
.join('\n')
|
||||||
|
return indented
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (srcId && srcNode?.type === 'config') configIdsUsed.add(srcId)
|
||||||
|
|
||||||
|
const resolvedPlantuml = resolveIncludes(plantumlText)
|
||||||
|
|
||||||
|
const varMap: Record<string, string> = {}
|
||||||
|
for (const e of edges) {
|
||||||
|
if (configIdsUsed.has(e.target)) {
|
||||||
|
const src = nodes.find((n: any) => n.id === e.source)
|
||||||
|
if (src?.type === 'variable') {
|
||||||
|
const v = src.data?.value
|
||||||
|
varMap[src.id] = v === undefined || v === null ? '' : String(v)
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!isReachable(refNode.id, id)) throw new Error(`Included node not connected to renderer: ${ref}`)
|
const resolveFunctionCalls = (text: string): string => {
|
||||||
|
const fnCallRegex = /\$\{([\w-]+)\s*\(([^)]*)\)\}/g
|
||||||
|
return text.replace(fnCallRegex, (match, funcId: string, argsStr: string) => {
|
||||||
|
const fnNode = nodes.find((n: any) => n.id === funcId && n.type === 'function')
|
||||||
|
if (!fnNode) return match
|
||||||
|
const body = fnNode.data?.body ?? 'return args[0];'
|
||||||
|
const argIds = argsStr.split(',').map((s: string) => s.trim()).filter(Boolean)
|
||||||
|
const argValues = argIds.map((argId: string) => varMap[argId] ?? '')
|
||||||
|
try {
|
||||||
|
const fn = new Function('args', body)
|
||||||
|
const result = fn(argValues)
|
||||||
|
if (result === undefined || result === null) return ''
|
||||||
|
if (typeof result === 'string' || typeof result === 'number' || typeof result === 'boolean') return String(result)
|
||||||
|
return String(result)
|
||||||
|
} catch {
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
visited.add(refNode.id)
|
const resolveVariables = (text: string): string =>
|
||||||
const includedRaw = String(refNode.data?.yaml ?? '')
|
text.replace(/\$\{([^}]+)\}/g, (_, varId: string) => varMap[varId.trim()] ?? '')
|
||||||
const resolved = resolveIncludes(includedRaw, visited)
|
|
||||||
visited.delete(refNode.id)
|
|
||||||
|
|
||||||
const indented = resolved
|
const afterFunctions = resolveFunctionCalls(resolvedPlantuml)
|
||||||
.split('\n')
|
let resolvedWithVars = resolveVariables(afterFunctions)
|
||||||
.map((line: string) => (line === '' ? '' : indent + line))
|
if (theme === 'dark') {
|
||||||
.join('\n')
|
resolvedWithVars = resolvedWithVars.replace(
|
||||||
return indented
|
/^(\s*@startuml\s*\n)/i,
|
||||||
|
`$1${DARK_SKINPARAMS}\n`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(KROKI_PLANTUML_SVG, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'text/plain' },
|
||||||
|
body: resolvedWithVars,
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
const resolvedYaml = resolveIncludes(yamlText)
|
if (cancelled || thisRunId !== runIdRef.current) return
|
||||||
const parsed = yaml.load(resolvedYaml)
|
|
||||||
const newOutput = JSON.stringify(parsed, null, 2)
|
if (!res.ok) {
|
||||||
setError(null)
|
const errText = await res.text()
|
||||||
setOutput((prev) => (prev === newOutput ? prev : newOutput))
|
throw new Error(res.status === 400 ? errText || 'Invalid PlantUML' : `Kroki error: ${res.status}`)
|
||||||
} catch (err: any) {
|
}
|
||||||
const msg = 'YAML parse/include error: ' + err.message
|
|
||||||
setOutput('')
|
const svg = await res.text()
|
||||||
setError({ kind: 'parse', message: msg })
|
if (thisRunId !== runIdRef.current) return
|
||||||
|
setSvgContent(svg)
|
||||||
|
setError(null)
|
||||||
|
} catch (err: any) {
|
||||||
|
if (cancelled || thisRunId !== runIdRef.current) return
|
||||||
|
const msg = err?.message ?? 'PlantUML render error'
|
||||||
|
setSvgContent(null)
|
||||||
|
setError({ kind: 'render', message: msg })
|
||||||
|
} finally {
|
||||||
|
if (!cancelled && thisRunId === runIdRef.current) setLoading(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [id, incomingIds.join(','), yamlText, nodes.length, edges.length])
|
|
||||||
|
run()
|
||||||
|
return () => { cancelled = true }
|
||||||
|
// Only re-run when inputs that affect the resolved diagram change (signatures + source + theme).
|
||||||
|
// Do not depend on nodes/edges refs to avoid flicker from unnecessary re-renders.
|
||||||
|
}, [id, theme, plantumlText, configSignature, edgesSignature, variablesSignature, functionsSignature])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseNode className="w-96">
|
<BaseNode className="w-96">
|
||||||
<BaseNodeHeader className="border-b">
|
<BaseNodeHeader className="border-b">
|
||||||
<Rocket className="size-4" />
|
<Sparkles className="size-4" />
|
||||||
<BaseNodeHeaderTitle>{id}</BaseNodeHeaderTitle>
|
<BaseNodeHeaderTitle>{id}</BaseNodeHeaderTitle>
|
||||||
</BaseNodeHeader>
|
</BaseNodeHeader>
|
||||||
|
|
||||||
@@ -118,10 +275,10 @@ export const RenderingNode = memo(function RenderingNode({ id }: Props) {
|
|||||||
<Empty>
|
<Empty>
|
||||||
<EmptyHeader>
|
<EmptyHeader>
|
||||||
<EmptyMedia variant="icon">
|
<EmptyMedia variant="icon">
|
||||||
<Rocket className="size-6" />
|
<Sparkles className="size-6" />
|
||||||
</EmptyMedia>
|
</EmptyMedia>
|
||||||
<EmptyTitle>No configuration connected</EmptyTitle>
|
<EmptyTitle>No configuration connected</EmptyTitle>
|
||||||
<EmptyDescription>Connect a Configuration node or create one. The renderer will display parsed YAML.</EmptyDescription>
|
<EmptyDescription>Connect a Configuration node or create one. The renderer will display the PlantUML diagram.</EmptyDescription>
|
||||||
</EmptyHeader>
|
</EmptyHeader>
|
||||||
<EmptyContent>
|
<EmptyContent>
|
||||||
<button
|
<button
|
||||||
@@ -133,7 +290,7 @@ export const RenderingNode = memo(function RenderingNode({ id }: Props) {
|
|||||||
const thisNode = nodes.find((n: any) => n.id === id)
|
const thisNode = nodes.find((n: any) => n.id === id)
|
||||||
const pos = thisNode?.position ?? { x: 0, y: 0 }
|
const pos = thisNode?.position ?? { x: 0, y: 0 }
|
||||||
const newPos = { x: pos.x - 220, y: pos.y }
|
const newPos = { x: pos.x - 220, y: pos.y }
|
||||||
const newNode = { id: nid, type: 'config', position: newPos, data: { yaml: '# Enter YAML\n', title: `config-${nid}` } }
|
const newNode = { id: nid, type: 'config', position: newPos, data: { plantuml: '@startuml\n\n@enduml\n', title: `config-${nid}` } }
|
||||||
setNodes((nds: any[]) => nds.concat(newNode))
|
setNodes((nds: any[]) => nds.concat(newNode))
|
||||||
const edgeId = `e-${nid}-${id}`
|
const edgeId = `e-${nid}-${id}`
|
||||||
setEdges((eds: any[]) => eds.concat({ id: edgeId, source: nid, target: id }))
|
setEdges((eds: any[]) => eds.concat({ id: edgeId, source: nid, target: id }))
|
||||||
@@ -147,25 +304,32 @@ export const RenderingNode = memo(function RenderingNode({ id }: Props) {
|
|||||||
srcData?.renderError ? (
|
srcData?.renderError ? (
|
||||||
srcData.renderError(error)
|
srcData.renderError(error)
|
||||||
) : srcData?.errorHtml ? (
|
) : srcData?.errorHtml ? (
|
||||||
<div className="p-3 text-xs text-red-700" dangerouslySetInnerHTML={{ __html: String(srcData.errorHtml) }} />
|
<div className="p-3 text-xs text-red-700 dark:text-red-400" dangerouslySetInnerHTML={{ __html: String(srcData.errorHtml) }} />
|
||||||
) : (
|
) : (
|
||||||
<div className="p-3 text-xs text-red-700">{error.message}</div>
|
<div className="p-3 text-xs text-red-700 dark:text-red-400">{error.message}</div>
|
||||||
)
|
)
|
||||||
) : (
|
) : loading ? (
|
||||||
<pre className="text-xs text-gray-800 max-h-48 overflow-auto">{output}</pre>
|
<div className="flex min-h-[120px] items-center justify-center text-xs text-muted-foreground">Rendering…</div>
|
||||||
)}
|
) : svgContent ? (
|
||||||
|
<div
|
||||||
|
className="min-h-[120px] w-full overflow-auto rounded border border-input bg-white dark:bg-gray-900 [&_svg]:max-w-full [&_svg]:h-auto"
|
||||||
|
dangerouslySetInnerHTML={{ __html: svgContent }}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</BaseNodeContent>
|
</BaseNodeContent>
|
||||||
|
|
||||||
<BaseNodeFooter>
|
<BaseNodeFooter>
|
||||||
<div className="w-full text-xs text-muted-foreground">Parsed YAML output</div>
|
<div className="w-full text-xs text-muted-foreground">
|
||||||
|
{svgContent ? 'PlantUML diagram' : error ? 'Error' : '—'}
|
||||||
|
</div>
|
||||||
</BaseNodeFooter>
|
</BaseNodeFooter>
|
||||||
|
|
||||||
<Handle type="target" position={Position.Left} id="ain" style={{ background: '#F97316', top: 20, width: 12, height: 12, borderRadius: 3 }} />
|
<InputHandle id="ain" />
|
||||||
<Handle type="source" position={Position.Right} id="out" style={{ background: '#10B981', top: 20, width: 12, height: 12, borderRadius: 3 }} />
|
<OutputHandle id="out" />
|
||||||
</BaseNode>
|
</BaseNode>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
RenderingNode.displayName = 'RenderingNode'
|
RenderingNode.displayName = 'RenderingNode'
|
||||||
|
|
||||||
export default RenderingNode
|
export default RenderingNode
|
||||||
|
|||||||
141
src/components/graph/VariableNode.tsx
Normal file
141
src/components/graph/VariableNode.tsx
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import React, { memo, useCallback, useContext } from 'react'
|
||||||
|
import FlowContext from '../../lib/flowContext'
|
||||||
|
import {
|
||||||
|
BaseNode,
|
||||||
|
BaseNodeContent,
|
||||||
|
BaseNodeHeader,
|
||||||
|
BaseNodeHeaderTitle,
|
||||||
|
} from './BaseNode'
|
||||||
|
import { Input } from '../ui/input'
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
|
||||||
|
import { Switch } from '../ui/switch'
|
||||||
|
import { OutputHandle } from './NodeHandles'
|
||||||
|
import { Variable } from 'lucide-react'
|
||||||
|
|
||||||
|
type ValueType = 'string' | 'number' | 'boolean'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: string
|
||||||
|
data: {
|
||||||
|
value?: string | number | boolean
|
||||||
|
valueType?: ValueType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_BY_TYPE: Record<ValueType, string | number | boolean> = {
|
||||||
|
string: '',
|
||||||
|
number: 0,
|
||||||
|
boolean: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
function coerceValue(raw: string, valueType: ValueType): string | number | boolean {
|
||||||
|
switch (valueType) {
|
||||||
|
case 'number': {
|
||||||
|
const n = Number(raw)
|
||||||
|
return Number.isNaN(n) ? 0 : n
|
||||||
|
}
|
||||||
|
case 'boolean':
|
||||||
|
return /^(1|true|yes|on)$/i.test(raw.trim())
|
||||||
|
default:
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VariableNode = memo(function VariableNode({ id, data }: Props) {
|
||||||
|
const ctx = useContext(FlowContext)
|
||||||
|
const setNodes = ctx?.setNodes
|
||||||
|
|
||||||
|
const valueType: ValueType = data?.valueType ?? 'string'
|
||||||
|
const value = data?.value ?? DEFAULT_BY_TYPE[valueType]
|
||||||
|
const displayValue = typeof value === 'string' ? value : String(value)
|
||||||
|
|
||||||
|
const updateData = useCallback(
|
||||||
|
(updates: { value?: string | number | boolean; valueType?: ValueType }) => {
|
||||||
|
if (!setNodes) return
|
||||||
|
setNodes((nds: any[]) =>
|
||||||
|
nds.map((n) =>
|
||||||
|
n.id === id ? { ...n, data: { ...n.data, ...updates } } : n
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[id, setNodes]
|
||||||
|
)
|
||||||
|
|
||||||
|
const onTypeChange = useCallback(
|
||||||
|
(nextType: string) => {
|
||||||
|
const type = nextType as ValueType
|
||||||
|
const raw = typeof value === 'boolean' ? (value ? 'true' : 'false') : String(value)
|
||||||
|
const nextValue = coerceValue(raw, type)
|
||||||
|
updateData({ valueType: type, value: nextValue })
|
||||||
|
},
|
||||||
|
[value, updateData]
|
||||||
|
)
|
||||||
|
|
||||||
|
const onValueChange = useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const raw = e.target.value
|
||||||
|
const nextValue = coerceValue(raw, valueType)
|
||||||
|
updateData({ value: nextValue })
|
||||||
|
},
|
||||||
|
[valueType, updateData]
|
||||||
|
)
|
||||||
|
|
||||||
|
const onBooleanChange = useCallback(
|
||||||
|
(checked: boolean) => {
|
||||||
|
updateData({ value: checked })
|
||||||
|
},
|
||||||
|
[updateData]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseNode className="w-56">
|
||||||
|
<BaseNodeHeader className="border-b">
|
||||||
|
<Variable className="size-4" />
|
||||||
|
<BaseNodeHeaderTitle>{id}</BaseNodeHeaderTitle>
|
||||||
|
</BaseNodeHeader>
|
||||||
|
|
||||||
|
<BaseNodeContent className="gap-2 p-3">
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<label className="text-xs font-medium text-muted-foreground">Type</label>
|
||||||
|
<Select value={valueType} onValueChange={onTypeChange}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Type" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="string">String</SelectItem>
|
||||||
|
<SelectItem value="number">Number</SelectItem>
|
||||||
|
<SelectItem value="boolean">Boolean</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<label className="text-xs font-medium text-muted-foreground">Value</label>
|
||||||
|
{valueType === 'boolean' ? (
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<Switch
|
||||||
|
checked={value === true}
|
||||||
|
onCheckedChange={onBooleanChange}
|
||||||
|
/>
|
||||||
|
<span className="text-muted-foreground">{value ? 'true' : 'false'}</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
type={valueType === 'number' ? 'number' : 'text'}
|
||||||
|
value={valueType === 'number' ? (value as number) : displayValue}
|
||||||
|
onChange={onValueChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-muted-foreground">
|
||||||
|
Use in config: <code className="rounded bg-muted px-0.5">prop: ${{id}}</code>
|
||||||
|
</p>
|
||||||
|
</BaseNodeContent>
|
||||||
|
|
||||||
|
<OutputHandle id="out" />
|
||||||
|
</BaseNode>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
VariableNode.displayName = 'VariableNode'
|
||||||
|
|
||||||
|
export default VariableNode
|
||||||
22
src/components/ui/input.tsx
Normal file
22
src/components/ui/input.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||||
|
({ className, type, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
className={cn(
|
||||||
|
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Input.displayName = "Input"
|
||||||
|
|
||||||
|
export { Input }
|
||||||
157
src/components/ui/select.tsx
Normal file
157
src/components/ui/select.tsx
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||||
|
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Select = SelectPrimitive.Root
|
||||||
|
|
||||||
|
const SelectGroup = SelectPrimitive.Group
|
||||||
|
|
||||||
|
const SelectValue = SelectPrimitive.Value
|
||||||
|
|
||||||
|
const SelectTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<SelectPrimitive.Icon asChild>
|
||||||
|
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||||
|
</SelectPrimitive.Icon>
|
||||||
|
</SelectPrimitive.Trigger>
|
||||||
|
))
|
||||||
|
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||||
|
|
||||||
|
const SelectScrollUpButton = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.ScrollUpButton
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronUp className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ScrollUpButton>
|
||||||
|
))
|
||||||
|
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
||||||
|
|
||||||
|
const SelectScrollDownButton = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.ScrollDownButton
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ScrollDownButton>
|
||||||
|
))
|
||||||
|
SelectScrollDownButton.displayName =
|
||||||
|
SelectPrimitive.ScrollDownButton.displayName
|
||||||
|
|
||||||
|
const SelectContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||||
|
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Portal>
|
||||||
|
<SelectPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-select-content-transform-origin]",
|
||||||
|
position === "popper" &&
|
||||||
|
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
position={position}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SelectScrollUpButton />
|
||||||
|
<SelectPrimitive.Viewport
|
||||||
|
className={cn(
|
||||||
|
"p-1",
|
||||||
|
position === "popper" &&
|
||||||
|
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SelectPrimitive.Viewport>
|
||||||
|
<SelectScrollDownButton />
|
||||||
|
</SelectPrimitive.Content>
|
||||||
|
</SelectPrimitive.Portal>
|
||||||
|
))
|
||||||
|
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const SelectLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Label
|
||||||
|
ref={ref}
|
||||||
|
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||||
|
|
||||||
|
const SelectItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<SelectPrimitive.ItemIndicator>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
|
</SelectPrimitive.Item>
|
||||||
|
))
|
||||||
|
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||||
|
|
||||||
|
const SelectSeparator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Separator
|
||||||
|
ref={ref}
|
||||||
|
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
Select,
|
||||||
|
SelectGroup,
|
||||||
|
SelectValue,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectContent,
|
||||||
|
SelectLabel,
|
||||||
|
SelectItem,
|
||||||
|
SelectSeparator,
|
||||||
|
SelectScrollUpButton,
|
||||||
|
SelectScrollDownButton,
|
||||||
|
}
|
||||||
27
src/components/ui/switch.tsx
Normal file
27
src/components/ui/switch.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Switch = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SwitchPrimitives.Root
|
||||||
|
className={cn(
|
||||||
|
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
<SwitchPrimitives.Thumb
|
||||||
|
className={cn(
|
||||||
|
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SwitchPrimitives.Root>
|
||||||
|
))
|
||||||
|
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||||
|
|
||||||
|
export { Switch }
|
||||||
41
src/lib/plantumlLanguage.ts
Normal file
41
src/lib/plantumlLanguage.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { StreamLanguage } from '@codemirror/language'
|
||||||
|
|
||||||
|
/** Simple PlantUML stream parser for syntax highlighting in CodeMirror */
|
||||||
|
const plantumlParser = StreamLanguage.define({
|
||||||
|
name: 'plantuml',
|
||||||
|
token(stream) {
|
||||||
|
// Single-quote line comment (PlantUML)
|
||||||
|
if (stream.match(/^'/)) {
|
||||||
|
stream.skipToEnd()
|
||||||
|
return 'comment'
|
||||||
|
}
|
||||||
|
// Double-quoted string
|
||||||
|
if (stream.match(/^"/)) {
|
||||||
|
let escaped = false
|
||||||
|
while (!stream.eol()) {
|
||||||
|
if (escaped) {
|
||||||
|
escaped = false
|
||||||
|
stream.next()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const ch = stream.next()
|
||||||
|
if (ch === '\\') escaped = true
|
||||||
|
else if (ch === '"') break
|
||||||
|
}
|
||||||
|
return 'string'
|
||||||
|
}
|
||||||
|
// @directives (@startuml, @enduml, etc.)
|
||||||
|
if (stream.match(/^@\w+/)) return 'meta'
|
||||||
|
// Skip whitespace
|
||||||
|
if (stream.eatSpace()) return null
|
||||||
|
// Arrows and connectors
|
||||||
|
if (stream.match(/^->>?|<-<?|-->>?|<<--?|<-?>/)) return 'keyword'
|
||||||
|
// Keywords (participant, actor, as, title, etc.)
|
||||||
|
if (stream.match(/^(participant|actor|as|title|autonumber|left|right|of|over|activate|deactivate|destroy|create|group|opt|alt|else|loop|par|end|note|legend|skinparam|start|stop|if|endif|elseif|while|endwhile|repeat|until|switch|case|endswitch|class|interface|enum|package|namespace|abstract|static|extends|implements)\b/i)) return 'keyword'
|
||||||
|
// Any other character (identifier, punctuation, etc.)
|
||||||
|
stream.next()
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const plantumlLanguage = plantumlParser
|
||||||
83
src/lib/themeContext.tsx
Normal file
83
src/lib/themeContext.tsx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'zui-theme'
|
||||||
|
|
||||||
|
export type Theme = 'light' | 'dark'
|
||||||
|
|
||||||
|
function readStored(): Theme | null {
|
||||||
|
try {
|
||||||
|
const s = localStorage.getItem(STORAGE_KEY)
|
||||||
|
if (s === 'light' || s === 'dark') return s
|
||||||
|
} catch (_) {}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function systemPrefersDark(): boolean {
|
||||||
|
try {
|
||||||
|
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
|
} catch (_) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ThemeContextValue = {
|
||||||
|
theme: Theme
|
||||||
|
setTheme: (theme: Theme) => void
|
||||||
|
toggleTheme: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemeContext = createContext<ThemeContextValue | null>(null)
|
||||||
|
|
||||||
|
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const [theme, setThemeState] = useState<Theme>(() => {
|
||||||
|
const stored = readStored()
|
||||||
|
if (stored) return stored
|
||||||
|
return systemPrefersDark() ? 'dark' : 'light'
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const root = document.documentElement
|
||||||
|
if (theme === 'dark') {
|
||||||
|
root.classList.add('dark')
|
||||||
|
} else {
|
||||||
|
root.classList.remove('dark')
|
||||||
|
}
|
||||||
|
}, [theme])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const m = window.matchMedia('(prefers-color-scheme: dark)')
|
||||||
|
const handler = () => {
|
||||||
|
if (readStored() != null) return
|
||||||
|
setThemeState(m.matches ? 'dark' : 'light')
|
||||||
|
}
|
||||||
|
m.addEventListener('change', handler)
|
||||||
|
return () => m.removeEventListener('change', handler)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const setTheme = useCallback((next: Theme) => {
|
||||||
|
setThemeState(next)
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEY, next)
|
||||||
|
} catch (_) {}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const toggleTheme = useCallback(() => {
|
||||||
|
setThemeState((prev) => {
|
||||||
|
const next = prev === 'light' ? 'dark' : 'light'
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEY, next)
|
||||||
|
} catch (_) {}
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const value = useMemo(() => ({ theme, setTheme, toggleTheme }), [theme, setTheme, toggleTheme])
|
||||||
|
|
||||||
|
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTheme(): ThemeContextValue {
|
||||||
|
const ctx = useContext(ThemeContext)
|
||||||
|
if (!ctx) throw new Error('useTheme must be used within ThemeProvider')
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import App from './App'
|
import App from './App'
|
||||||
|
import { ThemeProvider } from './lib/themeContext'
|
||||||
import './styles.css'
|
import './styles.css'
|
||||||
import 'reactflow/dist/style.css'
|
import '@xyflow/react/dist/style.css'
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<ThemeProvider>
|
||||||
|
<App />
|
||||||
|
</ThemeProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ body,
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-gray-50 text-gray-900;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reactflow-wrapper {
|
.reactflow-wrapper {
|
||||||
@@ -28,6 +28,10 @@ body {
|
|||||||
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.06);
|
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dark .react-flow__node .react-flow__handle {
|
||||||
|
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
/* Small helper for monospace pre output */
|
/* Small helper for monospace pre output */
|
||||||
pre {
|
pre {
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, 'Roboto Mono', 'Courier New', monospace;
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, 'Roboto Mono', 'Courier New', monospace;
|
||||||
|
|||||||
@@ -5,6 +5,16 @@ import path from 'path'
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
// Avoid CORS: browser calls same origin, Vite forwards to Kroki
|
||||||
|
'/api/kroki': {
|
||||||
|
target: 'https://kroki.io',
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(/^\/api\/kroki/, ''),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': path.resolve(__dirname, './src'),
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
|||||||
Reference in New Issue
Block a user