├── .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 | You need to enable JavaScript to run this app.
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 |
281 | Allow Relinking?
282 |
283 |
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 | });
--------------------------------------------------------------------------------