@visualli/react 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +335 -0
  2. package/dist/index.cjs +3262 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.cts +375 -0
  5. package/dist/index.d.ts +375 -0
  6. package/dist/index.js +3289 -0
  7. package/dist/index.js.map +1 -0
  8. package/package.json +48 -0
  9. package/src/VisualliCanvas.tsx +1087 -0
  10. package/src/VisualliRenderer.tsx +427 -0
  11. package/src/animations/index.ts +2 -0
  12. package/src/animations/konvaLayerTransition.ts +223 -0
  13. package/src/components/KonvaContainer.tsx +255 -0
  14. package/src/components/KonvaContainerLayer.tsx +63 -0
  15. package/src/components/KonvaEdge.tsx +254 -0
  16. package/src/components/KonvaEdgeLayer.tsx +72 -0
  17. package/src/components/KonvaNode.tsx +177 -0
  18. package/src/components/KonvaNodeLayer.tsx +61 -0
  19. package/src/components/KonvaStage.tsx +80 -0
  20. package/src/components/NavigationStack.tsx +184 -0
  21. package/src/components/SketchyBoxKonva.tsx +121 -0
  22. package/src/components/ZoomControls.tsx +110 -0
  23. package/src/config/blobShapes.ts +204 -0
  24. package/src/config/index.ts +21 -0
  25. package/src/config/textScaling.ts +69 -0
  26. package/src/context/VisualliContext.tsx +50 -0
  27. package/src/hooks/index.ts +7 -0
  28. package/src/hooks/useKonvaLayerTransition.ts +221 -0
  29. package/src/hooks/useKonvaRenderer.ts +185 -0
  30. package/src/hooks/useViewportNodes.ts +59 -0
  31. package/src/index.ts +81 -0
  32. package/src/konvaCompat.ts +29 -0
  33. package/src/stores/index.ts +3 -0
  34. package/src/stores/stores.ts +17 -0
  35. package/src/stores/useNodeStore.ts +101 -0
  36. package/src/stores/useViewportStore.ts +95 -0
  37. package/src/utils/index.ts +1 -0
  38. package/src/utils/layerNavigation.ts +15 -0
package/dist/index.cjs ADDED
@@ -0,0 +1,3262 @@
1
+ 'use client';
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/index.ts
32
+ var index_exports = {};
33
+ __export(index_exports, {
34
+ ACTIVE_BLOB_TYPES: () => import_core7.ACTIVE_BLOB_TYPES,
35
+ ALL_BLOB_SHAPES: () => import_core7.ALL_BLOB_SHAPES,
36
+ BLOB_LAYER_CONFIG: () => import_core7.BLOB_LAYER_CONFIG,
37
+ BLOB_TEXT_OFFSETS: () => import_core7.BLOB_TEXT_OFFSETS,
38
+ DESCRIPTION_TEXT_BASE_FONT_PX: () => import_core7.DESCRIPTION_TEXT_BASE_FONT_PX,
39
+ KonvaContainer: () => KonvaContainer_default,
40
+ KonvaContainerLayer: () => KonvaContainerLayer,
41
+ KonvaEdge: () => KonvaEdge_default,
42
+ KonvaEdgeLayer: () => KonvaEdgeLayer,
43
+ KonvaLayerTransitionAnimator: () => KonvaLayerTransitionAnimator,
44
+ KonvaNode: () => KonvaNode_default,
45
+ KonvaNodeLayer: () => KonvaNodeLayer,
46
+ KonvaStage: () => KonvaStage_default,
47
+ NODE_LAYER_CONFIG: () => import_core7.NODE_LAYER_CONFIG,
48
+ NODE_TEXT_BASE_FONT_PX: () => import_core7.NODE_TEXT_BASE_FONT_PX,
49
+ NavigationStack: () => NavigationStack_default,
50
+ SketchyBoxKonva: () => SketchyBoxKonva_default,
51
+ VisualliCanvas: () => VisualliCanvas,
52
+ VisualliProvider: () => VisualliProvider,
53
+ VisualliRenderer: () => VisualliRenderer,
54
+ ZoomControls: () => ZoomControls_default,
55
+ buildBlobPathData: () => buildBlobPathData,
56
+ calculateFitCenter: () => import_core5.calculateFitCenter,
57
+ calculateFitView: () => import_core5.calculateFitView,
58
+ calculateFitZoom: () => import_core5.calculateFitZoom,
59
+ computeEdgeLabelScale: () => import_core7.computeEdgeLabelScale,
60
+ computeNodeTextScreenScale: () => import_core7.computeNodeTextScreenScale,
61
+ computeNodeTextWorldScale: () => import_core7.computeNodeTextWorldScale,
62
+ computeOverlayScale: () => import_core7.computeOverlayScale,
63
+ drawBlobPath: () => import_core7.drawBlobPath,
64
+ getBlobTypeForLayer: () => import_core7.getBlobTypeForLayer,
65
+ getChildLayerForNode: () => import_core5.getChildLayerForNode,
66
+ getConnectionsForLayer: () => import_core5.getConnectionsForLayer,
67
+ getLayerForNavigation: () => import_core5.getLayerForNavigation,
68
+ konvaLayerTransitionAnimator: () => konvaLayerTransitionAnimator,
69
+ useKonvaLayerTransition: () => useKonvaLayerTransition,
70
+ useKonvaRenderer: () => useKonvaRenderer,
71
+ useNodeStore: () => useNodeStore,
72
+ useRenderConfigStore: () => useRenderConfigStore,
73
+ useSelectionStore: () => useSelectionStore,
74
+ useViewportNodes: () => useViewportNodes,
75
+ useViewportStore: () => useViewportStore,
76
+ useVisualli: () => useVisualli
77
+ });
78
+ module.exports = __toCommonJS(index_exports);
79
+
80
+ // src/VisualliRenderer.tsx
81
+ var import_react15 = require("react");
82
+ var import_core14 = require("@visualli/core");
83
+
84
+ // src/VisualliCanvas.tsx
85
+ var import_react14 = require("react");
86
+ var import_core13 = require("@visualli/core");
87
+
88
+ // src/stores/useNodeStore.ts
89
+ var import_zustand = require("zustand");
90
+ function buildChildrenIndex(nodes) {
91
+ const index = /* @__PURE__ */ new Map();
92
+ for (const node of nodes.values()) {
93
+ const pid = node.parentId ?? null;
94
+ if (!index.has(pid)) index.set(pid, /* @__PURE__ */ new Set());
95
+ index.get(pid).add(node.id);
96
+ }
97
+ return index;
98
+ }
99
+ var useNodeStore = (0, import_zustand.create)((set, get) => ({
100
+ nodes: /* @__PURE__ */ new Map(),
101
+ childrenIndex: /* @__PURE__ */ new Map(),
102
+ setNodes: (nodes) => set({ nodes, childrenIndex: buildChildrenIndex(nodes) }),
103
+ updateNode: (nodeId, updates) => set((s) => {
104
+ const node = s.nodes.get(nodeId);
105
+ if (!node) return s;
106
+ const next = new Map(s.nodes);
107
+ next.set(nodeId, { ...node, ...updates, updatedAt: /* @__PURE__ */ new Date() });
108
+ return { nodes: next };
109
+ }),
110
+ updateNodePosition: (nodeId, x, y) => set((s) => {
111
+ const node = s.nodes.get(nodeId);
112
+ if (!node || node.x === x && node.y === y) return s;
113
+ const next = new Map(s.nodes);
114
+ next.set(nodeId, { ...node, x, y });
115
+ return { nodes: next };
116
+ }),
117
+ addNode: (node) => set((s) => {
118
+ const next = new Map(s.nodes);
119
+ next.set(node.id, node);
120
+ const idx = new Map(s.childrenIndex);
121
+ const pid = node.parentId ?? null;
122
+ if (!idx.has(pid)) idx.set(pid, /* @__PURE__ */ new Set());
123
+ idx.get(pid).add(node.id);
124
+ return { nodes: next, childrenIndex: idx };
125
+ }),
126
+ removeNode: (nodeId) => set((s) => {
127
+ const node = s.nodes.get(nodeId);
128
+ if (!node) return s;
129
+ const next = new Map(s.nodes);
130
+ next.delete(nodeId);
131
+ const idx = new Map(s.childrenIndex);
132
+ const pid = node.parentId ?? null;
133
+ if (idx.has(pid)) {
134
+ const set2 = new Set(idx.get(pid));
135
+ set2.delete(nodeId);
136
+ if (set2.size === 0) idx.delete(pid);
137
+ else idx.set(pid, set2);
138
+ }
139
+ return { nodes: next, childrenIndex: idx };
140
+ }),
141
+ setNodeExpanded: (nodeId, expanded) => set((s) => {
142
+ const node = s.nodes.get(nodeId);
143
+ if (!node) return s;
144
+ const next = new Map(s.nodes);
145
+ next.set(nodeId, { ...node, isExpanded: expanded, updatedAt: /* @__PURE__ */ new Date() });
146
+ return { nodes: next };
147
+ }),
148
+ getNode: (nodeId) => get().nodes.get(nodeId),
149
+ getChildren: (pid) => {
150
+ const { nodes, childrenIndex } = get();
151
+ return Array.from(childrenIndex.get(pid) ?? []).map((id) => nodes.get(id)).filter(Boolean);
152
+ },
153
+ getChildrenIds: (pid) => get().childrenIndex.get(pid) ?? /* @__PURE__ */ new Set(),
154
+ getAllNodes: () => Array.from(get().nodes.values())
155
+ }));
156
+
157
+ // src/stores/useViewportStore.ts
158
+ var import_zustand2 = require("zustand");
159
+ var import_core = require("@visualli/core");
160
+ function makeInitial(w, h) {
161
+ const vp = {
162
+ centerX: 0,
163
+ centerY: 0,
164
+ zoomLevel: import_core.ZOOM_DEFAULT,
165
+ rotation: 0,
166
+ visibleBounds: { minX: 0, minY: 0, maxX: 0, maxY: 0 }
167
+ };
168
+ return (0, import_core.recalculateViewportBounds)(vp, w, h);
169
+ }
170
+ var getVP = (s) => ({
171
+ centerX: s.centerX,
172
+ centerY: s.centerY,
173
+ zoomLevel: s.zoomLevel,
174
+ rotation: s.rotation,
175
+ visibleBounds: s.visibleBounds
176
+ });
177
+ var useViewportStore = (0, import_zustand2.create)((set, get) => {
178
+ const init = makeInitial(import_core.DEFAULT_CANVAS_WIDTH, import_core.DEFAULT_CANVAS_HEIGHT);
179
+ return {
180
+ ...init,
181
+ canvasWidth: import_core.DEFAULT_CANVAS_WIDTH,
182
+ canvasHeight: import_core.DEFAULT_CANVAS_HEIGHT,
183
+ pan: (dx, dy) => set((s) => {
184
+ const updated = (0, import_core.recalculateViewportBounds)((0, import_core.panViewport)(dx, dy, getVP(s)), s.canvasWidth, s.canvasHeight);
185
+ return { centerX: updated.centerX, centerY: updated.centerY, visibleBounds: updated.visibleBounds };
186
+ }),
187
+ zoom: (delta, pivotX, pivotY) => set((s) => {
188
+ const updated = (0, import_core.recalculateViewportBounds)(
189
+ (0, import_core.zoomViewport)(delta, getVP(s), pivotX, pivotY, s.canvasWidth, s.canvasHeight),
190
+ s.canvasWidth,
191
+ s.canvasHeight
192
+ );
193
+ return { centerX: updated.centerX, centerY: updated.centerY, zoomLevel: updated.zoomLevel, visibleBounds: updated.visibleBounds };
194
+ }),
195
+ setCenter: (x, y) => set((s) => {
196
+ const updated = (0, import_core.recalculateViewportBounds)((0, import_core.setViewportCenter)(x, y, getVP(s)), s.canvasWidth, s.canvasHeight);
197
+ return { centerX: updated.centerX, centerY: updated.centerY, visibleBounds: updated.visibleBounds };
198
+ }),
199
+ setZoom: (level) => set((s) => {
200
+ const clamped = (0, import_core.clampZoom)(level);
201
+ const updated = (0, import_core.recalculateViewportBounds)({ ...getVP(s), zoomLevel: clamped }, s.canvasWidth, s.canvasHeight);
202
+ return { zoomLevel: updated.zoomLevel, visibleBounds: updated.visibleBounds };
203
+ }),
204
+ updateCanvasSize: (w, h) => set((s) => {
205
+ const updated = (0, import_core.recalculateViewportBounds)(getVP(s), w, h);
206
+ return { canvasWidth: w, canvasHeight: h, visibleBounds: updated.visibleBounds };
207
+ }),
208
+ getVisibleBounds: () => get().visibleBounds,
209
+ worldToScreen: (x, y) => {
210
+ const s = get();
211
+ return (0, import_core.worldToScreen)(x, y, getVP(s), s.canvasWidth, s.canvasHeight);
212
+ },
213
+ screenToWorld: (x, y) => {
214
+ const s = get();
215
+ return (0, import_core.screenToWorld)(x, y, getVP(s), s.canvasWidth, s.canvasHeight);
216
+ }
217
+ };
218
+ });
219
+
220
+ // src/stores/stores.ts
221
+ var import_zustand3 = require("zustand");
222
+ var import_core2 = require("@visualli/core");
223
+ var useSelectionStore = (0, import_zustand3.create)((set, get, store) => {
224
+ import_core2.selectionStore.subscribe((state) => set(state));
225
+ return import_core2.selectionStore.getState();
226
+ });
227
+ var useRenderConfigStore = (0, import_zustand3.create)((set, get, store) => {
228
+ import_core2.renderConfigStore.subscribe((state) => set(state));
229
+ return import_core2.renderConfigStore.getState();
230
+ });
231
+
232
+ // src/hooks/useKonvaRenderer.ts
233
+ var import_react = require("react");
234
+ function useKonvaRenderer({
235
+ containerRef,
236
+ stageRef,
237
+ enabled = true
238
+ }) {
239
+ const pan = useViewportStore((s) => s.pan);
240
+ const zoom = useViewportStore((s) => s.zoom);
241
+ const setZoom = useViewportStore((s) => s.setZoom);
242
+ const setCenter = useViewportStore((s) => s.setCenter);
243
+ const updateCanvasSize = useViewportStore((s) => s.updateCanvasSize);
244
+ const autoAdjust = useRenderConfigStore((s) => s.autoAdjustQuality);
245
+ const [canvasWidth, setCanvasWidth] = (0, import_react.useState)(0);
246
+ const [canvasHeight, setCanvasHeight] = (0, import_react.useState)(0);
247
+ const cachedSizeRef = (0, import_react.useRef)({ width: 0, height: 0 });
248
+ const isPanningRef = (0, import_react.useRef)(false);
249
+ const lastPanPointRef = (0, import_react.useRef)({ x: 0, y: 0 });
250
+ const panAccumRef = (0, import_react.useRef)({ x: 0, y: 0 });
251
+ const fpsRef = (0, import_react.useRef)({ frames: 0, last: performance.now() });
252
+ const fpsRafId = (0, import_react.useRef)(null);
253
+ (0, import_react.useEffect)(() => {
254
+ if (!enabled) return;
255
+ const tick = (now) => {
256
+ fpsRef.current.frames++;
257
+ if (now - fpsRef.current.last >= 2e3) {
258
+ const fps = fpsRef.current.frames / ((now - fpsRef.current.last) / 1e3) | 0;
259
+ autoAdjust(fps);
260
+ fpsRef.current = { frames: 0, last: now };
261
+ }
262
+ fpsRafId.current = requestAnimationFrame(tick);
263
+ };
264
+ fpsRafId.current = requestAnimationFrame(tick);
265
+ return () => {
266
+ if (fpsRafId.current !== null) cancelAnimationFrame(fpsRafId.current);
267
+ };
268
+ }, [enabled, autoAdjust]);
269
+ (0, import_react.useEffect)(() => {
270
+ if (!enabled) return;
271
+ let rafId = null;
272
+ const measure = () => {
273
+ const el = containerRef.current;
274
+ const w = el ? Math.round(el.getBoundingClientRect().width) : 800;
275
+ const h = el ? Math.round(el.getBoundingClientRect().height) : 600;
276
+ return { w, h };
277
+ };
278
+ const updateSize = () => {
279
+ if (rafId) cancelAnimationFrame(rafId);
280
+ rafId = requestAnimationFrame(() => {
281
+ rafId = null;
282
+ const { w, h } = measure();
283
+ if (w > 0 && h > 0 && (cachedSizeRef.current.width !== w || cachedSizeRef.current.height !== h)) {
284
+ cachedSizeRef.current = { width: w, height: h };
285
+ setCanvasWidth(w);
286
+ setCanvasHeight(h);
287
+ updateCanvasSize(w, h);
288
+ }
289
+ });
290
+ };
291
+ updateSize();
292
+ let ro = null;
293
+ if (typeof ResizeObserver !== "undefined" && containerRef.current) {
294
+ ro = new ResizeObserver(updateSize);
295
+ ro.observe(containerRef.current);
296
+ } else {
297
+ window.addEventListener("resize", updateSize);
298
+ }
299
+ return () => {
300
+ ro ? ro.disconnect() : window.removeEventListener("resize", updateSize);
301
+ if (rafId) cancelAnimationFrame(rafId);
302
+ };
303
+ }, [enabled, containerRef, updateCanvasSize]);
304
+ const handleWheel = (0, import_react.useCallback)((e) => {
305
+ e.evt.preventDefault();
306
+ const stage = e.target.getStage();
307
+ if (!stage) return;
308
+ const pointer = stage.getPointerPosition();
309
+ if (!pointer) return;
310
+ const delta = -e.evt.deltaY / 1e3;
311
+ zoom(delta, pointer.x, pointer.y);
312
+ }, [zoom]);
313
+ const handleMouseDown = (0, import_react.useCallback)((e) => {
314
+ const clickedOnEmpty = e.target === e.target.getStage();
315
+ if (clickedOnEmpty && e.evt.button === 0) {
316
+ e.evt.preventDefault();
317
+ isPanningRef.current = true;
318
+ lastPanPointRef.current = { x: e.evt.clientX, y: e.evt.clientY };
319
+ panAccumRef.current = { x: 0, y: 0 };
320
+ const stage = e.target.getStage();
321
+ if (stage) stage.container().style.cursor = "grabbing";
322
+ }
323
+ }, []);
324
+ const handleMouseMove = (0, import_react.useCallback)((e) => {
325
+ if (!isPanningRef.current) {
326
+ const isOverEmpty = e.target === e.target.getStage();
327
+ const stage2 = e.target.getStage();
328
+ if (stage2) stage2.container().style.cursor = isOverEmpty ? "grab" : "default";
329
+ return;
330
+ }
331
+ e.evt.preventDefault();
332
+ const deltaX = e.evt.clientX - lastPanPointRef.current.x;
333
+ const deltaY = e.evt.clientY - lastPanPointRef.current.y;
334
+ const stage = e.target.getStage();
335
+ if (stage) {
336
+ stage.x(stage.x() + deltaX);
337
+ stage.y(stage.y() + deltaY);
338
+ stage.batchDraw();
339
+ }
340
+ panAccumRef.current.x += deltaX;
341
+ panAccumRef.current.y += deltaY;
342
+ lastPanPointRef.current = { x: e.evt.clientX, y: e.evt.clientY };
343
+ }, []);
344
+ const handleMouseUp = (0, import_react.useCallback)((e) => {
345
+ if (isPanningRef.current) {
346
+ isPanningRef.current = false;
347
+ const { x, y } = panAccumRef.current;
348
+ if (x !== 0 || y !== 0) {
349
+ pan(x, y);
350
+ panAccumRef.current = { x: 0, y: 0 };
351
+ }
352
+ const stage = e.target.getStage();
353
+ if (stage) stage.container().style.cursor = "grab";
354
+ }
355
+ }, [pan]);
356
+ return { handleWheel, handleMouseDown, handleMouseMove, handleMouseUp, canvasWidth, canvasHeight };
357
+ }
358
+
359
+ // src/hooks/useKonvaLayerTransition.ts
360
+ var import_react2 = require("react");
361
+
362
+ // src/animations/konvaLayerTransition.ts
363
+ var import_core3 = require("@visualli/core");
364
+ var import_core4 = require("@visualli/core");
365
+ function parseColor(color) {
366
+ if (color.startsWith("#")) {
367
+ const h = color.slice(1);
368
+ return { r: parseInt(h.slice(0, 2), 16), g: parseInt(h.slice(2, 4), 16), b: parseInt(h.slice(4, 6), 16) };
369
+ }
370
+ const m = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
371
+ return m ? { r: +m[1], g: +m[2], b: +m[3] } : { r: 255, g: 255, b: 255 };
372
+ }
373
+ function interpolateRgb(a, b, t) {
374
+ const e = (0, import_core3.easeInOutSine)(t);
375
+ return `rgb(${a.r + (b.r - a.r) * e + 0.5 | 0},${a.g + (b.g - a.g) * e + 0.5 | 0},${a.b + (b.b - a.b) * e + 0.5 | 0})`;
376
+ }
377
+ function lerp(a, b, t) {
378
+ return a + (b - a) * t;
379
+ }
380
+ var KonvaLayerTransitionAnimator = class {
381
+ constructor() {
382
+ this.rafId = null;
383
+ this.timers = [];
384
+ this._active = false;
385
+ }
386
+ get active() {
387
+ return this._active;
388
+ }
389
+ cancel() {
390
+ if (this.rafId !== null) {
391
+ cancelAnimationFrame(this.rafId);
392
+ this.rafId = null;
393
+ }
394
+ this.timers.forEach(clearTimeout);
395
+ this.timers = [];
396
+ this._active = false;
397
+ }
398
+ // ── Zoom into child layer ──────────────────────────────────────────────────
399
+ animateZoomIntoLayer(current, targetNode, childCenter, fromColor, toColor, cb, targetZoom = 1, canvasWidth = 800, canvasHeight = 600) {
400
+ if (this._active) return;
401
+ this._active = true;
402
+ const ZOOM_DURATION = import_core4.ANIMATION_PHASES.ZOOM_IN.ZOOM_TO_TARGET;
403
+ const FADE_OUT_DELAY = import_core4.ANIMATION_PHASES.ZOOM_IN.FADE_OUT_START;
404
+ const FADE_OUT_DURATION = import_core4.ANIMATION_PHASES.ZOOM_IN.FADE_OUT;
405
+ const SWAP_AT_MS = import_core4.ANIMATION_PHASES.ZOOM_IN.CONTENT_SWAP;
406
+ const FADE_IN_DURATION = import_core4.ANIMATION_PHASES.ZOOM_IN.FADE_IN;
407
+ const SETTLE_FACTOR = 0.94;
408
+ const z0 = current.zoomLevel;
409
+ const dx = targetNode.x - current.centerX;
410
+ const dy = targetNode.y - current.centerY;
411
+ const vmin = Math.min(canvasWidth, canvasHeight);
412
+ const fillZoom = Math.max(4, vmin * 1.5 / Math.max(targetNode.width, 100));
413
+ const fromRgb = parseColor(fromColor);
414
+ const toRgb = parseColor(toColor);
415
+ const state = { viewport: { centerX: 0, centerY: 0, zoomLevel: 0 }, opacity: 1, backgroundColor: fromColor };
416
+ const start = performance.now();
417
+ let swapped = false;
418
+ let fadeInStart = 0;
419
+ cb.onPhaseChange?.("zoom");
420
+ const tick = (now) => {
421
+ if (!this._active) return;
422
+ const elapsed = now - start;
423
+ if (!swapped) {
424
+ if (elapsed >= SWAP_AT_MS) {
425
+ swapped = true;
426
+ fadeInStart = now;
427
+ state.viewport = { centerX: childCenter.x, centerY: childCenter.y, zoomLevel: targetZoom * SETTLE_FACTOR };
428
+ state.opacity = 0;
429
+ state.backgroundColor = toColor;
430
+ cb.onUpdate(state);
431
+ cb.onPhaseChange?.("swap");
432
+ this.rafId = requestAnimationFrame(tick);
433
+ return;
434
+ }
435
+ const rawT = Math.min(elapsed / ZOOM_DURATION, 1);
436
+ const zoomT = (0, import_core3.easeZoomTransition)(rawT);
437
+ const zl = lerp(z0, fillZoom, zoomT);
438
+ state.viewport = {
439
+ centerX: targetNode.x - dx * z0 * (1 - zoomT) / zl,
440
+ centerY: targetNode.y - dy * z0 * (1 - zoomT) / zl,
441
+ zoomLevel: zl
442
+ };
443
+ state.opacity = elapsed > FADE_OUT_DELAY ? Math.max(0, 1 - (0, import_core3.easeOutCubic)(Math.min((elapsed - FADE_OUT_DELAY) / FADE_OUT_DURATION, 1))) : 1;
444
+ state.backgroundColor = interpolateRgb(fromRgb, toRgb, rawT);
445
+ cb.onUpdate(state);
446
+ } else {
447
+ const fi = now - fadeInStart;
448
+ const ft = Math.min(fi / FADE_IN_DURATION, 1);
449
+ const eased = (0, import_core3.easeOutCubic)(ft);
450
+ state.viewport = { centerX: childCenter.x, centerY: childCenter.y, zoomLevel: lerp(targetZoom * SETTLE_FACTOR, targetZoom, eased) };
451
+ state.opacity = eased;
452
+ state.backgroundColor = toColor;
453
+ cb.onUpdate(state);
454
+ if (ft >= 1) {
455
+ cb.onPhaseChange?.("fade-in");
456
+ this.rafId = null;
457
+ this._complete(cb.onComplete);
458
+ return;
459
+ }
460
+ }
461
+ this.rafId = requestAnimationFrame(tick);
462
+ };
463
+ this.rafId = requestAnimationFrame(tick);
464
+ }
465
+ // ── Zoom out to parent layer ───────────────────────────────────────────────
466
+ animateZoomOutToParent(current, parent, fromColor, toColor, cb) {
467
+ if (this._active) return;
468
+ this._active = true;
469
+ const FADE_OUT = import_core4.ANIMATION_PHASES.ZOOM_OUT.FADE_OUT;
470
+ const FADE_IN = import_core4.ANIMATION_PHASES.ZOOM_OUT.FADE_IN;
471
+ const OPACITY_SETTLE = 66;
472
+ const fromRgb = parseColor(fromColor);
473
+ const toRgb = parseColor(toColor);
474
+ const state = { viewport: { centerX: 0, centerY: 0, zoomLevel: 0 }, opacity: 1, backgroundColor: fromColor };
475
+ const start = performance.now();
476
+ let swapped = false;
477
+ let fadeInStart = 0;
478
+ cb.onPhaseChange?.("fade-out");
479
+ const tick = (now) => {
480
+ if (!this._active) return;
481
+ const elapsed = now - start;
482
+ if (!swapped) {
483
+ if (elapsed >= FADE_OUT) {
484
+ swapped = true;
485
+ fadeInStart = now;
486
+ state.viewport = { centerX: parent.centerX, centerY: parent.centerY, zoomLevel: parent.zoomLevel * 5 };
487
+ state.opacity = 0;
488
+ state.backgroundColor = toColor;
489
+ cb.onUpdate(state);
490
+ cb.onPhaseChange?.("zoom-back");
491
+ this.rafId = requestAnimationFrame(tick);
492
+ return;
493
+ }
494
+ const rawT = Math.min(elapsed / FADE_OUT, 1);
495
+ const t = (0, import_core3.easeOutCubic)(rawT);
496
+ state.viewport = { centerX: current.centerX, centerY: current.centerY, zoomLevel: lerp(current.zoomLevel, current.zoomLevel * 0.6, t) };
497
+ state.opacity = 1 - t;
498
+ state.backgroundColor = interpolateRgb(fromRgb, toRgb, rawT);
499
+ cb.onUpdate(state);
500
+ } else {
501
+ const fi = now - fadeInStart;
502
+ const rawT = Math.min(fi / FADE_IN, 1);
503
+ const t = (0, import_core3.easeZoomTransition)(rawT);
504
+ state.viewport = { centerX: parent.centerX, centerY: parent.centerY, zoomLevel: lerp(parent.zoomLevel * 5, parent.zoomLevel, t) };
505
+ state.opacity = fi < OPACITY_SETTLE ? 0 : (0, import_core3.easeZoomTransition)(Math.min((fi - OPACITY_SETTLE) / (FADE_IN - OPACITY_SETTLE), 1));
506
+ state.backgroundColor = toColor;
507
+ cb.onUpdate(state);
508
+ if (rawT >= 1) {
509
+ cb.onPhaseChange?.("fade-in");
510
+ this.rafId = null;
511
+ this._complete(cb.onComplete);
512
+ return;
513
+ }
514
+ }
515
+ this.rafId = requestAnimationFrame(tick);
516
+ };
517
+ this.rafId = requestAnimationFrame(tick);
518
+ }
519
+ _complete(onComplete) {
520
+ this.timers.forEach(clearTimeout);
521
+ this.timers = [];
522
+ this._active = false;
523
+ onComplete();
524
+ }
525
+ };
526
+ var konvaLayerTransitionAnimator = new KonvaLayerTransitionAnimator();
527
+
528
+ // src/utils/layerNavigation.ts
529
+ var import_core5 = require("@visualli/core");
530
+
531
+ // src/hooks/useKonvaLayerTransition.ts
532
+ function useKonvaLayerTransition(opts) {
533
+ const {
534
+ stageRef,
535
+ canvasWrapperRef,
536
+ bgRef,
537
+ onSwapLayer,
538
+ onSwapBack,
539
+ onComplete,
540
+ onViewportFrame,
541
+ onViewportFinal,
542
+ onPhaseChange: onPhaseChangeExt,
543
+ onTransitionLifecycle
544
+ } = opts;
545
+ const setZoom = useViewportStore((s) => s.setZoom);
546
+ const setCenter = useViewportStore((s) => s.setCenter);
547
+ const getState = useViewportStore.getState;
548
+ const swappedRef = (0, import_react2.useRef)(false);
549
+ const canvasSizeRef = (0, import_react2.useRef)({ width: 800, height: 600 });
550
+ (0, import_react2.useEffect)(() => {
551
+ const update = () => {
552
+ const vp = useViewportStore.getState();
553
+ canvasSizeRef.current = { width: vp.canvasWidth || 800, height: vp.canvasHeight || 600 };
554
+ };
555
+ update();
556
+ const unsubscribe = useViewportStore.subscribe(update);
557
+ return unsubscribe;
558
+ }, []);
559
+ const applyViewport = (0, import_react2.useCallback)((vp) => {
560
+ const stage = stageRef.current;
561
+ if (!stage) return;
562
+ const { width, height } = canvasSizeRef.current;
563
+ stage.x(width / 2 - vp.centerX * vp.zoomLevel);
564
+ stage.y(height / 2 - vp.centerY * vp.zoomLevel);
565
+ stage.scaleX(vp.zoomLevel);
566
+ stage.scaleY(vp.zoomLevel);
567
+ stage.getLayers().forEach((l) => l.drawScene());
568
+ setCenter(vp.centerX, vp.centerY);
569
+ setZoom(vp.zoomLevel);
570
+ }, [stageRef, setCenter, setZoom]);
571
+ const applyOpacity = (0, import_react2.useCallback)((opacity) => {
572
+ if (canvasWrapperRef.current) {
573
+ canvasWrapperRef.current.style.opacity = String(opacity);
574
+ }
575
+ }, [canvasWrapperRef]);
576
+ const applyBgColor = (0, import_react2.useCallback)((color) => {
577
+ if (bgRef?.current) {
578
+ bgRef.current.style.backgroundColor = color;
579
+ }
580
+ }, [bgRef]);
581
+ const zoomIntoLayer = (0, import_react2.useCallback)((_doc, targetNode, childLayerId, childNodes, fromColor, toColor) => {
582
+ if (konvaLayerTransitionAnimator.active) return;
583
+ const vp = getState();
584
+ const stage = stageRef.current;
585
+ const w = stage?.width() ?? 800;
586
+ const h = stage?.height() ?? 600;
587
+ const fit = (0, import_core5.calculateFitView)(childNodes, w, h);
588
+ swappedRef.current = false;
589
+ onTransitionLifecycle?.("start");
590
+ konvaLayerTransitionAnimator.animateZoomIntoLayer(
591
+ { centerX: vp.centerX, centerY: vp.centerY, zoomLevel: vp.zoomLevel },
592
+ { x: targetNode.x, y: targetNode.y, width: targetNode.width, height: targetNode.height },
593
+ { x: fit.centerX, y: fit.centerY },
594
+ fromColor,
595
+ toColor,
596
+ {
597
+ onUpdate: ({ viewport, opacity, backgroundColor }) => {
598
+ if (onViewportFrame) {
599
+ onViewportFrame(viewport);
600
+ } else {
601
+ applyViewport(viewport);
602
+ }
603
+ applyOpacity(opacity);
604
+ applyBgColor(backgroundColor);
605
+ },
606
+ onComplete: () => {
607
+ const finalVp = { centerX: fit.centerX, centerY: fit.centerY, zoomLevel: fit.zoomLevel };
608
+ if (onViewportFinal) {
609
+ onViewportFinal(finalVp);
610
+ } else {
611
+ applyViewport(finalVp);
612
+ }
613
+ applyOpacity(1);
614
+ onComplete();
615
+ onTransitionLifecycle?.("end");
616
+ },
617
+ onPhaseChange: (phase) => {
618
+ onPhaseChangeExt?.(phase);
619
+ if (phase === "swap" && !swappedRef.current) {
620
+ swappedRef.current = true;
621
+ onSwapLayer(childLayerId);
622
+ }
623
+ }
624
+ },
625
+ fit.zoomLevel,
626
+ w,
627
+ h
628
+ );
629
+ }, [stageRef, getState, applyViewport, applyOpacity, applyBgColor, onSwapLayer, onComplete, onViewportFrame, onViewportFinal, onPhaseChangeExt, onTransitionLifecycle]);
630
+ const zoomOutToParent = (0, import_react2.useCallback)((parentViewport, fromColor, toColor) => {
631
+ if (konvaLayerTransitionAnimator.active) return;
632
+ const vp = getState();
633
+ swappedRef.current = false;
634
+ onTransitionLifecycle?.("start");
635
+ konvaLayerTransitionAnimator.animateZoomOutToParent(
636
+ { centerX: vp.centerX, centerY: vp.centerY, zoomLevel: vp.zoomLevel },
637
+ parentViewport,
638
+ fromColor,
639
+ toColor,
640
+ {
641
+ onUpdate: ({ viewport, opacity, backgroundColor }) => {
642
+ if (onViewportFrame) {
643
+ onViewportFrame(viewport);
644
+ } else {
645
+ applyViewport(viewport);
646
+ }
647
+ applyOpacity(opacity);
648
+ applyBgColor(backgroundColor);
649
+ },
650
+ onComplete: () => {
651
+ if (onViewportFinal) {
652
+ onViewportFinal(parentViewport);
653
+ } else {
654
+ applyViewport(parentViewport);
655
+ }
656
+ applyOpacity(1);
657
+ onComplete();
658
+ onTransitionLifecycle?.("end");
659
+ },
660
+ onPhaseChange: (phase) => {
661
+ onPhaseChangeExt?.(phase);
662
+ if (phase === "zoom-back" && !swappedRef.current) {
663
+ swappedRef.current = true;
664
+ onSwapBack();
665
+ }
666
+ }
667
+ }
668
+ );
669
+ }, [getState, applyViewport, applyOpacity, applyBgColor, onSwapBack, onComplete, onViewportFrame, onViewportFinal, onPhaseChangeExt, onTransitionLifecycle]);
670
+ const isTransitioning = (0, import_react2.useCallback)(() => konvaLayerTransitionAnimator.active, []);
671
+ (0, import_react2.useEffect)(() => () => {
672
+ konvaLayerTransitionAnimator.cancel();
673
+ }, []);
674
+ return { zoomIntoLayer, zoomOutToParent, isTransitioning };
675
+ }
676
+
677
+ // src/components/KonvaStage.tsx
678
+ var import_react3 = require("react");
679
+
680
+ // src/konvaCompat.ts
681
+ var import_react_konva = require("react-konva");
682
+ var KStage = import_react_konva.Stage;
683
+ var KLayer = import_react_konva.Layer;
684
+ var KGroup = import_react_konva.Group;
685
+ var KShape = import_react_konva.Shape;
686
+ var KRect = import_react_konva.Rect;
687
+ var KText = import_react_konva.Text;
688
+ var KLine = import_react_konva.Line;
689
+ var KTextPath = import_react_konva.TextPath;
690
+
691
+ // src/components/KonvaStage.tsx
692
+ var import_jsx_runtime = require("react/jsx-runtime");
693
+ var KonvaStage = (0, import_react3.forwardRef)(function KonvaStage2({
694
+ children,
695
+ onWheel,
696
+ onMouseDown,
697
+ onMouseMove,
698
+ onMouseUp,
699
+ onContextMenu,
700
+ onTouchStart,
701
+ onTouchMove,
702
+ onTouchEnd,
703
+ onStageReady
704
+ }, ref) {
705
+ const internalRef = (0, import_react3.useRef)(null);
706
+ const { centerX, centerY, zoomLevel, canvasWidth, canvasHeight } = useViewportStore();
707
+ (0, import_react3.useEffect)(() => {
708
+ if (internalRef.current) {
709
+ if (typeof ref === "function") ref(internalRef.current);
710
+ else if (ref) ref.current = internalRef.current;
711
+ onStageReady?.(internalRef.current);
712
+ }
713
+ }, []);
714
+ (0, import_react3.useEffect)(() => {
715
+ return () => {
716
+ internalRef.current?.destroy();
717
+ };
718
+ }, []);
719
+ const w = canvasWidth || 800;
720
+ const h = canvasHeight || 600;
721
+ const stageX = w / 2 - centerX * zoomLevel;
722
+ const stageY = h / 2 - centerY * zoomLevel;
723
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
724
+ KStage,
725
+ {
726
+ ref: internalRef,
727
+ width: w,
728
+ height: h,
729
+ x: stageX,
730
+ y: stageY,
731
+ scaleX: zoomLevel,
732
+ scaleY: zoomLevel,
733
+ onWheel,
734
+ onMouseDown,
735
+ onMouseMove,
736
+ onMouseUp,
737
+ onContextMenu,
738
+ onTouchStart,
739
+ onTouchMove,
740
+ onTouchEnd,
741
+ draggable: false,
742
+ pixelRatio: 1,
743
+ children
744
+ }
745
+ );
746
+ });
747
+ var KonvaStage_default = KonvaStage;
748
+
749
+ // src/components/KonvaNodeLayer.tsx
750
+ var import_react6 = require("react");
751
+
752
+ // src/hooks/useViewportNodes.ts
753
+ var import_react4 = require("react");
754
+ var import_core6 = require("@visualli/core");
755
+ var SPATIAL_INDEX_THRESHOLD = 200;
756
+ function useViewportNodes(nodes, levelFilter, skipCulling = false, isDragging = false) {
757
+ const { centerX, centerY, zoomLevel, canvasWidth, canvasHeight } = useViewportStore();
758
+ const indexRef = (0, import_react4.useRef)(null);
759
+ const nodesHashRef = (0, import_react4.useRef)(0);
760
+ const nodesHash = nodes.length;
761
+ if (nodesHash !== nodesHashRef.current) {
762
+ nodesHashRef.current = nodesHash;
763
+ indexRef.current = null;
764
+ }
765
+ return (0, import_react4.useMemo)(() => {
766
+ if (nodes.length === 0) return [];
767
+ if (!indexRef.current && nodes.length >= SPATIAL_INDEX_THRESHOLD && !skipCulling && !isDragging) {
768
+ indexRef.current = new import_core6.RBushSpatialIndex();
769
+ indexRef.current.bulkLoad(nodes.map((n) => ({
770
+ nodeId: n.id,
771
+ bounds: { minX: n.x, minY: n.y, maxX: n.x + n.width, maxY: n.y + n.height }
772
+ })));
773
+ }
774
+ return (0, import_core6.getViewportVisibleNodes)({
775
+ nodes,
776
+ viewport: { centerX, centerY, zoomLevel, rotation: 0, visibleBounds: { minX: 0, minY: 0, maxX: 0, maxY: 0 } },
777
+ canvasWidth,
778
+ canvasHeight,
779
+ levelFilter,
780
+ skipCulling,
781
+ isDragging,
782
+ spatialIndex: indexRef.current,
783
+ spatialIndexThreshold: SPATIAL_INDEX_THRESHOLD
784
+ });
785
+ }, [nodes, levelFilter, skipCulling, isDragging, centerX, centerY, zoomLevel, canvasWidth, canvasHeight]);
786
+ }
787
+
788
+ // src/components/KonvaNode.tsx
789
+ var import_react5 = __toESM(require("react"), 1);
790
+ var import_core8 = require("@visualli/core");
791
+
792
+ // src/config/index.ts
793
+ var import_core7 = require("@visualli/core");
794
+
795
+ // src/config/blobShapes.ts
796
+ var ALL_BLOB_SHAPES = [
797
+ // Shape 0 · Blob 1 (root / L0)
798
+ [
799
+ { x: 1.12, y: 0 },
800
+ { x: 0.86, y: 0.56 },
801
+ { x: 0.46, y: 0.94 },
802
+ { x: -0.08, y: 1.06 },
803
+ { x: -0.68, y: 0.82 },
804
+ { x: -0.96, y: 0.08 },
805
+ { x: -0.55, y: -0.82 },
806
+ { x: 0.04, y: -1.02 },
807
+ { x: 0.72, y: -0.8 },
808
+ { x: 1.08, y: -0.38 }
809
+ ],
810
+ // Shape 1 · Blob 2
811
+ [
812
+ { x: -0.489, y: -0.625 },
813
+ { x: -0.384, y: -0.716 },
814
+ { x: -0.285, y: -0.792 },
815
+ { x: -0.189, y: -0.855 },
816
+ { x: -0.09, y: -0.903 },
817
+ { x: 0.014, y: -0.932 },
818
+ { x: 0.129, y: -0.94 },
819
+ { x: 0.255, y: -0.924 },
820
+ { x: 0.412, y: -0.876 },
821
+ { x: 0.516, y: -0.832 },
822
+ { x: 0.612, y: -0.78 },
823
+ { x: 0.695, y: -0.725 },
824
+ { x: 0.767, y: -0.666 },
825
+ { x: 0.83, y: -0.606 },
826
+ { x: 0.885, y: -0.546 },
827
+ { x: 0.933, y: -0.486 },
828
+ { x: 0.979, y: -0.425 },
829
+ { x: 1.006, y: -0.385 },
830
+ { x: 1.034, y: -0.334 },
831
+ { x: 1.058, y: -0.276 },
832
+ { x: 1.078, y: -0.209 },
833
+ { x: 1.092, y: -0.133 },
834
+ { x: 1.101, y: -0.046 },
835
+ { x: 1.102, y: 0.052 },
836
+ { x: 1.094, y: 0.173 },
837
+ { x: 1.083, y: 0.244 },
838
+ { x: 1.063, y: 0.324 },
839
+ { x: 1.034, y: 0.401 },
840
+ { x: 0.996, y: 0.477 },
841
+ { x: 0.948, y: 0.549 },
842
+ { x: 0.889, y: 0.617 },
843
+ { x: 0.82, y: 0.681 },
844
+ { x: 0.735, y: 0.741 },
845
+ { x: 0.658, y: 0.783 },
846
+ { x: 0.569, y: 0.822 },
847
+ { x: 0.474, y: 0.855 },
848
+ { x: 0.372, y: 0.882 },
849
+ { x: 0.265, y: 0.904 },
850
+ { x: 0.156, y: 0.92 },
851
+ { x: 0.046, y: 0.931 },
852
+ { x: -0.069, y: 0.938 },
853
+ { x: -0.155, y: 0.935 },
854
+ { x: -0.25, y: 0.919 },
855
+ { x: -0.346, y: 0.89 },
856
+ { x: -0.443, y: 0.851 },
857
+ { x: -0.539, y: 0.8 },
858
+ { x: -0.633, y: 0.738 },
859
+ { x: -0.723, y: 0.667 },
860
+ { x: -0.808, y: 0.585 },
861
+ { x: -0.882, y: 0.501 },
862
+ { x: -0.944, y: 0.421 },
863
+ { x: -0.994, y: 0.343 },
864
+ { x: -1.034, y: 0.267 },
865
+ { x: -1.064, y: 0.192 },
866
+ { x: -1.085, y: 0.12 },
867
+ { x: -1.097, y: 0.042 },
868
+ { x: -1.102, y: 1e-3 },
869
+ { x: -1.09, y: -0.243 },
870
+ { x: -1.05, y: -0.365 },
871
+ { x: -0.99, y: -0.441 },
872
+ { x: -0.911, y: -0.479 },
873
+ { x: -0.819, y: -0.499 },
874
+ { x: -0.715, y: -0.517 },
875
+ { x: -0.605, y: -0.551 }
876
+ ],
877
+ // Shape 2 · Blob 3
878
+ [
879
+ { x: 3e-3, y: -0.97 },
880
+ { x: 0.163, y: -0.984 },
881
+ { x: 0.286, y: -1.028 },
882
+ { x: 0.389, y: -1.096 },
883
+ { x: 0.488, y: -1.174 },
884
+ { x: 0.597, y: -1.21 },
885
+ { x: 0.728, y: -1.165 },
886
+ { x: 0.867, y: -1.036 },
887
+ { x: 0.996, y: -0.839 },
888
+ { x: 1.098, y: -0.59 },
889
+ { x: 1.158, y: -0.304 },
890
+ { x: 1.153, y: 0.012 },
891
+ { x: 1.08, y: 0.336 },
892
+ { x: 0.944, y: 0.639 },
893
+ { x: 0.756, y: 0.889 },
894
+ { x: 0.484, y: 1.072 },
895
+ { x: 0.339, y: 1.079 },
896
+ { x: 0.219, y: 1.051 },
897
+ { x: 0.131, y: 0.994 },
898
+ { x: 0.059, y: 0.939 },
899
+ { x: 8e-3, y: 0.915 },
900
+ { x: -0.105, y: 0.931 },
901
+ { x: -0.176, y: 0.965 },
902
+ { x: -0.249, y: 1.005 },
903
+ { x: -0.345, y: 1.039 },
904
+ { x: -0.497, y: 1.055 },
905
+ { x: -0.672, y: 1.032 },
906
+ { x: -0.817, y: 0.952 },
907
+ { x: -0.945, y: 0.81 },
908
+ { x: -1.054, y: 0.6 },
909
+ { x: -1.16, y: 0.302 },
910
+ { x: -1.187, y: -0.011 },
911
+ { x: -1.165, y: -0.326 },
912
+ { x: -1.097, y: -0.599 },
913
+ { x: -0.991, y: -0.838 },
914
+ { x: -0.852, y: -0.912 },
915
+ { x: -0.695, y: -0.958 },
916
+ { x: -0.53, y: -0.988 },
917
+ { x: -0.357, y: -0.992 },
918
+ { x: -0.18, y: -0.983 }
919
+ ],
920
+ // Shape 3 · Blob 4
921
+ [
922
+ { x: -0.734, y: -1.127 },
923
+ { x: -0.656, y: -1.146 },
924
+ { x: -0.567, y: -1.142 },
925
+ { x: -0.468, y: -1.115 },
926
+ { x: -0.358, y: -1.079 },
927
+ { x: -0.238, y: -1.034 },
928
+ { x: -0.107, y: -0.989 },
929
+ { x: 0.035, y: -0.953 },
930
+ { x: 0.188, y: -0.929 },
931
+ { x: 0.325, y: -0.916 },
932
+ { x: 0.484, y: -0.896 },
933
+ { x: 0.65, y: -0.854 },
934
+ { x: 0.811, y: -0.77 },
935
+ { x: 0.955, y: -0.631 },
936
+ { x: 1.068, y: -0.422 },
937
+ { x: 1.138, y: -0.126 },
938
+ { x: 1.152, y: 0.272 },
939
+ { x: 1.121, y: 0.451 },
940
+ { x: 1.046, y: 0.602 },
941
+ { x: 0.928, y: 0.727 },
942
+ { x: 0.767, y: 0.833 },
943
+ { x: 0.565, y: 0.925 },
944
+ { x: 0.323, y: 1.001 },
945
+ { x: 0.042, y: 1.075 },
946
+ { x: -0.277, y: 1.142 },
947
+ { x: -0.368, y: 1.147 },
948
+ { x: -0.475, y: 1.136 },
949
+ { x: -0.589, y: 1.104 },
950
+ { x: -0.704, y: 1.05 },
951
+ { x: -0.815, y: 0.973 },
952
+ { x: -0.913, y: 0.871 },
953
+ { x: -0.993, y: 0.746 },
954
+ { x: -1.048, y: 0.593 },
955
+ { x: -1.109, y: 0.298 },
956
+ { x: -1.145, y: 0.018 },
957
+ { x: -1.154, y: -0.243 },
958
+ { x: -1.135, y: -0.482 },
959
+ { x: -1.085, y: -0.695 },
960
+ { x: -1.003, y: -0.874 },
961
+ { x: -0.886, y: -1.02 }
962
+ ],
963
+ // Shape 4 · Blob 5 (inverted Blob 4)
964
+ [
965
+ { x: -0.734, y: 1.127 },
966
+ { x: -0.656, y: 1.146 },
967
+ { x: -0.567, y: 1.142 },
968
+ { x: -0.468, y: 1.115 },
969
+ { x: -0.358, y: 1.079 },
970
+ { x: -0.238, y: 1.034 },
971
+ { x: -0.107, y: 0.989 },
972
+ { x: 0.035, y: 0.953 },
973
+ { x: 0.188, y: 0.929 },
974
+ { x: 0.325, y: 0.916 },
975
+ { x: 0.484, y: 0.896 },
976
+ { x: 0.65, y: 0.854 },
977
+ { x: 0.811, y: 0.77 },
978
+ { x: 0.955, y: 0.631 },
979
+ { x: 1.068, y: 0.422 },
980
+ { x: 1.138, y: 0.126 },
981
+ { x: 1.152, y: -0.272 },
982
+ { x: 1.121, y: -0.451 },
983
+ { x: 1.046, y: -0.602 },
984
+ { x: 0.928, y: -0.727 },
985
+ { x: 0.767, y: -0.833 },
986
+ { x: 0.565, y: -0.925 },
987
+ { x: 0.323, y: -1.001 },
988
+ { x: 0.042, y: -1.075 },
989
+ { x: -0.277, y: -1.142 },
990
+ { x: -0.368, y: -1.147 },
991
+ { x: -0.475, y: -1.136 },
992
+ { x: -0.589, y: -1.104 },
993
+ { x: -0.704, y: -1.05 },
994
+ { x: -0.815, y: -0.973 },
995
+ { x: -0.913, y: -0.871 },
996
+ { x: -0.993, y: -0.746 },
997
+ { x: -1.048, y: -0.593 },
998
+ { x: -1.109, y: -0.298 },
999
+ { x: -1.145, y: -0.018 },
1000
+ { x: -1.154, y: 0.243 },
1001
+ { x: -1.135, y: 0.482 },
1002
+ { x: -1.085, y: 0.695 },
1003
+ { x: -1.003, y: 0.874 },
1004
+ { x: -0.886, y: 1.02 }
1005
+ ],
1006
+ // Shape 5 · Blob 6
1007
+ [
1008
+ { x: 1e-3, y: -1.006 },
1009
+ { x: 0.134, y: -1.001 },
1010
+ { x: 0.262, y: -1.024 },
1011
+ { x: 0.382, y: -1.073 },
1012
+ { x: 0.498, y: -1.107 },
1013
+ { x: 0.614, y: -1.105 },
1014
+ { x: 0.733, y: -1.049 },
1015
+ { x: 0.85, y: -0.932 },
1016
+ { x: 0.953, y: -0.755 },
1017
+ { x: 1.03, y: -0.529 },
1018
+ { x: 1.067, y: -0.265 },
1019
+ { x: 1.054, y: 0.018 },
1020
+ { x: 0.986, y: 0.3 },
1021
+ { x: 0.868, y: 0.557 },
1022
+ { x: 0.715, y: 0.766 },
1023
+ { x: 0.55, y: 0.909 },
1024
+ { x: 0.396, y: 0.984 },
1025
+ { x: 0.262, y: 1.005 },
1026
+ { x: 0.148, y: 1.003 },
1027
+ { x: 0.037, y: 1.008 },
1028
+ { x: -0.083, y: 1.04 },
1029
+ { x: -0.22, y: 1.118 },
1030
+ { x: -0.36, y: 1.192 },
1031
+ { x: -0.5, y: 1.234 },
1032
+ { x: -0.65, y: 1.175 },
1033
+ { x: -0.81, y: 1.045 },
1034
+ { x: -0.99, y: 0.855 },
1035
+ { x: -1.14, y: 0.61 },
1036
+ { x: -1.24, y: 0.33 },
1037
+ { x: -1.27, y: 0.04 },
1038
+ { x: -1.275, y: -0.25 },
1039
+ { x: -1.225, y: -0.5 },
1040
+ { x: -1.08, y: -0.72 },
1041
+ { x: -0.89, y: -0.92 },
1042
+ { x: -0.734, y: -1.053 },
1043
+ { x: -0.614, y: -1.112 },
1044
+ { x: -0.498, y: -1.127 },
1045
+ { x: -0.381, y: -1.11 },
1046
+ { x: -0.261, y: -1.095 },
1047
+ { x: -0.133, y: -1.058 },
1048
+ { x: 1e-3, y: -1.01 },
1049
+ { x: 0.134, y: -1.001 }
1050
+ ]
1051
+ ];
1052
+ function buildBlobPathData(_shape, cx, cy, width, height) {
1053
+ const rx = width / 2;
1054
+ const ry = height / 2;
1055
+ const blobType = 0;
1056
+ const srcPts = ALL_BLOB_SHAPES[blobType];
1057
+ const pts = srcPts.map((p) => ({ x: cx + p.x * rx, y: cy + p.y * ry }));
1058
+ const mid = (a, b) => ({ x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 });
1059
+ const last = pts[pts.length - 1];
1060
+ const first = pts[0];
1061
+ const s = mid(last, first);
1062
+ let d = `M ${s.x} ${s.y}`;
1063
+ for (let i = 0; i < pts.length; i++) {
1064
+ const curr = pts[i];
1065
+ const next = pts[(i + 1) % pts.length];
1066
+ const m = mid(curr, next);
1067
+ d += ` Q ${curr.x} ${curr.y} ${m.x} ${m.y}`;
1068
+ }
1069
+ d += " Z";
1070
+ return d;
1071
+ }
1072
+
1073
+ // src/components/KonvaNode.tsx
1074
+ var import_jsx_runtime2 = require("react/jsx-runtime");
1075
+ var KonvaNode = import_react5.default.memo(function KonvaNode2({
1076
+ node,
1077
+ zoomLevel,
1078
+ isExternallyHovered = false,
1079
+ isExternallyPressed = false,
1080
+ isDragging = false
1081
+ }) {
1082
+ const borderColor = (0, import_react5.useMemo)(() => (0, import_core8.darkenHexColor)(node.color, 0.13), [node.color]);
1083
+ const layerCount = (0, import_react5.useMemo)(() => {
1084
+ const count = node.branchCount ?? 0;
1085
+ return Math.min(count, 3);
1086
+ }, [node.branchCount]);
1087
+ const baseRadiusX = node.width / 2;
1088
+ const baseRadiusY = Math.max(import_core8.NODE_HEIGHT * 1.6 / 2, baseRadiusX * 0.74);
1089
+ const calculateFontSize = (0, import_react5.useMemo)(() => {
1090
+ return 30;
1091
+ }, [node.title]);
1092
+ const textNaturalWidth = node.width - 40;
1093
+ const textNaturalHeight = import_core8.NODE_HEIGHT * 1.2;
1094
+ const maxScale = (0, import_core7.computeNodeTextWorldScale)(node.width, zoomLevel);
1095
+ const blobType = (0, import_core7.getBlobTypeForLayer)(node.level);
1096
+ const textOffsetX = (import_core7.BLOB_TEXT_OFFSETS[blobType] ?? 0) * baseRadiusX;
1097
+ const effectiveHover = isExternallyHovered;
1098
+ const isPressed = isExternallyPressed;
1099
+ const topFill = effectiveHover || isPressed ? borderColor : node.color;
1100
+ const topStroke = isPressed ? node.color : borderColor;
1101
+ const backgroundLayers = (0, import_react5.useMemo)(() => {
1102
+ if (layerCount === 0) return [];
1103
+ const startIdx = import_core7.BLOB_LAYER_CONFIG.length - layerCount;
1104
+ const layers = [];
1105
+ for (let i = startIdx; i < import_core7.BLOB_LAYER_CONFIG.length; i++) {
1106
+ const config = import_core7.BLOB_LAYER_CONFIG[i];
1107
+ layers.push({
1108
+ ...config,
1109
+ radiusX: baseRadiusX * config.scaleUp,
1110
+ radiusY: baseRadiusY * config.scaleUp
1111
+ });
1112
+ }
1113
+ return layers;
1114
+ }, [layerCount, baseRadiusX, baseRadiusY]);
1115
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(KGroup, { x: node.x, y: node.y, listening: false, children: [
1116
+ backgroundLayers.map((layer, idx) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1117
+ KShape,
1118
+ {
1119
+ rotation: layer.rotation,
1120
+ opacity: layer.opacity,
1121
+ sceneFunc: (ctx, shape) => {
1122
+ (0, import_core7.drawBlobPath)(ctx, layer.radiusX, layer.radiusY, blobType);
1123
+ ctx.fillStrokeShape(shape);
1124
+ },
1125
+ fill: node.color,
1126
+ stroke: borderColor,
1127
+ strokeWidth: layer.strokeWidth,
1128
+ dash: layer.dash ? [...layer.dash] : void 0,
1129
+ listening: false,
1130
+ perfectDrawEnabled: false
1131
+ },
1132
+ `blob-layer-${idx}`
1133
+ )),
1134
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1135
+ KShape,
1136
+ {
1137
+ rotation: import_core7.NODE_LAYER_CONFIG.rotation,
1138
+ opacity: import_core7.NODE_LAYER_CONFIG.opacity,
1139
+ sceneFunc: (ctx, shape) => {
1140
+ (0, import_core7.drawBlobPath)(ctx, baseRadiusX, baseRadiusY, blobType);
1141
+ ctx.fillStrokeShape(shape);
1142
+ },
1143
+ hitFunc: (ctx, shape) => {
1144
+ (0, import_core7.drawBlobPath)(ctx, baseRadiusX, baseRadiusY, blobType);
1145
+ ctx.fillStrokeShape(shape);
1146
+ },
1147
+ fill: topFill,
1148
+ stroke: topStroke,
1149
+ strokeWidth: import_core7.NODE_LAYER_CONFIG.strokeWidth,
1150
+ shadowEnabled: !isDragging,
1151
+ shadowColor: "rgba(255,255,255,1)",
1152
+ shadowBlur: effectiveHover || isPressed ? 40 : 15,
1153
+ shadowOpacity: effectiveHover || isPressed ? 0.4 : 0.15,
1154
+ shadowOffsetX: 0,
1155
+ shadowOffsetY: 0,
1156
+ perfectDrawEnabled: false
1157
+ }
1158
+ ),
1159
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1160
+ KGroup,
1161
+ {
1162
+ clipFunc: (ctx) => {
1163
+ (0, import_core7.drawBlobPath)(ctx, baseRadiusX, baseRadiusY, blobType);
1164
+ },
1165
+ visible: zoomLevel >= import_core8.TEXT_LABEL_HIDE_BELOW_ZOOM,
1166
+ listening: false,
1167
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1168
+ KText,
1169
+ {
1170
+ x: textOffsetX,
1171
+ y: 0,
1172
+ offsetX: textNaturalWidth / 2,
1173
+ offsetY: textNaturalHeight / 2,
1174
+ width: textNaturalWidth,
1175
+ height: textNaturalHeight,
1176
+ scaleX: maxScale,
1177
+ scaleY: maxScale,
1178
+ text: node.title,
1179
+ fontSize: calculateFontSize,
1180
+ fontFamily: "'Story Script', cursive",
1181
+ fontStyle: "normal",
1182
+ fill: import_core8.CANVAS_COLORS.node.titleColor,
1183
+ align: "center",
1184
+ verticalAlign: "middle",
1185
+ wrap: "word",
1186
+ ellipsis: false,
1187
+ listening: false,
1188
+ perfectDrawEnabled: false
1189
+ }
1190
+ )
1191
+ }
1192
+ )
1193
+ ] });
1194
+ });
1195
+ var KonvaNode_default = KonvaNode;
1196
+
1197
+ // src/components/KonvaNodeLayer.tsx
1198
+ var import_jsx_runtime3 = require("react/jsx-runtime");
1199
+ function KonvaNodeLayer({
1200
+ nodes: propNodes,
1201
+ isTransitioning = false,
1202
+ isDragging = false,
1203
+ hoveredNodeId = null,
1204
+ pressedNodeId = null
1205
+ }) {
1206
+ const layerRef = (0, import_react6.useRef)(null);
1207
+ const zoomLevel = useViewportStore((s) => s.zoomLevel);
1208
+ const storeNodes = useNodeStore((s) => s.nodes);
1209
+ const allNodes = (0, import_react6.useMemo)(
1210
+ () => propNodes ?? Array.from(storeNodes.values()),
1211
+ [propNodes, storeNodes]
1212
+ );
1213
+ const visibleNodes = useViewportNodes(allNodes, void 0, isTransitioning, isDragging);
1214
+ (0, import_react6.useLayoutEffect)(() => {
1215
+ layerRef.current?.batchDraw();
1216
+ }, [visibleNodes, hoveredNodeId, pressedNodeId, isDragging, zoomLevel]);
1217
+ (0, import_react6.useEffect)(() => {
1218
+ layerRef.current?.batchDraw();
1219
+ }, [isTransitioning]);
1220
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(KLayer, { ref: layerRef, name: "nodes", children: visibleNodes.map((n) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1221
+ KonvaNode_default,
1222
+ {
1223
+ node: n,
1224
+ zoomLevel,
1225
+ isExternallyHovered: hoveredNodeId === n.id,
1226
+ isExternallyPressed: pressedNodeId === n.id,
1227
+ isDragging
1228
+ },
1229
+ n.id
1230
+ )) });
1231
+ }
1232
+
1233
+ // src/components/KonvaEdgeLayer.tsx
1234
+ var import_react8 = require("react");
1235
+
1236
+ // src/components/KonvaEdge.tsx
1237
+ var import_react7 = require("react");
1238
+ var import_core9 = require("@visualli/core");
1239
+ var import_jsx_runtime4 = require("react/jsx-runtime");
1240
+ var getOutermostScaleUp = (branchCount) => {
1241
+ const layerCount = Math.min(branchCount ?? 0, 3);
1242
+ if (layerCount === 0) return 1;
1243
+ const scaleUps = [1.3, 1.2, 1.1];
1244
+ return scaleUps[scaleUps.length - layerCount];
1245
+ };
1246
+ function getEllipseIntersection(cx, cy, rx, ry, angle) {
1247
+ return {
1248
+ x: cx + rx * Math.cos(angle),
1249
+ y: cy + ry * Math.sin(angle)
1250
+ };
1251
+ }
1252
+ var EDGE_SPACING = 25;
1253
+ var STROKE_COLOR = "#000000";
1254
+ var STROKE_WIDTH = 2.5;
1255
+ var KonvaEdge = (0, import_react7.memo)(function KonvaEdge2({
1256
+ sourceNode: from,
1257
+ targetNode: to,
1258
+ connection,
1259
+ zoomLevel = 1
1260
+ }) {
1261
+ const label = connection?.label;
1262
+ const points = (0, import_react7.useMemo)(() => {
1263
+ const fromBaseX = from.width / 2;
1264
+ const fromBaseY = Math.max(import_core9.NODE_HEIGHT * 1.6 / 2, fromBaseX * 0.74);
1265
+ const fromScale = getOutermostScaleUp(from.branchCount);
1266
+ const fromRadiusX = fromBaseX * fromScale;
1267
+ const fromRadiusY = fromBaseY * fromScale;
1268
+ const toBaseX = to.width / 2;
1269
+ const toBaseY = Math.max(import_core9.NODE_HEIGHT * 1.6 / 2, toBaseX * 0.74);
1270
+ const toScale = getOutermostScaleUp(to.branchCount);
1271
+ const toRadiusX = toBaseX * toScale;
1272
+ const toRadiusY = toBaseY * toScale;
1273
+ const angle = Math.atan2(to.y - from.y, to.x - from.x);
1274
+ const fromEdge = getEllipseIntersection(from.x, from.y, fromRadiusX, fromRadiusY, angle);
1275
+ const toEdge = getEllipseIntersection(to.x, to.y, toRadiusX, toRadiusY, angle + Math.PI);
1276
+ const fromX = fromEdge.x + EDGE_SPACING * Math.cos(angle);
1277
+ const fromY = fromEdge.y + EDGE_SPACING * Math.sin(angle);
1278
+ const toX = toEdge.x - EDGE_SPACING * Math.cos(angle);
1279
+ const toY = toEdge.y - EDGE_SPACING * Math.sin(angle);
1280
+ const midX = (fromX + toX) / 2;
1281
+ const midY = (fromY + toY) / 2;
1282
+ const distance = Math.sqrt((toX - fromX) ** 2 + (toY - fromY) ** 2);
1283
+ const arcHeight = Math.min(45, 45 / (1 + Math.max(0, (distance - 200) / 400)));
1284
+ const perpX = -(toY - fromY) / distance;
1285
+ const perpY = (toX - fromX) / distance;
1286
+ const offsetMidX = midX + perpX * arcHeight;
1287
+ const offsetMidY = midY + perpY * arcHeight;
1288
+ const ctrl1X = fromX + (offsetMidX - fromX) * 0.66;
1289
+ const ctrl1Y = fromY + (offsetMidY - fromY) * 0.66;
1290
+ const ctrl2X = toX + (offsetMidX - toX) * 0.66;
1291
+ const ctrl2Y = toY + (offsetMidY - toY) * 0.66;
1292
+ const NUM_SEGMENTS = 20;
1293
+ const pts = [];
1294
+ for (let i = 0; i <= NUM_SEGMENTS; i++) {
1295
+ const t = i / NUM_SEGMENTS;
1296
+ const mt = 1 - t;
1297
+ const mt2 = mt * mt;
1298
+ const mt3 = mt2 * mt;
1299
+ const t2 = t * t;
1300
+ const t3 = t2 * t;
1301
+ pts.push(
1302
+ mt3 * fromX + 3 * mt2 * t * ctrl1X + 3 * mt * t2 * ctrl2X + t3 * toX,
1303
+ mt3 * fromY + 3 * mt2 * t * ctrl1Y + 3 * mt * t2 * ctrl2Y + t3 * toY
1304
+ );
1305
+ }
1306
+ return pts;
1307
+ }, [from.x, from.y, from.width, from.branchCount, to.x, to.y, to.width, to.branchCount]);
1308
+ const arrowHeadPoints = (0, import_react7.useMemo)(() => {
1309
+ const len = points.length;
1310
+ const tipX = points[len - 2];
1311
+ const tipY = points[len - 1];
1312
+ const prevX = points[len - 4];
1313
+ const prevY = points[len - 3];
1314
+ const angle = Math.atan2(tipY - prevY, tipX - prevX);
1315
+ const armLength = 11;
1316
+ const armAngle = Math.PI / 5.5;
1317
+ return [
1318
+ tipX - armLength * Math.cos(angle - armAngle),
1319
+ tipY - armLength * Math.sin(angle - armAngle),
1320
+ tipX,
1321
+ tipY,
1322
+ tipX - armLength * Math.cos(angle + armAngle),
1323
+ tipY - armLength * Math.sin(angle + armAngle)
1324
+ ];
1325
+ }, [points]);
1326
+ const { pathData, fontSize } = (0, import_react7.useMemo)(() => {
1327
+ if (!label) return { pathData: null, fontSize: 12 };
1328
+ const fromBaseX = from.width / 2;
1329
+ const fromBaseY = Math.max(import_core9.NODE_HEIGHT * 1.6 / 2, fromBaseX * 0.74);
1330
+ const fromScale = getOutermostScaleUp(from.branchCount);
1331
+ const fromRadiusX = fromBaseX * fromScale;
1332
+ const fromRadiusY = fromBaseY * fromScale;
1333
+ const toBaseX = to.width / 2;
1334
+ const toBaseY = Math.max(import_core9.NODE_HEIGHT * 1.6 / 2, toBaseX * 0.74);
1335
+ const toScale = getOutermostScaleUp(to.branchCount);
1336
+ const toRadiusX = toBaseX * toScale;
1337
+ const toRadiusY = toBaseY * toScale;
1338
+ const angle = Math.atan2(to.y - from.y, to.x - from.x);
1339
+ const fromEdge = getEllipseIntersection(from.x, from.y, fromRadiusX, fromRadiusY, angle);
1340
+ const toEdge = getEllipseIntersection(to.x, to.y, toRadiusX, toRadiusY, angle + Math.PI);
1341
+ const fromX = fromEdge.x + EDGE_SPACING * Math.cos(angle);
1342
+ const fromY = fromEdge.y + EDGE_SPACING * Math.sin(angle);
1343
+ const toX = toEdge.x - EDGE_SPACING * Math.cos(angle);
1344
+ const toY = toEdge.y - EDGE_SPACING * Math.sin(angle);
1345
+ const midX = (fromX + toX) / 2;
1346
+ const midY = (fromY + toY) / 2;
1347
+ const distance = Math.sqrt((toX - fromX) ** 2 + (toY - fromY) ** 2);
1348
+ const arcHeight = Math.min(45, 45 / (1 + Math.max(0, (distance - 200) / 400)));
1349
+ const perpX = -(toY - fromY) / distance;
1350
+ const perpY = (toX - fromX) / distance;
1351
+ const offsetMidX = midX + perpX * arcHeight;
1352
+ const offsetMidY = midY + perpY * arcHeight;
1353
+ const ctrl1X = fromX + (offsetMidX - fromX) * 0.66;
1354
+ const ctrl1Y = fromY + (offsetMidY - fromY) * 0.66;
1355
+ const ctrl2X = toX + (offsetMidX - toX) * 0.66;
1356
+ const ctrl2Y = toY + (offsetMidY - toY) * 0.66;
1357
+ const TEXT_OFFSET = 15;
1358
+ const fxo = fromX + perpX * TEXT_OFFSET;
1359
+ const fyo = fromY + perpY * TEXT_OFFSET;
1360
+ const txo = toX + perpX * TEXT_OFFSET;
1361
+ const tyo = toY + perpY * TEXT_OFFSET;
1362
+ const c1xo = ctrl1X + perpX * TEXT_OFFSET;
1363
+ const c1yo = ctrl1Y + perpY * TEXT_OFFSET;
1364
+ const c2xo = ctrl2X + perpX * TEXT_OFFSET;
1365
+ const c2yo = ctrl2Y + perpY * TEXT_OFFSET;
1366
+ const baseFontSize = 14;
1367
+ const zoomFactor = Math.max(1, 1 / zoomLevel);
1368
+ const scaledFontSize = Math.max(12, Math.min(24, baseFontSize * (from.width / 180) * zoomFactor));
1369
+ let d = "";
1370
+ if (fromX > toX) {
1371
+ d = `M ${txo},${tyo} C ${c2xo},${c2yo} ${c1xo},${c1yo} ${fxo},${fyo}`;
1372
+ } else {
1373
+ d = `M ${fxo},${fyo} C ${c1xo},${c1yo} ${c2xo},${c2yo} ${txo},${tyo}`;
1374
+ }
1375
+ return { pathData: d, fontSize: scaledFontSize };
1376
+ }, [from.x, from.y, from.width, from.branchCount, to.x, to.y, to.width, to.branchCount, label, zoomLevel]);
1377
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(KGroup, { children: [
1378
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1379
+ KLine,
1380
+ {
1381
+ points,
1382
+ stroke: STROKE_COLOR,
1383
+ strokeWidth: STROKE_WIDTH,
1384
+ tension: 0,
1385
+ opacity: 0.9,
1386
+ lineCap: "round",
1387
+ lineJoin: "round",
1388
+ listening: false,
1389
+ perfectDrawEnabled: false
1390
+ }
1391
+ ),
1392
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1393
+ KLine,
1394
+ {
1395
+ points: arrowHeadPoints,
1396
+ stroke: STROKE_COLOR,
1397
+ strokeWidth: STROKE_WIDTH,
1398
+ lineCap: "round",
1399
+ lineJoin: "round",
1400
+ opacity: 0.9,
1401
+ listening: false,
1402
+ perfectDrawEnabled: false
1403
+ }
1404
+ ),
1405
+ label && pathData && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
1406
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1407
+ KTextPath,
1408
+ {
1409
+ data: pathData,
1410
+ text: label,
1411
+ fontSize,
1412
+ fontFamily: "'Playpen Sans', cursive",
1413
+ fontStyle: "normal",
1414
+ fill: "#ffffff",
1415
+ align: "center",
1416
+ letterSpacing: 0.5,
1417
+ listening: false,
1418
+ opacity: 0.9
1419
+ }
1420
+ ),
1421
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1422
+ KTextPath,
1423
+ {
1424
+ data: pathData,
1425
+ text: label,
1426
+ fontSize,
1427
+ fontFamily: "'Playpen Sans', cursive",
1428
+ fontStyle: "normal",
1429
+ fill: "#6b6860",
1430
+ align: "center",
1431
+ letterSpacing: 0.5,
1432
+ listening: false,
1433
+ opacity: 0.9
1434
+ }
1435
+ )
1436
+ ] })
1437
+ ] });
1438
+ });
1439
+ var KonvaEdge_default = KonvaEdge;
1440
+
1441
+ // src/components/KonvaEdgeLayer.tsx
1442
+ var import_jsx_runtime5 = require("react/jsx-runtime");
1443
+ function KonvaEdgeLayer({
1444
+ nodes,
1445
+ connections,
1446
+ currentLevel,
1447
+ isDark = false,
1448
+ isDragging = false
1449
+ }) {
1450
+ const layerRef = (0, import_react8.useRef)(null);
1451
+ const zoomLevel = useViewportStore((s) => s.zoomLevel);
1452
+ const storeNodes = useNodeStore((s) => s.nodes);
1453
+ const visibleNodes = useViewportNodes(nodes, currentLevel, false, isDragging);
1454
+ const visibleSet = (0, import_react8.useMemo)(() => new Set(visibleNodes.map((n) => n.id)), [visibleNodes]);
1455
+ (0, import_react8.useLayoutEffect)(() => {
1456
+ layerRef.current?.batchDraw();
1457
+ }, [connections, isDark, isDragging, visibleSet, storeNodes]);
1458
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(KLayer, { ref: layerRef, listening: false, children: connections.map((conn, idx) => {
1459
+ const src = storeNodes.get(conn.from);
1460
+ const tgt = storeNodes.get(conn.to);
1461
+ if (!src || !tgt) return null;
1462
+ if (!isDragging && !visibleSet.has(src.id) && !visibleSet.has(tgt.id)) return null;
1463
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1464
+ KonvaEdge_default,
1465
+ {
1466
+ sourceNode: src,
1467
+ targetNode: tgt,
1468
+ connection: conn,
1469
+ isDark,
1470
+ zoomLevel
1471
+ },
1472
+ `${conn.from}-${conn.to}-${idx}`
1473
+ );
1474
+ }) });
1475
+ }
1476
+
1477
+ // src/components/KonvaContainerLayer.tsx
1478
+ var import_react10 = require("react");
1479
+
1480
+ // src/components/KonvaContainer.tsx
1481
+ var import_react9 = require("react");
1482
+ var import_core11 = require("@visualli/core");
1483
+
1484
+ // src/config/textScaling.ts
1485
+ var import_core10 = require("@visualli/core");
1486
+ var DESCRIPTION_TEXT_BASE_FONT_PX2 = 12;
1487
+ var CONTAINER_LABEL_BASE_FONT_PX2 = 24;
1488
+ var NODE_TEXT_SAFE_BLOB_FILL_RATIO = 0.88;
1489
+ function computeNodeTextWorldScale2(nodeWidth, zoomLevel) {
1490
+ const safeZoom = Math.max(zoomLevel, 1e-4);
1491
+ const baseWidth = 300;
1492
+ const baseRadiusX = baseWidth / 2;
1493
+ const baseRadiusY = Math.max(import_core10.NODE_HEIGHT * 1.6 / 2, baseRadiusX * 0.74);
1494
+ const textNaturalWidth = baseWidth - 40;
1495
+ const textNaturalHeight = import_core10.NODE_HEIGHT * 1.2;
1496
+ const maxScaleX = baseRadiusX * 2 * NODE_TEXT_SAFE_BLOB_FILL_RATIO / Math.max(textNaturalWidth, 1);
1497
+ const maxScaleY = baseRadiusY * 2 * NODE_TEXT_SAFE_BLOB_FILL_RATIO / Math.max(textNaturalHeight, 1);
1498
+ const fitScale = Math.max(0.01, Math.min(maxScaleX, maxScaleY));
1499
+ return Math.min(1 / safeZoom, fitScale);
1500
+ }
1501
+ function computeNodeTextScreenScale2(nodeWidth, zoomLevel) {
1502
+ return computeNodeTextWorldScale2(nodeWidth, zoomLevel) * zoomLevel;
1503
+ }
1504
+
1505
+ // src/components/KonvaContainer.tsx
1506
+ var import_jsx_runtime6 = require("react/jsx-runtime");
1507
+ var CONTAINER_PADDING = 50;
1508
+ var CONTAINER_DASH = [10, 5];
1509
+ var LABEL_BG = "#ddd9d0";
1510
+ var LABEL_BORDER_RADIUS = 20;
1511
+ var LABEL_FONT_SIZE = CONTAINER_LABEL_BASE_FONT_PX2;
1512
+ var LABEL_FONT_WEIGHT = "800";
1513
+ var LABEL_FONT_FAMILY = "'Playpen Sans', Quicksand, sans-serif";
1514
+ var LABEL_PAD_H = 20;
1515
+ var LABEL_PAD_V = 10;
1516
+ var LABEL_LINE_HEIGHT = 1.2;
1517
+ var MAX_BLOB_EXTENT_X = 1.275;
1518
+ var MAX_BLOB_EXTENT_Y = 1.234;
1519
+ var _labelCtx = null;
1520
+ function measureLabelBoxWidth(text, fontSize) {
1521
+ if (!_labelCtx) {
1522
+ const c = document.createElement("canvas");
1523
+ _labelCtx = c.getContext("2d");
1524
+ }
1525
+ if (!_labelCtx) return Math.ceil(text.length * fontSize * 0.65 + 2 * LABEL_PAD_H);
1526
+ _labelCtx.font = `${LABEL_FONT_WEIGHT} ${fontSize}px ${LABEL_FONT_FAMILY}`;
1527
+ return Math.ceil(_labelCtx.measureText(text).width + 2 * LABEL_PAD_H);
1528
+ }
1529
+ function isLeftTurn(p1, p2, p3) {
1530
+ return (p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x) > 0;
1531
+ }
1532
+ function convexHull(points) {
1533
+ if (points.length < 3) return points;
1534
+ let start = 0;
1535
+ for (let i = 1; i < points.length; i++) {
1536
+ if (points[i].y < points[start].y || points[i].y === points[start].y && points[i].x < points[start].x) start = i;
1537
+ }
1538
+ const sorted = [...points];
1539
+ const sp = sorted.splice(start, 1)[0];
1540
+ sorted.sort((a, b) => {
1541
+ const da = Math.atan2(a.y - sp.y, a.x - sp.x);
1542
+ const db = Math.atan2(b.y - sp.y, b.x - sp.x);
1543
+ if (da !== db) return da - db;
1544
+ return Math.hypot(a.x - sp.x, a.y - sp.y) - Math.hypot(b.x - sp.x, b.y - sp.y);
1545
+ });
1546
+ const hull = [sp, sorted[0]];
1547
+ for (let i = 1; i < sorted.length; i++) {
1548
+ while (hull.length > 1 && !isLeftTurn(hull[hull.length - 2], hull[hull.length - 1], sorted[i])) hull.pop();
1549
+ hull.push(sorted[i]);
1550
+ }
1551
+ return hull;
1552
+ }
1553
+ function chaikinSmooth(pts, passes = 6) {
1554
+ let cur = pts;
1555
+ for (let iter = 0; iter < passes; iter++) {
1556
+ const next = [];
1557
+ const n = cur.length;
1558
+ for (let i = 0; i < n; i++) {
1559
+ const a = cur[i], b = cur[(i + 1) % n];
1560
+ next.push({ x: 0.75 * a.x + 0.25 * b.x, y: 0.75 * a.y + 0.25 * b.y });
1561
+ next.push({ x: 0.25 * a.x + 0.75 * b.x, y: 0.25 * a.y + 0.75 * b.y });
1562
+ }
1563
+ cur = next;
1564
+ }
1565
+ return cur.flatMap((p) => [p.x, p.y]);
1566
+ }
1567
+ function hullEdgeYAtX(points, targetX, isBottom) {
1568
+ const n = points.length;
1569
+ let result = isBottom ? -Infinity : Infinity;
1570
+ for (let i = 0; i < n; i += 2) {
1571
+ const x1 = points[i], y1 = points[i + 1];
1572
+ const x2 = points[(i + 2) % n], y2 = points[(i + 3) % n];
1573
+ if (x1 < targetX && x2 >= targetX || x2 < targetX && x1 >= targetX) {
1574
+ const t = (targetX - x1) / (x2 - x1);
1575
+ const y = y1 + t * (y2 - y1);
1576
+ if (isBottom) result = Math.max(result, y);
1577
+ else result = Math.min(result, y);
1578
+ }
1579
+ }
1580
+ if (!isFinite(result)) {
1581
+ for (let i = 1; i < n; i += 2) {
1582
+ if (isBottom) result = Math.max(result, points[i]);
1583
+ else result = Math.min(result, points[i]);
1584
+ }
1585
+ }
1586
+ return result;
1587
+ }
1588
+ function calculateHull(containerNodes) {
1589
+ if (containerNodes.length === 0) return null;
1590
+ const NUM_SAMPLES = 48;
1591
+ const samples = [];
1592
+ for (const node of containerNodes) {
1593
+ const baseRX = node.width / 2;
1594
+ const baseRY = Math.max(import_core11.NODE_HEIGHT * 1.6 / 2, baseRX * 0.74);
1595
+ const halfW = baseRX * MAX_BLOB_EXTENT_X + CONTAINER_PADDING;
1596
+ const halfH = baseRY * MAX_BLOB_EXTENT_Y + CONTAINER_PADDING;
1597
+ for (let j = 0; j < NUM_SAMPLES; j++) {
1598
+ const angle = j / NUM_SAMPLES * Math.PI * 2;
1599
+ samples.push({ x: node.x + halfW * Math.cos(angle), y: node.y + halfH * Math.sin(angle) });
1600
+ }
1601
+ }
1602
+ const hull = convexHull(samples);
1603
+ const points = chaikinSmooth(hull, 6);
1604
+ const xs = containerNodes.map((n) => n.x);
1605
+ const ys = containerNodes.map((n) => n.y);
1606
+ const spanX = Math.max(...xs) - Math.min(...xs);
1607
+ const spanY = Math.max(...ys) - Math.min(...ys);
1608
+ const useTopEdge = spanY > spanX * 2;
1609
+ let hullMinX = Infinity, hullMaxX = -Infinity;
1610
+ for (let i = 0; i < points.length; i += 2) {
1611
+ if (points[i] < hullMinX) hullMinX = points[i];
1612
+ if (points[i] > hullMaxX) hullMaxX = points[i];
1613
+ }
1614
+ const anchorX = (hullMinX + hullMaxX) / 2;
1615
+ const anchorEdgeY = hullEdgeYAtX(points, anchorX, !useTopEdge);
1616
+ return { points, anchorX, anchorEdgeY };
1617
+ }
1618
+ var KonvaContainer = (0, import_react9.memo)(function KonvaContainer2({
1619
+ nodes,
1620
+ label = ""
1621
+ }) {
1622
+ const zoomLevel = useViewportStore((s) => s.zoomLevel);
1623
+ const hullData = (0, import_react9.useMemo)(() => calculateHull(nodes), [nodes]);
1624
+ if (!hullData) return null;
1625
+ const refNode = nodes[0];
1626
+ const screenScale = refNode ? computeNodeTextScreenScale2(refNode.width, zoomLevel) : 1;
1627
+ const labelFontSize = LABEL_FONT_SIZE * screenScale;
1628
+ const labelBoxHeight = Math.ceil(labelFontSize * LABEL_LINE_HEIGHT + 2 * LABEL_PAD_V);
1629
+ const boxWidth = label ? measureLabelBoxWidth(label, labelFontSize) : 0;
1630
+ const boxX = hullData.anchorX - boxWidth / 2;
1631
+ const boxY = hullData.anchorEdgeY - labelBoxHeight / 2;
1632
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(KGroup, { listening: false, children: [
1633
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1634
+ KLine,
1635
+ {
1636
+ points: hullData.points,
1637
+ stroke: "#747270",
1638
+ strokeWidth: 1,
1639
+ dash: CONTAINER_DASH,
1640
+ closed: true,
1641
+ tension: 0,
1642
+ listening: false,
1643
+ perfectDrawEnabled: false
1644
+ }
1645
+ ),
1646
+ label.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1647
+ KGroup,
1648
+ {
1649
+ x: boxX + boxWidth / 2,
1650
+ y: boxY + labelBoxHeight / 2,
1651
+ offsetX: boxWidth / 2,
1652
+ offsetY: labelBoxHeight / 2,
1653
+ scaleX: 1 / zoomLevel,
1654
+ scaleY: 1 / zoomLevel,
1655
+ visible: zoomLevel >= import_core11.TEXT_LABEL_HIDE_BELOW_ZOOM,
1656
+ listening: false,
1657
+ children: [
1658
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1659
+ KRect,
1660
+ {
1661
+ width: boxWidth,
1662
+ height: labelBoxHeight,
1663
+ fill: LABEL_BG,
1664
+ cornerRadius: LABEL_BORDER_RADIUS,
1665
+ shadowEnabled: false,
1666
+ perfectDrawEnabled: false
1667
+ }
1668
+ ),
1669
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1670
+ KText,
1671
+ {
1672
+ x: LABEL_PAD_H,
1673
+ y: LABEL_PAD_V,
1674
+ width: boxWidth - 2 * LABEL_PAD_H,
1675
+ height: labelBoxHeight - 2 * LABEL_PAD_V,
1676
+ text: label,
1677
+ fontSize: labelFontSize,
1678
+ fontFamily: LABEL_FONT_FAMILY,
1679
+ fontStyle: LABEL_FONT_WEIGHT,
1680
+ fill: "rgba(0,0,0,1)",
1681
+ align: "center",
1682
+ verticalAlign: "middle",
1683
+ lineHeight: LABEL_LINE_HEIGHT,
1684
+ perfectDrawEnabled: false,
1685
+ listening: false
1686
+ }
1687
+ )
1688
+ ]
1689
+ }
1690
+ )
1691
+ ] });
1692
+ });
1693
+ var KonvaContainer_default = KonvaContainer;
1694
+
1695
+ // src/components/KonvaContainerLayer.tsx
1696
+ var import_jsx_runtime7 = require("react/jsx-runtime");
1697
+ function KonvaContainerLayer({ nodes, containers, isDark = false }) {
1698
+ const layerRef = (0, import_react10.useRef)(null);
1699
+ const storeNodes = useNodeStore((s) => s.nodes);
1700
+ (0, import_react10.useLayoutEffect)(() => {
1701
+ layerRef.current?.batchDraw();
1702
+ }, [containers, isDark, storeNodes]);
1703
+ if (containers.length === 0) return null;
1704
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(KLayer, { ref: layerRef, listening: false, children: containers.map((c) => {
1705
+ const groupNodes = c.nodeIds.map((id) => storeNodes.get(id)).filter((n) => !!n);
1706
+ if (groupNodes.length === 0) return null;
1707
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1708
+ KonvaContainer_default,
1709
+ {
1710
+ nodes: groupNodes,
1711
+ label: c.label,
1712
+ isDark
1713
+ },
1714
+ c.id
1715
+ );
1716
+ }) });
1717
+ }
1718
+
1719
+ // src/components/NavigationStack.tsx
1720
+ var import_react11 = require("react");
1721
+ var import_jsx_runtime8 = require("react/jsx-runtime");
1722
+ var PASTEL_FILLS = [
1723
+ "#b6eea7",
1724
+ "#d5bcfe",
1725
+ "#a1c4fc",
1726
+ "#a6f5d8",
1727
+ "#ffc9de",
1728
+ "#c4e8ff",
1729
+ "#f0c4ff",
1730
+ "#ffe0a3"
1731
+ ];
1732
+ var ORGANIC_PATHS_SMALL = [
1733
+ "M13.72,6.31 C15.12,6.29 17.96,8.57 17.99,10.60 C18.07,14.62 16.72,17.90 11.12,18.00 C7.26,18.07 4.07,14.86 4.00,10.85 C3.93,6.83 9.44,2.02 13.69,4.85",
1734
+ "M13.88,6.41 C15.28,6.43 18.04,8.82 18.00,10.85 C17.93,14.86 16.48,18.10 10.88,18.00 C7.02,17.93 3.94,14.62 4.01,10.60 C4.08,6.59 9.76,1.97 13.91,4.95"
1735
+ ];
1736
+ var ORGANIC_PATH_ACTIVE = "M16.55,9.22 C17.95,9.15 20.87,11.33 20.97,13.36 C21.19,17.37 19.96,20.70 14.37,20.99 C10.51,21.19 7.20,18.10 7.00,14.09 C6.78,10.08 12.13,5.08 16.48,7.77";
1737
+ var CIRCLE_SIZE = 14;
1738
+ function hexToRgba(hex, opacity) {
1739
+ const h = hex.replace("#", "");
1740
+ const r = parseInt(h.substring(0, 2), 16);
1741
+ const g = parseInt(h.substring(2, 4), 16);
1742
+ const b = parseInt(h.substring(4, 6), 16);
1743
+ return `rgba(${r},${g},${b},${opacity})`;
1744
+ }
1745
+ var OrganicCircle = (0, import_react11.memo)(function OrganicCircle2({
1746
+ size,
1747
+ fill,
1748
+ stroke,
1749
+ strokeWidth,
1750
+ variant,
1751
+ isActive
1752
+ }) {
1753
+ const path = isActive ? ORGANIC_PATH_ACTIVE : ORGANIC_PATHS_SMALL[variant % ORGANIC_PATHS_SMALL.length];
1754
+ const vb = isActive ? "4 3 20 20" : "2 1 18 18";
1755
+ const vbSz = isActive ? 20 : 18;
1756
+ const sw = strokeWidth * vbSz / size;
1757
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("svg", { width: size, height: size, viewBox: vb, fill: "none", style: { display: "block" }, children: [
1758
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: path, fill, fillOpacity: 1 }),
1759
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: path, fill: "none", stroke, strokeWidth: sw, strokeLinecap: "round" })
1760
+ ] });
1761
+ });
1762
+ var NavigationStack = (0, import_react11.memo)(function NavigationStack2({
1763
+ stack,
1764
+ onNavigateBack,
1765
+ isDark = false,
1766
+ top = "16px",
1767
+ left = "16px"
1768
+ }) {
1769
+ const [hoveredIndex, setHoveredIndex] = (0, import_react11.useState)(null);
1770
+ if (stack.length === 0) return null;
1771
+ const visitedStroke = isDark ? "#908e8c" : "#747270";
1772
+ const visitedText = isDark ? "#908e8c" : "#747270";
1773
+ const activeStroke = isDark ? "#e8e6e4" : "#1a1a18";
1774
+ const activeText = isDark ? "#e8e6e4" : "#1a1a18";
1775
+ const connectorColor = isDark ? "#908e8c" : "#747270";
1776
+ const connectorMarginLeft = 10.5;
1777
+ const currentIndex = stack.length - 1;
1778
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1779
+ "div",
1780
+ {
1781
+ "data-help": "navigation-stack",
1782
+ style: {
1783
+ position: "absolute",
1784
+ top,
1785
+ left,
1786
+ zIndex: 30,
1787
+ pointerEvents: "auto"
1788
+ },
1789
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { style: { display: "flex", flexDirection: "column", padding: "8px" }, children: stack.flatMap((entry, index) => {
1790
+ const isCurrent = index === currentIndex;
1791
+ const isClickable = index < currentIndex;
1792
+ const isHovered = hoveredIndex === index;
1793
+ const pastelFill = PASTEL_FILLS[index % PASTEL_FILLS.length];
1794
+ const rowBg = isCurrent ? hexToRgba(pastelFill, 0.6) : isHovered && isClickable ? hexToRgba(pastelFill, 0.3) : "transparent";
1795
+ const items = [];
1796
+ if (index > 0) {
1797
+ items.push(
1798
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1799
+ "div",
1800
+ {
1801
+ style: {
1802
+ width: "1px",
1803
+ height: "20px",
1804
+ marginLeft: `${connectorMarginLeft}px`,
1805
+ marginTop: "-6px",
1806
+ marginBottom: "-6px",
1807
+ background: connectorColor,
1808
+ alignSelf: "flex-start",
1809
+ flexShrink: 0,
1810
+ position: "relative",
1811
+ zIndex: 1
1812
+ }
1813
+ },
1814
+ `conn-${index}`
1815
+ )
1816
+ );
1817
+ }
1818
+ items.push(
1819
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1820
+ "div",
1821
+ {
1822
+ style: {
1823
+ position: "relative",
1824
+ zIndex: 2,
1825
+ alignSelf: "flex-start",
1826
+ display: "flex",
1827
+ alignItems: "center",
1828
+ gap: "4px",
1829
+ height: "22px",
1830
+ padding: "4px",
1831
+ borderRadius: "8px",
1832
+ cursor: isClickable ? "pointer" : "default",
1833
+ background: rowBg,
1834
+ transition: "background 0.15s"
1835
+ },
1836
+ onMouseEnter: () => setHoveredIndex(index),
1837
+ onMouseLeave: () => setHoveredIndex(null),
1838
+ onClick: (e) => {
1839
+ e.stopPropagation();
1840
+ if (isClickable) onNavigateBack(index);
1841
+ },
1842
+ children: [
1843
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { style: { width: CIRCLE_SIZE, height: CIRCLE_SIZE, display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0, zIndex: 4 }, children: isCurrent ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(OrganicCircle, { size: CIRCLE_SIZE, fill: pastelFill, stroke: activeStroke, strokeWidth: 1.5, variant: index, isActive: true }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(OrganicCircle, { size: CIRCLE_SIZE, fill: pastelFill, stroke: visitedStroke, strokeWidth: 1, variant: index }) }),
1844
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1845
+ "span",
1846
+ {
1847
+ style: {
1848
+ fontFamily: "'Nunito', sans-serif",
1849
+ fontSize: isCurrent || isHovered && isClickable ? "11px" : "9px",
1850
+ fontWeight: isCurrent ? 600 : 400,
1851
+ color: isCurrent ? activeText : visitedText,
1852
+ lineHeight: 1,
1853
+ whiteSpace: "nowrap",
1854
+ transition: "color 0.15s, font-size 0.15s"
1855
+ },
1856
+ children: entry.label
1857
+ }
1858
+ )
1859
+ ]
1860
+ },
1861
+ `${entry.layerId}-${index}`
1862
+ )
1863
+ );
1864
+ return items;
1865
+ }) })
1866
+ }
1867
+ );
1868
+ });
1869
+ var NavigationStack_default = NavigationStack;
1870
+
1871
+ // src/components/ZoomControls.tsx
1872
+ var import_react12 = require("react");
1873
+ var import_core12 = require("@visualli/core");
1874
+ var import_jsx_runtime9 = require("react/jsx-runtime");
1875
+ var ZoomInIcon = () => /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1876
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("circle", { cx: "11", cy: "11", r: "8" }),
1877
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("line", { x1: "21", y1: "21", x2: "16.65", y2: "16.65" }),
1878
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("line", { x1: "11", y1: "8", x2: "11", y2: "14" }),
1879
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("line", { x1: "8", y1: "11", x2: "14", y2: "11" })
1880
+ ] });
1881
+ var ZoomOutIcon = () => /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1882
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("circle", { cx: "11", cy: "11", r: "8" }),
1883
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("line", { x1: "21", y1: "21", x2: "16.65", y2: "16.65" }),
1884
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("line", { x1: "8", y1: "11", x2: "14", y2: "11" })
1885
+ ] });
1886
+ var ZoomControls = (0, import_react12.memo)(function ZoomControls2({ isDark = false }) {
1887
+ const zoomLevel = useViewportStore((s) => s.zoomLevel);
1888
+ const setZoom = useViewportStore((s) => s.setZoom);
1889
+ const handleZoomIn = (0, import_react12.useCallback)(() => setZoom(Math.min(import_core12.ZOOM_MAX, zoomLevel * 1.2)), [zoomLevel, setZoom]);
1890
+ const handleZoomOut = (0, import_react12.useCallback)(() => setZoom(Math.max(import_core12.ZOOM_MIN, zoomLevel / 1.2)), [zoomLevel, setZoom]);
1891
+ const surfaceBg = isDark ? "var(--bg-surface, #1C1C1A)" : "var(--bg-surface, #FAF8F4)";
1892
+ const border = isDark ? "var(--border-color, #333330)" : "var(--border-color, #DDD9D0)";
1893
+ const primary = isDark ? "var(--text-primary, #F0EDE6)" : "var(--text-primary, #1A1A18)";
1894
+ const muted = isDark ? "var(--text-muted, #8E8C88)" : "var(--text-muted, #747270)";
1895
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
1896
+ "div",
1897
+ {
1898
+ style: {
1899
+ position: "relative",
1900
+ background: surfaceBg,
1901
+ border: `0.5px solid ${border}`,
1902
+ borderRadius: "8px",
1903
+ overflow: "hidden",
1904
+ width: "65px",
1905
+ height: "32px",
1906
+ flexShrink: 0
1907
+ },
1908
+ role: "group",
1909
+ "aria-label": "Zoom controls",
1910
+ children: [
1911
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1912
+ "button",
1913
+ {
1914
+ onClick: handleZoomIn,
1915
+ disabled: zoomLevel >= import_core12.ZOOM_MAX,
1916
+ style: {
1917
+ position: "absolute",
1918
+ top: 0,
1919
+ left: 0,
1920
+ width: "32px",
1921
+ height: "100%",
1922
+ display: "flex",
1923
+ alignItems: "center",
1924
+ justifyContent: "center",
1925
+ border: "none",
1926
+ background: "transparent",
1927
+ cursor: zoomLevel >= import_core12.ZOOM_MAX ? "not-allowed" : "pointer",
1928
+ color: zoomLevel >= import_core12.ZOOM_MAX ? muted : primary,
1929
+ transition: "background 0.15s"
1930
+ },
1931
+ onMouseEnter: (e) => {
1932
+ if (zoomLevel < import_core12.ZOOM_MAX) e.currentTarget.style.background = isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.05)";
1933
+ },
1934
+ onMouseLeave: (e) => {
1935
+ e.currentTarget.style.background = "transparent";
1936
+ },
1937
+ "aria-label": "Zoom in",
1938
+ children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ZoomInIcon, {})
1939
+ }
1940
+ ),
1941
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: { position: "absolute", top: 0, bottom: 0, left: "32px", width: "0.5px", background: border } }),
1942
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1943
+ "button",
1944
+ {
1945
+ onClick: handleZoomOut,
1946
+ disabled: zoomLevel <= import_core12.ZOOM_MIN,
1947
+ style: {
1948
+ position: "absolute",
1949
+ top: 0,
1950
+ right: 0,
1951
+ width: "33px",
1952
+ height: "100%",
1953
+ display: "flex",
1954
+ alignItems: "center",
1955
+ justifyContent: "center",
1956
+ border: "none",
1957
+ background: "transparent",
1958
+ cursor: zoomLevel <= import_core12.ZOOM_MIN ? "not-allowed" : "pointer",
1959
+ color: zoomLevel <= import_core12.ZOOM_MIN ? muted : primary,
1960
+ transition: "background 0.15s"
1961
+ },
1962
+ onMouseEnter: (e) => {
1963
+ if (zoomLevel > import_core12.ZOOM_MIN) e.currentTarget.style.background = isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.05)";
1964
+ },
1965
+ onMouseLeave: (e) => {
1966
+ e.currentTarget.style.background = "transparent";
1967
+ },
1968
+ "aria-label": "Zoom out",
1969
+ children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ZoomOutIcon, {})
1970
+ }
1971
+ )
1972
+ ]
1973
+ }
1974
+ );
1975
+ });
1976
+ var ZoomControls_default = ZoomControls;
1977
+
1978
+ // src/components/SketchyBoxKonva.tsx
1979
+ var import_react13 = require("react");
1980
+ var import_konva = __toESM(require("konva"), 1);
1981
+ var import_jsx_runtime10 = require("react/jsx-runtime");
1982
+ var BACK_PATH = "M0.653,0 L485.653,1 C485.653,1,484.782,152.773,486.653,165 C413.653,171,14.653,178,5.653,168C2.653,158,0.653,0,0.653,0";
1983
+ var FRONT_PATH = "M485,167.524 L0,166.524C0,166.524,1.870,8.751,0,0C73,0,479,0,487,8C490,18,485,167.524,485,167.524";
1984
+ var BACK_W = 486;
1985
+ var BACK_H = 178;
1986
+ var FRONT_W = 490;
1987
+ var FRONT_H = 168;
1988
+ var BACK_OFFSET_Y = 8;
1989
+ var FRONT_OFFSET_X = 5.65;
1990
+ var SHADOW_PAD = 12;
1991
+ var SketchyBoxKonva = ({
1992
+ children,
1993
+ fill,
1994
+ stroke,
1995
+ backStroke,
1996
+ padding = "1rem 1.25rem"
1997
+ }) => {
1998
+ const wrapperRef = (0, import_react13.useRef)(null);
1999
+ const konvaContainerRef = (0, import_react13.useRef)(null);
2000
+ const stageRef = (0, import_react13.useRef)(null);
2001
+ (0, import_react13.useEffect)(() => {
2002
+ const wrapper = wrapperRef.current;
2003
+ const konvaEl = konvaContainerRef.current;
2004
+ if (!wrapper || !konvaEl) return;
2005
+ function draw(W, H) {
2006
+ if (!konvaEl || W <= 0 || H <= 0) return;
2007
+ if (stageRef.current) {
2008
+ stageRef.current.destroy();
2009
+ stageRef.current = null;
2010
+ }
2011
+ const stage = new import_konva.default.Stage({ container: konvaEl, width: W, height: H });
2012
+ const layer = new import_konva.default.Layer({ listening: false });
2013
+ const contentH = H - SHADOW_PAD;
2014
+ layer.add(new import_konva.default.Path({
2015
+ x: 0,
2016
+ y: BACK_OFFSET_Y,
2017
+ data: BACK_PATH,
2018
+ scaleX: W / BACK_W,
2019
+ scaleY: contentH / BACK_H,
2020
+ fill: "#ffffff",
2021
+ stroke: backStroke,
2022
+ strokeWidth: 2,
2023
+ strokeScaleEnabled: false,
2024
+ listening: false
2025
+ }));
2026
+ layer.add(new import_konva.default.Path({
2027
+ x: FRONT_OFFSET_X,
2028
+ y: 0,
2029
+ data: FRONT_PATH,
2030
+ scaleX: (W - FRONT_OFFSET_X) / FRONT_W,
2031
+ scaleY: contentH / FRONT_H,
2032
+ fill,
2033
+ stroke,
2034
+ strokeWidth: 3,
2035
+ strokeScaleEnabled: false,
2036
+ listening: false
2037
+ }));
2038
+ stage.add(layer);
2039
+ stageRef.current = stage;
2040
+ }
2041
+ draw(wrapper.offsetWidth, wrapper.offsetHeight);
2042
+ const ro = new ResizeObserver((entries) => {
2043
+ const el = entries[0].target;
2044
+ draw(el.offsetWidth, el.offsetHeight);
2045
+ });
2046
+ ro.observe(wrapper);
2047
+ return () => {
2048
+ ro.disconnect();
2049
+ stageRef.current?.destroy();
2050
+ stageRef.current = null;
2051
+ };
2052
+ }, [fill, stroke, backStroke]);
2053
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { ref: wrapperRef, style: { position: "relative", paddingBottom: `${SHADOW_PAD}px` }, children: [
2054
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2055
+ "div",
2056
+ {
2057
+ ref: konvaContainerRef,
2058
+ style: { position: "absolute", inset: 0, pointerEvents: "none", zIndex: 0, overflow: "hidden" }
2059
+ }
2060
+ ),
2061
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { style: { position: "relative", padding, zIndex: 1 }, children })
2062
+ ] });
2063
+ };
2064
+ var SketchyBoxKonva_default = SketchyBoxKonva;
2065
+
2066
+ // src/VisualliCanvas.tsx
2067
+ var import_jsx_runtime11 = require("react/jsx-runtime");
2068
+ function hexToRgba2(hex, alpha) {
2069
+ hex = hex.replace(/^#/, "");
2070
+ let r, g, b;
2071
+ if (hex.length === 3) {
2072
+ r = parseInt(hex[0] + hex[0], 16);
2073
+ g = parseInt(hex[1] + hex[1], 16);
2074
+ b = parseInt(hex[2] + hex[2], 16);
2075
+ } else if (hex.length === 6) {
2076
+ r = parseInt(hex.substring(0, 2), 16);
2077
+ g = parseInt(hex.substring(2, 4), 16);
2078
+ b = parseInt(hex.substring(4, 6), 16);
2079
+ } else {
2080
+ return `rgba(240, 237, 230, ${alpha})`;
2081
+ }
2082
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
2083
+ }
2084
+ function resolveDocument(props, fileText) {
2085
+ if (props.preParsedVisualli) return props.preParsedVisualli;
2086
+ const rawContent = fileText ?? props.visualliString;
2087
+ if (rawContent) {
2088
+ try {
2089
+ return (0, import_core13.parseVisualliFile)(rawContent);
2090
+ } catch {
2091
+ return null;
2092
+ }
2093
+ }
2094
+ return null;
2095
+ }
2096
+ function layerLabel(doc, layer, layerId) {
2097
+ if (layer.parentNodeId) {
2098
+ const parentLayer = layer.parentLayerId ? doc.layers.get(layer.parentLayerId) : null;
2099
+ if (parentLayer) {
2100
+ const parentNode = parentLayer.nodes.find((n) => n.id === layer.parentNodeId);
2101
+ if (parentNode) {
2102
+ return Array.isArray(parentNode.data.label) ? parentNode.data.label.join(" ") : parentNode.data.label || "Untitled";
2103
+ }
2104
+ }
2105
+ }
2106
+ return layer.description ?? layerId;
2107
+ }
2108
+ function getRootLayerId(doc) {
2109
+ for (const [id, layer] of doc.layers) {
2110
+ if (!layer.parentLayerId) return id;
2111
+ }
2112
+ return doc.layers.keys().next().value ?? null;
2113
+ }
2114
+ function VisualliCanvas(props) {
2115
+ const { isDark = false, chromaticImmersion = false, onNodeClick, onLayerChange, renderOverlay, renderNodeContent, navigationStackTop = "16px", navigationStackLeft = "16px", className = "", style } = props;
2116
+ const [fileText, setFileText] = (0, import_react14.useState)(void 0);
2117
+ (0, import_react14.useEffect)(() => {
2118
+ if (!props.visualliFile) {
2119
+ setFileText(void 0);
2120
+ return;
2121
+ }
2122
+ let cancelled = false;
2123
+ if (typeof props.visualliFile === "string") {
2124
+ fetch(props.visualliFile).then((response) => {
2125
+ if (!response.ok) throw new Error(`Failed to fetch: ${response.statusText}`);
2126
+ return response.text();
2127
+ }).then((t) => {
2128
+ if (!cancelled) setFileText(t);
2129
+ }).catch(() => {
2130
+ if (!cancelled) setFileText(void 0);
2131
+ });
2132
+ } else {
2133
+ props.visualliFile.text().then((t) => {
2134
+ if (!cancelled) setFileText(t);
2135
+ }).catch(() => {
2136
+ if (!cancelled) setFileText(void 0);
2137
+ });
2138
+ }
2139
+ return () => {
2140
+ cancelled = true;
2141
+ };
2142
+ }, [props.visualliFile]);
2143
+ const doc = (0, import_react14.useMemo)(() => resolveDocument(props, fileText), [props.preParsedVisualli, props.visualliString, props.visualliFile, fileText]);
2144
+ const [navStack, setNavStack] = (0, import_react14.useState)([]);
2145
+ const [currentLayerId, setCurrentLayerId] = (0, import_react14.useState)(null);
2146
+ const parentViewports = (0, import_react14.useRef)([]);
2147
+ const [isTransitioning, setIsTransitioning] = (0, import_react14.useState)(false);
2148
+ const isTransitioningRef = (0, import_react14.useRef)(false);
2149
+ (0, import_react14.useEffect)(() => {
2150
+ if (!doc) return;
2151
+ const rootId = getRootLayerId(doc);
2152
+ if (!rootId) return;
2153
+ const rootLayer = doc.layers.get(rootId);
2154
+ setCurrentLayerId(rootId);
2155
+ setNavStack([{ layerId: rootId, layer: rootLayer, label: "Home" }]);
2156
+ parentViewports.current = [];
2157
+ }, [doc]);
2158
+ const { flatNodes, connections, containers } = (0, import_react14.useMemo)(() => {
2159
+ if (!doc || !currentLayerId) return { flatNodes: [], connections: [], containers: [] };
2160
+ try {
2161
+ return {
2162
+ flatNodes: (0, import_core13.getNodesForLayer)(doc, currentLayerId),
2163
+ connections: (0, import_core5.getConnectionsForLayer)(doc, currentLayerId),
2164
+ containers: (0, import_core5.getContainersForLayer)(doc, currentLayerId)
2165
+ };
2166
+ } catch {
2167
+ return { flatNodes: [], connections: [], containers: [] };
2168
+ }
2169
+ }, [doc, currentLayerId]);
2170
+ const setNodes = useNodeStore((s) => s.setNodes);
2171
+ const nodes = useNodeStore((s) => s.nodes);
2172
+ const updateNodePosition = useNodeStore((s) => s.updateNodePosition);
2173
+ const setCenter = useViewportStore((s) => s.setCenter);
2174
+ const setZoom = useViewportStore((s) => s.setZoom);
2175
+ const viewport = useViewportStore((s) => ({
2176
+ centerX: s.centerX,
2177
+ centerY: s.centerY,
2178
+ zoomLevel: s.zoomLevel,
2179
+ canvasWidth: s.canvasWidth,
2180
+ canvasHeight: s.canvasHeight
2181
+ }));
2182
+ const clearSel = useSelectionStore((s) => s.clear);
2183
+ const select = useSelectionStore((s) => s.select);
2184
+ (0, import_react14.useEffect)(() => {
2185
+ const map = new Map(flatNodes.map((n) => [n.id, n]));
2186
+ setNodes(map);
2187
+ clearSel();
2188
+ }, [flatNodes, setNodes, clearSel]);
2189
+ const containerRef = (0, import_react14.useRef)(null);
2190
+ const stageRef = (0, import_react14.useRef)(null);
2191
+ const canvasWrapperRef = (0, import_react14.useRef)(null);
2192
+ const baselineTransformRef = (0, import_react14.useRef)(null);
2193
+ const konvaContentDivRef = (0, import_react14.useRef)(null);
2194
+ const lastAnimViewportRef = (0, import_react14.useRef)(null);
2195
+ const zoomOutTargetRef = (0, import_react14.useRef)(null);
2196
+ const canvasSizeRef = (0, import_react14.useRef)({ width: 0, height: 0 });
2197
+ const dragMoveRafRef = (0, import_react14.useRef)(null);
2198
+ const dragMoveLatestRef = (0, import_react14.useRef)(null);
2199
+ const dragStartPosRef = (0, import_react14.useRef)(/* @__PURE__ */ new Map());
2200
+ const baseBgColor = isDark ? "#141412" : "#F0EDE6";
2201
+ const chromaticBgColor = (0, import_react14.useMemo)(() => {
2202
+ if (!chromaticImmersion || !doc || !currentLayerId) return baseBgColor;
2203
+ const currentLayer = doc.layers.get(currentLayerId);
2204
+ if (!currentLayer) return baseBgColor;
2205
+ if (currentLayer.level === 0 || !currentLayer.parentNodeId) return baseBgColor;
2206
+ const parentLayer = currentLayer.parentLayerId ? doc.layers.get(currentLayer.parentLayerId) : null;
2207
+ if (parentLayer) {
2208
+ const parentNode = parentLayer.nodes.find((n) => n.id === currentLayer.parentNodeId);
2209
+ if (parentNode) {
2210
+ const nodeColor = parentNode.data.color || baseBgColor;
2211
+ return hexToRgba2(nodeColor, 0.15);
2212
+ }
2213
+ }
2214
+ return baseBgColor;
2215
+ }, [chromaticImmersion, doc, currentLayerId, baseBgColor]);
2216
+ const bgColor = chromaticImmersion ? chromaticBgColor : baseBgColor;
2217
+ const spatialIndexRef = (0, import_react14.useRef)(null);
2218
+ (0, import_react14.useEffect)(() => {
2219
+ if (flatNodes.length === 0) {
2220
+ spatialIndexRef.current?.clear();
2221
+ return;
2222
+ }
2223
+ const idx = new import_core13.RBushSpatialIndex();
2224
+ idx.bulkLoad(flatNodes.map((n) => {
2225
+ const blobHalfH = Math.max(import_core13.NODE_HEIGHT * 1.6 / 2, n.width / 2 * 0.74);
2226
+ return {
2227
+ nodeId: n.id,
2228
+ bounds: {
2229
+ minX: n.x - n.width / 2,
2230
+ minY: n.y - blobHalfH,
2231
+ maxX: n.x + n.width / 2,
2232
+ maxY: n.y + blobHalfH
2233
+ }
2234
+ };
2235
+ }));
2236
+ spatialIndexRef.current = idx;
2237
+ return () => {
2238
+ spatialIndexRef.current?.clear();
2239
+ };
2240
+ }, [flatNodes]);
2241
+ const {
2242
+ handleWheel: rendererHandleWheel,
2243
+ handleMouseDown: rendererHandleMouseDown,
2244
+ handleMouseMove: rendererHandleMouseMove,
2245
+ handleMouseUp: rendererHandleMouseUp,
2246
+ canvasWidth,
2247
+ canvasHeight
2248
+ } = useKonvaRenderer({ containerRef, stageRef });
2249
+ const _cw = canvasWidth || containerRef.current?.getBoundingClientRect().width || 800;
2250
+ const _ch = canvasHeight || containerRef.current?.getBoundingClientRect().height || 600;
2251
+ canvasSizeRef.current = { width: _cw, height: _ch };
2252
+ const handleDragStart = (0, import_react14.useCallback)((nodeId) => {
2253
+ isDraggingRef.current = true;
2254
+ setIsDraggingState(true);
2255
+ setHoveredNode(null);
2256
+ const node = nodes.get(nodeId);
2257
+ if (node) dragStartPosRef.current.set(nodeId, { x: node.x, y: node.y });
2258
+ }, [nodes]);
2259
+ const handleDragMove = (0, import_react14.useCallback)((nodeId, x, y) => {
2260
+ dragMoveLatestRef.current = { nodeId, x, y };
2261
+ if (dragMoveRafRef.current !== null) return;
2262
+ dragMoveRafRef.current = requestAnimationFrame(() => {
2263
+ dragMoveRafRef.current = null;
2264
+ const latest = dragMoveLatestRef.current;
2265
+ if (latest) {
2266
+ dragMoveLatestRef.current = null;
2267
+ updateNodePosition(latest.nodeId, latest.x, latest.y);
2268
+ }
2269
+ });
2270
+ }, [updateNodePosition]);
2271
+ const handleDragEnd = (0, import_react14.useCallback)((nodeId, x, y) => {
2272
+ if (dragMoveRafRef.current !== null) {
2273
+ cancelAnimationFrame(dragMoveRafRef.current);
2274
+ dragMoveRafRef.current = null;
2275
+ }
2276
+ dragMoveLatestRef.current = null;
2277
+ isDraggingRef.current = false;
2278
+ setIsDraggingState(false);
2279
+ updateNodePosition(nodeId, x, y);
2280
+ if (spatialIndexRef.current) {
2281
+ const node = nodes.get(nodeId);
2282
+ if (node) {
2283
+ const oldPos = dragStartPosRef.current.get(nodeId);
2284
+ if (oldPos) {
2285
+ const oldHalfH = Math.max(import_core13.NODE_HEIGHT * 1.6 / 2, node.width / 2 * 0.74);
2286
+ spatialIndexRef.current.remove(nodeId, {
2287
+ minX: oldPos.x - node.width / 2,
2288
+ minY: oldPos.y - oldHalfH,
2289
+ maxX: oldPos.x + node.width / 2,
2290
+ maxY: oldPos.y + oldHalfH
2291
+ });
2292
+ }
2293
+ const newHalfH = Math.max(import_core13.NODE_HEIGHT * 1.6 / 2, node.width / 2 * 0.74);
2294
+ spatialIndexRef.current.insert(nodeId, {
2295
+ minX: x - node.width / 2,
2296
+ minY: y - newHalfH,
2297
+ maxX: x + node.width / 2,
2298
+ maxY: y + newHalfH
2299
+ });
2300
+ }
2301
+ }
2302
+ dragStartPosRef.current.delete(nodeId);
2303
+ }, [updateNodePosition, nodes]);
2304
+ const fitToScreenRef = (0, import_react14.useRef)(() => {
2305
+ });
2306
+ const fitToScreen = (0, import_react14.useCallback)(() => {
2307
+ if (flatNodes.length === 0) return;
2308
+ if (isTransitioningRef.current) return;
2309
+ const cw = canvasSizeRef.current.width || containerRef.current?.getBoundingClientRect().width || 800;
2310
+ const ch = canvasSizeRef.current.height || containerRef.current?.getBoundingClientRect().height || 600;
2311
+ const { centerX, centerY } = (0, import_core5.calculateFitView)(flatNodes, cw, ch);
2312
+ const isRootLayer = navStack.length <= 1;
2313
+ const zoomLevel = isRootLayer ? 1 : (0, import_core5.calculateFitView)(flatNodes, cw, ch).zoomLevel;
2314
+ setCenter(centerX, centerY);
2315
+ setZoom(zoomLevel);
2316
+ }, [flatNodes, navStack.length, setCenter, setZoom]);
2317
+ fitToScreenRef.current = fitToScreen;
2318
+ (0, import_react14.useEffect)(() => {
2319
+ fitToScreen();
2320
+ }, [currentLayerId]);
2321
+ const didFitAfterMeasureRef = (0, import_react14.useRef)(false);
2322
+ (0, import_react14.useEffect)(() => {
2323
+ if (!canvasWidth || didFitAfterMeasureRef.current) return;
2324
+ didFitAfterMeasureRef.current = true;
2325
+ fitToScreen();
2326
+ }, [canvasWidth, fitToScreen]);
2327
+ const navBackTargetRef = (0, import_react14.useRef)(-1);
2328
+ const { zoomIntoLayer, zoomOutToParent, isTransitioning: isAnimating } = useKonvaLayerTransition({
2329
+ stageRef,
2330
+ canvasWrapperRef,
2331
+ onSwapLayer: (childLayerId) => {
2332
+ const childLayer = doc?.layers.get(childLayerId);
2333
+ if (!childLayer || !doc) return;
2334
+ setCurrentLayerId(childLayerId);
2335
+ setNavStack((prev) => [...prev, { layerId: childLayerId, layer: childLayer, label: layerLabel(doc, childLayer, childLayerId) }]);
2336
+ onLayerChange?.(childLayerId, childLayer);
2337
+ },
2338
+ onSwapBack: () => {
2339
+ const targetIndex = navBackTargetRef.current;
2340
+ setNavStack((prev) => {
2341
+ const newStack = prev.slice(0, targetIndex + 1);
2342
+ parentViewports.current = parentViewports.current.slice(0, targetIndex);
2343
+ const newLayerId = newStack[newStack.length - 1].layerId;
2344
+ setCurrentLayerId(newLayerId);
2345
+ onLayerChange?.(newLayerId, newStack[newStack.length - 1].layer);
2346
+ return newStack;
2347
+ });
2348
+ },
2349
+ onComplete: () => {
2350
+ setIsTransitioning(false);
2351
+ isTransitioningRef.current = false;
2352
+ requestAnimationFrame(() => {
2353
+ fitToScreenRef.current();
2354
+ });
2355
+ },
2356
+ // GPU CSS-transform callbacks (matching visualli.ai exactly):
2357
+ onViewportFrame: (vp) => {
2358
+ lastAnimViewportRef.current = vp;
2359
+ const baseline = baselineTransformRef.current;
2360
+ const contentDiv = konvaContentDivRef.current;
2361
+ const { width, height } = canvasSizeRef.current;
2362
+ if (baseline && contentDiv) {
2363
+ const targetX = width / 2 - vp.centerX * vp.zoomLevel;
2364
+ const targetY = height / 2 - vp.centerY * vp.zoomLevel;
2365
+ const ratio = vp.zoomLevel / baseline.scaleX;
2366
+ const tx = targetX - baseline.x * ratio;
2367
+ const ty = targetY - baseline.y * ratio;
2368
+ contentDiv.style.transform = `translate(${tx}px, ${ty}px) scale(${ratio})`;
2369
+ } else {
2370
+ const stage = stageRef.current;
2371
+ if (stage) {
2372
+ stage.x(width / 2 - vp.centerX * vp.zoomLevel);
2373
+ stage.y(height / 2 - vp.centerY * vp.zoomLevel);
2374
+ stage.scaleX(vp.zoomLevel);
2375
+ stage.scaleY(vp.zoomLevel);
2376
+ stage.batchDraw();
2377
+ }
2378
+ }
2379
+ },
2380
+ onViewportFinal: (vp) => {
2381
+ if (konvaContentDivRef.current) konvaContentDivRef.current.style.transform = "";
2382
+ const stage = stageRef.current;
2383
+ if (stage) {
2384
+ const { width, height } = canvasSizeRef.current;
2385
+ stage.x(width / 2 - vp.centerX * vp.zoomLevel);
2386
+ stage.y(height / 2 - vp.centerY * vp.zoomLevel);
2387
+ stage.scaleX(vp.zoomLevel);
2388
+ stage.scaleY(vp.zoomLevel);
2389
+ stage.getLayers().forEach((l) => l.drawScene());
2390
+ }
2391
+ setCenter(vp.centerX, vp.centerY);
2392
+ setZoom(vp.zoomLevel);
2393
+ },
2394
+ onPhaseChange: (phase) => {
2395
+ const stage = stageRef.current;
2396
+ const contentDiv = konvaContentDivRef.current;
2397
+ if (stage && contentDiv && (phase === "swap" || phase === "zoom-back")) {
2398
+ contentDiv.style.transform = "";
2399
+ const vp = phase === "zoom-back" ? zoomOutTargetRef.current : lastAnimViewportRef.current;
2400
+ if (vp) {
2401
+ const { width, height } = canvasSizeRef.current;
2402
+ const nx = width / 2 - vp.centerX * vp.zoomLevel;
2403
+ const ny = height / 2 - vp.centerY * vp.zoomLevel;
2404
+ stage.x(nx);
2405
+ stage.y(ny);
2406
+ stage.scaleX(vp.zoomLevel);
2407
+ stage.scaleY(vp.zoomLevel);
2408
+ stage.getLayers().forEach((l) => l.drawScene());
2409
+ baselineTransformRef.current = { x: nx, y: ny, scaleX: vp.zoomLevel, scaleY: vp.zoomLevel };
2410
+ }
2411
+ }
2412
+ },
2413
+ onTransitionLifecycle: (phase) => {
2414
+ const stage = stageRef.current;
2415
+ if (phase === "start") {
2416
+ if (canvasWrapperRef.current) canvasWrapperRef.current.style.willChange = "opacity, transform";
2417
+ if (stage) {
2418
+ baselineTransformRef.current = { x: stage.x(), y: stage.y(), scaleX: stage.scaleX(), scaleY: stage.scaleY() };
2419
+ const container = stage.container();
2420
+ const cd = container?.querySelector(".konvajs-content");
2421
+ if (cd) {
2422
+ konvaContentDivRef.current = cd;
2423
+ cd.style.transformOrigin = "0 0";
2424
+ cd.style.willChange = "transform";
2425
+ }
2426
+ stage.getLayers().forEach((l) => {
2427
+ l.listening(false);
2428
+ });
2429
+ }
2430
+ } else {
2431
+ if (canvasWrapperRef.current) canvasWrapperRef.current.style.willChange = "auto";
2432
+ if (konvaContentDivRef.current) {
2433
+ konvaContentDivRef.current.style.willChange = "auto";
2434
+ konvaContentDivRef.current = null;
2435
+ }
2436
+ baselineTransformRef.current = null;
2437
+ zoomOutTargetRef.current = null;
2438
+ if (stage) {
2439
+ stage.getLayers().forEach((l) => {
2440
+ l.listening(true);
2441
+ });
2442
+ stage.batchDraw();
2443
+ }
2444
+ }
2445
+ }
2446
+ });
2447
+ const handleNavigate = (0, import_react14.useCallback)((nodeId) => {
2448
+ if (!doc || !currentLayerId) {
2449
+ return;
2450
+ }
2451
+ if (isTransitioningRef.current || isAnimating()) {
2452
+ return;
2453
+ }
2454
+ const childLayer = (0, import_core5.getChildLayerForNode)(doc, nodeId, currentLayerId);
2455
+ if (!childLayer) {
2456
+ return;
2457
+ }
2458
+ const childLayerId = [...doc.layers.entries()].find(([, l]) => l === childLayer)?.[0];
2459
+ if (!childLayerId) {
2460
+ return;
2461
+ }
2462
+ const childNodes = (() => {
2463
+ try {
2464
+ return (0, import_core13.getNodesForLayer)(doc, childLayerId);
2465
+ } catch {
2466
+ return [];
2467
+ }
2468
+ })();
2469
+ if (childNodes.length === 0) {
2470
+ return;
2471
+ }
2472
+ const node = nodes.get(nodeId);
2473
+ if (!node) return;
2474
+ const cw = canvasSizeRef.current.width || 800;
2475
+ const ch = canvasSizeRef.current.height || 600;
2476
+ const fitVp = (0, import_core5.calculateFitView)(flatNodes, cw, ch);
2477
+ const isRootNow = navStack.length <= 1;
2478
+ parentViewports.current.push({ centerX: fitVp.centerX, centerY: fitVp.centerY, zoomLevel: isRootNow ? 0.85 : fitVp.zoomLevel });
2479
+ setIsTransitioning(true);
2480
+ isTransitioningRef.current = true;
2481
+ zoomIntoLayer(
2482
+ doc,
2483
+ node,
2484
+ childLayerId,
2485
+ childNodes,
2486
+ node.color ?? (0, import_core13.getThemeBackground)(isDark, "secondary"),
2487
+ node.color ?? (0, import_core13.getThemeBackground)(isDark, "primary")
2488
+ );
2489
+ }, [doc, currentLayerId, nodes, isDark, isAnimating, zoomIntoLayer]);
2490
+ const handleNavigateBack = (0, import_react14.useCallback)((targetIndex) => {
2491
+ if (targetIndex >= navStack.length - 1) return;
2492
+ if (isTransitioningRef.current || isAnimating()) return;
2493
+ const savedVp = parentViewports.current[targetIndex];
2494
+ if (!savedVp) return;
2495
+ navBackTargetRef.current = targetIndex;
2496
+ setIsTransitioning(true);
2497
+ isTransitioningRef.current = true;
2498
+ zoomOutToParent(savedVp, (0, import_core13.getThemeBackground)(isDark, "primary"), (0, import_core13.getThemeBackground)(isDark, "secondary"));
2499
+ }, [navStack.length, isDark, isAnimating, zoomOutToParent]);
2500
+ const baseZoomRef = (0, import_react14.useRef)(1);
2501
+ const lastZoomTransitionRef = (0, import_react14.useRef)(0);
2502
+ const ZOOM_COOLDOWN = 1e3;
2503
+ (0, import_react14.useEffect)(() => {
2504
+ baseZoomRef.current = 1;
2505
+ }, [currentLayerId]);
2506
+ (0, import_react14.useEffect)(() => {
2507
+ if (isTransitioningRef.current || isAnimating()) return;
2508
+ const now = Date.now();
2509
+ if (now - lastZoomTransitionRef.current < ZOOM_COOLDOWN) return;
2510
+ const relative = viewport.zoomLevel / baseZoomRef.current;
2511
+ if (relative >= import_core13.ZOOM_NAV_IN_THRESHOLD) {
2512
+ let closest = null;
2513
+ let minDist = Infinity;
2514
+ for (const node of nodes.values()) {
2515
+ const dx = node.x - viewport.centerX;
2516
+ const dy = node.y - viewport.centerY;
2517
+ const d = dx * dx + dy * dy;
2518
+ if (d < minDist) {
2519
+ minDist = d;
2520
+ closest = node;
2521
+ }
2522
+ }
2523
+ if (closest) {
2524
+ const childLayer = doc ? (0, import_core5.getChildLayerForNode)(doc, closest.id, currentLayerId ?? "") : null;
2525
+ if (childLayer) {
2526
+ lastZoomTransitionRef.current = now;
2527
+ handleNavigate(closest.id);
2528
+ baseZoomRef.current = 1;
2529
+ }
2530
+ }
2531
+ } else if (relative < import_core13.ZOOM_NAV_OUT_THRESHOLD && navStack.length > 1) {
2532
+ lastZoomTransitionRef.current = now;
2533
+ handleNavigateBack(navStack.length - 2);
2534
+ baseZoomRef.current = 1;
2535
+ }
2536
+ }, [viewport.zoomLevel, viewport.centerX, viewport.centerY, nodes, doc, currentLayerId, navStack, isAnimating, handleNavigate, handleNavigateBack]);
2537
+ const toWorldCoords = (0, import_react14.useCallback)((stage, pointer) => {
2538
+ const sp = stage.position();
2539
+ const sc = stage.scaleX();
2540
+ return { worldX: (pointer.x - sp.x) / sc, worldY: (pointer.y - sp.y) / sc };
2541
+ }, []);
2542
+ const hitTestNode = (0, import_react14.useCallback)((worldX, worldY) => {
2543
+ if (!spatialIndexRef.current) return null;
2544
+ const hits = spatialIndexRef.current.query({ minX: worldX, minY: worldY, maxX: worldX, maxY: worldY });
2545
+ if (!hits.length) return null;
2546
+ let targetId = null, minDist = Infinity;
2547
+ for (const nodeId of hits) {
2548
+ const node = nodes.get(nodeId);
2549
+ if (!node) continue;
2550
+ const d = (worldX - node.x) ** 2 + (worldY - node.y) ** 2;
2551
+ if (d < minDist) {
2552
+ minDist = d;
2553
+ targetId = nodeId;
2554
+ }
2555
+ }
2556
+ return targetId;
2557
+ }, [nodes]);
2558
+ const stageActiveNodeIdRef = (0, import_react14.useRef)(null);
2559
+ const stageDragNodeOffsetRef = (0, import_react14.useRef)({ x: 0, y: 0 });
2560
+ const stageMouseDownPosRef = (0, import_react14.useRef)(null);
2561
+ const stageDragCommittedRef = (0, import_react14.useRef)(false);
2562
+ const stageLastPointerCheckRef = (0, import_react14.useRef)(0);
2563
+ const pointerNodeIdRef = (0, import_react14.useRef)(null);
2564
+ const isCanvasPanningRef = (0, import_react14.useRef)(false);
2565
+ const isDraggingRef = (0, import_react14.useRef)(false);
2566
+ const [pointerNodeId, setPointerNodeId] = (0, import_react14.useState)(null);
2567
+ const [pressedNodeId, setPressedNodeId] = (0, import_react14.useState)(null);
2568
+ const [isDraggingState, setIsDraggingState] = (0, import_react14.useState)(false);
2569
+ const [hoveredNode, setHoveredNode] = (0, import_react14.useState)(null);
2570
+ const tooltipHoverRef = (0, import_react14.useRef)(false);
2571
+ const keepTooltipOpenRef = (0, import_react14.useRef)(false);
2572
+ const clearTooltipTimerRef = (0, import_react14.useRef)(null);
2573
+ const hoveredNodePosition = (0, import_react14.useMemo)(() => {
2574
+ if (!hoveredNode) return null;
2575
+ const node = nodes.get(hoveredNode.nodeId);
2576
+ if (!node) return null;
2577
+ const cw = viewport.canvasWidth || canvasSizeRef.current.width;
2578
+ const ch = viewport.canvasHeight || canvasSizeRef.current.height;
2579
+ return {
2580
+ screenX: (node.x - viewport.centerX) * viewport.zoomLevel + cw / 2,
2581
+ screenY: (node.y - viewport.centerY) * viewport.zoomLevel + ch / 2,
2582
+ zoom: viewport.zoomLevel
2583
+ };
2584
+ }, [hoveredNode, nodes, viewport]);
2585
+ const [canvasContextMenu, setCanvasContextMenu] = (0, import_react14.useState)(null);
2586
+ (0, import_react14.useEffect)(() => {
2587
+ if (!canvasContextMenu) return;
2588
+ const close = () => setCanvasContextMenu(null);
2589
+ const onKey = (e) => {
2590
+ if (e.key === "Escape") close();
2591
+ };
2592
+ window.addEventListener("pointerdown", close);
2593
+ window.addEventListener("keydown", onKey);
2594
+ return () => {
2595
+ window.removeEventListener("pointerdown", close);
2596
+ window.removeEventListener("keydown", onKey);
2597
+ };
2598
+ }, [canvasContextMenu]);
2599
+ const handleStageMouseDown = (0, import_react14.useCallback)((e) => {
2600
+ if (e.evt.button !== 0) {
2601
+ rendererHandleMouseDown(e);
2602
+ return;
2603
+ }
2604
+ const stage = e.target.getStage();
2605
+ if (!stage) {
2606
+ rendererHandleMouseDown(e);
2607
+ return;
2608
+ }
2609
+ const pointer = stage.getPointerPosition();
2610
+ if (!pointer) {
2611
+ rendererHandleMouseDown(e);
2612
+ return;
2613
+ }
2614
+ const { worldX, worldY } = toWorldCoords(stage, pointer);
2615
+ const nodeId = hitTestNode(worldX, worldY);
2616
+ if (nodeId) {
2617
+ const node = nodes.get(nodeId);
2618
+ if (node) {
2619
+ select(nodeId);
2620
+ stageActiveNodeIdRef.current = nodeId;
2621
+ stageDragNodeOffsetRef.current = { x: worldX - node.x, y: worldY - node.y };
2622
+ stageMouseDownPosRef.current = { x: e.evt.clientX, y: e.evt.clientY };
2623
+ stageDragCommittedRef.current = false;
2624
+ setPressedNodeId(nodeId);
2625
+ setHoveredNode(null);
2626
+ return;
2627
+ }
2628
+ }
2629
+ setHoveredNode(null);
2630
+ isCanvasPanningRef.current = true;
2631
+ rendererHandleMouseDown(e);
2632
+ }, [hitTestNode, toWorldCoords, nodes, select, rendererHandleMouseDown]);
2633
+ const handleStageMouseMove = (0, import_react14.useCallback)((e) => {
2634
+ const stage = e.target.getStage();
2635
+ if (!stage) {
2636
+ rendererHandleMouseMove(e);
2637
+ return;
2638
+ }
2639
+ const pointer = stage.getPointerPosition();
2640
+ if (!pointer) {
2641
+ rendererHandleMouseMove(e);
2642
+ return;
2643
+ }
2644
+ const { worldX, worldY } = toWorldCoords(stage, pointer);
2645
+ if (stageActiveNodeIdRef.current && stageMouseDownPosRef.current) {
2646
+ const moved = Math.hypot(e.evt.clientX - stageMouseDownPosRef.current.x, e.evt.clientY - stageMouseDownPosRef.current.y);
2647
+ if (!stageDragCommittedRef.current && moved >= 5) {
2648
+ stageDragCommittedRef.current = true;
2649
+ isDraggingRef.current = true;
2650
+ setIsDraggingState(true);
2651
+ pointerNodeIdRef.current = null;
2652
+ setPointerNodeId(null);
2653
+ handleDragStart(stageActiveNodeIdRef.current);
2654
+ }
2655
+ if (stageDragCommittedRef.current) {
2656
+ const offset = stageDragNodeOffsetRef.current;
2657
+ handleDragMove(stageActiveNodeIdRef.current, worldX - offset.x, worldY - offset.y);
2658
+ stage.container().style.cursor = "grabbing";
2659
+ return;
2660
+ }
2661
+ return;
2662
+ }
2663
+ if (isCanvasPanningRef.current) {
2664
+ rendererHandleMouseMove(e);
2665
+ return;
2666
+ }
2667
+ const now = performance.now();
2668
+ if (now - stageLastPointerCheckRef.current >= 50) {
2669
+ stageLastPointerCheckRef.current = now;
2670
+ const hovered = hitTestNode(worldX, worldY);
2671
+ if (hovered !== pointerNodeIdRef.current) {
2672
+ pointerNodeIdRef.current = hovered;
2673
+ setPointerNodeId(hovered);
2674
+ }
2675
+ if (hovered) {
2676
+ stage.container().style.cursor = "pointer";
2677
+ } else {
2678
+ stage.container().style.cursor = "grab";
2679
+ }
2680
+ }
2681
+ rendererHandleMouseMove(e);
2682
+ }, [toWorldCoords, hitTestNode, handleDragStart, handleDragMove, rendererHandleMouseMove]);
2683
+ const handleStageMouseUp = (0, import_react14.useCallback)((e) => {
2684
+ setPressedNodeId(null);
2685
+ if (stageActiveNodeIdRef.current) {
2686
+ const nodeId = stageActiveNodeIdRef.current;
2687
+ if (stageDragCommittedRef.current) {
2688
+ const stage = e.target.getStage();
2689
+ if (stage) {
2690
+ const pointer = stage.getPointerPosition();
2691
+ if (pointer) {
2692
+ const { worldX, worldY } = toWorldCoords(stage, pointer);
2693
+ const offset = stageDragNodeOffsetRef.current;
2694
+ handleDragEnd(nodeId, worldX - offset.x, worldY - offset.y);
2695
+ }
2696
+ }
2697
+ } else {
2698
+ const node = nodes.get(nodeId);
2699
+ if (node) {
2700
+ const childLayer = doc ? (0, import_core5.getChildLayerForNode)(doc, nodeId, currentLayerId ?? "") : null;
2701
+ if (childLayer) {
2702
+ handleNavigate(nodeId);
2703
+ }
2704
+ onNodeClick?.(node);
2705
+ }
2706
+ }
2707
+ isDraggingRef.current = false;
2708
+ setIsDraggingState(false);
2709
+ stageActiveNodeIdRef.current = null;
2710
+ stageMouseDownPosRef.current = null;
2711
+ stageDragCommittedRef.current = false;
2712
+ return;
2713
+ }
2714
+ isCanvasPanningRef.current = false;
2715
+ rendererHandleMouseUp(e);
2716
+ }, [toWorldCoords, handleDragEnd, nodes, handleNavigate, onNodeClick, rendererHandleMouseUp]);
2717
+ const handleStageContextMenu = (0, import_react14.useCallback)((e) => {
2718
+ e.evt.preventDefault();
2719
+ if (navStack.length <= 1) {
2720
+ setCanvasContextMenu(null);
2721
+ return;
2722
+ }
2723
+ setCanvasContextMenu({ x: e.evt.clientX, y: e.evt.clientY });
2724
+ }, [navStack.length]);
2725
+ const handleStageTouchStart = (0, import_react14.useCallback)((e) => handleStageMouseDown(e), [handleStageMouseDown]);
2726
+ const handleStageTouchMove = (0, import_react14.useCallback)((e) => handleStageMouseMove(e), [handleStageMouseMove]);
2727
+ const handleStageTouchEnd = (0, import_react14.useCallback)((e) => handleStageMouseUp(e), [handleStageMouseUp]);
2728
+ const handleWheel = (0, import_react14.useCallback)((e) => {
2729
+ const isZoomOut = e.evt.deltaY > 0;
2730
+ if (!isZoomOut) {
2731
+ rendererHandleWheel(e);
2732
+ return;
2733
+ }
2734
+ const liveZoom = useViewportStore.getState().zoomLevel;
2735
+ const projected = Math.max(import_core13.ZOOM_MIN, liveZoom * (1 - e.evt.deltaY / 1e3));
2736
+ if (navStack.length > 1 && projected <= import_core13.ZOOM_NAV_OUT_THRESHOLD && !isTransitioningRef.current && !isAnimating()) {
2737
+ e.evt.preventDefault();
2738
+ const now = Date.now();
2739
+ if (now - lastZoomTransitionRef.current >= ZOOM_COOLDOWN) {
2740
+ lastZoomTransitionRef.current = now;
2741
+ handleNavigateBack(navStack.length - 2);
2742
+ baseZoomRef.current = 1;
2743
+ }
2744
+ return;
2745
+ }
2746
+ rendererHandleWheel(e);
2747
+ }, [rendererHandleWheel, navStack, setZoom, isAnimating, handleNavigateBack]);
2748
+ const lastHoverCheckRef = (0, import_react14.useRef)(0);
2749
+ const handleCanvasMouseMove = (0, import_react14.useCallback)((e) => {
2750
+ if (!containerRef.current || isDraggingRef.current || isCanvasPanningRef.current) {
2751
+ setHoveredNode(null);
2752
+ return;
2753
+ }
2754
+ const now = Date.now();
2755
+ if (now - lastHoverCheckRef.current < 50) return;
2756
+ lastHoverCheckRef.current = now;
2757
+ const target = e.target;
2758
+ const isOverTooltip = target.closest('[data-node-tooltip="true"]') || target.closest('[data-semantic-tooltip="true"]');
2759
+ if (tooltipHoverRef.current || keepTooltipOpenRef.current || isOverTooltip) {
2760
+ return;
2761
+ }
2762
+ const cw = viewport.canvasWidth || canvasSizeRef.current.width;
2763
+ const ch = viewport.canvasHeight || canvasSizeRef.current.height;
2764
+ const rect = containerRef.current?.getBoundingClientRect();
2765
+ const localX = rect ? e.clientX - rect.left : e.clientX;
2766
+ const localY = rect ? e.clientY - rect.top : e.clientY;
2767
+ const worldX = (localX - cw / 2) / viewport.zoomLevel + viewport.centerX;
2768
+ const worldY = (localY - ch / 2) / viewport.zoomLevel + viewport.centerY;
2769
+ let found = false;
2770
+ if (spatialIndexRef.current) {
2771
+ const hits = spatialIndexRef.current.query({ minX: worldX, minY: worldY, maxX: worldX, maxY: worldY });
2772
+ for (const nodeId of hits) {
2773
+ const node = nodes.get(nodeId);
2774
+ if (node?.description) {
2775
+ setHoveredNode({ nodeId: node.id, summary: node.description, screenX: e.clientX, screenY: e.clientY });
2776
+ found = true;
2777
+ break;
2778
+ }
2779
+ }
2780
+ }
2781
+ if (!found) {
2782
+ if (clearTooltipTimerRef.current) clearTimeout(clearTooltipTimerRef.current);
2783
+ clearTooltipTimerRef.current = setTimeout(() => {
2784
+ if (!tooltipHoverRef.current && !keepTooltipOpenRef.current) setHoveredNode(null);
2785
+ }, 150);
2786
+ } else {
2787
+ if (clearTooltipTimerRef.current) {
2788
+ clearTimeout(clearTooltipTimerRef.current);
2789
+ clearTooltipTimerRef.current = null;
2790
+ }
2791
+ }
2792
+ }, [nodes, viewport, spatialIndexRef]);
2793
+ (0, import_react14.useEffect)(() => {
2794
+ const handleKeepTooltipOpen = (event) => {
2795
+ keepTooltipOpenRef.current = event.detail.keep;
2796
+ };
2797
+ window.addEventListener("keepNodeTooltipOpen", handleKeepTooltipOpen);
2798
+ return () => {
2799
+ window.removeEventListener("keepNodeTooltipOpen", handleKeepTooltipOpen);
2800
+ };
2801
+ }, []);
2802
+ (0, import_react14.useEffect)(() => {
2803
+ return () => {
2804
+ if (clearTooltipTimerRef.current) clearTimeout(clearTooltipTimerRef.current);
2805
+ spatialIndexRef.current?.clear();
2806
+ if (dragMoveRafRef.current !== null) cancelAnimationFrame(dragMoveRafRef.current);
2807
+ };
2808
+ }, []);
2809
+ const handleZoomIn = (0, import_react14.useCallback)(() => setZoom(Math.min(viewport.zoomLevel * 1.2, 5)), [viewport.zoomLevel, setZoom]);
2810
+ const handleZoomOut = (0, import_react14.useCallback)(() => {
2811
+ setZoom(Math.max(viewport.zoomLevel / 1.2, import_core13.ZOOM_MIN));
2812
+ }, [viewport.zoomLevel, setZoom]);
2813
+ if (!doc) {
2814
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className, style: { position: "relative", display: "flex", alignItems: "center", justifyContent: "center", background: bgColor, ...style }, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { style: { color: isDark ? "rgba(255,255,255,0.4)" : "rgba(0,0,0,0.3)", fontSize: 14 }, children: "No document" }) });
2815
+ }
2816
+ const hasBackOption = navStack.length > 1;
2817
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
2818
+ "div",
2819
+ {
2820
+ ref: containerRef,
2821
+ className,
2822
+ style: {
2823
+ position: "relative",
2824
+ overflow: "hidden",
2825
+ userSelect: "none",
2826
+ background: bgColor,
2827
+ width: "100%",
2828
+ height: "100%",
2829
+ ...style
2830
+ },
2831
+ onMouseMove: handleCanvasMouseMove,
2832
+ onMouseLeave: () => {
2833
+ if (!tooltipHoverRef.current && !keepTooltipOpenRef.current) {
2834
+ setHoveredNode(null);
2835
+ }
2836
+ },
2837
+ children: [
2838
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { ref: canvasWrapperRef, style: { position: "relative", zIndex: 1, pointerEvents: "auto" }, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
2839
+ KonvaStage_default,
2840
+ {
2841
+ ref: stageRef,
2842
+ onWheel: handleWheel,
2843
+ onMouseDown: handleStageMouseDown,
2844
+ onMouseMove: handleStageMouseMove,
2845
+ onMouseUp: handleStageMouseUp,
2846
+ onContextMenu: handleStageContextMenu,
2847
+ onTouchStart: handleStageTouchStart,
2848
+ onTouchMove: handleStageTouchMove,
2849
+ onTouchEnd: handleStageTouchEnd,
2850
+ children: [
2851
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(KonvaContainerLayer, { nodes: flatNodes, containers, isDark }),
2852
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2853
+ KonvaEdgeLayer,
2854
+ {
2855
+ nodes: flatNodes,
2856
+ connections,
2857
+ isDark,
2858
+ isDragging: isDraggingState
2859
+ }
2860
+ ),
2861
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2862
+ KonvaNodeLayer,
2863
+ {
2864
+ isTransitioning,
2865
+ isDragging: isDraggingState,
2866
+ hoveredNodeId: pointerNodeId,
2867
+ pressedNodeId
2868
+ }
2869
+ )
2870
+ ]
2871
+ }
2872
+ ) }),
2873
+ canvasContextMenu && hasBackOption && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2874
+ "div",
2875
+ {
2876
+ role: "menu",
2877
+ "aria-label": "Canvas options",
2878
+ style: {
2879
+ position: "fixed",
2880
+ zIndex: 60,
2881
+ pointerEvents: "auto",
2882
+ left: `${Math.min(canvasContextMenu.x, Math.max(8, (canvasSizeRef.current.width || 800) - 152))}px`,
2883
+ top: `${Math.min(canvasContextMenu.y, Math.max(8, (canvasSizeRef.current.height || 600) - 72))}px`
2884
+ },
2885
+ onContextMenu: (e) => e.preventDefault(),
2886
+ onPointerDown: (e) => e.stopPropagation(),
2887
+ children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2888
+ "button",
2889
+ {
2890
+ type: "button",
2891
+ style: {
2892
+ padding: "6px 16px",
2893
+ borderRadius: "10px",
2894
+ border: `1px solid ${isDark ? "#333330" : "#DDD9D0"}`,
2895
+ background: isDark ? "#1C1C1A" : "#FAF8F4",
2896
+ color: isDark ? "#F0EDE6" : "#1A1A18",
2897
+ cursor: "pointer",
2898
+ fontSize: 13,
2899
+ boxShadow: "0 4px 12px rgba(0,0,0,0.15)"
2900
+ },
2901
+ onClick: () => {
2902
+ setCanvasContextMenu(null);
2903
+ handleNavigateBack(navStack.length - 2);
2904
+ },
2905
+ children: "Back"
2906
+ }
2907
+ )
2908
+ }
2909
+ ),
2910
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(NavigationStack_default, { stack: navStack, onNavigateBack: handleNavigateBack, isDark, top: navigationStackTop, left: navigationStackLeft }),
2911
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { "data-help": "zoom-controls", style: { position: "absolute", top: "24px", right: "16px", zIndex: 50, pointerEvents: "auto" }, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(ZoomControls_default, { isDark }) }),
2912
+ renderOverlay?.({ isDark, containerWidth: canvasSizeRef.current.width || 0, containerHeight: canvasSizeRef.current.height || 0 }),
2913
+ hoveredNode && hoveredNodePosition && viewport.zoomLevel < 3 && viewport.zoomLevel >= import_core13.TEXT_LABEL_HIDE_BELOW_ZOOM && (() => {
2914
+ const nodeData = nodes.get(hoveredNode.nodeId);
2915
+ const borderColor = nodeData?.color ? (0, import_core13.darkenHexColor)(nodeData.color, 0.13) : "#5BA8D4";
2916
+ const VP_PAD = 8;
2917
+ const HALF_W = 160;
2918
+ const APPROX_H = 90;
2919
+ const containerW = canvasSizeRef.current.width || 800;
2920
+ const safeLeft = Math.max(HALF_W + VP_PAD, Math.min(hoveredNodePosition.screenX, containerW - HALF_W - VP_PAD));
2921
+ const rawTop = hoveredNodePosition.screenY - 60 * hoveredNodePosition.zoom;
2922
+ const safeTop = Math.max(VP_PAD + APPROX_H, rawTop);
2923
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2924
+ "div",
2925
+ {
2926
+ "data-node-tooltip": "true",
2927
+ style: {
2928
+ position: "fixed",
2929
+ zIndex: 50,
2930
+ left: safeLeft,
2931
+ top: safeTop,
2932
+ transform: "translate(-50%, -100%)",
2933
+ transformOrigin: "bottom center",
2934
+ pointerEvents: "auto"
2935
+ },
2936
+ onMouseEnter: () => {
2937
+ tooltipHoverRef.current = true;
2938
+ if (clearTooltipTimerRef.current) {
2939
+ clearTimeout(clearTooltipTimerRef.current);
2940
+ clearTooltipTimerRef.current = null;
2941
+ }
2942
+ },
2943
+ onMouseLeave: () => {
2944
+ tooltipHoverRef.current = false;
2945
+ if (clearTooltipTimerRef.current) clearTimeout(clearTooltipTimerRef.current);
2946
+ clearTooltipTimerRef.current = setTimeout(() => {
2947
+ if (!keepTooltipOpenRef.current) setHoveredNode(null);
2948
+ }, 100);
2949
+ },
2950
+ children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { style: { maxWidth: "20rem", minWidth: "12rem", maxHeight: "15rem", overflowY: "auto" }, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(SketchyBoxKonva_default, { fill: "#ffffff", stroke: borderColor, backStroke: borderColor, padding: "1rem 1.25rem", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { style: {
2951
+ fontFamily: "'Playpen Sans', cursive",
2952
+ fontSize: `${DESCRIPTION_TEXT_BASE_FONT_PX2}px`,
2953
+ fontWeight: 300,
2954
+ color: "#000000",
2955
+ textAlign: "center",
2956
+ lineHeight: 1.6,
2957
+ margin: 0
2958
+ }, children: renderNodeContent ? renderNodeContent({
2959
+ summary: hoveredNode.summary,
2960
+ nodeId: hoveredNode.nodeId,
2961
+ nodeColor: nodeData?.color,
2962
+ zoom: viewport.zoomLevel
2963
+ }) : hoveredNode.summary }) }) })
2964
+ }
2965
+ );
2966
+ })()
2967
+ ]
2968
+ }
2969
+ );
2970
+ }
2971
+
2972
+ // src/VisualliRenderer.tsx
2973
+ var import_jsx_runtime12 = require("react/jsx-runtime");
2974
+ function useSystemDark() {
2975
+ const mq = typeof window !== "undefined" ? window.matchMedia("(prefers-color-scheme: dark)") : null;
2976
+ const [dark, setDark] = (0, import_react15.useState)(mq?.matches ?? false);
2977
+ (0, import_react15.useEffect)(() => {
2978
+ if (!mq) return;
2979
+ const handler = (e) => setDark(e.matches);
2980
+ mq.addEventListener("change", handler);
2981
+ return () => mq.removeEventListener("change", handler);
2982
+ }, [mq]);
2983
+ return dark;
2984
+ }
2985
+ function useFontInjection() {
2986
+ (0, import_react15.useEffect)(() => {
2987
+ if (typeof document === "undefined") return;
2988
+ const links = [
2989
+ "https://fonts.googleapis.com",
2990
+ "https://fonts.gstatic.com",
2991
+ "https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800&family=Nunito+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300&display=swap",
2992
+ "https://fonts.googleapis.com/css2?family=Playpen+Sans:wght@300;400;600;800&family=Story+Script:wght@400&display=swap"
2993
+ ];
2994
+ links.forEach((href, i) => {
2995
+ const id = `visualli-font-${i}`;
2996
+ if (!document.getElementById(id)) {
2997
+ const link = document.createElement("link");
2998
+ link.id = id;
2999
+ link.rel = i < 2 ? "preconnect" : "stylesheet";
3000
+ link.href = href;
3001
+ if (i === 1) link.crossOrigin = "anonymous";
3002
+ document.head.appendChild(link);
3003
+ }
3004
+ });
3005
+ }, []);
3006
+ }
3007
+ function EmptyState({ isDark }) {
3008
+ const bg = isDark ? "#131311" : "#F5F3EF";
3009
+ const fg = isDark ? "rgba(240,237,230,0.35)" : "rgba(26,26,24,0.35)";
3010
+ const bd = isDark ? "#2A2A28" : "#DDD9D0";
3011
+ const code = { fontFamily: "monospace", fontSize: 12, padding: "1px 4px", borderRadius: 4, background: isDark ? "#222220" : "#E8E4DC", border: `1px solid ${bd}` };
3012
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { style: { display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", width: "100%", height: "100%", gap: 16, background: bg, color: fg, fontFamily: "'Nunito', system-ui, sans-serif" }, children: [
3013
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("svg", { width: "48", height: "48", viewBox: "0 0 48 48", fill: "none", stroke: fg, strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: [
3014
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("rect", { x: "10", y: "6", width: "28", height: "36", rx: "3" }),
3015
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("line", { x1: "16", y1: "16", x2: "32", y2: "16" }),
3016
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("line", { x1: "16", y1: "22", x2: "32", y2: "22" }),
3017
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("line", { x1: "16", y1: "28", x2: "26", y2: "28" })
3018
+ ] }),
3019
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { style: { textAlign: "center", lineHeight: 1.7 }, children: [
3020
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { style: { margin: 0, fontSize: 15, fontWeight: 600, color: isDark ? "rgba(240,237,230,0.7)" : "rgba(26,26,24,0.7)" }, children: "No .visualli file provided" }),
3021
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("p", { style: { margin: "4px 0 0", fontSize: 13 }, children: [
3022
+ "Pass a ",
3023
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("code", { style: code, children: "visualliFile" }),
3024
+ " or ",
3025
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("code", { style: code, children: "visualliString" }),
3026
+ " prop."
3027
+ ] })
3028
+ ] })
3029
+ ] });
3030
+ }
3031
+ function LoadingState({ isDark }) {
3032
+ const bg = isDark ? "#131311" : "#F5F3EF";
3033
+ const fg = isDark ? "rgba(240,237,230,0.5)" : "rgba(26,26,24,0.5)";
3034
+ const dot = isDark ? "rgba(240,237,230,0.6)" : "rgba(26,26,24,0.45)";
3035
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { style: { display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", width: "100%", height: "100%", gap: 16, background: bg, color: fg, fontFamily: "'Nunito', system-ui, sans-serif" }, children: [
3036
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { style: { display: "flex", gap: 8 }, children: [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { style: { width: 8, height: 8, borderRadius: "50%", background: dot, animation: `vr-pulse 1.2s ease-in-out ${i * 0.2}s infinite` } }, i)) }),
3037
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("style", { children: `@keyframes vr-pulse{0%,80%,100%{opacity:.2;transform:scale(.9)}40%{opacity:1;transform:scale(1.1)}}` }),
3038
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { style: { margin: 0, fontSize: 14 }, children: "Loading .visualli file\u2026" })
3039
+ ] });
3040
+ }
3041
+ function ErrorState({ message, isDark }) {
3042
+ const bg = isDark ? "#131311" : "#F5F3EF";
3043
+ const bd = isDark ? "#3a1f1f" : "#F0CECE";
3044
+ const card = isDark ? "#1e1212" : "#FFF5F5";
3045
+ const head = isDark ? "#F87171" : "#DC2626";
3046
+ const body = isDark ? "rgba(248,113,113,0.8)" : "rgba(180,40,40,0.8)";
3047
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { style: { display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", width: "100%", height: "100%", padding: "24px", background: bg, fontFamily: "'Nunito', system-ui, sans-serif" }, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { style: { maxWidth: 480, width: "100%", padding: "20px 24px", borderRadius: 12, border: `1px solid ${bd}`, background: card }, children: [
3048
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 10, marginBottom: 12 }, children: [
3049
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: head, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
3050
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("circle", { cx: "12", cy: "12", r: "10" }),
3051
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("line", { x1: "12", y1: "8", x2: "12", y2: "12" }),
3052
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("line", { x1: "12", y1: "16", x2: "12.01", y2: "16" })
3053
+ ] }),
3054
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { style: { fontSize: 15, fontWeight: 700, color: head }, children: "Failed to parse .visualli file" })
3055
+ ] }),
3056
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { style: { margin: 0, fontSize: 13, color: body, lineHeight: 1.6, wordBreak: "break-word", fontFamily: "monospace" }, children: message })
3057
+ ] }) });
3058
+ }
3059
+ var WORKER_SCRIPT = `
3060
+ self.onmessage = function(e) {
3061
+ var content = e.data;
3062
+ try {
3063
+ var lines = content.trim().split('\\n');
3064
+ var doc = {
3065
+ meta: null,
3066
+ layers: new Map(),
3067
+ layersByLevel: new Map(),
3068
+ rootLayer: null,
3069
+ };
3070
+ for (var i = 0; i < lines.length; i++) {
3071
+ var line = lines[i].trim();
3072
+ if (!line) continue;
3073
+ var obj;
3074
+ try { obj = JSON.parse(line); }
3075
+ catch(err) { throw new Error('Failed to parse JSON at line ' + (i+1) + ': ' + err); }
3076
+ if (!obj.type) throw new Error("Missing 'type' field at line " + (i+1));
3077
+ if (obj.type === 'meta') {
3078
+ doc.meta = obj;
3079
+ } else if (obj.type === 'layer') {
3080
+ doc.layers.set(obj.id, obj);
3081
+ if (!doc.layersByLevel.has(obj.level)) doc.layersByLevel.set(obj.level, []);
3082
+ doc.layersByLevel.get(obj.level).push(obj);
3083
+ if (obj.level === 0) doc.rootLayer = obj;
3084
+ }
3085
+ }
3086
+ if (!doc.meta) throw new Error('Missing required meta section');
3087
+ if (!doc.rootLayer) throw new Error('Missing root layer (level 0)');
3088
+ self.postMessage({ ok: true, doc: doc });
3089
+ } catch(err) {
3090
+ self.postMessage({ ok: false, message: err.message || String(err) });
3091
+ }
3092
+ };
3093
+ `;
3094
+ var _workerUrl = null;
3095
+ function getWorkerUrl() {
3096
+ if (!_workerUrl) {
3097
+ _workerUrl = URL.createObjectURL(new Blob([WORKER_SCRIPT], { type: "text/javascript" }));
3098
+ }
3099
+ return _workerUrl;
3100
+ }
3101
+ function useAsyncParse(content, useWorker) {
3102
+ const [state, setState] = (0, import_react15.useState)({ status: "idle" });
3103
+ const workerRef = (0, import_react15.useRef)(null);
3104
+ (0, import_react15.useEffect)(() => {
3105
+ if (!content) {
3106
+ setState({ status: "idle" });
3107
+ return;
3108
+ }
3109
+ setState({ status: "loading" });
3110
+ let cancelled = false;
3111
+ if (useWorker && typeof Worker !== "undefined") {
3112
+ workerRef.current?.terminate();
3113
+ const w = new Worker(getWorkerUrl());
3114
+ workerRef.current = w;
3115
+ w.onmessage = (e) => {
3116
+ if (cancelled) return;
3117
+ if (e.data.ok) {
3118
+ setState({ status: "ready", doc: e.data.doc });
3119
+ } else {
3120
+ setState({ status: "error", message: e.data.message });
3121
+ }
3122
+ };
3123
+ w.onerror = (e) => {
3124
+ if (!cancelled) setState({ status: "error", message: e.message });
3125
+ };
3126
+ w.postMessage(content);
3127
+ return () => {
3128
+ cancelled = true;
3129
+ w.terminate();
3130
+ workerRef.current = null;
3131
+ };
3132
+ }
3133
+ Promise.resolve().then(() => {
3134
+ if (cancelled) return;
3135
+ try {
3136
+ const doc = (0, import_core14.parseVisualliFile)(content);
3137
+ if (!cancelled) setState({ status: "ready", doc });
3138
+ } catch (err) {
3139
+ if (!cancelled) setState({ status: "error", message: err instanceof Error ? err.message : String(err) });
3140
+ }
3141
+ });
3142
+ return () => {
3143
+ cancelled = true;
3144
+ };
3145
+ }, [content, useWorker]);
3146
+ (0, import_react15.useEffect)(() => () => {
3147
+ workerRef.current?.terminate();
3148
+ }, []);
3149
+ return state;
3150
+ }
3151
+ function useFileText(file) {
3152
+ const [text, setText] = (0, import_react15.useState)(void 0);
3153
+ const [error, setError] = (0, import_react15.useState)(void 0);
3154
+ const [loading, setLoading] = (0, import_react15.useState)(false);
3155
+ (0, import_react15.useEffect)(() => {
3156
+ if (!file) {
3157
+ setText(void 0);
3158
+ setError(void 0);
3159
+ setLoading(false);
3160
+ return;
3161
+ }
3162
+ setLoading(true);
3163
+ setText(void 0);
3164
+ setError(void 0);
3165
+ let cancelled = false;
3166
+ if (typeof file === "string") {
3167
+ fetch(file).then((response) => {
3168
+ if (!response.ok) throw new Error(`Failed to fetch file: ${response.statusText}`);
3169
+ return response.text();
3170
+ }).then((t) => {
3171
+ if (!cancelled) {
3172
+ setText(t);
3173
+ setLoading(false);
3174
+ }
3175
+ }).catch((err) => {
3176
+ if (!cancelled) {
3177
+ setError(err instanceof Error ? err.message : String(err));
3178
+ setLoading(false);
3179
+ }
3180
+ });
3181
+ } else {
3182
+ file.text().then((t) => {
3183
+ if (!cancelled) {
3184
+ setText(t);
3185
+ setLoading(false);
3186
+ }
3187
+ }).catch((err) => {
3188
+ if (!cancelled) {
3189
+ setError(err instanceof Error ? err.message : String(err));
3190
+ setLoading(false);
3191
+ }
3192
+ });
3193
+ }
3194
+ return () => {
3195
+ cancelled = true;
3196
+ };
3197
+ }, [file]);
3198
+ return { text, error, loading };
3199
+ }
3200
+ function VisualliRenderer({
3201
+ visualliString,
3202
+ visualliFile,
3203
+ theme = "light",
3204
+ width = "100%",
3205
+ height = "100%",
3206
+ useWorker = true,
3207
+ chromaticImmersion = false,
3208
+ className,
3209
+ style
3210
+ }) {
3211
+ useFontInjection();
3212
+ const systemDark = useSystemDark();
3213
+ const isDark = theme === "dark" ? true : theme === "light" ? false : systemDark;
3214
+ const cssWidth = typeof width === "number" ? `${width}px` : width;
3215
+ const cssHeight = typeof height === "number" ? `${height}px` : height;
3216
+ const { text: fileText, error: fileReadError, loading: fileReading } = useFileText(visualliFile);
3217
+ const rawContent = fileText ?? visualliString;
3218
+ const parseState = useAsyncParse(rawContent, useWorker);
3219
+ const resolvedDoc = (0, import_react15.useMemo)(() => {
3220
+ if (parseState.status === "ready") return parseState.doc;
3221
+ return null;
3222
+ }, [parseState]);
3223
+ const wrapperStyle = { width: cssWidth, height: cssHeight, overflow: "hidden", ...style };
3224
+ if (!visualliString && !visualliFile) {
3225
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className, style: wrapperStyle, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(EmptyState, { isDark }) });
3226
+ }
3227
+ if (fileReading || parseState.status === "loading") {
3228
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className, style: wrapperStyle, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(LoadingState, { isDark }) });
3229
+ }
3230
+ if (fileReadError || parseState.status === "error") {
3231
+ const msg = fileReadError ?? (parseState.status === "error" ? parseState.message : "Unknown error");
3232
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className, style: wrapperStyle, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ErrorState, { message: msg, isDark }) });
3233
+ }
3234
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className, style: wrapperStyle, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
3235
+ VisualliCanvas,
3236
+ {
3237
+ preParsedVisualli: resolvedDoc ?? void 0,
3238
+ isDark,
3239
+ chromaticImmersion,
3240
+ style: { width: "100%", height: "100%" }
3241
+ }
3242
+ ) });
3243
+ }
3244
+
3245
+ // src/context/VisualliContext.tsx
3246
+ var import_react16 = require("react");
3247
+ var import_jsx_runtime13 = require("react/jsx-runtime");
3248
+ var VisualliContext = (0, import_react16.createContext)(null);
3249
+ function VisualliProvider({
3250
+ children
3251
+ }) {
3252
+ const value = {};
3253
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(VisualliContext.Provider, { value, children });
3254
+ }
3255
+ function useVisualli() {
3256
+ const ctx = (0, import_react16.useContext)(VisualliContext);
3257
+ if (!ctx) {
3258
+ return {};
3259
+ }
3260
+ return ctx;
3261
+ }
3262
+ //# sourceMappingURL=index.cjs.map