├── .gitignore ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── public └── favicon.ico ├── src ├── App.css ├── App.tsx ├── GuidedDraggingTool.ts ├── components │ ├── Diagram.css │ ├── DiagramWrapper.tsx │ ├── Inspector.css │ ├── InspectorRow.tsx │ └── SelectionInspector.tsx ├── main.tsx └── vite-env.d.ts ├── tsconfig.json └── vite.config.mts /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /dist 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | .eslintcache 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gojs-react-basic 2 | 3 | ### By Northwoods Software for [GoJS](https://gojs.net) 4 | 5 | This project provides a basic example of using GoJS in a React app. 6 | Check out the [Intro page on using GoJS with React](https://gojs.net/latest/intro/react.html) for more information. 7 | 8 | It makes use of the [gojs-react](https://github.com/NorthwoodsSoftware/gojs-react) package to handle some boilerplate for setting up and tearing down a Diagram component. 9 | 10 | When running the sample, try moving around nodes, editing text, relinking, undoing (Ctrl-Z), etc. within the diagram 11 | and you'll notice the changes are reflected in the inspector area. You'll also notice that changes 12 | made in the inspector are reflected in the diagram. If you use the React dev tools, 13 | you can inspect the React state and see it updated as changes happen. 14 | 15 | For additional samples, see [gojs-react-samples](https://github.com/NorthwoodsSoftware/gojs-react-samples). 16 | 17 | This project uses [immer](https://immerjs.github.io/immer/) to simplify state update operations. 18 | 19 | ## Installation 20 | 21 | Start by running npm install to install all necessary dependencies. 22 | 23 | ## Available Scripts 24 | 25 | In the project directory, you can run: 26 | 27 | ### `npm run dev` 28 | 29 | Runs the app in the development mode.
30 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 31 | 32 | The page will reload if you make edits.
33 | You will also see any lint errors in the console. 34 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | GoJS React App 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gojs-react-basic", 3 | "version": "1.1.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "gojs-react-basic", 9 | "version": "1.1.0", 10 | "dependencies": { 11 | "gojs": "^3.0.21", 12 | "gojs-react": "^1.1.3", 13 | "react": "^19.1.0", 14 | "react-dom": "^19.1.0", 15 | "use-immer": "^0.11.0" 16 | }, 17 | "devDependencies": { 18 | "@types/react": "^19.1.2", 19 | "@types/react-dom": "^19.1.2", 20 | "@vitejs/plugin-react": "^4.4.1", 21 | "typescript": "^5.8.3", 22 | "vite": "^6.3.3" 23 | } 24 | }, 25 | "node_modules/@ampproject/remapping": { 26 | "version": "2.3.0", 27 | "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", 28 | "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", 29 | "dev": true, 30 | "dependencies": { 31 | "@jridgewell/gen-mapping": "^0.3.5", 32 | "@jridgewell/trace-mapping": "^0.3.24" 33 | }, 34 | "engines": { 35 | "node": ">=6.0.0" 36 | } 37 | }, 38 | "node_modules/@babel/code-frame": { 39 | "version": "7.27.1", 40 | "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", 41 | "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", 42 | "dev": true, 43 | "dependencies": { 44 | "@babel/helper-validator-identifier": "^7.27.1", 45 | "js-tokens": "^4.0.0", 46 | "picocolors": "^1.1.1" 47 | }, 48 | "engines": { 49 | "node": ">=6.9.0" 50 | } 51 | }, 52 | "node_modules/@babel/compat-data": { 53 | "version": "7.27.2", 54 | "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.2.tgz", 55 | "integrity": "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==", 56 | "dev": true, 57 | "engines": { 58 | "node": ">=6.9.0" 59 | } 60 | }, 61 | "node_modules/@babel/core": { 62 | "version": "7.27.1", 63 | "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz", 64 | "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==", 65 | "dev": true, 66 | "dependencies": { 67 | "@ampproject/remapping": "^2.2.0", 68 | "@babel/code-frame": "^7.27.1", 69 | "@babel/generator": "^7.27.1", 70 | "@babel/helper-compilation-targets": "^7.27.1", 71 | "@babel/helper-module-transforms": "^7.27.1", 72 | "@babel/helpers": "^7.27.1", 73 | "@babel/parser": "^7.27.1", 74 | "@babel/template": "^7.27.1", 75 | "@babel/traverse": "^7.27.1", 76 | "@babel/types": "^7.27.1", 77 | "convert-source-map": "^2.0.0", 78 | "debug": "^4.1.0", 79 | "gensync": "^1.0.0-beta.2", 80 | "json5": "^2.2.3", 81 | "semver": "^6.3.1" 82 | }, 83 | "engines": { 84 | "node": ">=6.9.0" 85 | }, 86 | "funding": { 87 | "type": "opencollective", 88 | "url": "https://opencollective.com/babel" 89 | } 90 | }, 91 | "node_modules/@babel/generator": { 92 | "version": "7.27.1", 93 | "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz", 94 | "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==", 95 | "dev": true, 96 | "dependencies": { 97 | "@babel/parser": "^7.27.1", 98 | "@babel/types": "^7.27.1", 99 | "@jridgewell/gen-mapping": "^0.3.5", 100 | "@jridgewell/trace-mapping": "^0.3.25", 101 | "jsesc": "^3.0.2" 102 | }, 103 | "engines": { 104 | "node": ">=6.9.0" 105 | } 106 | }, 107 | "node_modules/@babel/helper-compilation-targets": { 108 | "version": "7.27.2", 109 | "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", 110 | "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", 111 | "dev": true, 112 | "dependencies": { 113 | "@babel/compat-data": "^7.27.2", 114 | "@babel/helper-validator-option": "^7.27.1", 115 | "browserslist": "^4.24.0", 116 | "lru-cache": "^5.1.1", 117 | "semver": "^6.3.1" 118 | }, 119 | "engines": { 120 | "node": ">=6.9.0" 121 | } 122 | }, 123 | "node_modules/@babel/helper-module-imports": { 124 | "version": "7.27.1", 125 | "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", 126 | "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", 127 | "dev": true, 128 | "dependencies": { 129 | "@babel/traverse": "^7.27.1", 130 | "@babel/types": "^7.27.1" 131 | }, 132 | "engines": { 133 | "node": ">=6.9.0" 134 | } 135 | }, 136 | "node_modules/@babel/helper-module-transforms": { 137 | "version": "7.27.1", 138 | "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz", 139 | "integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==", 140 | "dev": true, 141 | "dependencies": { 142 | "@babel/helper-module-imports": "^7.27.1", 143 | "@babel/helper-validator-identifier": "^7.27.1", 144 | "@babel/traverse": "^7.27.1" 145 | }, 146 | "engines": { 147 | "node": ">=6.9.0" 148 | }, 149 | "peerDependencies": { 150 | "@babel/core": "^7.0.0" 151 | } 152 | }, 153 | "node_modules/@babel/helper-plugin-utils": { 154 | "version": "7.27.1", 155 | "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", 156 | "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", 157 | "dev": true, 158 | "engines": { 159 | "node": ">=6.9.0" 160 | } 161 | }, 162 | "node_modules/@babel/helper-string-parser": { 163 | "version": "7.27.1", 164 | "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", 165 | "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", 166 | "dev": true, 167 | "engines": { 168 | "node": ">=6.9.0" 169 | } 170 | }, 171 | "node_modules/@babel/helper-validator-identifier": { 172 | "version": "7.27.1", 173 | "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", 174 | "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", 175 | "dev": true, 176 | "engines": { 177 | "node": ">=6.9.0" 178 | } 179 | }, 180 | "node_modules/@babel/helper-validator-option": { 181 | "version": "7.27.1", 182 | "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", 183 | "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", 184 | "dev": true, 185 | "engines": { 186 | "node": ">=6.9.0" 187 | } 188 | }, 189 | "node_modules/@babel/helpers": { 190 | "version": "7.27.1", 191 | "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", 192 | "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", 193 | "dev": true, 194 | "dependencies": { 195 | "@babel/template": "^7.27.1", 196 | "@babel/types": "^7.27.1" 197 | }, 198 | "engines": { 199 | "node": ">=6.9.0" 200 | } 201 | }, 202 | "node_modules/@babel/parser": { 203 | "version": "7.27.2", 204 | "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", 205 | "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", 206 | "dev": true, 207 | "dependencies": { 208 | "@babel/types": "^7.27.1" 209 | }, 210 | "bin": { 211 | "parser": "bin/babel-parser.js" 212 | }, 213 | "engines": { 214 | "node": ">=6.0.0" 215 | } 216 | }, 217 | "node_modules/@babel/plugin-transform-react-jsx-self": { 218 | "version": "7.27.1", 219 | "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", 220 | "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", 221 | "dev": true, 222 | "dependencies": { 223 | "@babel/helper-plugin-utils": "^7.27.1" 224 | }, 225 | "engines": { 226 | "node": ">=6.9.0" 227 | }, 228 | "peerDependencies": { 229 | "@babel/core": "^7.0.0-0" 230 | } 231 | }, 232 | "node_modules/@babel/plugin-transform-react-jsx-source": { 233 | "version": "7.27.1", 234 | "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", 235 | "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", 236 | "dev": true, 237 | "dependencies": { 238 | "@babel/helper-plugin-utils": "^7.27.1" 239 | }, 240 | "engines": { 241 | "node": ">=6.9.0" 242 | }, 243 | "peerDependencies": { 244 | "@babel/core": "^7.0.0-0" 245 | } 246 | }, 247 | "node_modules/@babel/template": { 248 | "version": "7.27.2", 249 | "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", 250 | "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", 251 | "dev": true, 252 | "dependencies": { 253 | "@babel/code-frame": "^7.27.1", 254 | "@babel/parser": "^7.27.2", 255 | "@babel/types": "^7.27.1" 256 | }, 257 | "engines": { 258 | "node": ">=6.9.0" 259 | } 260 | }, 261 | "node_modules/@babel/traverse": { 262 | "version": "7.27.1", 263 | "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz", 264 | "integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==", 265 | "dev": true, 266 | "dependencies": { 267 | "@babel/code-frame": "^7.27.1", 268 | "@babel/generator": "^7.27.1", 269 | "@babel/parser": "^7.27.1", 270 | "@babel/template": "^7.27.1", 271 | "@babel/types": "^7.27.1", 272 | "debug": "^4.3.1", 273 | "globals": "^11.1.0" 274 | }, 275 | "engines": { 276 | "node": ">=6.9.0" 277 | } 278 | }, 279 | "node_modules/@babel/types": { 280 | "version": "7.27.1", 281 | "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", 282 | "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", 283 | "dev": true, 284 | "dependencies": { 285 | "@babel/helper-string-parser": "^7.27.1", 286 | "@babel/helper-validator-identifier": "^7.27.1" 287 | }, 288 | "engines": { 289 | "node": ">=6.9.0" 290 | } 291 | }, 292 | "node_modules/@esbuild/aix-ppc64": { 293 | "version": "0.25.4", 294 | "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", 295 | "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", 296 | "cpu": [ 297 | "ppc64" 298 | ], 299 | "dev": true, 300 | "optional": true, 301 | "os": [ 302 | "aix" 303 | ], 304 | "engines": { 305 | "node": ">=18" 306 | } 307 | }, 308 | "node_modules/@esbuild/android-arm": { 309 | "version": "0.25.4", 310 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", 311 | "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", 312 | "cpu": [ 313 | "arm" 314 | ], 315 | "dev": true, 316 | "optional": true, 317 | "os": [ 318 | "android" 319 | ], 320 | "engines": { 321 | "node": ">=18" 322 | } 323 | }, 324 | "node_modules/@esbuild/android-arm64": { 325 | "version": "0.25.4", 326 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", 327 | "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", 328 | "cpu": [ 329 | "arm64" 330 | ], 331 | "dev": true, 332 | "optional": true, 333 | "os": [ 334 | "android" 335 | ], 336 | "engines": { 337 | "node": ">=18" 338 | } 339 | }, 340 | "node_modules/@esbuild/android-x64": { 341 | "version": "0.25.4", 342 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", 343 | "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", 344 | "cpu": [ 345 | "x64" 346 | ], 347 | "dev": true, 348 | "optional": true, 349 | "os": [ 350 | "android" 351 | ], 352 | "engines": { 353 | "node": ">=18" 354 | } 355 | }, 356 | "node_modules/@esbuild/darwin-arm64": { 357 | "version": "0.25.4", 358 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", 359 | "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", 360 | "cpu": [ 361 | "arm64" 362 | ], 363 | "dev": true, 364 | "optional": true, 365 | "os": [ 366 | "darwin" 367 | ], 368 | "engines": { 369 | "node": ">=18" 370 | } 371 | }, 372 | "node_modules/@esbuild/darwin-x64": { 373 | "version": "0.25.4", 374 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", 375 | "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", 376 | "cpu": [ 377 | "x64" 378 | ], 379 | "dev": true, 380 | "optional": true, 381 | "os": [ 382 | "darwin" 383 | ], 384 | "engines": { 385 | "node": ">=18" 386 | } 387 | }, 388 | "node_modules/@esbuild/freebsd-arm64": { 389 | "version": "0.25.4", 390 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", 391 | "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", 392 | "cpu": [ 393 | "arm64" 394 | ], 395 | "dev": true, 396 | "optional": true, 397 | "os": [ 398 | "freebsd" 399 | ], 400 | "engines": { 401 | "node": ">=18" 402 | } 403 | }, 404 | "node_modules/@esbuild/freebsd-x64": { 405 | "version": "0.25.4", 406 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", 407 | "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", 408 | "cpu": [ 409 | "x64" 410 | ], 411 | "dev": true, 412 | "optional": true, 413 | "os": [ 414 | "freebsd" 415 | ], 416 | "engines": { 417 | "node": ">=18" 418 | } 419 | }, 420 | "node_modules/@esbuild/linux-arm": { 421 | "version": "0.25.4", 422 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", 423 | "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", 424 | "cpu": [ 425 | "arm" 426 | ], 427 | "dev": true, 428 | "optional": true, 429 | "os": [ 430 | "linux" 431 | ], 432 | "engines": { 433 | "node": ">=18" 434 | } 435 | }, 436 | "node_modules/@esbuild/linux-arm64": { 437 | "version": "0.25.4", 438 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", 439 | "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", 440 | "cpu": [ 441 | "arm64" 442 | ], 443 | "dev": true, 444 | "optional": true, 445 | "os": [ 446 | "linux" 447 | ], 448 | "engines": { 449 | "node": ">=18" 450 | } 451 | }, 452 | "node_modules/@esbuild/linux-ia32": { 453 | "version": "0.25.4", 454 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", 455 | "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", 456 | "cpu": [ 457 | "ia32" 458 | ], 459 | "dev": true, 460 | "optional": true, 461 | "os": [ 462 | "linux" 463 | ], 464 | "engines": { 465 | "node": ">=18" 466 | } 467 | }, 468 | "node_modules/@esbuild/linux-loong64": { 469 | "version": "0.25.4", 470 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", 471 | "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", 472 | "cpu": [ 473 | "loong64" 474 | ], 475 | "dev": true, 476 | "optional": true, 477 | "os": [ 478 | "linux" 479 | ], 480 | "engines": { 481 | "node": ">=18" 482 | } 483 | }, 484 | "node_modules/@esbuild/linux-mips64el": { 485 | "version": "0.25.4", 486 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", 487 | "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", 488 | "cpu": [ 489 | "mips64el" 490 | ], 491 | "dev": true, 492 | "optional": true, 493 | "os": [ 494 | "linux" 495 | ], 496 | "engines": { 497 | "node": ">=18" 498 | } 499 | }, 500 | "node_modules/@esbuild/linux-ppc64": { 501 | "version": "0.25.4", 502 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", 503 | "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", 504 | "cpu": [ 505 | "ppc64" 506 | ], 507 | "dev": true, 508 | "optional": true, 509 | "os": [ 510 | "linux" 511 | ], 512 | "engines": { 513 | "node": ">=18" 514 | } 515 | }, 516 | "node_modules/@esbuild/linux-riscv64": { 517 | "version": "0.25.4", 518 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", 519 | "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", 520 | "cpu": [ 521 | "riscv64" 522 | ], 523 | "dev": true, 524 | "optional": true, 525 | "os": [ 526 | "linux" 527 | ], 528 | "engines": { 529 | "node": ">=18" 530 | } 531 | }, 532 | "node_modules/@esbuild/linux-s390x": { 533 | "version": "0.25.4", 534 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", 535 | "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", 536 | "cpu": [ 537 | "s390x" 538 | ], 539 | "dev": true, 540 | "optional": true, 541 | "os": [ 542 | "linux" 543 | ], 544 | "engines": { 545 | "node": ">=18" 546 | } 547 | }, 548 | "node_modules/@esbuild/linux-x64": { 549 | "version": "0.25.4", 550 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", 551 | "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", 552 | "cpu": [ 553 | "x64" 554 | ], 555 | "dev": true, 556 | "optional": true, 557 | "os": [ 558 | "linux" 559 | ], 560 | "engines": { 561 | "node": ">=18" 562 | } 563 | }, 564 | "node_modules/@esbuild/netbsd-arm64": { 565 | "version": "0.25.4", 566 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", 567 | "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", 568 | "cpu": [ 569 | "arm64" 570 | ], 571 | "dev": true, 572 | "optional": true, 573 | "os": [ 574 | "netbsd" 575 | ], 576 | "engines": { 577 | "node": ">=18" 578 | } 579 | }, 580 | "node_modules/@esbuild/netbsd-x64": { 581 | "version": "0.25.4", 582 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", 583 | "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", 584 | "cpu": [ 585 | "x64" 586 | ], 587 | "dev": true, 588 | "optional": true, 589 | "os": [ 590 | "netbsd" 591 | ], 592 | "engines": { 593 | "node": ">=18" 594 | } 595 | }, 596 | "node_modules/@esbuild/openbsd-arm64": { 597 | "version": "0.25.4", 598 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", 599 | "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", 600 | "cpu": [ 601 | "arm64" 602 | ], 603 | "dev": true, 604 | "optional": true, 605 | "os": [ 606 | "openbsd" 607 | ], 608 | "engines": { 609 | "node": ">=18" 610 | } 611 | }, 612 | "node_modules/@esbuild/openbsd-x64": { 613 | "version": "0.25.4", 614 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", 615 | "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", 616 | "cpu": [ 617 | "x64" 618 | ], 619 | "dev": true, 620 | "optional": true, 621 | "os": [ 622 | "openbsd" 623 | ], 624 | "engines": { 625 | "node": ">=18" 626 | } 627 | }, 628 | "node_modules/@esbuild/sunos-x64": { 629 | "version": "0.25.4", 630 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", 631 | "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", 632 | "cpu": [ 633 | "x64" 634 | ], 635 | "dev": true, 636 | "optional": true, 637 | "os": [ 638 | "sunos" 639 | ], 640 | "engines": { 641 | "node": ">=18" 642 | } 643 | }, 644 | "node_modules/@esbuild/win32-arm64": { 645 | "version": "0.25.4", 646 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", 647 | "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", 648 | "cpu": [ 649 | "arm64" 650 | ], 651 | "dev": true, 652 | "optional": true, 653 | "os": [ 654 | "win32" 655 | ], 656 | "engines": { 657 | "node": ">=18" 658 | } 659 | }, 660 | "node_modules/@esbuild/win32-ia32": { 661 | "version": "0.25.4", 662 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", 663 | "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", 664 | "cpu": [ 665 | "ia32" 666 | ], 667 | "dev": true, 668 | "optional": true, 669 | "os": [ 670 | "win32" 671 | ], 672 | "engines": { 673 | "node": ">=18" 674 | } 675 | }, 676 | "node_modules/@esbuild/win32-x64": { 677 | "version": "0.25.4", 678 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", 679 | "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", 680 | "cpu": [ 681 | "x64" 682 | ], 683 | "dev": true, 684 | "optional": true, 685 | "os": [ 686 | "win32" 687 | ], 688 | "engines": { 689 | "node": ">=18" 690 | } 691 | }, 692 | "node_modules/@jridgewell/gen-mapping": { 693 | "version": "0.3.8", 694 | "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", 695 | "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", 696 | "dev": true, 697 | "dependencies": { 698 | "@jridgewell/set-array": "^1.2.1", 699 | "@jridgewell/sourcemap-codec": "^1.4.10", 700 | "@jridgewell/trace-mapping": "^0.3.24" 701 | }, 702 | "engines": { 703 | "node": ">=6.0.0" 704 | } 705 | }, 706 | "node_modules/@jridgewell/resolve-uri": { 707 | "version": "3.1.2", 708 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", 709 | "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", 710 | "dev": true, 711 | "engines": { 712 | "node": ">=6.0.0" 713 | } 714 | }, 715 | "node_modules/@jridgewell/set-array": { 716 | "version": "1.2.1", 717 | "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", 718 | "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", 719 | "dev": true, 720 | "engines": { 721 | "node": ">=6.0.0" 722 | } 723 | }, 724 | "node_modules/@jridgewell/sourcemap-codec": { 725 | "version": "1.5.0", 726 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", 727 | "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", 728 | "dev": true 729 | }, 730 | "node_modules/@jridgewell/trace-mapping": { 731 | "version": "0.3.25", 732 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", 733 | "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", 734 | "dev": true, 735 | "dependencies": { 736 | "@jridgewell/resolve-uri": "^3.1.0", 737 | "@jridgewell/sourcemap-codec": "^1.4.14" 738 | } 739 | }, 740 | "node_modules/@rollup/rollup-android-arm-eabi": { 741 | "version": "4.40.2", 742 | "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz", 743 | "integrity": "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==", 744 | "cpu": [ 745 | "arm" 746 | ], 747 | "dev": true, 748 | "optional": true, 749 | "os": [ 750 | "android" 751 | ] 752 | }, 753 | "node_modules/@rollup/rollup-android-arm64": { 754 | "version": "4.40.2", 755 | "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz", 756 | "integrity": "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==", 757 | "cpu": [ 758 | "arm64" 759 | ], 760 | "dev": true, 761 | "optional": true, 762 | "os": [ 763 | "android" 764 | ] 765 | }, 766 | "node_modules/@rollup/rollup-darwin-arm64": { 767 | "version": "4.40.2", 768 | "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz", 769 | "integrity": "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==", 770 | "cpu": [ 771 | "arm64" 772 | ], 773 | "dev": true, 774 | "optional": true, 775 | "os": [ 776 | "darwin" 777 | ] 778 | }, 779 | "node_modules/@rollup/rollup-darwin-x64": { 780 | "version": "4.40.2", 781 | "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz", 782 | "integrity": "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==", 783 | "cpu": [ 784 | "x64" 785 | ], 786 | "dev": true, 787 | "optional": true, 788 | "os": [ 789 | "darwin" 790 | ] 791 | }, 792 | "node_modules/@rollup/rollup-freebsd-arm64": { 793 | "version": "4.40.2", 794 | "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz", 795 | "integrity": "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==", 796 | "cpu": [ 797 | "arm64" 798 | ], 799 | "dev": true, 800 | "optional": true, 801 | "os": [ 802 | "freebsd" 803 | ] 804 | }, 805 | "node_modules/@rollup/rollup-freebsd-x64": { 806 | "version": "4.40.2", 807 | "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz", 808 | "integrity": "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==", 809 | "cpu": [ 810 | "x64" 811 | ], 812 | "dev": true, 813 | "optional": true, 814 | "os": [ 815 | "freebsd" 816 | ] 817 | }, 818 | "node_modules/@rollup/rollup-linux-arm-gnueabihf": { 819 | "version": "4.40.2", 820 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz", 821 | "integrity": "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==", 822 | "cpu": [ 823 | "arm" 824 | ], 825 | "dev": true, 826 | "optional": true, 827 | "os": [ 828 | "linux" 829 | ] 830 | }, 831 | "node_modules/@rollup/rollup-linux-arm-musleabihf": { 832 | "version": "4.40.2", 833 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz", 834 | "integrity": "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==", 835 | "cpu": [ 836 | "arm" 837 | ], 838 | "dev": true, 839 | "optional": true, 840 | "os": [ 841 | "linux" 842 | ] 843 | }, 844 | "node_modules/@rollup/rollup-linux-arm64-gnu": { 845 | "version": "4.40.2", 846 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz", 847 | "integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==", 848 | "cpu": [ 849 | "arm64" 850 | ], 851 | "dev": true, 852 | "optional": true, 853 | "os": [ 854 | "linux" 855 | ] 856 | }, 857 | "node_modules/@rollup/rollup-linux-arm64-musl": { 858 | "version": "4.40.2", 859 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz", 860 | "integrity": "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==", 861 | "cpu": [ 862 | "arm64" 863 | ], 864 | "dev": true, 865 | "optional": true, 866 | "os": [ 867 | "linux" 868 | ] 869 | }, 870 | "node_modules/@rollup/rollup-linux-loongarch64-gnu": { 871 | "version": "4.40.2", 872 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz", 873 | "integrity": "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==", 874 | "cpu": [ 875 | "loong64" 876 | ], 877 | "dev": true, 878 | "optional": true, 879 | "os": [ 880 | "linux" 881 | ] 882 | }, 883 | "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { 884 | "version": "4.40.2", 885 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz", 886 | "integrity": "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==", 887 | "cpu": [ 888 | "ppc64" 889 | ], 890 | "dev": true, 891 | "optional": true, 892 | "os": [ 893 | "linux" 894 | ] 895 | }, 896 | "node_modules/@rollup/rollup-linux-riscv64-gnu": { 897 | "version": "4.40.2", 898 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz", 899 | "integrity": "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==", 900 | "cpu": [ 901 | "riscv64" 902 | ], 903 | "dev": true, 904 | "optional": true, 905 | "os": [ 906 | "linux" 907 | ] 908 | }, 909 | "node_modules/@rollup/rollup-linux-riscv64-musl": { 910 | "version": "4.40.2", 911 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz", 912 | "integrity": "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==", 913 | "cpu": [ 914 | "riscv64" 915 | ], 916 | "dev": true, 917 | "optional": true, 918 | "os": [ 919 | "linux" 920 | ] 921 | }, 922 | "node_modules/@rollup/rollup-linux-s390x-gnu": { 923 | "version": "4.40.2", 924 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz", 925 | "integrity": "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==", 926 | "cpu": [ 927 | "s390x" 928 | ], 929 | "dev": true, 930 | "optional": true, 931 | "os": [ 932 | "linux" 933 | ] 934 | }, 935 | "node_modules/@rollup/rollup-linux-x64-gnu": { 936 | "version": "4.40.2", 937 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz", 938 | "integrity": "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==", 939 | "cpu": [ 940 | "x64" 941 | ], 942 | "dev": true, 943 | "optional": true, 944 | "os": [ 945 | "linux" 946 | ] 947 | }, 948 | "node_modules/@rollup/rollup-linux-x64-musl": { 949 | "version": "4.40.2", 950 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz", 951 | "integrity": "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==", 952 | "cpu": [ 953 | "x64" 954 | ], 955 | "dev": true, 956 | "optional": true, 957 | "os": [ 958 | "linux" 959 | ] 960 | }, 961 | "node_modules/@rollup/rollup-win32-arm64-msvc": { 962 | "version": "4.40.2", 963 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz", 964 | "integrity": "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==", 965 | "cpu": [ 966 | "arm64" 967 | ], 968 | "dev": true, 969 | "optional": true, 970 | "os": [ 971 | "win32" 972 | ] 973 | }, 974 | "node_modules/@rollup/rollup-win32-ia32-msvc": { 975 | "version": "4.40.2", 976 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz", 977 | "integrity": "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==", 978 | "cpu": [ 979 | "ia32" 980 | ], 981 | "dev": true, 982 | "optional": true, 983 | "os": [ 984 | "win32" 985 | ] 986 | }, 987 | "node_modules/@rollup/rollup-win32-x64-msvc": { 988 | "version": "4.40.2", 989 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz", 990 | "integrity": "sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==", 991 | "cpu": [ 992 | "x64" 993 | ], 994 | "dev": true, 995 | "optional": true, 996 | "os": [ 997 | "win32" 998 | ] 999 | }, 1000 | "node_modules/@types/babel__core": { 1001 | "version": "7.20.5", 1002 | "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", 1003 | "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", 1004 | "dev": true, 1005 | "dependencies": { 1006 | "@babel/parser": "^7.20.7", 1007 | "@babel/types": "^7.20.7", 1008 | "@types/babel__generator": "*", 1009 | "@types/babel__template": "*", 1010 | "@types/babel__traverse": "*" 1011 | } 1012 | }, 1013 | "node_modules/@types/babel__generator": { 1014 | "version": "7.27.0", 1015 | "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", 1016 | "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", 1017 | "dev": true, 1018 | "dependencies": { 1019 | "@babel/types": "^7.0.0" 1020 | } 1021 | }, 1022 | "node_modules/@types/babel__template": { 1023 | "version": "7.4.4", 1024 | "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", 1025 | "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", 1026 | "dev": true, 1027 | "dependencies": { 1028 | "@babel/parser": "^7.1.0", 1029 | "@babel/types": "^7.0.0" 1030 | } 1031 | }, 1032 | "node_modules/@types/babel__traverse": { 1033 | "version": "7.20.7", 1034 | "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", 1035 | "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", 1036 | "dev": true, 1037 | "dependencies": { 1038 | "@babel/types": "^7.20.7" 1039 | } 1040 | }, 1041 | "node_modules/@types/estree": { 1042 | "version": "1.0.7", 1043 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", 1044 | "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", 1045 | "dev": true 1046 | }, 1047 | "node_modules/@types/react": { 1048 | "version": "19.1.3", 1049 | "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.3.tgz", 1050 | "integrity": "sha512-dLWQ+Z0CkIvK1J8+wrDPwGxEYFA4RAyHoZPxHVGspYmFVnwGSNT24cGIhFJrtfRnWVuW8X7NO52gCXmhkVUWGQ==", 1051 | "dev": true, 1052 | "dependencies": { 1053 | "csstype": "^3.0.2" 1054 | } 1055 | }, 1056 | "node_modules/@types/react-dom": { 1057 | "version": "19.1.3", 1058 | "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.3.tgz", 1059 | "integrity": "sha512-rJXC08OG0h3W6wDMFxQrZF00Kq6qQvw0djHRdzl3U5DnIERz0MRce3WVc7IS6JYBwtaP/DwYtRRjVlvivNveKg==", 1060 | "dev": true, 1061 | "peerDependencies": { 1062 | "@types/react": "^19.0.0" 1063 | } 1064 | }, 1065 | "node_modules/@vitejs/plugin-react": { 1066 | "version": "4.4.1", 1067 | "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.1.tgz", 1068 | "integrity": "sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==", 1069 | "dev": true, 1070 | "dependencies": { 1071 | "@babel/core": "^7.26.10", 1072 | "@babel/plugin-transform-react-jsx-self": "^7.25.9", 1073 | "@babel/plugin-transform-react-jsx-source": "^7.25.9", 1074 | "@types/babel__core": "^7.20.5", 1075 | "react-refresh": "^0.17.0" 1076 | }, 1077 | "engines": { 1078 | "node": "^14.18.0 || >=16.0.0" 1079 | }, 1080 | "peerDependencies": { 1081 | "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" 1082 | } 1083 | }, 1084 | "node_modules/browserslist": { 1085 | "version": "4.24.5", 1086 | "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", 1087 | "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", 1088 | "dev": true, 1089 | "funding": [ 1090 | { 1091 | "type": "opencollective", 1092 | "url": "https://opencollective.com/browserslist" 1093 | }, 1094 | { 1095 | "type": "tidelift", 1096 | "url": "https://tidelift.com/funding/github/npm/browserslist" 1097 | }, 1098 | { 1099 | "type": "github", 1100 | "url": "https://github.com/sponsors/ai" 1101 | } 1102 | ], 1103 | "dependencies": { 1104 | "caniuse-lite": "^1.0.30001716", 1105 | "electron-to-chromium": "^1.5.149", 1106 | "node-releases": "^2.0.19", 1107 | "update-browserslist-db": "^1.1.3" 1108 | }, 1109 | "bin": { 1110 | "browserslist": "cli.js" 1111 | }, 1112 | "engines": { 1113 | "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" 1114 | } 1115 | }, 1116 | "node_modules/caniuse-lite": { 1117 | "version": "1.0.30001717", 1118 | "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001717.tgz", 1119 | "integrity": "sha512-auPpttCq6BDEG8ZAuHJIplGw6GODhjw+/11e7IjpnYCxZcW/ONgPs0KVBJ0d1bY3e2+7PRe5RCLyP+PfwVgkYw==", 1120 | "dev": true, 1121 | "funding": [ 1122 | { 1123 | "type": "opencollective", 1124 | "url": "https://opencollective.com/browserslist" 1125 | }, 1126 | { 1127 | "type": "tidelift", 1128 | "url": "https://tidelift.com/funding/github/npm/caniuse-lite" 1129 | }, 1130 | { 1131 | "type": "github", 1132 | "url": "https://github.com/sponsors/ai" 1133 | } 1134 | ] 1135 | }, 1136 | "node_modules/convert-source-map": { 1137 | "version": "2.0.0", 1138 | "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", 1139 | "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", 1140 | "dev": true 1141 | }, 1142 | "node_modules/csstype": { 1143 | "version": "3.1.3", 1144 | "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", 1145 | "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", 1146 | "dev": true 1147 | }, 1148 | "node_modules/debug": { 1149 | "version": "4.4.0", 1150 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", 1151 | "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", 1152 | "dev": true, 1153 | "dependencies": { 1154 | "ms": "^2.1.3" 1155 | }, 1156 | "engines": { 1157 | "node": ">=6.0" 1158 | }, 1159 | "peerDependenciesMeta": { 1160 | "supports-color": { 1161 | "optional": true 1162 | } 1163 | } 1164 | }, 1165 | "node_modules/electron-to-chromium": { 1166 | "version": "1.5.150", 1167 | "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.150.tgz", 1168 | "integrity": "sha512-rOOkP2ZUMx1yL4fCxXQKDHQ8ZXwisb2OycOQVKHgvB3ZI4CvehOd4y2tfnnLDieJ3Zs1RL1Dlp3cMkyIn7nnXA==", 1169 | "dev": true 1170 | }, 1171 | "node_modules/esbuild": { 1172 | "version": "0.25.4", 1173 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", 1174 | "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", 1175 | "dev": true, 1176 | "hasInstallScript": true, 1177 | "bin": { 1178 | "esbuild": "bin/esbuild" 1179 | }, 1180 | "engines": { 1181 | "node": ">=18" 1182 | }, 1183 | "optionalDependencies": { 1184 | "@esbuild/aix-ppc64": "0.25.4", 1185 | "@esbuild/android-arm": "0.25.4", 1186 | "@esbuild/android-arm64": "0.25.4", 1187 | "@esbuild/android-x64": "0.25.4", 1188 | "@esbuild/darwin-arm64": "0.25.4", 1189 | "@esbuild/darwin-x64": "0.25.4", 1190 | "@esbuild/freebsd-arm64": "0.25.4", 1191 | "@esbuild/freebsd-x64": "0.25.4", 1192 | "@esbuild/linux-arm": "0.25.4", 1193 | "@esbuild/linux-arm64": "0.25.4", 1194 | "@esbuild/linux-ia32": "0.25.4", 1195 | "@esbuild/linux-loong64": "0.25.4", 1196 | "@esbuild/linux-mips64el": "0.25.4", 1197 | "@esbuild/linux-ppc64": "0.25.4", 1198 | "@esbuild/linux-riscv64": "0.25.4", 1199 | "@esbuild/linux-s390x": "0.25.4", 1200 | "@esbuild/linux-x64": "0.25.4", 1201 | "@esbuild/netbsd-arm64": "0.25.4", 1202 | "@esbuild/netbsd-x64": "0.25.4", 1203 | "@esbuild/openbsd-arm64": "0.25.4", 1204 | "@esbuild/openbsd-x64": "0.25.4", 1205 | "@esbuild/sunos-x64": "0.25.4", 1206 | "@esbuild/win32-arm64": "0.25.4", 1207 | "@esbuild/win32-ia32": "0.25.4", 1208 | "@esbuild/win32-x64": "0.25.4" 1209 | } 1210 | }, 1211 | "node_modules/escalade": { 1212 | "version": "3.2.0", 1213 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", 1214 | "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", 1215 | "dev": true, 1216 | "engines": { 1217 | "node": ">=6" 1218 | } 1219 | }, 1220 | "node_modules/fdir": { 1221 | "version": "6.4.4", 1222 | "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", 1223 | "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", 1224 | "dev": true, 1225 | "peerDependencies": { 1226 | "picomatch": "^3 || ^4" 1227 | }, 1228 | "peerDependenciesMeta": { 1229 | "picomatch": { 1230 | "optional": true 1231 | } 1232 | } 1233 | }, 1234 | "node_modules/fsevents": { 1235 | "version": "2.3.3", 1236 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 1237 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 1238 | "dev": true, 1239 | "hasInstallScript": true, 1240 | "optional": true, 1241 | "os": [ 1242 | "darwin" 1243 | ], 1244 | "engines": { 1245 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 1246 | } 1247 | }, 1248 | "node_modules/gensync": { 1249 | "version": "1.0.0-beta.2", 1250 | "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", 1251 | "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", 1252 | "dev": true, 1253 | "engines": { 1254 | "node": ">=6.9.0" 1255 | } 1256 | }, 1257 | "node_modules/globals": { 1258 | "version": "11.12.0", 1259 | "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", 1260 | "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", 1261 | "dev": true, 1262 | "engines": { 1263 | "node": ">=4" 1264 | } 1265 | }, 1266 | "node_modules/gojs": { 1267 | "version": "3.0.21", 1268 | "resolved": "https://registry.npmjs.org/gojs/-/gojs-3.0.21.tgz", 1269 | "integrity": "sha512-XOxxGOPeJUOWAgUlNXzWkpmyXw+5UkWAZx7qB15VwEZONmi44W4wGyRdHE9BcZA1wAqssEF4oC4TLBrZ7tVShg==" 1270 | }, 1271 | "node_modules/gojs-react": { 1272 | "version": "1.1.3", 1273 | "resolved": "https://registry.npmjs.org/gojs-react/-/gojs-react-1.1.3.tgz", 1274 | "integrity": "sha512-FDjESQUZMgNIx4qY77eLDOXW7N3mCI/3xGyuGogOsuQDXFbRszawsVGMuQGZPyWCGnM3BOTp/oSnP8XexDQblg==", 1275 | "dependencies": { 1276 | "tslib": "~2.5.0" 1277 | }, 1278 | "peerDependencies": { 1279 | "gojs": ">=2.1.22", 1280 | "react": ">=16.0.0" 1281 | } 1282 | }, 1283 | "node_modules/immer": { 1284 | "version": "10.1.1", 1285 | "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", 1286 | "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", 1287 | "peer": true, 1288 | "funding": { 1289 | "type": "opencollective", 1290 | "url": "https://opencollective.com/immer" 1291 | } 1292 | }, 1293 | "node_modules/js-tokens": { 1294 | "version": "4.0.0", 1295 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 1296 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 1297 | "dev": true 1298 | }, 1299 | "node_modules/jsesc": { 1300 | "version": "3.1.0", 1301 | "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", 1302 | "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", 1303 | "dev": true, 1304 | "bin": { 1305 | "jsesc": "bin/jsesc" 1306 | }, 1307 | "engines": { 1308 | "node": ">=6" 1309 | } 1310 | }, 1311 | "node_modules/json5": { 1312 | "version": "2.2.3", 1313 | "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", 1314 | "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", 1315 | "dev": true, 1316 | "bin": { 1317 | "json5": "lib/cli.js" 1318 | }, 1319 | "engines": { 1320 | "node": ">=6" 1321 | } 1322 | }, 1323 | "node_modules/lru-cache": { 1324 | "version": "5.1.1", 1325 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", 1326 | "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", 1327 | "dev": true, 1328 | "dependencies": { 1329 | "yallist": "^3.0.2" 1330 | } 1331 | }, 1332 | "node_modules/ms": { 1333 | "version": "2.1.3", 1334 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1335 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 1336 | "dev": true 1337 | }, 1338 | "node_modules/nanoid": { 1339 | "version": "3.3.11", 1340 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", 1341 | "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", 1342 | "dev": true, 1343 | "funding": [ 1344 | { 1345 | "type": "github", 1346 | "url": "https://github.com/sponsors/ai" 1347 | } 1348 | ], 1349 | "bin": { 1350 | "nanoid": "bin/nanoid.cjs" 1351 | }, 1352 | "engines": { 1353 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 1354 | } 1355 | }, 1356 | "node_modules/node-releases": { 1357 | "version": "2.0.19", 1358 | "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", 1359 | "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", 1360 | "dev": true 1361 | }, 1362 | "node_modules/picocolors": { 1363 | "version": "1.1.1", 1364 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", 1365 | "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", 1366 | "dev": true 1367 | }, 1368 | "node_modules/picomatch": { 1369 | "version": "4.0.2", 1370 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", 1371 | "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", 1372 | "dev": true, 1373 | "engines": { 1374 | "node": ">=12" 1375 | }, 1376 | "funding": { 1377 | "url": "https://github.com/sponsors/jonschlinkert" 1378 | } 1379 | }, 1380 | "node_modules/postcss": { 1381 | "version": "8.5.3", 1382 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", 1383 | "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", 1384 | "dev": true, 1385 | "funding": [ 1386 | { 1387 | "type": "opencollective", 1388 | "url": "https://opencollective.com/postcss/" 1389 | }, 1390 | { 1391 | "type": "tidelift", 1392 | "url": "https://tidelift.com/funding/github/npm/postcss" 1393 | }, 1394 | { 1395 | "type": "github", 1396 | "url": "https://github.com/sponsors/ai" 1397 | } 1398 | ], 1399 | "dependencies": { 1400 | "nanoid": "^3.3.8", 1401 | "picocolors": "^1.1.1", 1402 | "source-map-js": "^1.2.1" 1403 | }, 1404 | "engines": { 1405 | "node": "^10 || ^12 || >=14" 1406 | } 1407 | }, 1408 | "node_modules/react": { 1409 | "version": "19.1.0", 1410 | "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", 1411 | "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", 1412 | "engines": { 1413 | "node": ">=0.10.0" 1414 | } 1415 | }, 1416 | "node_modules/react-dom": { 1417 | "version": "19.1.0", 1418 | "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", 1419 | "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", 1420 | "dependencies": { 1421 | "scheduler": "^0.26.0" 1422 | }, 1423 | "peerDependencies": { 1424 | "react": "^19.1.0" 1425 | } 1426 | }, 1427 | "node_modules/react-refresh": { 1428 | "version": "0.17.0", 1429 | "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", 1430 | "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", 1431 | "dev": true, 1432 | "engines": { 1433 | "node": ">=0.10.0" 1434 | } 1435 | }, 1436 | "node_modules/rollup": { 1437 | "version": "4.40.2", 1438 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz", 1439 | "integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==", 1440 | "dev": true, 1441 | "dependencies": { 1442 | "@types/estree": "1.0.7" 1443 | }, 1444 | "bin": { 1445 | "rollup": "dist/bin/rollup" 1446 | }, 1447 | "engines": { 1448 | "node": ">=18.0.0", 1449 | "npm": ">=8.0.0" 1450 | }, 1451 | "optionalDependencies": { 1452 | "@rollup/rollup-android-arm-eabi": "4.40.2", 1453 | "@rollup/rollup-android-arm64": "4.40.2", 1454 | "@rollup/rollup-darwin-arm64": "4.40.2", 1455 | "@rollup/rollup-darwin-x64": "4.40.2", 1456 | "@rollup/rollup-freebsd-arm64": "4.40.2", 1457 | "@rollup/rollup-freebsd-x64": "4.40.2", 1458 | "@rollup/rollup-linux-arm-gnueabihf": "4.40.2", 1459 | "@rollup/rollup-linux-arm-musleabihf": "4.40.2", 1460 | "@rollup/rollup-linux-arm64-gnu": "4.40.2", 1461 | "@rollup/rollup-linux-arm64-musl": "4.40.2", 1462 | "@rollup/rollup-linux-loongarch64-gnu": "4.40.2", 1463 | "@rollup/rollup-linux-powerpc64le-gnu": "4.40.2", 1464 | "@rollup/rollup-linux-riscv64-gnu": "4.40.2", 1465 | "@rollup/rollup-linux-riscv64-musl": "4.40.2", 1466 | "@rollup/rollup-linux-s390x-gnu": "4.40.2", 1467 | "@rollup/rollup-linux-x64-gnu": "4.40.2", 1468 | "@rollup/rollup-linux-x64-musl": "4.40.2", 1469 | "@rollup/rollup-win32-arm64-msvc": "4.40.2", 1470 | "@rollup/rollup-win32-ia32-msvc": "4.40.2", 1471 | "@rollup/rollup-win32-x64-msvc": "4.40.2", 1472 | "fsevents": "~2.3.2" 1473 | } 1474 | }, 1475 | "node_modules/scheduler": { 1476 | "version": "0.26.0", 1477 | "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", 1478 | "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==" 1479 | }, 1480 | "node_modules/semver": { 1481 | "version": "6.3.1", 1482 | "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", 1483 | "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", 1484 | "dev": true, 1485 | "bin": { 1486 | "semver": "bin/semver.js" 1487 | } 1488 | }, 1489 | "node_modules/source-map-js": { 1490 | "version": "1.2.1", 1491 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", 1492 | "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", 1493 | "dev": true, 1494 | "engines": { 1495 | "node": ">=0.10.0" 1496 | } 1497 | }, 1498 | "node_modules/tinyglobby": { 1499 | "version": "0.2.13", 1500 | "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", 1501 | "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", 1502 | "dev": true, 1503 | "dependencies": { 1504 | "fdir": "^6.4.4", 1505 | "picomatch": "^4.0.2" 1506 | }, 1507 | "engines": { 1508 | "node": ">=12.0.0" 1509 | }, 1510 | "funding": { 1511 | "url": "https://github.com/sponsors/SuperchupuDev" 1512 | } 1513 | }, 1514 | "node_modules/tslib": { 1515 | "version": "2.5.3", 1516 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.3.tgz", 1517 | "integrity": "sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==" 1518 | }, 1519 | "node_modules/typescript": { 1520 | "version": "5.8.3", 1521 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", 1522 | "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", 1523 | "dev": true, 1524 | "bin": { 1525 | "tsc": "bin/tsc", 1526 | "tsserver": "bin/tsserver" 1527 | }, 1528 | "engines": { 1529 | "node": ">=14.17" 1530 | } 1531 | }, 1532 | "node_modules/update-browserslist-db": { 1533 | "version": "1.1.3", 1534 | "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", 1535 | "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", 1536 | "dev": true, 1537 | "funding": [ 1538 | { 1539 | "type": "opencollective", 1540 | "url": "https://opencollective.com/browserslist" 1541 | }, 1542 | { 1543 | "type": "tidelift", 1544 | "url": "https://tidelift.com/funding/github/npm/browserslist" 1545 | }, 1546 | { 1547 | "type": "github", 1548 | "url": "https://github.com/sponsors/ai" 1549 | } 1550 | ], 1551 | "dependencies": { 1552 | "escalade": "^3.2.0", 1553 | "picocolors": "^1.1.1" 1554 | }, 1555 | "bin": { 1556 | "update-browserslist-db": "cli.js" 1557 | }, 1558 | "peerDependencies": { 1559 | "browserslist": ">= 4.21.0" 1560 | } 1561 | }, 1562 | "node_modules/use-immer": { 1563 | "version": "0.11.0", 1564 | "resolved": "https://registry.npmjs.org/use-immer/-/use-immer-0.11.0.tgz", 1565 | "integrity": "sha512-RNAqi3GqsWJ4bcCd4LMBgdzvPmTABam24DUaFiKfX9s3MSorNRz9RDZYJkllJoMHUxVLMDetwAuCDeyWNrp1yA==", 1566 | "peerDependencies": { 1567 | "immer": ">=8.0.0", 1568 | "react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0" 1569 | } 1570 | }, 1571 | "node_modules/vite": { 1572 | "version": "6.3.5", 1573 | "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", 1574 | "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", 1575 | "dev": true, 1576 | "dependencies": { 1577 | "esbuild": "^0.25.0", 1578 | "fdir": "^6.4.4", 1579 | "picomatch": "^4.0.2", 1580 | "postcss": "^8.5.3", 1581 | "rollup": "^4.34.9", 1582 | "tinyglobby": "^0.2.13" 1583 | }, 1584 | "bin": { 1585 | "vite": "bin/vite.js" 1586 | }, 1587 | "engines": { 1588 | "node": "^18.0.0 || ^20.0.0 || >=22.0.0" 1589 | }, 1590 | "funding": { 1591 | "url": "https://github.com/vitejs/vite?sponsor=1" 1592 | }, 1593 | "optionalDependencies": { 1594 | "fsevents": "~2.3.3" 1595 | }, 1596 | "peerDependencies": { 1597 | "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", 1598 | "jiti": ">=1.21.0", 1599 | "less": "*", 1600 | "lightningcss": "^1.21.0", 1601 | "sass": "*", 1602 | "sass-embedded": "*", 1603 | "stylus": "*", 1604 | "sugarss": "*", 1605 | "terser": "^5.16.0", 1606 | "tsx": "^4.8.1", 1607 | "yaml": "^2.4.2" 1608 | }, 1609 | "peerDependenciesMeta": { 1610 | "@types/node": { 1611 | "optional": true 1612 | }, 1613 | "jiti": { 1614 | "optional": true 1615 | }, 1616 | "less": { 1617 | "optional": true 1618 | }, 1619 | "lightningcss": { 1620 | "optional": true 1621 | }, 1622 | "sass": { 1623 | "optional": true 1624 | }, 1625 | "sass-embedded": { 1626 | "optional": true 1627 | }, 1628 | "stylus": { 1629 | "optional": true 1630 | }, 1631 | "sugarss": { 1632 | "optional": true 1633 | }, 1634 | "terser": { 1635 | "optional": true 1636 | }, 1637 | "tsx": { 1638 | "optional": true 1639 | }, 1640 | "yaml": { 1641 | "optional": true 1642 | } 1643 | } 1644 | }, 1645 | "node_modules/yallist": { 1646 | "version": "3.1.1", 1647 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", 1648 | "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", 1649 | "dev": true 1650 | } 1651 | } 1652 | } 1653 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gojs-react-basic", 3 | "version": "1.1.0", 4 | "description": "An example project demonstrating usage of GoJS and React together", 5 | "private": true, 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "serve": "vite preview" 10 | }, 11 | "keywords": [ 12 | "react", 13 | "gojs" 14 | ], 15 | "author": "Northwoods Software", 16 | "dependencies": { 17 | "gojs": "^3.0.21", 18 | "gojs-react": "^1.1.3", 19 | "react": "^19.1.0", 20 | "react-dom": "^19.1.0", 21 | "use-immer": "^0.11.0" 22 | }, 23 | "browserslist": [ 24 | ">0.2%", 25 | "not dead", 26 | "not ie <= 11", 27 | "not op_mini all" 28 | ], 29 | "devDependencies": { 30 | "@types/react": "^19.1.2", 31 | "@types/react-dom": "^19.1.2", 32 | "@vitejs/plugin-react": "^4.4.1", 33 | "typescript": "^5.8.3", 34 | "vite": "^6.3.3" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NorthwoodsSoftware/gojs-react-basic/8c27cc4c1f8b4c5863657ce9e8e238694b056007/public/favicon.ico -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | label { 2 | font: 300 1rem Roboto, helvetica, sans-serif; 3 | color: #212121; /* Grey 900 */ 4 | display: block; 5 | padding: 2px; 6 | } 7 | 8 | p { 9 | font: 300 1rem Roboto, helvetica, sans-serif; 10 | color: #212121; /* Grey 900 */ 11 | } -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 1998-2025 by Northwoods Software Corporation. All Rights Reserved. 3 | */ 4 | 5 | import * as go from "gojs"; 6 | import { useEffect } from "react"; 7 | import { useImmer } from "use-immer"; 8 | 9 | import { DiagramWrapper } from "./components/DiagramWrapper"; 10 | import { SelectionInspector } from "./components/SelectionInspector"; 11 | 12 | import "./App.css"; 13 | 14 | /** 15 | * Use a linkDataArray since we'll be using a GraphLinksModel, 16 | * and modelData for demonstration purposes. Note, though, that 17 | * both are optional props in ReactDiagram. 18 | */ 19 | interface AppState { 20 | nodeDataArray: Array; 21 | linkDataArray: Array; 22 | modelData: go.ObjectData; 23 | selectedData: go.ObjectData | null; 24 | skipsDiagramUpdate: boolean; 25 | } 26 | 27 | // Maps to store key -> arr index for quick lookups 28 | const mapNodeKeyIdx = new Map(); 29 | const mapLinkKeyIdx = new Map(); 30 | 31 | export const App = () => { 32 | // Diagram state 33 | const [diagramData, updateDiagramData] = useImmer({ 34 | nodeDataArray: [ 35 | { key: 0, text: "Alpha", color: "lightblue", loc: "0 0" }, 36 | { key: 1, text: "Beta", color: "orange", loc: "150 0" }, 37 | { key: 2, text: "Gamma", color: "lightgreen", loc: "0 150" }, 38 | { key: 3, text: "Delta", color: "pink", loc: "150 150" } 39 | ], 40 | linkDataArray: [ 41 | { key: -1, from: 0, to: 1 }, 42 | { key: -2, from: 0, to: 2 }, 43 | { key: -3, from: 1, to: 1 }, 44 | { key: -4, from: 2, to: 3 }, 45 | { key: -5, from: 3, to: 0 } 46 | ], 47 | modelData: { canRelink: true }, 48 | selectedData: null, 49 | skipsDiagramUpdate: false 50 | }); 51 | 52 | useEffect(() => { 53 | // init maps 54 | refreshNodeIndex(diagramData.nodeDataArray); 55 | refreshLinkIndex(diagramData.linkDataArray); 56 | }, []); 57 | 58 | /** 59 | * Update map of node keys to their index in the array. 60 | */ 61 | const refreshNodeIndex = (nodeArr: go.ObjectData[]) => { 62 | mapNodeKeyIdx.clear(); 63 | nodeArr.forEach((n, idx) => { 64 | mapNodeKeyIdx.set(n.key, idx); 65 | }); 66 | }; 67 | 68 | /** 69 | * Update map of link keys to their index in the array. 70 | */ 71 | const refreshLinkIndex = (linkArr: go.ObjectData[]) => { 72 | mapLinkKeyIdx.clear(); 73 | linkArr.forEach((l, idx) => { 74 | mapLinkKeyIdx.set(l.key, idx); 75 | }); 76 | }; 77 | 78 | /** 79 | * Handle any relevant DiagramEvents, in this case just selection changes. 80 | * On ChangedSelection, find the corresponding data and set the selectedData state. 81 | * @param e a GoJS DiagramEvent 82 | */ 83 | const handleDiagramEvent = (e: go.DiagramEvent) => { 84 | const name = e.name; 85 | switch (name) { 86 | case "ChangedSelection": { 87 | const sel = e.subject.first(); 88 | updateDiagramData((draft) => { 89 | if (sel) { 90 | if (sel instanceof go.Node) { 91 | const idx = mapNodeKeyIdx.get(sel.key); 92 | if (idx !== undefined && idx >= 0) { 93 | const nd = draft.nodeDataArray[idx]; 94 | draft.selectedData = nd; 95 | } 96 | } else if (sel instanceof go.Link) { 97 | const idx = mapLinkKeyIdx.get(sel.key); 98 | if (idx !== undefined && idx >= 0) { 99 | const ld = draft.linkDataArray[idx]; 100 | draft.selectedData = ld; 101 | } 102 | } 103 | } else { 104 | draft.selectedData = null; 105 | } 106 | }); 107 | break; 108 | } 109 | default: 110 | break; 111 | } 112 | }; 113 | 114 | /** 115 | * Handle GoJS model changes, which output an object of data changes via Model.toIncrementalData. 116 | * This method iterates over those changes and updates state to keep in sync with the GoJS model. 117 | * @param obj a JSON-formatted string 118 | */ 119 | const handleModelChange = (obj: go.IncrementalData, _: go.ChangedEvent) => { 120 | const insertedNodeKeys = obj.insertedNodeKeys; 121 | const modifiedNodeData = obj.modifiedNodeData; 122 | const removedNodeKeys = obj.removedNodeKeys; 123 | const insertedLinkKeys = obj.insertedLinkKeys; 124 | const modifiedLinkData = obj.modifiedLinkData; 125 | const removedLinkKeys = obj.removedLinkKeys; 126 | const modifiedModelData = obj.modelData; 127 | 128 | // maintain maps of modified data so insertions don't need slow lookups 129 | const modifiedNodeMap = new Map(); 130 | const modifiedLinkMap = new Map(); 131 | updateDiagramData((draft) => { 132 | let narr = draft.nodeDataArray; 133 | if (modifiedNodeData) { 134 | modifiedNodeData.forEach((nd) => { 135 | modifiedNodeMap.set(nd.key, nd); 136 | const idx = mapNodeKeyIdx.get(nd.key); 137 | if (idx !== undefined && idx >= 0) { 138 | narr[idx] = nd; 139 | if (draft.selectedData && draft.selectedData.key === nd.key) { 140 | draft.selectedData = nd; 141 | } 142 | } 143 | }); 144 | } 145 | if (insertedNodeKeys) { 146 | insertedNodeKeys.forEach((key) => { 147 | const nd = modifiedNodeMap.get(key); 148 | const idx = mapNodeKeyIdx.get(key); 149 | if (nd && idx === undefined) { 150 | // nodes won't be added if they already exist 151 | mapNodeKeyIdx.set(nd.key, narr.length); 152 | narr.push(nd); 153 | } 154 | }); 155 | } 156 | if (removedNodeKeys) { 157 | narr = narr.filter((nd) => { 158 | if (removedNodeKeys.includes(nd.key)) { 159 | return false; 160 | } 161 | return true; 162 | }); 163 | draft.nodeDataArray = narr; 164 | refreshNodeIndex(narr); 165 | } 166 | let larr = draft.linkDataArray; 167 | if (modifiedLinkData) { 168 | modifiedLinkData.forEach((ld) => { 169 | modifiedLinkMap.set(ld.key, ld); 170 | const idx = mapLinkKeyIdx.get(ld.key); 171 | if (idx !== undefined && idx >= 0) { 172 | larr[idx] = ld; 173 | if (draft.selectedData && draft.selectedData.key === ld.key) { 174 | draft.selectedData = ld; 175 | } 176 | } 177 | }); 178 | } 179 | if (insertedLinkKeys) { 180 | insertedLinkKeys.forEach((key) => { 181 | const ld = modifiedLinkMap.get(key); 182 | const idx = mapLinkKeyIdx.get(key); 183 | if (ld && idx === undefined) { 184 | // links won't be added if they already exist 185 | mapLinkKeyIdx.set(ld.key, larr.length); 186 | larr.push(ld); 187 | } 188 | }); 189 | } 190 | if (removedLinkKeys) { 191 | larr = larr.filter((ld: go.ObjectData) => { 192 | if (removedLinkKeys.includes(ld.key)) { 193 | return false; 194 | } 195 | return true; 196 | }); 197 | draft.linkDataArray = larr; 198 | refreshLinkIndex(larr); 199 | } 200 | // handle model data changes, for now just replacing with the supplied object 201 | if (modifiedModelData) { 202 | draft.modelData = modifiedModelData; 203 | } 204 | draft.skipsDiagramUpdate = true; // the GoJS model already knows about these updates 205 | }); 206 | }; 207 | 208 | /** 209 | * Handle inspector changes, and on input field blurs, update node/link data state. 210 | * @param path the path to the property being modified 211 | * @param value the new value of that property 212 | * @param isBlur whether the input event was a blur, indicating the edit is complete 213 | */ 214 | const handleInputChange = (path: string, value: any, isBlur: boolean) => { 215 | updateDiagramData((draft) => { 216 | if (!draft.selectedData) return; 217 | const data = draft.selectedData; 218 | data[path] = value; 219 | if (isBlur) { 220 | const key = data.key; 221 | if (key < 0) { 222 | // negative keys are links 223 | const idx = mapLinkKeyIdx.get(key); 224 | if (idx !== undefined && idx >= 0) { 225 | draft.linkDataArray[idx] = data; 226 | draft.skipsDiagramUpdate = false; 227 | } 228 | } else { 229 | const idx = mapNodeKeyIdx.get(key); 230 | if (idx !== undefined && idx >= 0) { 231 | draft.nodeDataArray[idx] = data; 232 | draft.skipsDiagramUpdate = false; 233 | } 234 | } 235 | } 236 | }); 237 | }; 238 | 239 | /** 240 | * Handle changes to the checkbox on whether to allow relinking. 241 | * @param e a change event from the checkbox 242 | */ 243 | const handleRelinkChange = (e: any) => { 244 | const target = e.target; 245 | const value = target.checked; 246 | updateDiagramData((draft) => { 247 | draft.modelData = { canRelink: value }; 248 | draft.skipsDiagramUpdate = false; 249 | }); 250 | }; 251 | 252 | const selectedData = diagramData.selectedData; 253 | let inspector; 254 | if (selectedData !== null) { 255 | inspector = ; 256 | } 257 | 258 | return ( 259 |
260 |

261 | Try moving around nodes, editing text, relinking, undoing (Ctrl-Z), etc. within the diagram and you'll notice the changes are reflected in the inspector 262 | area. You'll also notice that changes made in the inspector are reflected in the diagram. If you use the React dev tools, you can inspect the React 263 | state and see it updated as changes happen. 264 |

265 |

266 | Check out the{" "} 267 | 268 | Intro page on using GoJS with React 269 | {" "} 270 | for more information. 271 |

272 | 280 | 284 | {inspector} 285 |
286 | ); 287 | }; 288 | 289 | export default App; 290 | -------------------------------------------------------------------------------- /src/GuidedDraggingTool.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 1998-2025 by Northwoods Software Corporation. All Rights Reserved. 3 | */ 4 | 5 | /* 6 | * This is an extension and not part of the main GoJS library. 7 | * Note that the API for this class may change with any version, even point releases. 8 | * If you intend to use an extension in production, you should copy the code to your own source directory. 9 | * Extensions can be found in the GoJS kit under the extensions or extensionsTS folders. 10 | * See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information. 11 | */ 12 | import * as go from "gojs"; 13 | 14 | /** 15 | * The GuidedDraggingTool class makes guidelines visible as the parts are dragged around a diagram 16 | * when the selected part is nearly aligned with another part. 17 | * 18 | * If you want to experiment with this extension, try the Guided Dragging sample. 19 | * @category Tool Extension 20 | */ 21 | export class GuidedDraggingTool extends go.DraggingTool { 22 | // horizontal guidelines 23 | private guidelineHtop: go.Part; 24 | private guidelineHbottom: go.Part; 25 | private guidelineHcenter: go.Part; 26 | // vertical guidelines 27 | private guidelineVleft: go.Part; 28 | private guidelineVright: go.Part; 29 | private guidelineVcenter: go.Part; 30 | 31 | // properties that the programmer can modify 32 | private _guidelineSnapDistance: number = 6; 33 | private _isGuidelineEnabled: boolean = true; 34 | private _horizontalGuidelineColor: string = "gray"; 35 | private _verticalGuidelineColor: string = "gray"; 36 | private _centerGuidelineColor: string = "gray"; 37 | private _guidelineWidth: number = 1; 38 | private _searchDistance: number = 1000; 39 | private _isGuidelineSnapEnabled: boolean = true; 40 | 41 | /** 42 | * Constructs a GuidedDraggingTool and sets up the temporary guideline parts. 43 | */ 44 | constructor() { 45 | super(); 46 | 47 | const partProperties = { layerName: "Tool", isInDocumentBounds: false }; 48 | const shapeProperties = { stroke: "gray", isGeometryPositioned: true }; 49 | 50 | const $ = go.GraphObject.make; 51 | // temporary parts for horizonal guidelines 52 | this.guidelineHtop = $( 53 | go.Part, 54 | partProperties, 55 | $(go.Shape, shapeProperties, { geometryString: "M0 0 100 0" }) 56 | ); 57 | this.guidelineHbottom = $( 58 | go.Part, 59 | partProperties, 60 | $(go.Shape, shapeProperties, { geometryString: "M0 0 100 0" }) 61 | ); 62 | this.guidelineHcenter = $( 63 | go.Part, 64 | partProperties, 65 | $(go.Shape, shapeProperties, { geometryString: "M0 0 100 0" }) 66 | ); 67 | // temporary parts for vertical guidelines 68 | this.guidelineVleft = $( 69 | go.Part, 70 | partProperties, 71 | $(go.Shape, shapeProperties, { geometryString: "M0 0 0 100" }) 72 | ); 73 | this.guidelineVright = $( 74 | go.Part, 75 | partProperties, 76 | $(go.Shape, shapeProperties, { geometryString: "M0 0 0 100" }) 77 | ); 78 | this.guidelineVcenter = $( 79 | go.Part, 80 | partProperties, 81 | $(go.Shape, shapeProperties, { geometryString: "M0 0 0 100" }) 82 | ); 83 | } 84 | 85 | /** 86 | * Gets or sets the margin of error for which guidelines show up. 87 | * 88 | * The default value is 6. 89 | * Guidelines will show up when the aligned nods are ± 6px away from perfect alignment. 90 | */ 91 | get guidelineSnapDistance(): number { 92 | return this._guidelineSnapDistance; 93 | } 94 | set guidelineSnapDistance(val: number) { 95 | if (typeof val !== "number" || isNaN(val) || val < 0) 96 | throw new Error( 97 | "new value for GuideddraggingTool.guidelineSnapDistance must be a non-negative number" 98 | ); 99 | if (this._guidelineSnapDistance !== val) { 100 | this._guidelineSnapDistance = val; 101 | } 102 | } 103 | 104 | /** 105 | * Gets or sets whether the guidelines are enabled or disable. 106 | * 107 | * The default value is true. 108 | */ 109 | get isGuidelineEnabled(): boolean { 110 | return this._isGuidelineEnabled; 111 | } 112 | set isGuidelineEnabled(val: boolean) { 113 | if (typeof val !== "boolean") 114 | throw new Error( 115 | "new value for GuidedDraggingTool.isGuidelineEnabled must be a boolean value." 116 | ); 117 | if (this._isGuidelineEnabled !== val) { 118 | this._isGuidelineEnabled = val; 119 | } 120 | } 121 | 122 | /** 123 | * Gets or sets the color of horizontal guidelines. 124 | * 125 | * The default value is "gray". 126 | */ 127 | get horizontalGuidelineColor(): string { 128 | return this._horizontalGuidelineColor; 129 | } 130 | set horizontalGuidelineColor(val: string) { 131 | if (this._horizontalGuidelineColor !== val) { 132 | this._horizontalGuidelineColor = val; 133 | (this.guidelineHbottom.elements.first() as go.Shape).stroke = 134 | this._horizontalGuidelineColor; 135 | (this.guidelineHtop.elements.first() as go.Shape).stroke = 136 | this._horizontalGuidelineColor; 137 | } 138 | } 139 | 140 | /** 141 | * Gets or sets the color of vertical guidelines. 142 | * 143 | * The default value is "gray". 144 | */ 145 | get verticalGuidelineColor(): string { 146 | return this._verticalGuidelineColor; 147 | } 148 | set verticalGuidelineColor(val: string) { 149 | if (this._verticalGuidelineColor !== val) { 150 | this._verticalGuidelineColor = val; 151 | (this.guidelineVleft.elements.first() as go.Shape).stroke = 152 | this._verticalGuidelineColor; 153 | (this.guidelineVright.elements.first() as go.Shape).stroke = 154 | this._verticalGuidelineColor; 155 | } 156 | } 157 | 158 | /** 159 | * Gets or sets the color of center guidelines. 160 | * 161 | * The default value is "gray". 162 | */ 163 | get centerGuidelineColor(): string { 164 | return this._centerGuidelineColor; 165 | } 166 | set centerGuidelineColor(val: string) { 167 | if (this._centerGuidelineColor !== val) { 168 | this._centerGuidelineColor = val; 169 | (this.guidelineVcenter.elements.first() as go.Shape).stroke = 170 | this._centerGuidelineColor; 171 | (this.guidelineHcenter.elements.first() as go.Shape).stroke = 172 | this._centerGuidelineColor; 173 | } 174 | } 175 | 176 | /** 177 | * Gets or sets the width guidelines. 178 | * 179 | * The default value is 1. 180 | */ 181 | get guidelineWidth(): number { 182 | return this._guidelineWidth; 183 | } 184 | set guidelineWidth(val: number) { 185 | if (typeof val !== "number" || isNaN(val) || val < 0) 186 | throw new Error( 187 | "New value for GuidedDraggingTool.guidelineWidth must be a non-negative number." 188 | ); 189 | if (this._guidelineWidth !== val) { 190 | this._guidelineWidth = val; 191 | (this.guidelineVcenter.elements.first() as go.Shape).strokeWidth = val; 192 | (this.guidelineHcenter.elements.first() as go.Shape).strokeWidth = val; 193 | (this.guidelineVleft.elements.first() as go.Shape).strokeWidth = val; 194 | (this.guidelineVright.elements.first() as go.Shape).strokeWidth = val; 195 | (this.guidelineHbottom.elements.first() as go.Shape).strokeWidth = val; 196 | (this.guidelineHtop.elements.first() as go.Shape).strokeWidth = val; 197 | } 198 | } 199 | 200 | /** 201 | * Gets or sets the distance around the selected part to search for aligned parts. 202 | * 203 | * The default value is 1000. 204 | * Set this to Infinity if you want to search the entire diagram no matter how far away. 205 | */ 206 | get searchDistance(): number { 207 | return this._searchDistance; 208 | } 209 | set searchDistance(val: number) { 210 | if (typeof val !== "number" || isNaN(val) || val <= 0) 211 | throw new Error( 212 | "new value for GuidedDraggingTool.searchDistance must be a positive number." 213 | ); 214 | if (this._searchDistance !== val) { 215 | this._searchDistance = val; 216 | } 217 | } 218 | 219 | /** 220 | * Gets or sets whether snapping to guidelines is enabled. 221 | * 222 | * The default value is true. 223 | */ 224 | get isGuidelineSnapEnabled(): boolean { 225 | return this._isGuidelineSnapEnabled; 226 | } 227 | set isGuidelineSnapEnabled(val: boolean) { 228 | if (typeof val !== "boolean") 229 | throw new Error( 230 | "new value for GuidedDraggingTool.isGuidelineSnapEnabled must be a boolean." 231 | ); 232 | if (this._isGuidelineSnapEnabled !== val) { 233 | this._isGuidelineSnapEnabled = val; 234 | } 235 | } 236 | 237 | /** 238 | * Removes all of the guidelines from the grid. 239 | */ 240 | public clearGuidelines(): void { 241 | this.diagram.remove(this.guidelineHbottom); 242 | this.diagram.remove(this.guidelineHcenter); 243 | this.diagram.remove(this.guidelineHtop); 244 | this.diagram.remove(this.guidelineVleft); 245 | this.diagram.remove(this.guidelineVright); 246 | this.diagram.remove(this.guidelineVcenter); 247 | } 248 | 249 | /** 250 | * Calls the base method and removes the guidelines from the graph. 251 | */ 252 | public doDeactivate(): void { 253 | super.doDeactivate(); 254 | // clear any guidelines when dragging is done 255 | this.clearGuidelines(); 256 | } 257 | 258 | /** 259 | * Shows vertical and horizontal guidelines for the dragged part. 260 | */ 261 | public doDragOver(_pt: go.Point, _obj: go.GraphObject): void { 262 | // clear all existing guidelines in case either show... method decides to show a guideline 263 | this.clearGuidelines(); 264 | 265 | // gets the selected part 266 | const draggingParts = this.copiedParts || this.draggedParts; 267 | if (draggingParts === null) return; 268 | const partItr = draggingParts.iterator; 269 | if (partItr.next()) { 270 | const part = partItr.key; 271 | 272 | this.showHorizontalMatches(part, this.isGuidelineEnabled, false); 273 | this.showVerticalMatches(part, this.isGuidelineEnabled, false); 274 | } 275 | } 276 | 277 | /** 278 | * On a mouse-up, snaps the selected part to the nearest guideline. 279 | * If not snapping, the part remains at its position. 280 | */ 281 | public doDropOnto(_pt: go.Point, _obj: go.GraphObject): void { 282 | this.clearGuidelines(); 283 | 284 | // gets the selected (perhaps copied) Part 285 | const draggingParts = this.copiedParts || this.draggedParts; 286 | if (draggingParts === null) return; 287 | const partItr = draggingParts.iterator; 288 | if (partItr.next()) { 289 | const part = partItr.key; 290 | 291 | // snaps only when the mouse is released without shift modifier 292 | const e = this.diagram.lastInput; 293 | const snap = this.isGuidelineSnapEnabled && !e.shift; 294 | 295 | this.showHorizontalMatches(part, false, snap); // false means don't show guidelines 296 | this.showVerticalMatches(part, false, snap); 297 | } 298 | } 299 | 300 | /** 301 | * When nodes are shifted due to being guided upon a drop, make sure all connected link routes are invalidated, 302 | * since the node is likely to have moved a different amount than all its connected links in the regular 303 | * operation of the DraggingTool. 304 | */ 305 | public invalidateLinks(node: go.Part): void { 306 | if (node instanceof go.Node) node.invalidateConnectedLinks(); 307 | } 308 | 309 | /** 310 | * This finds parts that are aligned near the selected part along horizontal lines. It compares the selected 311 | * part to all parts within a rectangle approximately twice the {@link #searchDistance} wide. 312 | * The guidelines appear when a part is aligned within a margin-of-error equal to {@link #guidelineSnapDistance}. 313 | * @param {Node} part 314 | * @param {boolean} guideline if true, show guideline 315 | * @param {boolean} snap if true, snap the part to where the guideline would be 316 | */ 317 | public showHorizontalMatches( 318 | part: go.Part, 319 | guideline: boolean, 320 | snap: boolean 321 | ): void { 322 | const objBounds = part.locationObject.getDocumentBounds(); 323 | const p0 = objBounds.y; 324 | const p1 = objBounds.y + objBounds.height / 2; 325 | const p2 = objBounds.y + objBounds.height; 326 | 327 | const marginOfError = this.guidelineSnapDistance; 328 | const distance = this.searchDistance; 329 | // compares with parts within narrow vertical area 330 | const area = objBounds.copy(); 331 | area.inflate(distance, marginOfError + 1); 332 | const otherObjs = this.diagram.findObjectsIn( 333 | area, 334 | (obj) => obj.part as go.Part, 335 | (p) => 336 | p instanceof go.Part && 337 | !p.isSelected && 338 | !(p instanceof go.Link) && 339 | p.isTopLevel && 340 | p.layer !== null && 341 | !p.layer.isTemporary, 342 | true 343 | ) as go.Set; 344 | 345 | let bestDiff: number = marginOfError; 346 | let bestObj: any = null; // TS 2.6 won't let this be go.Part | null 347 | let bestSpot: go.Spot = go.Spot.Default; 348 | let bestOtherSpot: go.Spot = go.Spot.Default; 349 | // horizontal line -- comparing y-values 350 | otherObjs.each((other) => { 351 | if (other === part) return; // ignore itself 352 | 353 | const otherBounds = other.locationObject.getDocumentBounds(); 354 | const q0 = otherBounds.y; 355 | const q1 = otherBounds.y + otherBounds.height / 2; 356 | const q2 = otherBounds.y + otherBounds.height; 357 | 358 | // compare center with center of OTHER part 359 | if (Math.abs(p1 - q1) < bestDiff) { 360 | bestDiff = Math.abs(p1 - q1); 361 | bestObj = other; 362 | bestSpot = go.Spot.Center; 363 | bestOtherSpot = go.Spot.Center; 364 | } 365 | // compare top side with top and bottom sides of OTHER part 366 | if (Math.abs(p0 - q0) < bestDiff) { 367 | bestDiff = Math.abs(p0 - q0); 368 | bestObj = other; 369 | bestSpot = go.Spot.Top; 370 | bestOtherSpot = go.Spot.Top; 371 | } else if (Math.abs(p0 - q2) < bestDiff) { 372 | bestDiff = Math.abs(p0 - q2); 373 | bestObj = other; 374 | bestSpot = go.Spot.Top; 375 | bestOtherSpot = go.Spot.Bottom; 376 | } 377 | // compare bottom side with top and bottom sides of OTHER part 378 | if (Math.abs(p2 - q0) < bestDiff) { 379 | bestDiff = Math.abs(p2 - q0); 380 | bestObj = other; 381 | bestSpot = go.Spot.Bottom; 382 | bestOtherSpot = go.Spot.Top; 383 | } else if (Math.abs(p2 - q2) < bestDiff) { 384 | bestDiff = Math.abs(p2 - q2); 385 | bestObj = other; 386 | bestSpot = go.Spot.Bottom; 387 | bestOtherSpot = go.Spot.Bottom; 388 | } 389 | }); 390 | 391 | if (bestObj !== null) { 392 | const offsetX = objBounds.x - part.actualBounds.x; 393 | const offsetY = objBounds.y - part.actualBounds.y; 394 | const bestBounds = bestObj.locationObject.getDocumentBounds(); 395 | // line extends from x0 to x2 396 | const x0 = Math.min(objBounds.x, bestBounds.x) - 10; 397 | const x2 = 398 | Math.max( 399 | objBounds.x + objBounds.width, 400 | bestBounds.x + bestBounds.width 401 | ) + 10; 402 | // find bestObj's desired Y 403 | const bestPoint = new go.Point().setRectSpot(bestBounds, bestOtherSpot); 404 | if (bestSpot === go.Spot.Center) { 405 | if (snap) { 406 | // call Part.move in order to automatically move member Parts of Groups 407 | part.move( 408 | new go.Point( 409 | objBounds.x - offsetX, 410 | bestPoint.y - objBounds.height / 2 - offsetY 411 | ) 412 | ); 413 | this.invalidateLinks(part); 414 | } 415 | if (guideline) { 416 | this.guidelineHcenter.position = new go.Point(x0, bestPoint.y); 417 | this.guidelineHcenter.elt(0).width = x2 - x0; 418 | this.diagram.add(this.guidelineHcenter); 419 | } 420 | } else if (bestSpot === go.Spot.Top) { 421 | if (snap) { 422 | part.move(new go.Point(objBounds.x - offsetX, bestPoint.y - offsetY)); 423 | this.invalidateLinks(part); 424 | } 425 | if (guideline) { 426 | this.guidelineHtop.position = new go.Point(x0, bestPoint.y); 427 | this.guidelineHtop.elt(0).width = x2 - x0; 428 | this.diagram.add(this.guidelineHtop); 429 | } 430 | } else if (bestSpot === go.Spot.Bottom) { 431 | if (snap) { 432 | part.move( 433 | new go.Point( 434 | objBounds.x - offsetX, 435 | bestPoint.y - objBounds.height - offsetY 436 | ) 437 | ); 438 | this.invalidateLinks(part); 439 | } 440 | if (guideline) { 441 | this.guidelineHbottom.position = new go.Point(x0, bestPoint.y); 442 | this.guidelineHbottom.elt(0).width = x2 - x0; 443 | this.diagram.add(this.guidelineHbottom); 444 | } 445 | } 446 | } 447 | } 448 | 449 | /** 450 | * This finds parts that are aligned near the selected part along vertical lines. It compares the selected 451 | * part to all parts within a rectangle approximately twice the {@link #searchDistance} tall. 452 | * The guidelines appear when a part is aligned within a margin-of-error equal to {@link #guidelineSnapDistance}. 453 | * @param {Part} part 454 | * @param {boolean} guideline if true, show guideline 455 | * @param {boolean} snap if true, don't show guidelines but just snap the part to where the guideline would be 456 | */ 457 | public showVerticalMatches( 458 | part: go.Part, 459 | guideline: boolean, 460 | snap: boolean 461 | ): void { 462 | const objBounds = part.locationObject.getDocumentBounds(); 463 | const p0 = objBounds.x; 464 | const p1 = objBounds.x + objBounds.width / 2; 465 | const p2 = objBounds.x + objBounds.width; 466 | 467 | const marginOfError = this.guidelineSnapDistance; 468 | const distance = this.searchDistance; 469 | // compares with parts within narrow vertical area 470 | const area = objBounds.copy(); 471 | area.inflate(marginOfError + 1, distance); 472 | const otherObjs = this.diagram.findObjectsIn( 473 | area, 474 | (obj) => obj.part as go.Part, 475 | (p) => 476 | p instanceof go.Part && 477 | !p.isSelected && 478 | !(p instanceof go.Link) && 479 | p.isTopLevel && 480 | p.layer !== null && 481 | !p.layer.isTemporary, 482 | true 483 | ) as go.Set; 484 | 485 | let bestDiff: number = marginOfError; 486 | let bestObj: any = null; // TS 2.6 won't let this be go.Part | null 487 | let bestSpot: go.Spot = go.Spot.Default; 488 | let bestOtherSpot: go.Spot = go.Spot.Default; 489 | // vertical line -- comparing x-values 490 | otherObjs.each((other) => { 491 | if (other === part) return; // ignore itself 492 | 493 | const otherBounds = other.locationObject.getDocumentBounds(); 494 | const q0 = otherBounds.x; 495 | const q1 = otherBounds.x + otherBounds.width / 2; 496 | const q2 = otherBounds.x + otherBounds.width; 497 | 498 | // compare center with center of OTHER part 499 | if (Math.abs(p1 - q1) < bestDiff) { 500 | bestDiff = Math.abs(p1 - q1); 501 | bestObj = other; 502 | bestSpot = go.Spot.Center; 503 | bestOtherSpot = go.Spot.Center; 504 | } 505 | // compare left side with left and right sides of OTHER part 506 | if (Math.abs(p0 - q0) < bestDiff) { 507 | bestDiff = Math.abs(p0 - q0); 508 | bestObj = other; 509 | bestSpot = go.Spot.Left; 510 | bestOtherSpot = go.Spot.Left; 511 | } else if (Math.abs(p0 - q2) < bestDiff) { 512 | bestDiff = Math.abs(p0 - q2); 513 | bestObj = other; 514 | bestSpot = go.Spot.Left; 515 | bestOtherSpot = go.Spot.Right; 516 | } 517 | // compare right side with left and right sides of OTHER part 518 | if (Math.abs(p2 - q0) < bestDiff) { 519 | bestDiff = Math.abs(p2 - q0); 520 | bestObj = other; 521 | bestSpot = go.Spot.Right; 522 | bestOtherSpot = go.Spot.Left; 523 | } else if (Math.abs(p2 - q2) < bestDiff) { 524 | bestDiff = Math.abs(p2 - q2); 525 | bestObj = other; 526 | bestSpot = go.Spot.Right; 527 | bestOtherSpot = go.Spot.Right; 528 | } 529 | }); 530 | 531 | if (bestObj !== null) { 532 | const offsetX = objBounds.x - part.actualBounds.x; 533 | const offsetY = objBounds.y - part.actualBounds.y; 534 | const bestBounds = bestObj.locationObject.getDocumentBounds(); 535 | // line extends from y0 to y2 536 | const y0 = Math.min(objBounds.y, bestBounds.y) - 10; 537 | const y2 = 538 | Math.max( 539 | objBounds.y + objBounds.height, 540 | bestBounds.y + bestBounds.height 541 | ) + 10; 542 | // find bestObj's desired X 543 | const bestPoint = new go.Point().setRectSpot(bestBounds, bestOtherSpot); 544 | if (bestSpot === go.Spot.Center) { 545 | if (snap) { 546 | // call Part.move in order to automatically move member Parts of Groups 547 | part.move( 548 | new go.Point( 549 | bestPoint.x - objBounds.width / 2 - offsetX, 550 | objBounds.y - offsetY 551 | ) 552 | ); 553 | this.invalidateLinks(part); 554 | } 555 | if (guideline) { 556 | this.guidelineVcenter.position = new go.Point(bestPoint.x, y0); 557 | this.guidelineVcenter.elt(0).height = y2 - y0; 558 | this.diagram.add(this.guidelineVcenter); 559 | } 560 | } else if (bestSpot === go.Spot.Left) { 561 | if (snap) { 562 | part.move(new go.Point(bestPoint.x - offsetX, objBounds.y - offsetY)); 563 | this.invalidateLinks(part); 564 | } 565 | if (guideline) { 566 | this.guidelineVleft.position = new go.Point(bestPoint.x, y0); 567 | this.guidelineVleft.elt(0).height = y2 - y0; 568 | this.diagram.add(this.guidelineVleft); 569 | } 570 | } else if (bestSpot === go.Spot.Right) { 571 | if (snap) { 572 | part.move( 573 | new go.Point( 574 | bestPoint.x - objBounds.width - offsetX, 575 | objBounds.y - offsetY 576 | ) 577 | ); 578 | this.invalidateLinks(part); 579 | } 580 | if (guideline) { 581 | this.guidelineVright.position = new go.Point(bestPoint.x, y0); 582 | this.guidelineVright.elt(0).height = y2 - y0; 583 | this.diagram.add(this.guidelineVright); 584 | } 585 | } 586 | } 587 | } 588 | } 589 | -------------------------------------------------------------------------------- /src/components/Diagram.css: -------------------------------------------------------------------------------- 1 | .diagram-component { 2 | width: 400px; 3 | height: 400px; 4 | border: 1px solid black; 5 | } -------------------------------------------------------------------------------- /src/components/DiagramWrapper.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 1998-2025 by Northwoods Software Corporation. All Rights Reserved. 3 | */ 4 | 5 | import * as go from "gojs"; 6 | import { ReactDiagram } from "gojs-react"; 7 | import { useEffect, useRef } from "react"; 8 | 9 | import { GuidedDraggingTool } from "../GuidedDraggingTool"; 10 | 11 | import "./Diagram.css"; 12 | 13 | interface DiagramProps { 14 | nodeDataArray: Array; 15 | linkDataArray: Array; 16 | modelData: go.ObjectData; 17 | skipsDiagramUpdate: boolean; 18 | onDiagramEvent: (e: go.DiagramEvent) => void; 19 | onModelChange: (idata: go.IncrementalData, e: go.ChangedEvent) => void; 20 | } 21 | 22 | export const DiagramWrapper = (props: DiagramProps) => { 23 | const diagramRef = useRef(null); 24 | const diagramStyle = { backgroundColor: "#eee" }; 25 | 26 | // add/remove listeners 27 | // only done on mount, not any time there's a change to props.onDiagramEvent 28 | useEffect(() => { 29 | if (diagramRef.current === null) return; 30 | const diagram = diagramRef.current.getDiagram(); 31 | if (diagram instanceof go.Diagram) { 32 | diagram.addDiagramListener("ChangedSelection", props.onDiagramEvent); 33 | } 34 | return () => { 35 | if (diagram instanceof go.Diagram) { 36 | diagram.removeDiagramListener("ChangedSelection", props.onDiagramEvent); 37 | } 38 | }; 39 | }, []); 40 | 41 | const initDiagram = (): go.Diagram => { 42 | const diagram = new go.Diagram({ 43 | "undoManager.isEnabled": true, // must be set to allow for model change listening 44 | // 'undoManager.maxHistoryLength': 0, // uncomment disable undo/redo functionality 45 | "clickCreatingTool.archetypeNodeData": { text: "new node", color: "lightblue" }, 46 | draggingTool: new GuidedDraggingTool(), // defined in GuidedDraggingTool.ts 47 | "draggingTool.horizontalGuidelineColor": "blue", 48 | "draggingTool.verticalGuidelineColor": "blue", 49 | "draggingTool.centerGuidelineColor": "green", 50 | "draggingTool.guidelineWidth": 1, 51 | layout: new go.ForceDirectedLayout(), 52 | model: new go.GraphLinksModel({ 53 | linkKeyProperty: "key", // IMPORTANT! must be defined for merges and data sync when using GraphLinksModel 54 | // positive keys for nodes 55 | makeUniqueKeyFunction: (m: go.Model, data: any) => { 56 | let k = data.key || 1; 57 | while (m.findNodeDataForKey(k)) k++; 58 | data.key = k; 59 | return k; 60 | }, 61 | // negative keys for links 62 | makeUniqueLinkKeyFunction: (m: go.GraphLinksModel, data: any) => { 63 | let k = data.key || -1; 64 | while (m.findLinkDataForKey(k)) k--; 65 | data.key = k; 66 | return k; 67 | } 68 | // NOTE: the above 'KeyFunction's are simplistic and loop over data to avoid key collisions, 69 | // they are not suitable for applications with lots of data 70 | }) 71 | }); 72 | 73 | // define a simple Node template 74 | diagram.nodeTemplate = new go.Node("Auto").bind("location", "loc", go.Point.parse, go.Point.stringify).add( 75 | new go.Shape("RoundedRectangle", { 76 | name: "SHAPE", 77 | fill: "white", 78 | strokeWidth: 0, 79 | // set the port properties: 80 | portId: "", 81 | fromLinkable: true, 82 | toLinkable: true, 83 | cursor: "pointer" 84 | }).bind("fill", "color"), 85 | new go.TextBlock({ 86 | margin: 8, 87 | font: "400 .875rem Roboto, sans-serif", 88 | editable: true // some room around the text 89 | }).bind(new go.Binding("text").makeTwoWay()) 90 | ); 91 | 92 | // relinking depends on modelData 93 | diagram.linkTemplate = new go.Link() 94 | .bind(new go.Binding("relinkableFrom", "canRelink").ofModel()) 95 | .bind(new go.Binding("relinkableTo", "canRelink").ofModel()) 96 | .add(new go.Shape(), new go.Shape({ toArrow: "Standard" })); 97 | 98 | return diagram; 99 | }; 100 | 101 | return ( 102 | 113 | ); 114 | }; 115 | -------------------------------------------------------------------------------- /src/components/Inspector.css: -------------------------------------------------------------------------------- 1 | .inspector { 2 | font: 500 1rem Roboto, helvetica, sans-serif; 3 | color: #212121; /* Grey 900 */ 4 | cursor: default; 5 | margin-top: 2px; 6 | } 7 | 8 | .inspector td, th { 9 | padding: 2px; 10 | } 11 | 12 | .inspector input { 13 | font: 300 1rem Roboto, helvetica, sans-serif; 14 | border: 0; 15 | outline: none; 16 | border-bottom: 1px solid #212121; /* Grey 900 */ 17 | padding: 2px; 18 | } 19 | 20 | .inspector input:disabled { 21 | background-color: #EEEEEE; /* Grey 400 */ 22 | color: #616161; /* Grey 700 */ 23 | border-bottom: 1px solid #BDBDBD; /* Grey 900 */ 24 | } -------------------------------------------------------------------------------- /src/components/InspectorRow.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 1998-2025 by Northwoods Software Corporation. All Rights Reserved. 3 | */ 4 | 5 | import "./Inspector.css"; 6 | 7 | interface InspectorRowProps { 8 | id: string; 9 | value: string; 10 | onInputChange: (key: string, value: string, isBlur: boolean) => void; 11 | } 12 | 13 | export const InspectorRow = (props: InspectorRowProps) => { 14 | const handleInputChange = (e: any) => { 15 | props.onInputChange(props.id, e.target.value, e.type === "blur"); 16 | }; 17 | 18 | const formatLocation = (loc: string) => { 19 | const locArr = loc.split(" "); 20 | if (locArr.length === 2) { 21 | const x = parseFloat(locArr[0]); 22 | const y = parseFloat(locArr[1]); 23 | if (!isNaN(x) && !isNaN(y)) { 24 | return `${x.toFixed(0)} ${y.toFixed(0)}`; 25 | } 26 | } 27 | return loc; 28 | }; 29 | 30 | return ( 31 | 32 | {props.id} 33 | 34 | 40 | 41 | 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /src/components/SelectionInspector.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 1998-2025 by Northwoods Software Corporation. All Rights Reserved. 3 | */ 4 | 5 | import { InspectorRow } from "./InspectorRow"; 6 | 7 | import "./Inspector.css"; 8 | 9 | interface SelectionInspectorProps { 10 | selectedData: any; 11 | onInputChange: (id: string, value: string, isBlur: boolean) => void; 12 | } 13 | 14 | export const SelectionInspector = (props: SelectionInspectorProps) => { 15 | /** 16 | * Render the object data, passing down property keys and values. 17 | */ 18 | const renderObjectDetails = () => { 19 | const selObj = props.selectedData; 20 | const dets = []; 21 | for (const k in selObj) { 22 | const val = selObj[k]; 23 | const row = ; 24 | if (k === "key") { 25 | dets.unshift(row); // key always at start 26 | } else { 27 | dets.push(row); 28 | } 29 | } 30 | return dets; 31 | }; 32 | 33 | return ( 34 |
35 | 36 | {renderObjectDetails()} 37 |
38 |
39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | 4 | import App from "./App.tsx"; 5 | 6 | const root = createRoot(document.getElementById("root")!); 7 | root.render( 8 | 9 | 10 | 11 | ); 12 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": [ 6 | "ES2020", 7 | "DOM", 8 | "DOM.Iterable" 9 | ], 10 | "module": "ESNext", 11 | "skipLibCheck": true, 12 | /* Bundler mode */ 13 | "moduleResolution": "bundler", 14 | "allowImportingTsExtensions": true, 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true, 18 | "jsx": "react-jsx", 19 | /* Linting */ 20 | "strict": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "noFallthroughCasesInSwitch": true 24 | }, 25 | "include": [ 26 | "src" 27 | ] 28 | } -------------------------------------------------------------------------------- /vite.config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | server: { open: true, }, 8 | }); --------------------------------------------------------------------------------