@visualli/react 0.1.7 → 0.2.0

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 (49) hide show
  1. package/README.md +13 -5
  2. package/dist/index.cjs +1900 -2033
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +354 -97
  5. package/dist/index.d.ts +354 -97
  6. package/dist/index.js +2005 -2095
  7. package/dist/index.js.map +1 -1
  8. package/fonts/Caveat-Variable.ttf +0 -0
  9. package/licenses/visualli-design-system/LICENSE +9 -0
  10. package/licenses/visualli-design-system/NOTICE.md +6 -0
  11. package/package.json +9 -4
  12. package/src/VisualliCanvas.tsx +452 -409
  13. package/src/VisualliRenderer.tsx +90 -112
  14. package/src/components/A11yLayer.tsx +73 -0
  15. package/src/components/KonvaContainer.tsx +28 -237
  16. package/src/components/KonvaContainerLabelLayer.tsx +41 -0
  17. package/src/components/KonvaContainerLayer.tsx +34 -33
  18. package/src/components/KonvaEdge.tsx +25 -240
  19. package/src/components/KonvaEdgeLayer.tsx +69 -49
  20. package/src/components/KonvaNode.tsx +21 -156
  21. package/src/components/KonvaNodeLayer.tsx +102 -34
  22. package/src/components/KonvaStage.tsx +17 -3
  23. package/src/components/NavigationStack.tsx +16 -159
  24. package/src/components/Overlays.tsx +327 -0
  25. package/src/components/ZoomControls.tsx +3 -107
  26. package/src/components/useFrames.ts +30 -0
  27. package/src/context/VisualliContext.tsx +3 -3
  28. package/src/design/choreography.ts +157 -0
  29. package/src/design/containers.ts +150 -0
  30. package/src/design/design.ts +115 -0
  31. package/src/design/drawing.ts +303 -0
  32. package/src/design/measure.ts +34 -0
  33. package/src/design/runtime.ts +154 -0
  34. package/src/design/useDesign.ts +38 -0
  35. package/src/generated/specCss.ts +15 -0
  36. package/src/hooks/index.ts +0 -2
  37. package/src/hooks/useKonvaRenderer.ts +13 -18
  38. package/src/hooks/useLayerChoreography.ts +159 -0
  39. package/src/hooks/useViewportNodes.ts +0 -1
  40. package/src/index.ts +22 -22
  41. package/src/stores/stores.ts +2 -2
  42. package/src/stores/useViewportStore.ts +12 -2
  43. package/src/animations/index.ts +0 -2
  44. package/src/animations/konvaLayerTransition.ts +0 -223
  45. package/src/components/SketchyBoxKonva.tsx +0 -121
  46. package/src/config/blobShapes.ts +0 -204
  47. package/src/config/index.ts +0 -21
  48. package/src/config/textScaling.ts +0 -69
  49. package/src/hooks/useKonvaLayerTransition.ts +0 -221
package/dist/index.cjs CHANGED
@@ -12,9 +12,9 @@ var __export = (target, all) => {
12
12
  };
13
13
  var __copyProps = (to, from, except, desc) => {
14
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 });
15
+ for (let key2 of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key2) && key2 !== except)
17
+ __defProp(to, key2, { get: () => from[key2], enumerable: !(desc = __getOwnPropDesc(from, key2)) || desc.enumerable });
18
18
  }
19
19
  return to;
20
20
  };
@@ -31,44 +31,68 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  // src/index.ts
32
32
  var index_exports = {};
33
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,
34
+ A11yLayer: () => A11yLayer,
35
+ BLOB_SHAPES: () => import_core19.BLOB_SHAPES,
36
+ DEFAULT_DESIGN: () => DEFAULT_DESIGN,
37
+ DESIGN_SYSTEM_VERSION: () => import_core19.DESIGN_SYSTEM_VERSION,
38
+ DepthTrail: () => DepthTrail,
39
+ IDEA: () => import_core19.IDEA,
39
40
  KonvaContainer: () => KonvaContainer_default,
41
+ KonvaContainerLabelLayer: () => KonvaContainerLabelLayer,
40
42
  KonvaContainerLayer: () => KonvaContainerLayer,
41
43
  KonvaEdge: () => KonvaEdge_default,
42
44
  KonvaEdgeLayer: () => KonvaEdgeLayer,
43
- KonvaLayerTransitionAnimator: () => KonvaLayerTransitionAnimator,
44
45
  KonvaNode: () => KonvaNode_default,
45
46
  KonvaNodeLayer: () => KonvaNodeLayer,
46
47
  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,
48
+ LABEL_GROWTH: () => import_core19.LABEL_GROWTH,
49
+ METRICS: () => import_core19.METRICS,
49
50
  NavigationStack: () => NavigationStack_default,
50
- SketchyBoxKonva: () => SketchyBoxKonva_default,
51
+ PeekCard: () => PeekCard,
52
+ RINGS: () => import_core19.RINGS,
53
+ THEME_NAMES: () => import_core19.THEME_NAMES,
54
+ TOKENS: () => import_core19.TOKENS,
55
+ TOPICS: () => import_core19.TOPICS,
56
+ TermCard: () => TermCard,
51
57
  VisualliCanvas: () => VisualliCanvas,
52
58
  VisualliProvider: () => VisualliProvider,
53
59
  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,
60
+ ZoomControls: () => ZoomControls,
61
+ arrowPath: () => import_core19.arrowPath,
62
+ blobPath: () => import_core19.blobPath,
63
+ blobRadius: () => import_core19.blobRadius,
64
+ calculateFitCenter: () => import_core3.calculateFitCenter,
65
+ calculateFitView: () => import_core3.calculateFitView,
66
+ calculateFitZoom: () => import_core3.calculateFitZoom,
67
+ drawHull: () => drawHull,
68
+ drawPill: () => drawPill,
69
+ edgePath: () => import_core19.edgePath,
70
+ ensureDesignSystemStyles: () => ensureDesignSystemStyles,
71
+ getChildLayerForNode: () => import_core3.getChildLayerForNode,
72
+ getConnectionsForLayer: () => import_core3.getConnectionsForLayer,
73
+ getLayerForNavigation: () => import_core3.getLayerForNavigation,
74
+ getTokens: () => import_core19.getTokens,
75
+ ideaColor: () => import_core19.ideaColor,
76
+ ideaMeasure: () => ideaMeasure,
77
+ ideaSize: () => import_core19.ideaSize,
78
+ ideaStyle: () => import_core19.ideaStyle,
79
+ labelGrowth: () => import_core19.labelGrowth,
80
+ layoutContainers: () => layoutContainers,
81
+ loadCanvasFonts: () => loadCanvasFonts,
82
+ makeDesign: () => makeDesign,
83
+ measurePill: () => measurePill,
84
+ paintVars: () => paintVars,
85
+ resolveComfort: () => import_core19.resolveComfort,
86
+ resolveDesign: () => resolveDesign,
87
+ resolveTheme: () => import_core19.resolveTheme,
88
+ shapeForLevel: () => import_core19.shapeForLevel,
89
+ splitTerms: () => splitTerms,
90
+ topicStyle: () => import_core19.topicStyle,
91
+ useDesign: () => useDesign,
70
92
  useKonvaRenderer: () => useKonvaRenderer,
93
+ useLayerChoreography: () => useLayerChoreography,
71
94
  useNodeStore: () => useNodeStore,
95
+ useReaderEnv: () => useReaderEnv,
72
96
  useRenderConfigStore: () => useRenderConfigStore,
73
97
  useSelectionStore: () => useSelectionStore,
74
98
  useViewportNodes: () => useViewportNodes,
@@ -79,11 +103,11 @@ module.exports = __toCommonJS(index_exports);
79
103
 
80
104
  // src/VisualliRenderer.tsx
81
105
  var import_react15 = require("react");
82
- var import_core14 = require("@visualli/core");
106
+ var import_core16 = require("@visualli/core");
83
107
 
84
108
  // src/VisualliCanvas.tsx
85
109
  var import_react14 = require("react");
86
- var import_core13 = require("@visualli/core");
110
+ var import_core15 = require("@visualli/core");
87
111
 
88
112
  // src/stores/useNodeStore.ts
89
113
  var import_zustand = require("zustand");
@@ -180,13 +204,16 @@ var useViewportStore = (0, import_zustand2.create)((set, get) => {
180
204
  ...init,
181
205
  canvasWidth: import_core.DEFAULT_CANVAS_WIDTH,
182
206
  canvasHeight: import_core.DEFAULT_CANVAS_HEIGHT,
207
+ fitScale: 1,
208
+ fitCenterX: 0,
209
+ fitCenterY: 0,
183
210
  pan: (dx, dy) => set((s) => {
184
211
  const updated = (0, import_core.recalculateViewportBounds)((0, import_core.panViewport)(dx, dy, getVP(s)), s.canvasWidth, s.canvasHeight);
185
212
  return { centerX: updated.centerX, centerY: updated.centerY, visibleBounds: updated.visibleBounds };
186
213
  }),
187
214
  zoom: (delta, pivotX, pivotY) => set((s) => {
188
215
  const updated = (0, import_core.recalculateViewportBounds)(
189
- (0, import_core.zoomViewport)(delta, getVP(s), pivotX, pivotY, s.canvasWidth, s.canvasHeight),
216
+ (0, import_core.zoomViewport)(delta, getVP(s), pivotX, pivotY, s.canvasWidth, s.canvasHeight, s.fitScale),
190
217
  s.canvasWidth,
191
218
  s.canvasHeight
192
219
  );
@@ -197,10 +224,11 @@ var useViewportStore = (0, import_zustand2.create)((set, get) => {
197
224
  return { centerX: updated.centerX, centerY: updated.centerY, visibleBounds: updated.visibleBounds };
198
225
  }),
199
226
  setZoom: (level) => set((s) => {
200
- const clamped = (0, import_core.clampZoom)(level);
227
+ const clamped = (0, import_core.clampZoom)(level, s.fitScale);
201
228
  const updated = (0, import_core.recalculateViewportBounds)({ ...getVP(s), zoomLevel: clamped }, s.canvasWidth, s.canvasHeight);
202
229
  return { zoomLevel: updated.zoomLevel, visibleBounds: updated.visibleBounds };
203
230
  }),
231
+ setFit: (scale, centerX, centerY) => set({ fitScale: scale > 0 ? scale : 1, fitCenterX: centerX, fitCenterY: centerY }),
204
232
  updateCanvasSize: (w, h) => set((s) => {
205
233
  const updated = (0, import_core.recalculateViewportBounds)(getVP(s), w, h);
206
234
  return { canvasWidth: w, canvasHeight: h, visibleBounds: updated.visibleBounds };
@@ -220,11 +248,11 @@ var useViewportStore = (0, import_zustand2.create)((set, get) => {
220
248
  // src/stores/stores.ts
221
249
  var import_zustand3 = require("zustand");
222
250
  var import_core2 = require("@visualli/core");
223
- var useSelectionStore = (0, import_zustand3.create)((set, get, store) => {
251
+ var useSelectionStore = (0, import_zustand3.create)((set) => {
224
252
  import_core2.selectionStore.subscribe((state) => set(state));
225
253
  return import_core2.selectionStore.getState();
226
254
  });
227
- var useRenderConfigStore = (0, import_zustand3.create)((set, get, store) => {
255
+ var useRenderConfigStore = (0, import_zustand3.create)((set) => {
228
256
  import_core2.renderConfigStore.subscribe((state) => set(state));
229
257
  return import_core2.renderConfigStore.getState();
230
258
  });
@@ -233,13 +261,10 @@ var useRenderConfigStore = (0, import_zustand3.create)((set, get, store) => {
233
261
  var import_react = require("react");
234
262
  function useKonvaRenderer({
235
263
  containerRef,
236
- stageRef,
237
264
  enabled = true
238
265
  }) {
239
266
  const pan = useViewportStore((s) => s.pan);
240
267
  const zoom = useViewportStore((s) => s.zoom);
241
- const setZoom = useViewportStore((s) => s.setZoom);
242
- const setCenter = useViewportStore((s) => s.setCenter);
243
268
  const updateCanvasSize = useViewportStore((s) => s.updateCanvasSize);
244
269
  const autoAdjust = useRenderConfigStore((s) => s.autoAdjustQuality);
245
270
  const [canvasWidth, setCanvasWidth] = (0, import_react.useState)(0);
@@ -297,7 +322,8 @@ function useKonvaRenderer({
297
322
  window.addEventListener("resize", updateSize);
298
323
  }
299
324
  return () => {
300
- ro ? ro.disconnect() : window.removeEventListener("resize", updateSize);
325
+ if (ro) ro.disconnect();
326
+ else window.removeEventListener("resize", updateSize);
301
327
  if (rafId) cancelAnimationFrame(rafId);
302
328
  };
303
329
  }, [enabled, containerRef, updateCanvasSize]);
@@ -317,17 +343,10 @@ function useKonvaRenderer({
317
343
  isPanningRef.current = true;
318
344
  lastPanPointRef.current = { x: e.evt.clientX, y: e.evt.clientY };
319
345
  panAccumRef.current = { x: 0, y: 0 };
320
- const stage = e.target.getStage();
321
- if (stage) stage.container().style.cursor = "grabbing";
322
346
  }
323
347
  }, []);
324
348
  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
- }
349
+ if (!isPanningRef.current) return;
331
350
  e.evt.preventDefault();
332
351
  const deltaX = e.evt.clientX - lastPanPointRef.current.x;
333
352
  const deltaY = e.evt.clientY - lastPanPointRef.current.y;
@@ -341,7 +360,7 @@ function useKonvaRenderer({
341
360
  panAccumRef.current.y += deltaY;
342
361
  lastPanPointRef.current = { x: e.evt.clientX, y: e.evt.clientY };
343
362
  }, []);
344
- const handleMouseUp = (0, import_react.useCallback)((e) => {
363
+ const handleMouseUp = (0, import_react.useCallback)(() => {
345
364
  if (isPanningRef.current) {
346
365
  isPanningRef.current = false;
347
366
  const { x, y } = panAccumRef.current;
@@ -349,348 +368,422 @@ function useKonvaRenderer({
349
368
  pan(x, y);
350
369
  panAccumRef.current = { x: 0, y: 0 };
351
370
  }
352
- const stage = e.target.getStage();
353
- if (stage) stage.container().style.cursor = "grab";
354
371
  }
355
372
  }, [pan]);
356
- return { handleWheel, handleMouseDown, handleMouseMove, handleMouseUp, canvasWidth, canvasHeight };
373
+ const cancelPan = (0, import_react.useCallback)(() => {
374
+ if (!isPanningRef.current) return;
375
+ isPanningRef.current = false;
376
+ const { x, y } = panAccumRef.current;
377
+ if (x !== 0 || y !== 0) {
378
+ pan(x, y);
379
+ panAccumRef.current = { x: 0, y: 0 };
380
+ }
381
+ }, [pan]);
382
+ return { handleWheel, handleMouseDown, handleMouseMove, handleMouseUp, cancelPan, canvasWidth, canvasHeight };
357
383
  }
358
384
 
359
- // src/hooks/useKonvaLayerTransition.ts
385
+ // src/hooks/useLayerChoreography.ts
360
386
  var import_react2 = require("react");
387
+ var import_core4 = require("@visualli/core");
361
388
 
362
- // src/animations/konvaLayerTransition.ts
389
+ // src/utils/layerNavigation.ts
363
390
  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;
391
+
392
+ // src/hooks/useLayerChoreography.ts
393
+ function useLayerChoreography(o) {
394
+ const { stageRef, canvasWrapperRef } = o;
395
+ const opts = (0, import_react2.useRef)(o);
396
+ opts.current = o;
397
+ const busy = (0, import_react2.useRef)(false);
398
+ const waiting = (0, import_react2.useRef)(false);
399
+ const anims = (0, import_react2.useRef)([]);
400
+ const timers = (0, import_react2.useRef)([]);
401
+ const after = (0, import_react2.useRef)([]);
402
+ const clear = (0, import_react2.useCallback)(() => {
403
+ timers.current.forEach(clearTimeout);
404
+ timers.current = [];
405
+ after.current.forEach(cancelAnimationFrame);
406
+ after.current = [];
407
+ anims.current.forEach((a) => a.cancel());
408
+ anims.current = [];
409
+ }, []);
410
+ const applyViewport = (0, import_react2.useCallback)((vp) => {
411
+ const stage = stageRef.current;
412
+ if (stage) {
413
+ stage.x(stage.width() / 2 - vp.centerX * vp.zoomLevel);
414
+ stage.y(stage.height() / 2 - vp.centerY * vp.zoomLevel);
415
+ stage.scaleX(vp.zoomLevel);
416
+ stage.scaleY(vp.zoomLevel);
417
+ stage.getLayers().forEach((l) => l.drawScene());
418
+ }
419
+ const s = useViewportStore.getState();
420
+ s.setCenter(vp.centerX, vp.centerY);
421
+ s.setZoom(vp.zoomLevel);
422
+ }, [stageRef]);
423
+ const lifecycle = (0, import_react2.useCallback)((phase) => {
424
+ const stage = stageRef.current;
425
+ stage?.getLayers().forEach((l) => {
426
+ l.listening(phase === "end");
427
+ });
428
+ if (phase === "end") stage?.batchDraw();
429
+ opts.current.onLifecycle?.(phase);
430
+ }, [stageRef]);
431
+ const leave = (0, import_react2.useCallback)((wrap, dir, origin, vp, swap) => {
432
+ busy.current = true;
433
+ waiting.current = false;
434
+ lifecycle("start");
435
+ const reduced = opts.current.reducedMotion;
436
+ if (wrap && !reduced && typeof wrap.animate === "function") {
437
+ const m = import_core4.MOTION;
438
+ wrap.style.transformOrigin = origin;
439
+ wrap.style.willChange = "transform, opacity";
440
+ if (dir === "in") {
441
+ anims.current.push(
442
+ wrap.animate([{ transform: "scale(1)" }, { transform: `scale(${m.dive.scale})` }], { duration: m.dive.duration, easing: (0, import_core4.cssEase)(m.dive.ease), fill: "forwards" }),
443
+ wrap.animate([{ opacity: 1 }, { opacity: 0 }], { delay: m.dive.fadeDelay, duration: m.dive.fade, easing: "ease", fill: "both" })
444
+ );
445
+ } else {
446
+ anims.current.push(
447
+ wrap.animate([{ transform: "scale(1)" }, { transform: `scale(${m.surface.scale})` }], { duration: m.surface.duration, easing: (0, import_core4.cssEase)(m.surface.ease), fill: "forwards" }),
448
+ wrap.animate([{ opacity: 1 }, { opacity: 0 }], { duration: m.surface.fade, easing: "ease", fill: "both" })
449
+ );
450
+ }
451
+ }
452
+ timers.current.push(setTimeout(() => {
453
+ waiting.current = true;
454
+ swap();
455
+ applyViewport(vp);
456
+ timers.current.push(setTimeout(() => arriveRef.current(), 500));
457
+ }, (0, import_core4.swapDelay)(dir, reduced)));
458
+ }, [applyViewport, lifecycle]);
459
+ const arriveRef = (0, import_react2.useRef)(() => {
460
+ });
461
+ const arrive = (0, import_react2.useCallback)(() => {
462
+ if (!waiting.current) return;
463
+ waiting.current = false;
464
+ const wrap = canvasWrapperRef.current;
465
+ const reduced = opts.current.reducedMotion;
466
+ anims.current.forEach((a) => a.cancel());
467
+ anims.current = [];
468
+ let settle = reduced ? import_core4.MOTION.reduced.fade : import_core4.MOTION.settle.duration;
469
+ if (wrap && !reduced && typeof wrap.animate === "function") {
470
+ const a = wrap.animate([{ transform: `scale(${import_core4.MOTION.settle.fromScale})` }, { transform: "none" }], { duration: import_core4.MOTION.settle.duration, easing: (0, import_core4.cssEase)(import_core4.MOTION.settle.ease) });
471
+ anims.current.push(a);
472
+ settle = import_core4.MOTION.settle.duration;
473
+ }
474
+ timers.current.push(setTimeout(() => {
475
+ anims.current.forEach((a) => a.cancel());
476
+ anims.current = [];
477
+ if (wrap) {
478
+ wrap.style.willChange = "auto";
479
+ wrap.style.transformOrigin = "";
480
+ }
481
+ busy.current = false;
482
+ opts.current.onComplete();
483
+ lifecycle("end");
484
+ }, settle));
485
+ }, [canvasWrapperRef, lifecycle]);
486
+ arriveRef.current = arrive;
487
+ const stepInside = (0, import_react2.useCallback)((targetNode, childLayerId, childNodes) => {
488
+ if (busy.current) return;
489
+ const stage = stageRef.current;
490
+ const zoom = stage?.scaleX() ?? 1;
491
+ const origin = stage ? `${targetNode.x * zoom + stage.x()}px ${targetNode.y * zoom + stage.y()}px` : "50% 50%";
492
+ const fit = (0, import_core3.calculateFitView)(childNodes, stage?.width() ?? 800, stage?.height() ?? 600);
493
+ leave(canvasWrapperRef.current, "in", origin, { centerX: fit.centerX, centerY: fit.centerY, zoomLevel: fit.zoomLevel }, () => opts.current.onSwapLayer(childLayerId));
494
+ }, [stageRef, canvasWrapperRef, leave]);
495
+ const backOut = (0, import_react2.useCallback)((parent) => {
496
+ if (busy.current) return;
497
+ leave(canvasWrapperRef.current, "out", "50% 50%", parent, () => opts.current.onSwapBack());
498
+ }, [canvasWrapperRef, leave]);
499
+ const isTransitioning = (0, import_react2.useCallback)(() => busy.current, []);
500
+ (0, import_react2.useEffect)(() => () => {
501
+ clear();
502
+ busy.current = false;
503
+ }, [clear]);
504
+ return { stepInside, backOut, arrive, isTransitioning };
379
505
  }
380
- var KonvaLayerTransitionAnimator = class {
506
+
507
+ // src/design/choreography.ts
508
+ var import_core5 = require("@visualli/core");
509
+ var clamp01 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
510
+ var RevealClock = class {
381
511
  constructor() {
382
- this.rafId = null;
383
- this.timers = [];
384
- this._active = false;
512
+ this.t0 = -Infinity;
513
+ this.end = 0;
514
+ this.plan = (0, import_core5.layerReveal)(0, 0);
515
+ this.reduced = false;
516
+ this.center = { x: 0, y: 0 };
517
+ this.order = /* @__PURE__ */ new Map();
518
+ this.listeners = /* @__PURE__ */ new Set();
519
+ this.timer = null;
385
520
  }
386
- get active() {
387
- return this._active;
521
+ /** Begin a layer's arrival. `reduced` (reduced motion or instant reveal): everything fades in together. */
522
+ start(nodes, edgeCount, reduced, now = performance.now()) {
523
+ this.order.clear();
524
+ nodes.forEach((n, i) => this.order.set(n.id, i));
525
+ this.plan = (0, import_core5.layerReveal)(nodes.length, edgeCount);
526
+ this.reduced = reduced || this.plan.instant;
527
+ reduced = this.reduced;
528
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
529
+ for (const n of nodes) {
530
+ minX = Math.min(minX, n.x);
531
+ maxX = Math.max(maxX, n.x);
532
+ minY = Math.min(minY, n.y);
533
+ maxY = Math.max(maxY, n.y);
534
+ }
535
+ this.center = nodes.length ? { x: (minX + maxX) / 2, y: (minY + maxY) / 2 } : { x: 0, y: 0 };
536
+ this.t0 = now;
537
+ this.end = reduced ? import_core5.MOTION.reduced.fade : this.plan.total;
538
+ this.emit(true);
539
+ if (this.timer) clearTimeout(this.timer);
540
+ this.timer = setTimeout(() => {
541
+ this.timer = null;
542
+ this.emit(false);
543
+ }, this.end + 50);
388
544
  }
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;
545
+ /** Jump to the finished state (e.g. on unmount). */
546
+ stop() {
547
+ this.t0 = -Infinity;
548
+ if (this.timer) {
549
+ clearTimeout(this.timer);
550
+ this.timer = null;
551
+ }
552
+ this.emit(false);
397
553
  }
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);
554
+ /** True while the layer is still arriving. */
555
+ active(now = performance.now()) {
556
+ return now - this.t0 < this.end;
464
557
  }
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);
558
+ /** Called with true when an arrival starts and false when it has finished. Returns the unsubscribe function. */
559
+ subscribe(fn) {
560
+ this.listeners.add(fn);
561
+ return () => {
562
+ this.listeners.delete(fn);
516
563
  };
517
- this.rafId = requestAnimationFrame(tick);
518
564
  }
519
- _complete(onComplete) {
520
- this.timers.forEach(clearTimeout);
521
- this.timers = [];
522
- this._active = false;
523
- onComplete();
565
+ emit(running) {
566
+ this.listeners.forEach((fn) => fn(running));
567
+ }
568
+ /** Fills `out` with idea `id`'s arrival look. Returns false when it has arrived (nothing to apply). */
569
+ node(id, x, y, now, out) {
570
+ const el = now - this.t0;
571
+ if (el >= this.end) return false;
572
+ const i = this.order.get(id);
573
+ if (i === void 0) return false;
574
+ if (this.reduced) {
575
+ out.alpha = clamp01(el / import_core5.MOTION.reduced.fade);
576
+ out.scale = 1;
577
+ out.dx = 0;
578
+ out.dy = 0;
579
+ return true;
580
+ }
581
+ const r = import_core5.MOTION.reveal;
582
+ const t = el - this.plan.ideaDelay(i);
583
+ if (t >= r.duration) return false;
584
+ const e = t <= 0 ? 0 : (0, import_core5.easeBezier)(r.ease, t / r.duration);
585
+ const from = (0, import_core5.bloomOffset)({ x, y }, this.center);
586
+ out.alpha = clamp01(e);
587
+ out.scale = r.fromScale + (1 - r.fromScale) * e;
588
+ out.dx = from.x * (1 - e);
589
+ out.dy = from.y * (1 - e);
590
+ return true;
591
+ }
592
+ /** Fills `out` with connector `i`'s arrival look. Returns false when it has arrived. */
593
+ edge(i, dashed, now, out) {
594
+ const el = now - this.t0;
595
+ if (el >= this.end) return false;
596
+ if (this.reduced) {
597
+ out.alpha = clamp01(el / import_core5.MOTION.reduced.fade);
598
+ out.draw = 1;
599
+ return true;
600
+ }
601
+ const c = import_core5.MOTION.connector;
602
+ const t = el - this.plan.connectorDelay(i);
603
+ if (t >= c.draw && t >= c.fade) return false;
604
+ const group = t <= 0 ? 0 : (0, import_core5.easeBezier)(c.ease, clamp01(t / c.fade));
605
+ const line = t <= 0 ? 0 : (0, import_core5.easeBezier)(c.ease, clamp01(t / c.draw));
606
+ out.alpha = dashed ? group * line : group;
607
+ out.draw = dashed ? 1 : line;
608
+ return true;
609
+ }
610
+ };
611
+ var HoverTweens = class {
612
+ constructor() {
613
+ this.tweens = /* @__PURE__ */ new Map();
614
+ this.raised = /* @__PURE__ */ new Set();
615
+ this.cache = { lift: 0, ring: 0 };
616
+ }
617
+ /** Set which ideas are raised now. Returns true when anything started moving. */
618
+ set(next, reduced, now = performance.now()) {
619
+ let changed = false;
620
+ const ids = /* @__PURE__ */ new Set([...this.raised, ...next]);
621
+ for (const id of ids) {
622
+ const was = this.raised.has(id), is = next.has(id);
623
+ if (was === is) continue;
624
+ const cur = this.progress(id, now);
625
+ this.tweens.set(id, { fromLift: cur.lift, fromRing: cur.ring, to: is ? 1 : 0, t0: reduced ? -Infinity : now });
626
+ changed = true;
627
+ }
628
+ this.raised = new Set(next);
629
+ return changed;
630
+ }
631
+ /** Lift (bloom ease) and ring (standard ease) progress of an idea, 0 at rest and 1 fully raised. */
632
+ progress(id, now) {
633
+ const tw = this.tweens.get(id);
634
+ const out = this.cache;
635
+ if (!tw) {
636
+ out.lift = out.ring = this.raised.has(id) ? 1 : 0;
637
+ return out;
638
+ }
639
+ const u = clamp01((now - tw.t0) / import_core5.MOTION.hover.duration);
640
+ out.lift = tw.fromLift + (tw.to - tw.fromLift) * (0, import_core5.easeBezier)(import_core5.MOTION.hover.liftEase, u);
641
+ out.ring = tw.fromRing + (tw.to - tw.fromRing) * (0, import_core5.easeBezier)(import_core5.MOTION.hover.ringEase, u);
642
+ if (u >= 1) {
643
+ this.tweens.delete(id);
644
+ out.lift = out.ring = tw.to;
645
+ }
646
+ return out;
647
+ }
648
+ /** True while any idea is still moving. */
649
+ active(now = performance.now()) {
650
+ for (const tw of this.tweens.values()) if (now - tw.t0 < import_core5.MOTION.hover.duration) return true;
651
+ return false;
652
+ }
653
+ has(id) {
654
+ return this.tweens.has(id) || this.raised.has(id);
524
655
  }
525
656
  };
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
657
 
677
658
  // src/components/KonvaStage.tsx
678
- var import_react3 = require("react");
659
+ var import_react4 = require("react");
679
660
 
680
661
  // src/konvaCompat.ts
681
662
  var import_react_konva = require("react-konva");
682
663
  var KStage = import_react_konva.Stage;
683
664
  var KLayer = import_react_konva.Layer;
684
- var KGroup = import_react_konva.Group;
685
665
  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;
666
+
667
+ // src/components/KonvaStage.tsx
668
+ var import_konva = __toESM(require("konva"), 1);
669
+
670
+ // src/design/runtime.ts
671
+ var import_react3 = require("react");
672
+ var import_core6 = require("@visualli/core");
673
+
674
+ // src/generated/specCss.ts
675
+ var SPEC_TOKENS_CSS = '/* Spec tokens for all 8 themes \u2014 generated from the Visualli design system (v0.2.0). Do not edit here: changes come in through PRs from the private design system. MIT license, see ../LICENSE. */\n:root, [data-theme="light"] {\n --font-display: "Atkinson Hyperlegible Next", system-ui, sans-serif;\n --font-ui: "Atkinson Hyperlegible Next", system-ui, -apple-system, "Segoe UI", sans-serif;\n --font-hand: Kalam, "Atkinson Hyperlegible Next", cursive;\n --font-note: Caveat, Kalam, cursive;\n --font-mono: "Atkinson Hyperlegible Mono", ui-monospace, "SF Mono", Menlo, monospace;\n --canvas: #f6f3ee;\n --surface: #ffffff;\n --surface-sunken: #f0ece5;\n --surface-overlay: #ffffff;\n --line: #e4dfd6;\n --line-strong: #7a7f94;\n --ink: #182140;\n --ink-muted: #4f5671;\n --ink-subtle: #5f647c;\n --action: #182140;\n --on-action: #ffffff;\n --focus: #325e8c;\n --link: #2f5a87;\n --selection: rgba(18, 199, 211, 0.28);\n --node-ink: #182140;\n --edge: #2a3048;\n --edge-label: #4f5671;\n --topic-teal: #87f2f8;\n --topic-teal-ring: #00cdd5;\n --topic-harbor: #bcdbff;\n --topic-harbor-ring: #76b2f5;\n --topic-iris: #e0c8fe;\n --topic-iris-ring: #bd96ea;\n --topic-berry: #ffbdc4;\n --topic-berry-ring: #f47e8f;\n --topic-coral: #ffc7b2;\n --topic-coral-ring: #f38e65;\n --topic-amber: #ffcf9d;\n --topic-amber-ring: #eb9c38;\n --topic-sun: #ffea94;\n --topic-sun-ring: #dfbd04;\n --topic-stone: #e0dacf;\n --topic-stone-ring: #bab09f;\n --space-1: 4px;\n --space-2: 8px;\n --space-3: 12px;\n --space-4: 16px;\n --space-7: 48px;\n --radius-sm: 10px;\n --radius-md: 16px;\n --radius-lg: 24px;\n --radius-pill: 999px;\n --shadow-float: 0 1px 2px rgba(24, 33, 64, 0.06), 0 8px 24px rgba(24, 33, 64, 0.08);\n --shadow-pop: 0 2px 4px rgba(24, 33, 64, 0.08), 0 16px 40px rgba(24, 33, 64, 0.14);\n --node-width: 200px;\n --node-height: 148px;\n --node-stroke: 3px;\n --edge-width: 2.5px;\n --edge-gap: 15px;\n --edge-arc-max: 45px;\n --immersion-alpha: 0.15;\n --zoom-min: 0.3;\n --zoom-max: 5;\n --duration-instant: 100ms;\n --duration-quick: 160ms;\n --duration-base: 240ms;\n --duration-zoom: 500ms;\n --duration-reveal: 420ms;\n --stagger-reveal: 70ms;\n --ease-zoom: cubic-bezier(0.2, 0.8, 0.2, 1);\n --ease-standard: cubic-bezier(0.4, 0, 0.2, 1);\n --ease-bloom: cubic-bezier(0.34, 1.4, 0.64, 1);\n --z-chrome: 10;\n --z-card: 20;\n --z-menu: 40;\n }\n[data-theme="dark"] {\n --canvas: #0f1220;\n --surface: #171b2b;\n --surface-sunken: #0f1220;\n --surface-overlay: #212639;\n --line: #2b3148;\n --line-strong: #6c7392;\n --ink: #eef0f7;\n --ink-muted: #b3b8cc;\n --ink-subtle: #8f95ae;\n --action: #f6f3ee;\n --on-action: #182140;\n --focus: #12c7d3;\n --link: #5fd8e1;\n --selection: rgba(18, 199, 211, 0.32);\n --edge: #c9cddc;\n --edge-label: #b3b8cc;\n --topic-teal-ring: #0fc5cd;\n --topic-harbor-ring: #73aceb;\n --topic-iris-ring: #b691e1;\n --topic-berry-ring: #eb798a;\n --topic-coral-ring: #ea8962;\n --topic-amber-ring: #e29737;\n --topic-sun-ring: #d8b70a;\n --topic-stone-ring: #b4a999;\n --shadow-float: 0 1px 2px rgba(0, 0, 0, 0.40), 0 8px 24px rgba(0, 0, 0, 0.35);\n --shadow-pop: 0 2px 4px rgba(0, 0, 0, 0.50), 0 16px 40px rgba(0, 0, 0, 0.50);\n }\n[data-theme="focus-light"] {\n --canvas: #f4f2ee;\n --edge: #5a6079;\n --edge-label: #5a6079;\n --topic-teal: #bce8ea;\n --topic-teal-ring: #67c6cb;\n --topic-harbor: #c9d9ec;\n --topic-harbor-ring: #8bb2de;\n --topic-iris: #d9cfe8;\n --topic-iris-ring: #b79ed4;\n --topic-berry: #edc6c9;\n --topic-berry-ring: #da9099;\n --topic-coral: #eecec1;\n --topic-coral-ring: #db9b82;\n --topic-amber: #eed4ba;\n --topic-amber-ring: #d6a570;\n --topic-sun: #f4eac1;\n --topic-sun-ring: #d2bf73;\n --topic-stone: #dddad5;\n }\n[data-theme="focus-dark"] {\n --canvas: #12141c;\n --surface: #171b2b;\n --surface-sunken: #0f1220;\n --surface-overlay: #212639;\n --line: #2b3148;\n --line-strong: #6c7392;\n --ink: #eef0f7;\n --ink-muted: #b3b8cc;\n --ink-subtle: #8f95ae;\n --action: #f6f3ee;\n --on-action: #182140;\n --focus: #12c7d3;\n --link: #5fd8e1;\n --selection: rgba(18, 199, 211, 0.32);\n --edge: #a9aec0;\n --edge-label: #b3b8cc;\n --topic-teal: #bce8ea;\n --topic-teal-ring: #71bdc1;\n --topic-harbor: #c9d9ec;\n --topic-harbor-ring: #8cabce;\n --topic-iris: #d9cfe8;\n --topic-iris-ring: #ae9bc6;\n --topic-berry: #edc6c9;\n --topic-berry-ring: #cc8e95;\n --topic-coral: #eecec1;\n --topic-coral-ring: #cd9884;\n --topic-amber: #eed4ba;\n --topic-amber-ring: #c9a176;\n --topic-sun: #f4eac1;\n --topic-sun-ring: #c9b97a;\n --topic-stone: #dddad5;\n --topic-stone-ring: #b4a999;\n }\n[data-theme="colorsafe-light"] {\n --topic-teal: #b0ecbc;\n --topic-teal-ring: #6cc982;\n --topic-harbor: #abdbfe;\n --topic-harbor-ring: #60b4ec;\n --topic-iris: #f6bde2;\n --topic-iris-ring: #dc87c0;\n --topic-berry: #feb79e;\n --topic-berry-ring: #ec7f58;\n --topic-coral: #fece96;\n --topic-coral-ring: #e49d41;\n --topic-amber: #b2e8fa;\n --topic-amber-ring: #5cc4e2;\n --topic-sun: #f6f097;\n --topic-sun-ring: #d0c637;\n --topic-stone: #dbdbdb;\n --topic-stone-ring: #b7afb1;\n }\n[data-theme="colorsafe-dark"] {\n --canvas: #0f1220;\n --surface: #171b2b;\n --surface-sunken: #0f1220;\n --surface-overlay: #212639;\n --line: #2b3148;\n --line-strong: #6c7392;\n --ink: #eef0f7;\n --ink-muted: #b3b8cc;\n --ink-subtle: #8f95ae;\n --action: #f6f3ee;\n --on-action: #182140;\n --focus: #12c7d3;\n --link: #5fd8e1;\n --selection: rgba(18, 199, 211, 0.32);\n --edge: #c9cddc;\n --edge-label: #b3b8cc;\n --topic-teal: #b0ecbc;\n --topic-teal-ring: #69c27e;\n --topic-harbor: #abdbfe;\n --topic-harbor-ring: #5eade2;\n --topic-iris: #f6bde2;\n --topic-iris-ring: #d383b9;\n --topic-berry: #feb79e;\n --topic-berry-ring: #e37a55;\n --topic-coral: #fece96;\n --topic-coral-ring: #db9840;\n --topic-amber: #b2e8fa;\n --topic-amber-ring: #5bbdda;\n --topic-sun: #f6f097;\n --topic-sun-ring: #c9bf37;\n --topic-stone: #dbdbdb;\n --topic-stone-ring: #b1a8aa;\n }\n[data-theme="contrast-light"] {\n --canvas: #ffffff;\n --surface-sunken: #f2f2f2;\n --line: #5c6178;\n --line-strong: #000000;\n --ink: #000000;\n --ink-muted: #1f2433;\n --ink-subtle: #3a3f52;\n --action: #000000;\n --focus: #0b4a8f;\n --link: #0b4a8f;\n --node-ink: #000000;\n --edge: #000000;\n --edge-label: #1f2433;\n --topic-teal-ring: #006b70;\n --topic-harbor-ring: #0e599d;\n --topic-iris-ring: #683b92;\n --topic-berry-ring: #96173a;\n --topic-coral-ring: #923400;\n --topic-amber-ring: #7e4d00;\n --topic-sun-ring: #7a6600;\n --topic-stone-ring: #63594b;\n --shadow-float: none;\n --shadow-pop: none;\n }\n[data-theme="contrast-dark"] {\n --canvas: #000000;\n --surface: #000000;\n --surface-sunken: #0d0d0d;\n --surface-overlay: #0d0d0d;\n --line: #9aa0b8;\n --line-strong: #ffffff;\n --ink: #ffffff;\n --ink-muted: #e6e8f0;\n --ink-subtle: #c8ccda;\n --action: #ffffff;\n --on-action: #000000;\n --focus: #5fe8f0;\n --link: #7ff0f5;\n --selection: rgba(18, 199, 211, 0.32);\n --node-ink: #000000;\n --edge: #ffffff;\n --edge-label: #e6e8f0;\n --topic-teal-ring: #64eff7;\n --topic-harbor-ring: #b0d5fe;\n --topic-iris-ring: #dcbfff;\n --topic-berry-ring: #feb3bb;\n --topic-coral-ring: #ffbea5;\n --topic-amber-ring: #ffc78a;\n --topic-sun-ring: #ffe26d;\n --topic-stone-ring: #ddd3c2;\n --shadow-float: none;\n --shadow-pop: none;\n }\n[data-motion="reduced"] {\n --duration-instant: 120ms;\n --duration-quick: 0ms;\n --duration-base: 0ms;\n --duration-zoom: 0ms;\n --duration-reveal: 0ms;\n --stagger-reveal: 0ms;\n }\n@media (prefers-reduced-motion: reduce) {\n :root:not([data-motion="full"]) {\n --duration-instant: 120ms;\n --duration-quick: 0ms;\n --duration-base: 0ms;\n --duration-zoom: 0ms;\n --duration-reveal: 0ms;\n --stagger-reveal: 0ms;\n }\n}\n\n/* Type styles */\n.heading { font-family: var(--font-ui); font-size: 17px; line-height: 24px; font-weight: 700; }\n.body { font-family: var(--font-ui); font-size: 15px; line-height: 22px; font-weight: 400; }\n.body-sm { font-family: var(--font-ui); font-size: 13px; line-height: 18px; font-weight: 400; }\n.label { font-family: var(--font-ui); font-size: 13px; line-height: 16px; font-weight: 600; letter-spacing: 0.01em; }\n.caption { font-family: var(--font-ui); font-size: 12px; line-height: 16px; font-weight: 400; }\n.node-root { font-family: var(--font-hand); font-size: 30px; line-height: 34px; font-weight: 700; }\n.node-label { font-family: var(--font-hand); font-size: 22px; line-height: 26px; font-weight: 700; }\n.edge-label { font-family: var(--font-note); font-size: 18px; line-height: 20px; font-weight: 500; }\n.annotation { font-family: var(--font-note); font-size: 24px; line-height: 26px; font-weight: 600; }\n';
676
+ var SPEC_COMPONENT_CSS = '/* Styles for the spec components (map, nodes, connectors, peek, terms, depth trail, controls) \u2014 generated from the Visualli design system (v0.2.0). Do not edit here: changes come in through PRs from the private design system. MIT license, see ../LICENSE. */\n:root { --vi-fs-body: 15px; --vi-fs-sm: 13px; --vi-fs-cap: 12px; --vi-lh-body: 22px; --vi-label-scale: 1; color-scheme: light; }\n[data-theme$="dark"] { color-scheme: dark; }\n[data-scale="lg"] { --vi-fs-body: 17px; --vi-fs-sm: 15px; --vi-fs-cap: 14px; --vi-lh-body: 26px; --vi-label-scale: 1.15; }\n[data-type="readable"] { --font-hand: "Atkinson Hyperlegible Next", system-ui, sans-serif; --font-note: "Atkinson Hyperlegible Next", system-ui, sans-serif; --font-display: "Atkinson Hyperlegible Next", system-ui, sans-serif; letter-spacing: 0.01em; }\n[data-type="readable"] .vi-node__label, [data-type="readable"] .vi-edge__label, [data-type="readable"] .vi-map__group-label { font-family: var(--font-ui) !important; font-weight: 700; letter-spacing: 0.02em; font-style: normal; }\n\n.vi-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }\n.vi-icon { flex: none; display: block; }\n.vi-map :where(button, a, input, textarea, [tabindex]):focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }\n.vi-iconbtn { display: inline-grid; place-items: center; border-radius: var(--radius-sm); border: 1.5px solid transparent; background: transparent; color: var(--ink); cursor: pointer; transition: background var(--duration-instant); flex: none; }\n.vi-iconbtn--sm { width: 32px; height: 32px; }\n.vi-iconbtn--md { width: 40px; height: 40px; }\n.vi-iconbtn--lg { width: 48px; height: 48px; border-radius: var(--radius-md); }\n.vi-iconbtn:hover { background: var(--surface-sunken); }\n.vi-iconbtn--surface { background: var(--surface); border-color: var(--line); }\n.vi-iconbtn--surface:hover { border-color: var(--line-strong); }\n.vi-iconbtn--primary { background: var(--action); color: var(--on-action); border-radius: var(--radius-pill); }\n.vi-iconbtn.is-active { background: var(--action); color: var(--on-action); }\n[data-theme^="contrast"] .vi-ctrls { border: 2px solid var(--line-strong); }\n.vi-node { position: relative; display: inline-grid; place-items: center; border: 0; padding: 0; background: none; color: var(--node-ink); cursor: default; transition: transform var(--duration-base) var(--ease-bloom), opacity var(--duration-base); -webkit-tap-highlight-color: transparent; }\nbutton.vi-node { cursor: pointer; }\n.vi-node__svg { position: absolute; inset: 0; overflow: visible; }\n.vi-node__body { filter: drop-shadow(var(--vi-node-shadow, 0 6px 14px rgba(24, 33, 64, .10))); }\n[data-theme$="dark"] .vi-node__body { --vi-node-shadow: 0 0 22px rgba(255, 255, 255, .10); }\n[data-theme^="contrast"] .vi-node__body { filter: none; }\n.vi-node__rings { transform-origin: center; transition: transform var(--duration-base) var(--ease-standard); }\n@media (hover: hover) {\n button.vi-node:hover .vi-node__rings { transform: rotate(var(--vi-ring-turn, -2deg)) scale(var(--vi-ring-grow, 1.015)); }\n button.vi-node:hover { transform: translateY(calc(var(--vi-lift, 2px) * -1)); }\n}\n.vi-node { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; touch-action: manipulation; }\n.vi-node.is-selected { transform: translateY(-2px); }\n.vi-node.is-selected .vi-node__rings { transform: rotate(var(--vi-ring-turn, -2deg)) scale(var(--vi-ring-grow, 1.015)); }\n.vi-node.is-selected .vi-node__body { stroke-width: 4; }\n.vi-node:focus-visible { outline: none; }\n.vi-node:focus-visible .vi-node__body { stroke: var(--focus); stroke-width: 4; }\n.vi-node.is-selected .vi-node__body { stroke: var(--ink); }\n.vi-node__label { position: relative; text-align: center; color: var(--node-ink); display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; font-size: calc(var(--vi-nfs, 22px) * var(--vi-label-scale) * var(--vi-ninv, 1)); line-height: 1.15; padding-top: 2px; }\n.vi-node__label.node-root { --vi-nfs: 30px; }\n.vi-node--mini .vi-node__label { --vi-nfs: 15px; }\n.vi-node.is-dimmed { opacity: 1; }\n[data-theme^="focus"] .vi-node.is-dimmed { opacity: .4; }\n.vi-map.is-touch .vi-node.is-dimmed { opacity: .6; }\n[data-theme^="focus"] .vi-map.is-touch .vi-node.is-dimmed { opacity: .4; }\n.vi-edge__line { fill: none; stroke: var(--edge); stroke-width: 2.5; stroke-linecap: round; }\n.vi-edge.is-dashed .vi-edge__line { stroke-dasharray: 7 7; }\n.vi-edge__arrow { fill: none; stroke: var(--edge); stroke-width: 2.5; stroke-linecap: round; stroke-linejoin: round; }\n.vi-edge__label { font: 500 calc(18px * var(--vi-inv, 1)) / 1 var(--font-note); fill: var(--edge-label); paint-order: stroke; stroke: var(--canvas); stroke-width: 5px; stroke-linejoin: round; letter-spacing: .01em; }\n[data-type="readable"] .vi-edge__label { font-size: calc(14px * var(--vi-inv, 1)); font-weight: 600; }\n.vi-fact { position: relative; width: 300px; box-sizing: border-box; padding: var(--space-4) var(--space-4) var(--space-3); background: var(--surface-overlay); color: var(--ink); border: 2px solid var(--vi-fact-ring); border-radius: 14px 18px 12px 16px / 16px 12px 18px 14px; font: 400 var(--vi-fs-sm)/20px var(--font-ui); z-index: var(--z-card); animation: vi-pop var(--duration-quick) var(--ease-standard); }\n.vi-fact { box-shadow: 5px 6px 0 0 var(--vi-fact-fill), 5px 6px 0 2px var(--vi-fact-ring); }\n[data-theme^="contrast"] .vi-fact { border-color: var(--ink); }\n[data-theme^="contrast"] .vi-fact { box-shadow: none; }\n.vi-fact__title { font: 700 22px/26px var(--font-hand); margin-bottom: var(--space-1); }\n.vi-fact__body { color: var(--ink); }\n.vi-fact__foot { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); margin-top: var(--space-3); padding-top: var(--space-2); border-top: 1px dashed var(--line); }\n.vi-fact__acts { display: inline-flex; gap: var(--space-3); align-items: center; flex: none; }\n.vi-fact__link { color: var(--link); font: 600 var(--vi-fs-cap)/16px var(--font-ui); }\n.vi-fact__step { display: inline-flex; align-items: center; gap: 2px; border: 0; background: var(--action); color: var(--on-action); font: 600 var(--vi-fs-cap)/16px var(--font-ui); padding: 5px 8px 5px 10px; border-radius: var(--radius-pill); cursor: pointer; }\n.vi-trail ol { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; position: relative; }\n.vi-trail li { position: relative; }\n.vi-trail li + li::before { content: ""; position: absolute; left: 13px; top: -8px; height: 10px; border-left: 1.5px dashed var(--line-strong); }\n.vi-trail button { display: inline-flex; align-items: center; gap: 6px; border: 0; background: none; padding: 4px 8px 4px 5px; border-radius: var(--radius-sm); color: var(--ink-muted); font: 400 var(--vi-fs-cap)/16px var(--font-ui); cursor: pointer; }\n.vi-trail button span { display: inline-flex; align-items: center; gap: 4px; }\n.vi-trail button:hover:not(:disabled) { background: var(--surface-sunken); color: var(--ink); }\n.vi-trail li.is-current button { color: var(--ink); font-weight: 700; font-size: var(--vi-fs-sm); cursor: default; background: var(--surface); box-shadow: var(--shadow-float); }\n.vi-trail li.is-current button:disabled { opacity: 1; }\n.vi-ctrls { display: inline-flex; align-items: center; gap: 2px; padding: 4px; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-md); box-shadow: var(--shadow-float); }\n.vi-ctrls__pct { min-width: 44px; text-align: center; font: 600 var(--vi-fs-cap)/16px var(--font-ui); color: var(--ink-muted); font-variant-numeric: tabular-nums; }\n.vi-ctrls__div { width: 1px; height: 20px; background: var(--line); margin: 0 4px; }\n.vi-map { position: relative; overflow: hidden; background: var(--canvas); border-radius: inherit; isolation: isolate; }\n.vi-map__immersion { position: absolute; inset: 0; opacity: var(--immersion-alpha); transition: background var(--duration-base); z-index: -1; }\n.vi-map__world { position: absolute; left: 0; top: 0; transform-origin: 0 0; }\n.vi-map__edges { position: absolute; overflow: visible; pointer-events: none; }\n.vi-map__group rect { fill: none; stroke: var(--line-strong); stroke-width: 1.5; stroke-dasharray: 10 6; }\n.vi-map__group-label { position: absolute; transform: translate(-50%, -50%); z-index: 2; max-width: 420px; box-sizing: border-box; padding: 6px 22px 8px; background: var(--topic-stone); border: 1.5px solid var(--topic-stone-ring); border-radius: var(--radius-pill); font: 700 22px/1.1 var(--font-note); color: var(--node-ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; pointer-events: none; font-size: calc(26px * var(--vi-label-scale) * var(--vi-ninv, 1)); }\n[data-theme^="contrast"] .vi-map__group-label { border: 2px solid var(--line-strong); }\n.vi-map__slot { position: absolute; transform: translate(-50%, -50%); }\n.vi-map.is-reveal .vi-map__slot { animation: vi-bloom var(--duration-reveal) var(--ease-bloom) backwards; }\n.vi-map.is-reveal .vi-edge { animation: vi-fade var(--duration-base) var(--ease-standard) backwards; }\n.vi-map.is-reveal .vi-edge__line { stroke-dasharray: 1; stroke-dashoffset: 1; animation: vi-line var(--vi-line-draw, .5s) var(--ease-standard) forwards; animation-delay: inherit; }\n.vi-map.is-reveal .vi-edge.is-dashed .vi-edge__line { stroke-dasharray: 7 7; stroke-dashoffset: 0; animation: vi-fade var(--vi-line-draw, .5s) var(--ease-standard) backwards; animation-delay: inherit; }\n.vi-map:not(.is-reveal) .vi-edge__line { stroke-dasharray: none; }\n.vi-map:not(.is-reveal) .vi-edge.is-dashed .vi-edge__line { stroke-dasharray: 7 7; }\n.vi-map__lens { position: absolute; inset: 0; transform-origin: var(--vi-ox) var(--vi-oy); }\n.vi-map.is-diving .vi-map__lens { transform: scale(var(--vi-dive-scale, 3.2)); opacity: 0; transition: transform var(--duration-zoom) var(--ease-zoom), opacity var(--vi-dive-fade, 160ms) var(--vi-dive-fade-delay, 150ms); }\n.vi-map.is-surfacing .vi-map__lens { transform: scale(var(--vi-surface-scale, .6)); opacity: 0; transform-origin: 50% 50%; transition: transform var(--vi-surface-dur, 220ms) var(--ease-zoom), opacity var(--vi-surface-fade, 200ms); }\n.vi-map.is-reveal .vi-map__lens { animation: vi-settle var(--vi-settle-dur, 260ms) var(--ease-standard); }\n[data-motion="reduced"] .vi-map__lens { animation: none !important; transform: none !important; }\n.vi-map__fact { position: absolute; }\n.vi-map.is-idle .vi-map__world { transition: transform var(--duration-base) var(--ease-standard); }\n.vi-map__hint { position: absolute; left: var(--space-4); right: var(--space-4); margin: 0; text-align: center; color: var(--ink-muted); pointer-events: none; z-index: var(--z-chrome); animation: vi-fade var(--duration-base) var(--ease-standard); }\n.vi-fact__head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-2); }\n.vi-fact__back { display: inline-flex; align-items: center; gap: 2px; border: 0; background: none; padding: 2px 0; margin-bottom: var(--space-1); color: var(--link); font: 600 var(--vi-fs-cap)/16px var(--font-ui); cursor: pointer; }\n.vi-term { border: 0; background: none; padding: 0; font: inherit; color: var(--link); text-decoration: underline; text-decoration-style: dotted; text-decoration-thickness: 1.5px; text-underline-offset: 3px; cursor: pointer; }\n.vi-fact--sheet { position: absolute; left: 0; right: 0; bottom: 0; width: auto; max-width: 560px; margin: 0 auto; max-height: var(--vi-sheet-h, 300px); overflow-y: auto; overscroll-behavior: contain; padding: 0 var(--space-4) calc(var(--space-4) + env(safe-area-inset-bottom, 0px)); border: 0; border-top: 2px solid var(--vi-fact-ring); border-radius: var(--radius-lg) var(--radius-lg) 0 0; box-shadow: 0 -6px 0 0 var(--vi-fact-fill), var(--shadow-pop); font-size: var(--vi-fs-body); line-height: var(--vi-lh-body); animation: vi-sheet-in var(--duration-base) var(--ease-standard); transition: transform var(--duration-quick) var(--ease-standard), max-height var(--duration-base) var(--ease-standard); }\n.vi-fact--sheet.is-expanded { max-height: calc(100% - var(--space-7)); }\n.vi-fact__grip { position: sticky; top: 0; display: flex; justify-content: center; margin: 0 calc(var(--space-4) * -1); background: var(--surface-overlay); touch-action: none; cursor: grab; z-index: 1; }\n.vi-fact__handle { position: relative; width: 64px; height: 24px; border: 0; padding: 0; background: none; cursor: pointer; }\n.vi-fact__handle::after { content: ""; position: absolute; left: 12px; right: 12px; top: 10px; height: 5px; border-radius: var(--radius-pill); background: var(--line-strong); }\n.vi-fact--sheet .vi-fact__title { font-size: calc(24px * var(--vi-label-scale)); line-height: 1.15; padding-top: var(--space-1); }\n.vi-fact--sheet .vi-fact__close { margin: 0 calc(var(--space-2) * -1) 0 0; }\n.vi-fact--sheet .vi-fact__step { min-height: 44px; padding: 0 var(--space-3) 0 var(--space-4); font-size: var(--vi-fs-sm); }\n.vi-fact--sheet .vi-fact__link { font-size: var(--vi-fs-sm); min-height: 44px; display: inline-flex; align-items: center; }\n[data-motion="reduced"] .vi-fact--sheet { animation: vi-fade 120ms; }\n@media (prefers-reduced-motion: reduce) {\n .vi-fact--sheet { animation: vi-fade 120ms; }\n .vi-map.is-idle .vi-map__world { transition: none; }\n}\n.vi-map__trail { position: absolute; top: var(--space-4); left: var(--space-4); z-index: var(--z-chrome); }\n.vi-map__ctrls { position: absolute; top: var(--space-4); right: var(--space-4); z-index: var(--z-chrome); }\n[data-reveal="instant"] .vi-map.is-reveal .vi-map__slot, [data-motion="reduced"] .vi-map.is-reveal .vi-map__slot { animation: vi-fade var(--vi-reduced-fade, 120ms) backwards; animation-delay: 0ms !important; }\n[data-reveal="instant"] .vi-map.is-reveal .vi-edge, [data-reveal="instant"] .vi-map.is-reveal .vi-edge__line, [data-motion="reduced"] .vi-map.is-reveal .vi-edge, [data-motion="reduced"] .vi-map.is-reveal .vi-edge__line { animation: none; stroke-dashoffset: 0; }\n@media (prefers-reduced-motion: reduce) {\n .vi-map.is-reveal .vi-map__slot { animation: vi-fade var(--vi-reduced-fade, 120ms) backwards; animation-delay: 0ms !important; }\n .vi-map.is-reveal .vi-edge, .vi-map.is-reveal .vi-edge__line { animation: none; stroke-dashoffset: 0; }\n}\n[data-motion="reduced"] *, [data-motion="reduced"] *::before, [data-motion="reduced"] *::after { transition-duration: 1ms !important; }\n.vi-fact__acts { flex-wrap: wrap; justify-content: flex-end; }\n.vi-fact__link { display: inline-flex; align-items: center; gap: 2px; }\n.vi-anchor { position: relative; display: inline; }\n.vi-anchor.is-open .vi-term { background: var(--selection); border-radius: 3px; }\n.vi-anchor__pop { position: absolute; left: -12px; top: calc(100% + 6px); z-index: var(--z-menu); display: block; width: max-content; }\n.vi-anchor-card { position: relative; display: block; width: min(300px, calc(100vw - 32px)); box-sizing: border-box; padding: var(--space-3) var(--space-4) var(--space-3); background: var(--surface-overlay); color: var(--ink); text-align: left; white-space: normal; font: 400 var(--vi-fs-sm)/20px var(--font-ui); border: 2px solid var(--vi-fact-ring); border-radius: 12px 16px 10px 14px / 14px 10px 16px 12px; box-shadow: 4px 5px 0 0 var(--vi-fact-fill), 4px 5px 0 2px var(--vi-fact-ring), var(--shadow-pop); animation: vi-pop var(--duration-quick) var(--ease-standard); }\n.vi-anchor-card.is-in-sheet { width: auto; border: 0; box-shadow: none; padding: 0; animation: none; background: transparent; }\n[data-theme^="contrast"] .vi-anchor-card { border-color: var(--ink); box-shadow: none; }\n.vi-anchor-card__eyebrow { font: 400 11px/14px var(--font-display); letter-spacing: .14em; text-transform: uppercase; color: var(--ink-muted); }\n.vi-anchor-card__word { font: 700 20px/24px var(--font-hand); color: var(--ink); margin: 2px 0 var(--space-1); }\n.vi-fact--sheet .vi-anchor-card__word { font-size: calc(24px * var(--vi-label-scale)); line-height: 1.15; }\n.vi-anchor-card__desc { margin: 0; color: var(--ink); }\n.vi-anchor-card__acts { display: flex; align-items: center; justify-content: flex-end; flex-wrap: wrap; gap: var(--space-3); margin-top: var(--space-3); padding-top: var(--space-2); border-top: 1px dashed var(--line); }\n.vi-fact--sheet .vi-anchor-card__acts .vi-fact__link { min-height: 44px; }\n[data-type="readable"] .vi-anchor-card__word { font-family: var(--font-ui); }\n[data-motion="reduced"] .vi-anchor-card { animation: vi-fade 120ms; }\n@media (prefers-reduced-motion: reduce) {\n .vi-anchor-card { animation: vi-fade 120ms; }\n}\n.vi-peek .vi-fact__foot { flex-wrap: wrap; row-gap: var(--space-2); }\n.vi-peek .vi-fact__acts { margin-left: auto; }\n.vi-map.has-highlight .vi-node.is-dimmed { opacity: .35; transition: opacity var(--duration-quick) var(--ease-standard); }\n.vi-map.has-highlight .vi-node.is-selected .vi-node__body { stroke: var(--ink); }\n.vi-map { container-type: inline-size; }\n.vi-edge__hit { fill: none; stroke: transparent; stroke-width: 18; pointer-events: stroke; cursor: pointer; }\n.vi-edge__hit:hover + .vi-edge__line { stroke-width: 3.5; }\n.vi-edge.is-selected .vi-edge__line, .vi-edge.is-selected .vi-edge__arrow { stroke: var(--focus); stroke-width: 3.5; }\n.vi-map__group-hit { fill: none; stroke: transparent; stroke-width: 16; pointer-events: stroke; cursor: pointer; }\n.vi-map__group.is-selected rect:not(.vi-map__group-hit) { stroke: var(--focus); stroke-width: 2.5; stroke-dasharray: none; }\n.vi-map__group-label.is-selected { border-color: var(--focus); color: var(--focus); box-shadow: 0 0 0 1px var(--focus); }\n.vi-map { touch-action: pan-x pan-y; }\n.vi-map.is-app { touch-action: none; }\n.vi-map.is-pinching .vi-map__world { transition: none; }\n.vi-map.is-instant.is-reveal .vi-map__slot { animation: vi-fade var(--vi-reduced-fade, 120ms) backwards; animation-delay: 0ms !important; }\n.vi-map.is-instant.is-reveal .vi-edge, .vi-map.is-instant.is-reveal .vi-edge__line { animation: none; stroke-dashoffset: 0; }\n.vi-map.is-instant .vi-node, .vi-map.is-instant .vi-node__rings { transition: none; }\n.vi-map.is-detail-body .vi-node:not(.is-selected):not(:focus-visible):not(:hover) :is(.vi-node__rings, .vi-node__label), .vi-map.is-detail-speck .vi-node:not(.is-selected):not(:focus-visible):not(:hover) :is(.vi-node__rings, .vi-node__label) { display: none; }\n.vi-map.is-detail-body .vi-node:not(.is-selected):not(:focus-visible):not(:hover) .vi-node__body, .vi-map.is-detail-speck .vi-node:not(.is-selected):not(:focus-visible):not(:hover) .vi-node__body, .vi-map.no-shadows .vi-node__body { filter: none; }\n@keyframes vi-pop { from { opacity: 0; transform: translateY(-4px) scale(.98); } }\n@keyframes vi-fade { from { opacity: 0; } }\n@keyframes vi-bloom { from { opacity: 0; transform: translate(calc(-50% + var(--vi-dx)), calc(-50% + var(--vi-dy))) scale(var(--vi-bloom-from, .55)); } }\n@keyframes vi-line { to { stroke-dashoffset: 0; } }\n@keyframes vi-settle { from { transform: scale(var(--vi-settle-from, .94)); } }\n@keyframes vi-sheet-in { from { transform: translateY(100%); } }\n';
677
+ var SPEC_FONTS_URL = "https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible+Next:ital,wght@0,200..800;1,200..800&family=Atkinson+Hyperlegible+Mono:wght@400;600&family=Kalam:wght@400;700&display=swap";
678
+ var SPEC_DESIGN_SYSTEM_VERSION = "0.2.0";
679
+ var SPEC_BUNDLED_FONTS = [
680
+ {
681
+ "family": "Caveat",
682
+ "file": "Caveat-Variable.ttf",
683
+ "weight": "400 700"
684
+ }
685
+ ];
686
+
687
+ // src/design/runtime.ts
688
+ var STYLE_ID = "visualli-design-system";
689
+ var LAYOUT_CSS = `
690
+ .vi-map__ctrls { transition: bottom var(--duration-quick) var(--ease-standard); } /* controlsPosition 'bottom-right': rising above the sheet */
691
+ .vi-map.is-compact .vi-trail li:not(:first-child):not(.is-current):not(:nth-last-child(2)) { display: none; }
692
+ .vi-map.is-compact .vi-trail button span { max-width: 40vw; max-width: 40cqw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
693
+ .vi-map.is-compact .vi-fact:not(.vi-fact--sheet) { width: min(300px, calc(100cqw - 2 * var(--space-4))); }
694
+ .vi-map.is-touch .vi-trail button { min-height: 32px; }
695
+ `;
696
+ var FONTS_ID = "visualli-design-system-fonts";
697
+ var CAVEAT_ID = "visualli-design-system-caveat";
698
+ var SDK_VERSION = true ? "0.2.0" : "latest";
699
+ var webFontsSheet = Promise.resolve();
700
+ function ensureDesignSystemStyles(assets = {}) {
701
+ if (typeof document === "undefined") return Promise.resolve();
702
+ if (!document.getElementById(STYLE_ID)) {
703
+ const style = document.createElement("style");
704
+ style.id = STYLE_ID;
705
+ style.setAttribute("data-design-system", SPEC_DESIGN_SYSTEM_VERSION);
706
+ style.textContent = `${SPEC_TOKENS_CSS}
707
+ ${SPEC_COMPONENT_CSS}
708
+ ${LAYOUT_CSS}`;
709
+ document.head.appendChild(style);
710
+ }
711
+ if (assets.loadWebFonts !== false && SPEC_FONTS_URL && !document.getElementById(FONTS_ID)) {
712
+ const link = document.createElement("link");
713
+ link.id = FONTS_ID;
714
+ link.rel = "stylesheet";
715
+ link.href = SPEC_FONTS_URL;
716
+ webFontsSheet = new Promise((resolve) => {
717
+ link.addEventListener("load", () => resolve(), { once: true });
718
+ link.addEventListener("error", () => resolve(), { once: true });
719
+ });
720
+ document.head.appendChild(link);
721
+ }
722
+ if (!document.getElementById(CAVEAT_ID)) {
723
+ const base = (assets.fontBaseUrl ?? `https://cdn.jsdelivr.net/npm/@visualli/react@${SDK_VERSION}/fonts`).replace(/\/$/, "");
724
+ const style = document.createElement("style");
725
+ style.id = CAVEAT_ID;
726
+ style.textContent = SPEC_BUNDLED_FONTS.map((f) => `@font-face { font-family: "${f.family}"; src: url("${base}/${f.file}"); font-weight: ${f.weight}; font-display: swap; }`).join("\n");
727
+ document.head.appendChild(style);
728
+ }
729
+ return webFontsSheet;
730
+ }
731
+ var firstFamily = (stack) => stack.split(",")[0].trim();
732
+ var CANVAS_FONT_FACES = ["node-label", "edge-label", "body"].map((style) => {
733
+ const t = import_core6.TYPE_STYLES[style];
734
+ return `${t.weight} ${t.size}px ${firstFamily(import_core6.TOKENS.light[t.family])}`;
735
+ });
736
+ var fontsEpochValue = 0;
737
+ var fontListeners = /* @__PURE__ */ new Set();
738
+ var fontsEpoch = () => fontsEpochValue;
739
+ function useFontsEpoch() {
740
+ return (0, import_react3.useSyncExternalStore)(onFontsChange, fontsEpoch, () => 0);
741
+ }
742
+ var onFontsChange = (cb) => {
743
+ fontListeners.add(cb);
744
+ return () => fontListeners.delete(cb);
745
+ };
746
+ function loadCanvasFonts(sheet = Promise.resolve(), timeoutMs = 3e3) {
747
+ if (typeof document === "undefined" || !("fonts" in document)) return Promise.resolve();
748
+ const fonts = document.fonts;
749
+ const loads = sheet.then(() => Promise.all(CANVAS_FONT_FACES.map((f) => fonts.load(f).catch(() => [])))).then(() => fonts.ready).then(() => void 0);
750
+ const timeout = new Promise((r) => setTimeout(r, timeoutMs));
751
+ return Promise.race([loads, timeout]).then(() => {
752
+ fontsEpochValue++;
753
+ fontListeners.forEach((cb) => cb());
754
+ });
755
+ }
756
+ if (typeof document !== "undefined" && "fonts" in document) {
757
+ document.fonts.addEventListener?.("loadingdone", () => {
758
+ fontsEpochValue++;
759
+ fontListeners.forEach((cb) => cb());
760
+ });
761
+ }
762
+ var currentPixelRatio = () => typeof window === "undefined" ? 1 : Math.min(Math.max(window.devicePixelRatio || 1, 1), 3);
763
+ function watchPixelRatio(cb) {
764
+ if (typeof window === "undefined" || !window.matchMedia) return () => {
765
+ };
766
+ let mql;
767
+ let stopped = false;
768
+ const arm = () => {
769
+ mql = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
770
+ mql.addEventListener("change", onChange, { once: true });
771
+ };
772
+ const onChange = () => {
773
+ if (stopped) return;
774
+ cb(currentPixelRatio());
775
+ arm();
776
+ };
777
+ arm();
778
+ return () => {
779
+ stopped = true;
780
+ mql.removeEventListener("change", onChange);
781
+ };
782
+ }
690
783
 
691
784
  // src/components/KonvaStage.tsx
692
785
  var import_jsx_runtime = require("react/jsx-runtime");
693
- var KonvaStage = (0, import_react3.forwardRef)(function KonvaStage2({
786
+ var KonvaStage = (0, import_react4.forwardRef)(function KonvaStage2({
694
787
  children,
695
788
  onWheel,
696
789
  onMouseDown,
@@ -702,20 +795,32 @@ var KonvaStage = (0, import_react3.forwardRef)(function KonvaStage2({
702
795
  onTouchEnd,
703
796
  onStageReady
704
797
  }, ref) {
705
- const internalRef = (0, import_react3.useRef)(null);
798
+ const internalRef = (0, import_react4.useRef)(null);
706
799
  const { centerX, centerY, zoomLevel, canvasWidth, canvasHeight } = useViewportStore();
707
- (0, import_react3.useEffect)(() => {
800
+ (0, import_react4.useEffect)(() => {
708
801
  if (internalRef.current) {
709
802
  if (typeof ref === "function") ref(internalRef.current);
710
803
  else if (ref) ref.current = internalRef.current;
711
804
  onStageReady?.(internalRef.current);
712
805
  }
713
806
  }, []);
714
- (0, import_react3.useEffect)(() => {
807
+ (0, import_react4.useEffect)(() => {
808
+ const stage = internalRef.current;
715
809
  return () => {
716
- internalRef.current?.destroy();
810
+ stage?.destroy();
717
811
  };
718
812
  }, []);
813
+ import_konva.default.pixelRatio = currentPixelRatio();
814
+ (0, import_react4.useEffect)(() => watchPixelRatio((ratio) => {
815
+ import_konva.default.pixelRatio = ratio;
816
+ const stage = internalRef.current;
817
+ if (!stage) return;
818
+ stage.getLayers().forEach((l) => {
819
+ l.getCanvas().setPixelRatio(ratio);
820
+ l.getHitCanvas().setPixelRatio(ratio);
821
+ });
822
+ stage.batchDraw();
823
+ }), []);
719
824
  const w = canvasWidth || 800;
720
825
  const h = canvasHeight || 600;
721
826
  const stageX = w / 2 - centerX * zoomLevel;
@@ -739,7 +844,6 @@ var KonvaStage = (0, import_react3.forwardRef)(function KonvaStage2({
739
844
  onTouchMove,
740
845
  onTouchEnd,
741
846
  draggable: false,
742
- pixelRatio: 1,
743
847
  children
744
848
  }
745
849
  );
@@ -748,1348 +852,999 @@ var KonvaStage_default = KonvaStage;
748
852
 
749
853
  // src/components/KonvaNodeLayer.tsx
750
854
  var import_react6 = require("react");
855
+ var import_core9 = require("@visualli/core");
751
856
 
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;
857
+ // src/design/design.ts
858
+ var import_core7 = require("@visualli/core");
859
+ function parseShadow(css) {
860
+ const m = /^(-?[\d.]+)(?:px)?\s+(-?[\d.]+)(?:px)?\s+([\d.]+)(?:px)?\s+(.+)$/.exec(css.trim());
861
+ return m ? { x: +m[1], y: +m[2], blur: +m[3], color: m[4].trim() } : null;
862
+ }
863
+ var cache = /* @__PURE__ */ new Map();
864
+ function makeDesign(theme, comfortIn) {
865
+ const key2 = `${theme}|${+comfortIn.readableType}${+comfortIn.largerText}${+comfortIn.reducedMotion}`;
866
+ const hit = cache.get(key2);
867
+ if (hit) return hit;
868
+ const tokens = import_core7.TOKENS[theme];
869
+ const ui = tokens["font-ui"];
870
+ const d = {
871
+ theme,
872
+ comfort: comfortIn,
873
+ tokens,
874
+ metrics: (0, import_core7.metricsFor)(comfortIn),
875
+ isDark: (0, import_core7.isDarkTheme)(theme),
876
+ fontHand: comfortIn.readableType ? ui : tokens["font-hand"],
877
+ fontNote: comfortIn.readableType ? ui : tokens["font-note"],
878
+ fontUi: ui,
879
+ labelScale: (0, import_core7.labelScale)(comfortIn),
880
+ nodeShadow: (0, import_core7.isContrastTheme)(theme) ? null : parseShadow((0, import_core7.isDarkTheme)(theme) ? import_core7.CANVAS_STYLE.node.shadowDark : import_core7.CANVAS_STYLE.node.shadowLight),
881
+ attrs: {
882
+ "data-theme": theme,
883
+ ...comfortIn.readableType ? { "data-type": "readable" } : {},
884
+ ...comfortIn.largerText ? { "data-scale": "lg" } : {},
885
+ ...comfortIn.reducedMotion ? { "data-motion": "reduced" } : {}
886
+ },
887
+ edgeLabel: comfortIn.readableType ? { weight: import_core7.CANVAS_STYLE.edge.readableLabelWeight, size: import_core7.CANVAS_STYLE.edge.readableLabelSize } : { weight: import_core7.CANVAS_STYLE.edge.labelWeight, size: import_core7.CANVAS_STYLE.edge.labelSize }
888
+ };
889
+ cache.set(key2, d);
890
+ return d;
891
+ }
892
+ var DEFAULT_DESIGN = makeDesign("light", { readableType: false, largerText: false, reducedMotion: false });
893
+ function resolveDesign(props, env) {
894
+ const input = props.theme ?? (props.isDark === void 0 ? void 0 : props.isDark ? "dark" : "light");
895
+ let theme = (0, import_core7.resolveTheme)(input, { prefersDark: env.prefersDark, forcedColors: env.forcedColors });
896
+ if (env.forcedColors && props.respectForcedColors !== false && !(0, import_core7.isContrastTheme)(theme)) {
897
+ theme = (0, import_core7.isDarkTheme)(theme) ? "contrast-dark" : "contrast-light";
764
898
  }
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]);
899
+ return makeDesign(theme, (0, import_core7.resolveComfort)(props.comfort, { prefersReducedMotion: env.prefersReducedMotion }));
786
900
  }
787
901
 
788
- // src/components/KonvaNode.tsx
789
- var import_react5 = __toESM(require("react"), 1);
902
+ // src/design/drawing.ts
790
903
  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}`;
904
+ var DEG = Math.PI / 180;
905
+ var MAX_CACHED_PATHS = 4096;
906
+ var pathCache = /* @__PURE__ */ new Map();
907
+ function blobPath2D(shape, rx, ry) {
908
+ const key2 = `${shape}|${rx}|${ry}`;
909
+ let p = pathCache.get(key2);
910
+ if (!p) {
911
+ if (pathCache.size >= MAX_CACHED_PATHS) pathCache.delete(pathCache.keys().next().value);
912
+ p = new Path2D((0, import_core8.blobPath)(shape, rx, ry));
913
+ pathCache.set(key2, p);
914
+ }
915
+ return p;
916
+ }
917
+ var NO_DASH = [];
918
+ var NO_RINGS = [];
919
+ var dashCache = /* @__PURE__ */ new Map();
920
+ function dashOf(spec) {
921
+ if (!spec) return NO_DASH;
922
+ let d = dashCache.get(spec);
923
+ if (!d) {
924
+ d = spec.split(/\s+/).map(Number);
925
+ dashCache.set(spec, d);
1068
926
  }
1069
- d += " Z";
1070
927
  return d;
1071
928
  }
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
929
+ var lineCache = /* @__PURE__ */ new Map();
930
+ var MAX_CACHED_LAYOUTS = 8192;
931
+ function layoutLabel(c, text, font, maxWidth, maxLines = Infinity) {
932
+ const key2 = `${fontsEpoch()}|${font}|${maxWidth}|${text}`;
933
+ const hit = lineCache.get(key2);
934
+ if (hit) return hit;
935
+ c.font = font;
936
+ const words = text.split(/\s+/).filter(Boolean);
937
+ const lines = [];
938
+ let cur = "";
939
+ const fit = (s) => c.measureText(s).width <= maxWidth;
940
+ for (const w of words) {
941
+ const next = cur ? `${cur} ${w}` : w;
942
+ if (fit(next)) {
943
+ cur = next;
944
+ continue;
945
+ }
946
+ if (cur) lines.push(cur);
947
+ let rest = w;
948
+ while (!fit(rest) && rest.length > 1) {
949
+ let n = rest.length - 1;
950
+ while (n > 1 && !fit(rest.slice(0, n))) n--;
951
+ lines.push(rest.slice(0, n));
952
+ rest = rest.slice(n);
953
+ }
954
+ cur = rest;
955
+ }
956
+ if (cur) lines.push(cur);
957
+ if (lines.length > maxLines) {
958
+ lines.length = maxLines;
959
+ let last = lines[maxLines - 1] + "\u2026";
960
+ while (last.length > 1 && !fit(last)) last = last.slice(0, -2) + "\u2026";
961
+ lines[maxLines - 1] = last;
962
+ }
963
+ if (lineCache.size >= MAX_CACHED_LAYOUTS) lineCache.clear();
964
+ lineCache.set(key2, lines);
965
+ return lines;
966
+ }
967
+ var IDLE = { hovered: false, pressed: false, selected: false, focused: false, dimmed: false };
968
+ var ideaColors = (d, n) => (0, import_core8.ideaStyle)(d.theme, n);
969
+ var ideaLabelPx = (n, d) => import_core8.IDEA.labelSize[n.kind ?? "node"] * d.labelScale;
970
+ function drawIdea(c, node, d, zoom, st, opt) {
971
+ const { rx, ry } = (0, import_core8.nodeRadii)(node);
972
+ const { fill, ring } = ideaColors(d, node);
973
+ const screenW = node.width * zoom;
974
+ const base = opt.base;
975
+ const detail = (0, import_core8.ideaDetail)(screenW, st.selected || st.focused || st.hovered);
976
+ if (detail === "speck") {
977
+ const a = (st.dimmed ? opt.dimOpacity ?? import_core8.CANVAS_STYLE.node.dimmedOpacityFocus : 1) * (st.arrival ? st.arrival.alpha : 1);
978
+ if (a !== 1) c.globalAlpha = a;
979
+ c.fillStyle = fill;
980
+ c.fillRect(node.x - rx, node.y - ry, rx * 2, ry * 2);
981
+ if (a !== 1) c.globalAlpha = 1;
982
+ return;
983
+ }
984
+ const detailed = detail === "full";
985
+ const shape = (0, import_core8.shapeOfLevel)(node.level);
986
+ const rings = detailed ? (0, import_core8.ringsFor)(node.branchCount) : NO_RINGS;
987
+ const raised = st.hovered || st.pressed || st.selected;
988
+ const lift = st.lift ?? (raised ? 1 : 0);
989
+ const ringT = st.ring ?? (raised ? 1 : 0);
990
+ const arr = st.arrival;
991
+ const y = node.y + lift * import_core8.CANVAS_STYLE.node.hoverLift + (arr ? arr.dy : 0);
992
+ const x = node.x + (arr ? arr.dx : 0);
993
+ let k = opt.k;
994
+ if (k === void 0) {
995
+ const m = c.getTransform();
996
+ k = Math.hypot(m.a, m.b);
997
+ }
998
+ if (base) c.setTransform(base.a, base.b, base.c, base.d, base.e + base.a * x + base.c * y, base.f + base.b * x + base.d * y);
999
+ else {
1000
+ c.save();
1001
+ c.translate(x, y);
1002
+ }
1003
+ if (arr) c.scale(arr.scale, arr.scale);
1004
+ c.lineJoin = "round";
1005
+ const baseAlpha = (st.dimmed ? opt.dimOpacity ?? import_core8.CANVAS_STYLE.node.dimmedOpacityFocus : 1) * (arr ? arr.alpha : 1);
1006
+ c.globalAlpha = baseAlpha;
1007
+ if (rings.length) {
1008
+ const ringSpin = ringT * import_core8.CANVAS_STYLE.node.hoverRingsRotate;
1009
+ const ringGrow = 1 + ringT * (import_core8.CANVAS_STYLE.node.hoverRingsScale - 1);
1010
+ c.fillStyle = fill;
1011
+ c.strokeStyle = ring;
1012
+ let dashed = false;
1013
+ for (let i = rings.length - 1; i >= 0; i--) {
1014
+ const r = rings[i];
1015
+ const angle = (r.rotate + ringSpin) * DEG;
1016
+ c.rotate(angle);
1017
+ c.globalAlpha = baseAlpha * r.opacity;
1018
+ const path = blobPath2D(shape, rx * r.scale * ringGrow, ry * r.scale * ringGrow);
1019
+ c.fill(path);
1020
+ c.lineWidth = r.width;
1021
+ if (r.dash) {
1022
+ c.setLineDash(dashOf(r.dash));
1023
+ dashed = true;
1024
+ } else if (dashed) {
1025
+ c.setLineDash(NO_DASH);
1026
+ dashed = false;
1157
1027
  }
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
- )
1028
+ c.stroke(path);
1029
+ c.rotate(-angle);
1030
+ }
1031
+ if (dashed) c.setLineDash(NO_DASH);
1032
+ c.globalAlpha = baseAlpha;
1033
+ }
1034
+ const body = blobPath2D(shape, rx, ry);
1035
+ c.fillStyle = fill;
1036
+ const shadow = detailed && opt.shadows ? d.nodeShadow : null;
1037
+ if (shadow) {
1038
+ c.shadowColor = shadow.color;
1039
+ c.shadowBlur = shadow.blur * k;
1040
+ c.shadowOffsetX = shadow.x * k;
1041
+ c.shadowOffsetY = shadow.y * k;
1042
+ }
1043
+ c.fill(body);
1044
+ if (shadow) {
1045
+ c.shadowColor = "transparent";
1046
+ c.shadowBlur = 0;
1047
+ c.shadowOffsetX = 0;
1048
+ c.shadowOffsetY = 0;
1049
+ }
1050
+ c.strokeStyle = st.focused ? d.tokens["focus"] : st.selected ? d.tokens["ink"] : ring;
1051
+ c.lineWidth = st.selected || st.focused ? import_core8.CANVAS_STYLE.node.selectedStrokeWidth : d.metrics.nodeStroke;
1052
+ c.stroke(body);
1053
+ if (detailed && node.title) {
1054
+ const px = ideaLabelPx(node, d);
1055
+ const font = `${import_core8.TYPE_STYLES["node-label"].weight} ${px}px ${d.fontHand}`;
1056
+ const lines = layoutLabel(c, node.title, font, node.width - import_core8.IDEA.labelInset);
1057
+ const s = (0, import_core8.labelGrowth)(zoom).idea;
1058
+ const lh = px * import_core8.IDEA.labelLineHeight;
1059
+ c.scale(s, s);
1060
+ c.font = font;
1061
+ c.fillStyle = d.tokens["node-ink"];
1062
+ c.textAlign = "center";
1063
+ c.textBaseline = "middle";
1064
+ if (d.comfort.readableType && "letterSpacing" in c) c.letterSpacing = `${(px * 0.02).toFixed(2)}px`;
1065
+ const y0 = -((lines.length - 1) * lh) / 2;
1066
+ for (let i = 0; i < lines.length; i++) c.fillText(lines[i], 0, y0 + i * lh);
1067
+ }
1068
+ if (base) c.globalAlpha = 1;
1069
+ else c.restore();
1070
+ }
1071
+ function cubicLength(d) {
1072
+ const v = d.match(/-?\d+(?:\.\d+)?/g)?.map(Number);
1073
+ if (!v || v.length < 8) return 0;
1074
+ const [x0, y0, x1, y1, x2, y2, x3, y3] = v;
1075
+ let len = 0, px = x0, py = y0;
1076
+ for (let i = 1; i <= 16; i++) {
1077
+ const t = i / 16, u = 1 - t;
1078
+ const x = u * u * u * x0 + 3 * u * u * t * x1 + 3 * u * t * t * x2 + t * t * t * x3;
1079
+ const y = u * u * u * y0 + 3 * u * u * t * y1 + 3 * u * t * t * y2 + t * t * t * y3;
1080
+ len += Math.hypot(x - px, y - py);
1081
+ px = x;
1082
+ py = y;
1083
+ }
1084
+ return len;
1085
+ }
1086
+ function prepareConnector(g) {
1087
+ return { ...g, path: new Path2D(g.d), arrowPath: new Path2D(g.arrow), len: cubicLength(g.d) };
1088
+ }
1089
+ var CONNECTOR_MIN_SCREEN_PX = 3;
1090
+ var LABEL_MIN_SCREEN_PX = 6;
1091
+ function drawConnector(c, g, d, zoom, dashed, label, arrival) {
1092
+ if (Math.hypot(g.b.x - g.a.x, g.b.y - g.a.y) * zoom < CONNECTOR_MIN_SCREEN_PX) return;
1093
+ c.save();
1094
+ if (arrival) c.globalAlpha = arrival.alpha;
1095
+ c.strokeStyle = d.tokens["edge"];
1096
+ c.lineWidth = d.metrics.edgeWidth;
1097
+ c.lineCap = "round";
1098
+ c.lineJoin = "round";
1099
+ if (dashed) c.setLineDash([...import_core8.CANVAS_STYLE.edge.dash]);
1100
+ else if (arrival && arrival.draw < 1) c.setLineDash([g.len * arrival.draw, g.len * 2]);
1101
+ if (!(arrival && arrival.draw <= 1e-3)) c.stroke(g.path);
1102
+ c.setLineDash([]);
1103
+ c.stroke(g.arrowPath);
1104
+ const labelPx = label ? d.edgeLabel.size * (0, import_core8.labelGrowth)(zoom).connector : 0;
1105
+ if (label && labelPx * zoom >= LABEL_MIN_SCREEN_PX) {
1106
+ const px = labelPx;
1107
+ c.font = `${d.edgeLabel.weight} ${px}px ${d.fontNote}`;
1108
+ c.textAlign = "center";
1109
+ c.textBaseline = "middle";
1110
+ c.lineWidth = import_core8.CANVAS_STYLE.edge.labelHaloWidth;
1111
+ c.strokeStyle = d.tokens["canvas"];
1112
+ c.strokeText(label, g.mid.x, g.mid.y);
1113
+ c.fillStyle = d.tokens["edge-label"];
1114
+ c.fillText(label, g.mid.x, g.mid.y);
1115
+ }
1116
+ c.restore();
1117
+ }
1118
+
1119
+ // src/components/useFrames.ts
1120
+ var import_react5 = require("react");
1121
+ function useFrames(layerRef, busy) {
1122
+ const raf = (0, import_react5.useRef)(null);
1123
+ const busyRef = (0, import_react5.useRef)(busy);
1124
+ busyRef.current = busy;
1125
+ (0, import_react5.useEffect)(() => () => {
1126
+ if (raf.current !== null) cancelAnimationFrame(raf.current);
1127
+ raf.current = null;
1128
+ }, []);
1129
+ return (0, import_react5.useMemo)(() => {
1130
+ const tick = () => {
1131
+ raf.current = null;
1132
+ if (!busyRef.current()) return;
1133
+ layerRef.current?.batchDraw();
1134
+ raf.current = requestAnimationFrame(tick);
1135
+ };
1136
+ return {
1137
+ run: () => {
1138
+ layerRef.current?.batchDraw();
1139
+ if (raf.current === null) raf.current = requestAnimationFrame(tick);
1191
1140
  }
1192
- )
1193
- ] });
1194
- });
1195
- var KonvaNode_default = KonvaNode;
1141
+ };
1142
+ }, [layerRef]);
1143
+ }
1196
1144
 
1197
1145
  // src/components/KonvaNodeLayer.tsx
1198
- var import_jsx_runtime3 = require("react/jsx-runtime");
1146
+ var import_jsx_runtime2 = require("react/jsx-runtime");
1147
+ var SPATIAL_INDEX_THRESHOLD = 200;
1148
+ function stageViewRect(stage) {
1149
+ const s = stage.scaleX() || 1;
1150
+ return { minX: -stage.x() / s, minY: -stage.y() / s, maxX: (stage.width() - stage.x()) / s, maxY: (stage.height() - stage.y()) / s };
1151
+ }
1199
1152
  function KonvaNodeLayer({
1200
1153
  nodes: propNodes,
1201
- isTransitioning = false,
1202
1154
  isDragging = false,
1203
1155
  hoveredNodeId = null,
1204
- pressedNodeId = null
1156
+ pressedNodeId = null,
1157
+ selectedNodeId = null,
1158
+ focusedNodeId = null,
1159
+ design = DEFAULT_DESIGN,
1160
+ clock,
1161
+ touch = false,
1162
+ peekedNodeId = null
1205
1163
  }) {
1206
1164
  const layerRef = (0, import_react6.useRef)(null);
1207
- const zoomLevel = useViewportStore((s) => s.zoomLevel);
1165
+ const fonts = useFontsEpoch();
1208
1166
  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);
1167
+ const nodes = (0, import_react6.useMemo)(() => propNodes ?? Array.from(storeNodes.values()), [propNodes, storeNodes]);
1168
+ const index = (0, import_react6.useMemo)(() => {
1169
+ if (nodes.length < SPATIAL_INDEX_THRESHOLD || isDragging) return null;
1170
+ const idx = new import_core9.RBushSpatialIndex();
1171
+ idx.bulkLoad(nodes.map((n) => ({ nodeId: n.id, bounds: (0, import_core9.nodeBounds)(n) })));
1172
+ return idx;
1173
+ }, [nodes, isDragging]);
1174
+ const byId = (0, import_react6.useMemo)(() => index ? new Map(nodes.map((n) => [n.id, n])) : null, [index, nodes]);
1175
+ const highlighted = hoveredNodeId ?? peekedNodeId ?? selectedNodeId;
1176
+ const N = import_core9.CANVAS_STYLE.node;
1177
+ const focusTheme = design.theme.startsWith("focus");
1178
+ const dimOpacity = focusTheme ? touch ? N.dimmedOpacityFocusTouch : N.dimmedOpacityFocus : touch ? N.dimmedOpacityTouch : N.dimmedOpacity;
1179
+ const dimOthers = highlighted !== null && dimOpacity !== 1;
1180
+ const tweens = (0, import_react6.useRef)(new HoverTweens()).current;
1181
+ const frames = useFrames(layerRef, () => clock?.active() || tweens.active());
1182
+ (0, import_react6.useLayoutEffect)(() => {
1183
+ const raised = /* @__PURE__ */ new Set();
1184
+ if (hoveredNodeId) raised.add(hoveredNodeId);
1185
+ if (pressedNodeId) raised.add(pressedNodeId);
1186
+ if (selectedNodeId) raised.add(selectedNodeId);
1187
+ if (tweens.set(raised, design.comfort.reducedMotion || !(0, import_core9.layerBlooms)(nodes.length))) frames.run();
1188
+ }, [hoveredNodeId, pressedNodeId, selectedNodeId, design.comfort.reducedMotion, (0, import_core9.layerBlooms)(nodes.length), tweens, frames]);
1189
+ (0, import_react6.useLayoutEffect)(() => clock?.subscribe(() => frames.run()), [clock, frames]);
1214
1190
  (0, import_react6.useLayoutEffect)(() => {
1215
1191
  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)
1192
+ }, [nodes, index, design, fonts, hoveredNodeId, pressedNodeId, selectedNodeId, focusedNodeId, isDragging, touch, peekedNodeId]);
1193
+ const sceneFunc = (ctx2, shape) => {
1194
+ const stage = shape.getStage();
1195
+ if (!stage) return;
1196
+ const zoom = stage.scaleX() || 1;
1197
+ const view = stageViewRect(stage);
1198
+ let visible;
1199
+ if (isDragging) visible = nodes;
1200
+ else if (index && byId) visible = index.query(view).map((id) => byId.get(id)).filter(Boolean);
1201
+ else visible = nodes.filter((n) => (0, import_core9.intersectsViewport)(n, view));
1202
+ const c = ctx2._context;
1203
+ const m = c.getTransform();
1204
+ const opt = { shadows: !isDragging && (0, import_core9.shadowsShown)(visible.length), k: Math.hypot(m.a, m.b), base: m, dimOpacity };
1205
+ const st = { ...IDLE };
1206
+ const now = performance.now();
1207
+ const arrival = { alpha: 1, scale: 1, dx: 0, dy: 0 };
1208
+ const arriving = clock?.active(now) ?? false;
1209
+ for (let i = 0; i < visible.length; i++) {
1210
+ const n = visible[i];
1211
+ st.hovered = n.id === hoveredNodeId;
1212
+ st.pressed = n.id === pressedNodeId;
1213
+ st.selected = n.id === selectedNodeId;
1214
+ st.focused = n.id === focusedNodeId;
1215
+ st.dimmed = dimOthers && n.id !== highlighted;
1216
+ if (tweens.has(n.id)) {
1217
+ const p = tweens.progress(n.id, now);
1218
+ st.lift = p.lift;
1219
+ st.ring = p.ring;
1220
+ } else {
1221
+ st.lift = void 0;
1222
+ st.ring = void 0;
1223
+ }
1224
+ st.arrival = arriving && clock.node(n.id, n.x, n.y, now, arrival) ? arrival : null;
1225
+ drawIdea(c, n, design, zoom, st, opt);
1226
+ }
1227
+ c.setTransform(m);
1228
+ c.globalAlpha = 1;
1250
1229
  };
1230
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(KLayer, { ref: layerRef, name: "nodes", listening: false, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(KShape, { sceneFunc, listening: false, perfectDrawEnabled: false }) });
1251
1231
  }
1252
- var EDGE_SPACING = 25;
1253
- var STROKE_COLOR = "#000000";
1254
- var STROKE_COLOR_DARK = "#ffffff";
1255
- var STROKE_WIDTH = 2.5;
1256
- var KonvaEdge = (0, import_react7.memo)(function KonvaEdge2({
1257
- sourceNode: from,
1258
- targetNode: to,
1259
- connection,
1260
- isDark = false,
1261
- zoomLevel = 1
1262
- }) {
1263
- const label = connection?.label;
1264
- const points = (0, import_react7.useMemo)(() => {
1265
- const fromBaseX = from.width / 2;
1266
- const fromBaseY = Math.max(import_core9.NODE_HEIGHT * 1.6 / 2, fromBaseX * 0.74);
1267
- const fromScale = getOutermostScaleUp(from.branchCount);
1268
- const fromRadiusX = fromBaseX * fromScale;
1269
- const fromRadiusY = fromBaseY * fromScale;
1270
- const toBaseX = to.width / 2;
1271
- const toBaseY = Math.max(import_core9.NODE_HEIGHT * 1.6 / 2, toBaseX * 0.74);
1272
- const toScale = getOutermostScaleUp(to.branchCount);
1273
- const toRadiusX = toBaseX * toScale;
1274
- const toRadiusY = toBaseY * toScale;
1275
- const angle = Math.atan2(to.y - from.y, to.x - from.x);
1276
- const fromEdge = getEllipseIntersection(from.x, from.y, fromRadiusX, fromRadiusY, angle);
1277
- const toEdge = getEllipseIntersection(to.x, to.y, toRadiusX, toRadiusY, angle + Math.PI);
1278
- const fromX = fromEdge.x + EDGE_SPACING * Math.cos(angle);
1279
- const fromY = fromEdge.y + EDGE_SPACING * Math.sin(angle);
1280
- const toX = toEdge.x - EDGE_SPACING * Math.cos(angle);
1281
- const toY = toEdge.y - EDGE_SPACING * Math.sin(angle);
1282
- const midX = (fromX + toX) / 2;
1283
- const midY = (fromY + toY) / 2;
1284
- const distance = Math.sqrt((toX - fromX) ** 2 + (toY - fromY) ** 2);
1285
- const arcHeight = Math.min(45, 45 / (1 + Math.max(0, (distance - 200) / 400)));
1286
- const perpX = -(toY - fromY) / distance;
1287
- const perpY = (toX - fromX) / distance;
1288
- const offsetMidX = midX + perpX * arcHeight;
1289
- const offsetMidY = midY + perpY * arcHeight;
1290
- const ctrl1X = fromX + (offsetMidX - fromX) * 0.66;
1291
- const ctrl1Y = fromY + (offsetMidY - fromY) * 0.66;
1292
- const ctrl2X = toX + (offsetMidX - toX) * 0.66;
1293
- const ctrl2Y = toY + (offsetMidY - toY) * 0.66;
1294
- const NUM_SEGMENTS = 20;
1295
- const pts = [];
1296
- for (let i = 0; i <= NUM_SEGMENTS; i++) {
1297
- const t = i / NUM_SEGMENTS;
1298
- const mt = 1 - t;
1299
- const mt2 = mt * mt;
1300
- const mt3 = mt2 * mt;
1301
- const t2 = t * t;
1302
- const t3 = t2 * t;
1303
- pts.push(
1304
- mt3 * fromX + 3 * mt2 * t * ctrl1X + 3 * mt * t2 * ctrl2X + t3 * toX,
1305
- mt3 * fromY + 3 * mt2 * t * ctrl1Y + 3 * mt * t2 * ctrl2Y + t3 * toY
1306
- );
1307
- }
1308
- return pts;
1309
- }, [from.x, from.y, from.width, from.branchCount, to.x, to.y, to.width, to.branchCount]);
1310
- const arrowHeadPoints = (0, import_react7.useMemo)(() => {
1311
- const len = points.length;
1312
- const tipX = points[len - 2];
1313
- const tipY = points[len - 1];
1314
- const prevX = points[len - 4];
1315
- const prevY = points[len - 3];
1316
- const angle = Math.atan2(tipY - prevY, tipX - prevX);
1317
- const armLength = 11;
1318
- const armAngle = Math.PI / 5.5;
1319
- return [
1320
- tipX - armLength * Math.cos(angle - armAngle),
1321
- tipY - armLength * Math.sin(angle - armAngle),
1322
- tipX,
1323
- tipY,
1324
- tipX - armLength * Math.cos(angle + armAngle),
1325
- tipY - armLength * Math.sin(angle + armAngle)
1326
- ];
1327
- }, [points]);
1328
- const { pathData, fontSize } = (0, import_react7.useMemo)(() => {
1329
- if (!label) return { pathData: null, fontSize: 12 };
1330
- const fromBaseX = from.width / 2;
1331
- const fromBaseY = Math.max(import_core9.NODE_HEIGHT * 1.6 / 2, fromBaseX * 0.74);
1332
- const fromScale = getOutermostScaleUp(from.branchCount);
1333
- const fromRadiusX = fromBaseX * fromScale;
1334
- const fromRadiusY = fromBaseY * fromScale;
1335
- const toBaseX = to.width / 2;
1336
- const toBaseY = Math.max(import_core9.NODE_HEIGHT * 1.6 / 2, toBaseX * 0.74);
1337
- const toScale = getOutermostScaleUp(to.branchCount);
1338
- const toRadiusX = toBaseX * toScale;
1339
- const toRadiusY = toBaseY * toScale;
1340
- const angle = Math.atan2(to.y - from.y, to.x - from.x);
1341
- const fromEdge = getEllipseIntersection(from.x, from.y, fromRadiusX, fromRadiusY, angle);
1342
- const toEdge = getEllipseIntersection(to.x, to.y, toRadiusX, toRadiusY, angle + Math.PI);
1343
- const fromX = fromEdge.x + EDGE_SPACING * Math.cos(angle);
1344
- const fromY = fromEdge.y + EDGE_SPACING * Math.sin(angle);
1345
- const toX = toEdge.x - EDGE_SPACING * Math.cos(angle);
1346
- const toY = toEdge.y - EDGE_SPACING * Math.sin(angle);
1347
- const midX = (fromX + toX) / 2;
1348
- const midY = (fromY + toY) / 2;
1349
- const distance = Math.sqrt((toX - fromX) ** 2 + (toY - fromY) ** 2);
1350
- const arcHeight = Math.min(45, 45 / (1 + Math.max(0, (distance - 200) / 400)));
1351
- const perpX = -(toY - fromY) / distance;
1352
- const perpY = (toX - fromX) / distance;
1353
- const offsetMidX = midX + perpX * arcHeight;
1354
- const offsetMidY = midY + perpY * arcHeight;
1355
- const ctrl1X = fromX + (offsetMidX - fromX) * 0.66;
1356
- const ctrl1Y = fromY + (offsetMidY - fromY) * 0.66;
1357
- const ctrl2X = toX + (offsetMidX - toX) * 0.66;
1358
- const ctrl2Y = toY + (offsetMidY - toY) * 0.66;
1359
- const TEXT_OFFSET = 15;
1360
- const fxo = fromX + perpX * TEXT_OFFSET;
1361
- const fyo = fromY + perpY * TEXT_OFFSET;
1362
- const txo = toX + perpX * TEXT_OFFSET;
1363
- const tyo = toY + perpY * TEXT_OFFSET;
1364
- const c1xo = ctrl1X + perpX * TEXT_OFFSET;
1365
- const c1yo = ctrl1Y + perpY * TEXT_OFFSET;
1366
- const c2xo = ctrl2X + perpX * TEXT_OFFSET;
1367
- const c2yo = ctrl2Y + perpY * TEXT_OFFSET;
1368
- const baseFontSize = 14;
1369
- const zoomFactor = Math.max(1, 1 / zoomLevel);
1370
- const scaledFontSize = Math.max(12, Math.min(24, baseFontSize * (from.width / 180) * zoomFactor));
1371
- let d = "";
1372
- if (fromX > toX) {
1373
- d = `M ${txo},${tyo} C ${c2xo},${c2yo} ${c1xo},${c1yo} ${fxo},${fyo}`;
1374
- } else {
1375
- d = `M ${fxo},${fyo} C ${c1xo},${c1yo} ${c2xo},${c2yo} ${txo},${tyo}`;
1376
- }
1377
- return { pathData: d, fontSize: scaledFontSize };
1378
- }, [from.x, from.y, from.width, from.branchCount, to.x, to.y, to.width, to.branchCount, label, zoomLevel]);
1379
- const strokeColor = isDark ? STROKE_COLOR_DARK : STROKE_COLOR;
1380
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(KGroup, { children: [
1381
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1382
- KLine,
1383
- {
1384
- points,
1385
- stroke: strokeColor,
1386
- strokeWidth: STROKE_WIDTH,
1387
- tension: 0,
1388
- opacity: 0.9,
1389
- lineCap: "round",
1390
- lineJoin: "round",
1391
- listening: false,
1392
- perfectDrawEnabled: false
1393
- }
1394
- ),
1395
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1396
- KLine,
1397
- {
1398
- points: arrowHeadPoints,
1399
- stroke: strokeColor,
1400
- strokeWidth: STROKE_WIDTH,
1401
- lineCap: "round",
1402
- lineJoin: "round",
1403
- opacity: 0.9,
1404
- listening: false,
1405
- perfectDrawEnabled: false
1406
- }
1407
- ),
1408
- label && pathData && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
1409
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1410
- KTextPath,
1411
- {
1412
- data: pathData,
1413
- text: label,
1414
- fontSize,
1415
- fontFamily: "'Playpen Sans', cursive",
1416
- fontStyle: "normal",
1417
- fill: "#ffffff",
1418
- align: "center",
1419
- letterSpacing: 0.5,
1420
- listening: false,
1421
- opacity: 0.9
1422
- }
1423
- ),
1424
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1425
- KTextPath,
1426
- {
1427
- data: pathData,
1428
- text: label,
1429
- fontSize,
1430
- fontFamily: "'Playpen Sans', cursive",
1431
- fontStyle: "normal",
1432
- fill: "#6b6860",
1433
- align: "center",
1434
- letterSpacing: 0.5,
1435
- listening: false,
1436
- opacity: 0.9
1437
- }
1438
- )
1439
- ] })
1440
- ] });
1441
- });
1442
- var KonvaEdge_default = KonvaEdge;
1443
1232
 
1444
1233
  // src/components/KonvaEdgeLayer.tsx
1445
- var import_jsx_runtime5 = require("react/jsx-runtime");
1446
- function KonvaEdgeLayer({
1447
- nodes,
1448
- connections,
1449
- currentLevel,
1450
- isDark = false,
1451
- isDragging = false
1452
- }) {
1453
- const layerRef = (0, import_react8.useRef)(null);
1454
- const zoomLevel = useViewportStore((s) => s.zoomLevel);
1234
+ var import_react7 = require("react");
1235
+ var import_core10 = require("@visualli/core");
1236
+ var import_jsx_runtime3 = require("react/jsx-runtime");
1237
+ var key = (n) => `${n.x},${n.y},${n.width},${n.level},${n.branchCount ?? 0}`;
1238
+ var MARGIN = 80;
1239
+ function KonvaEdgeLayer({ connections, isDragging = false, design = DEFAULT_DESIGN, clock }) {
1240
+ const layerRef = (0, import_react7.useRef)(null);
1241
+ const fonts = useFontsEpoch();
1455
1242
  const storeNodes = useNodeStore((s) => s.nodes);
1456
- const visibleNodes = useViewportNodes(nodes, currentLevel, false, isDragging);
1457
- const visibleSet = (0, import_react8.useMemo)(() => new Set(visibleNodes.map((n) => n.id)), [visibleNodes]);
1458
- (0, import_react8.useLayoutEffect)(() => {
1243
+ const cache3 = (0, import_react7.useRef)(/* @__PURE__ */ new Map());
1244
+ const edges = (0, import_react7.useMemo)(() => {
1245
+ const out = [];
1246
+ const seen = /* @__PURE__ */ new Set();
1247
+ connections.forEach((conn, i) => {
1248
+ const src = storeNodes.get(conn.from);
1249
+ const tgt = storeNodes.get(conn.to);
1250
+ if (!src || !tgt) return;
1251
+ const id = `${conn.from}>${conn.to}#${i}`;
1252
+ seen.add(id);
1253
+ const k = `${key(src)}|${key(tgt)}`;
1254
+ let hit = cache3.current.get(id);
1255
+ if (!hit || hit.k !== k) {
1256
+ hit = { k, g: prepareConnector((0, import_core10.connectorGeometry)(src, tgt, design.metrics.edgeGap)) };
1257
+ cache3.current.set(id, hit);
1258
+ }
1259
+ out.push({ g: hit.g, dashed: conn.style === "dashed", label: conn.label });
1260
+ });
1261
+ for (const id of cache3.current.keys()) if (!seen.has(id)) cache3.current.delete(id);
1262
+ return out;
1263
+ }, [connections, storeNodes, design.metrics.edgeGap]);
1264
+ const frames = useFrames(layerRef, () => clock?.active() ?? false);
1265
+ (0, import_react7.useLayoutEffect)(() => clock?.subscribe(() => frames.run()), [clock, frames]);
1266
+ (0, import_react7.useLayoutEffect)(() => {
1459
1267
  layerRef.current?.batchDraw();
1460
- }, [connections, isDark, isDragging, visibleSet, storeNodes]);
1461
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(KLayer, { ref: layerRef, listening: false, children: connections.map((conn, idx) => {
1462
- const src = storeNodes.get(conn.from);
1463
- const tgt = storeNodes.get(conn.to);
1464
- if (!src || !tgt) return null;
1465
- if (!isDragging && !visibleSet.has(src.id) && !visibleSet.has(tgt.id)) return null;
1466
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1467
- KonvaEdge_default,
1468
- {
1469
- sourceNode: src,
1470
- targetNode: tgt,
1471
- connection: conn,
1472
- isDark,
1473
- zoomLevel
1474
- },
1475
- `${conn.from}-${conn.to}-${idx}`
1476
- );
1477
- }) });
1268
+ }, [edges, design, fonts, isDragging]);
1269
+ const sceneFunc = (ctx2, shape) => {
1270
+ const stage = shape.getStage();
1271
+ if (!stage) return;
1272
+ const zoom = stage.scaleX() || 1;
1273
+ const v = stageViewRect(stage);
1274
+ const c = ctx2._context;
1275
+ const now = performance.now();
1276
+ const arriving = clock?.active(now) ?? false;
1277
+ const arrival = { alpha: 1, draw: 1 };
1278
+ for (let i = 0; i < edges.length; i++) {
1279
+ const e = edges[i];
1280
+ const { a, b, mid } = e.g;
1281
+ const minX = Math.min(a.x, b.x, mid.x) - MARGIN, maxX = Math.max(a.x, b.x, mid.x) + MARGIN;
1282
+ const minY = Math.min(a.y, b.y, mid.y) - MARGIN, maxY = Math.max(a.y, b.y, mid.y) + MARGIN;
1283
+ if (maxX < v.minX || minX > v.maxX || maxY < v.minY || minY > v.maxY) continue;
1284
+ drawConnector(c, e.g, design, zoom, e.dashed, e.label, arriving && clock.edge(i, e.dashed, now, arrival) ? arrival : null);
1285
+ }
1286
+ };
1287
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(KLayer, { ref: layerRef, listening: false, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(KShape, { sceneFunc, listening: false, perfectDrawEnabled: false }) });
1478
1288
  }
1479
1289
 
1480
1290
  // src/components/KonvaContainerLayer.tsx
1481
- var import_react10 = require("react");
1291
+ var import_react8 = require("react");
1482
1292
 
1483
- // src/components/KonvaContainer.tsx
1484
- var import_react9 = require("react");
1293
+ // src/design/containers.ts
1485
1294
  var import_core11 = require("@visualli/core");
1486
-
1487
- // src/config/textScaling.ts
1488
- var import_core10 = require("@visualli/core");
1489
- var DESCRIPTION_TEXT_BASE_FONT_PX2 = 12;
1490
- var CONTAINER_LABEL_BASE_FONT_PX2 = 24;
1491
- var NODE_TEXT_SAFE_BLOB_FILL_RATIO = 0.88;
1492
- function computeNodeTextWorldScale2(nodeWidth, zoomLevel) {
1493
- const safeZoom = Math.max(zoomLevel, 1e-4);
1494
- const baseWidth = 300;
1495
- const baseRadiusX = baseWidth / 2;
1496
- const baseRadiusY = Math.max(import_core10.NODE_HEIGHT * 1.6 / 2, baseRadiusX * 0.74);
1497
- const textNaturalWidth = baseWidth - 40;
1498
- const textNaturalHeight = import_core10.NODE_HEIGHT * 1.2;
1499
- const maxScaleX = baseRadiusX * 2 * NODE_TEXT_SAFE_BLOB_FILL_RATIO / Math.max(textNaturalWidth, 1);
1500
- const maxScaleY = baseRadiusY * 2 * NODE_TEXT_SAFE_BLOB_FILL_RATIO / Math.max(textNaturalHeight, 1);
1501
- const fitScale = Math.max(0.01, Math.min(maxScaleX, maxScaleY));
1502
- return Math.min(1 / safeZoom, fitScale);
1295
+ var PILL = import_core11.CANVAS_STYLE.group;
1296
+ var pillFont = (d, px) => `${PILL.labelWeight} ${px}px ${d.fontNote}`;
1297
+ var measureCtx = null;
1298
+ function measurer() {
1299
+ if (!measureCtx && typeof document !== "undefined") measureCtx = document.createElement("canvas").getContext("2d");
1300
+ return measureCtx;
1503
1301
  }
1504
- function computeNodeTextScreenScale2(nodeWidth, zoomLevel) {
1505
- return computeNodeTextWorldScale2(nodeWidth, zoomLevel) * zoomLevel;
1506
- }
1507
-
1508
- // src/components/KonvaContainer.tsx
1509
- var import_jsx_runtime6 = require("react/jsx-runtime");
1510
- var CONTAINER_PADDING = 50;
1511
- var CONTAINER_DASH = [10, 5];
1512
- var LABEL_BG = "#ddd9d0";
1513
- var LABEL_BORDER_RADIUS = 20;
1514
- var LABEL_FONT_SIZE = CONTAINER_LABEL_BASE_FONT_PX2;
1515
- var LABEL_FONT_WEIGHT = "800";
1516
- var LABEL_FONT_FAMILY = "'Playpen Sans', Quicksand, sans-serif";
1517
- var LABEL_PAD_H = 20;
1518
- var LABEL_PAD_V = 10;
1519
- var LABEL_LINE_HEIGHT = 1.2;
1520
- var MAX_BLOB_EXTENT_X = 1.275;
1521
- var MAX_BLOB_EXTENT_Y = 1.234;
1522
- var _labelCtx = null;
1523
- function measureLabelBoxWidth(text, fontSize) {
1524
- if (!_labelCtx) {
1525
- const c = document.createElement("canvas");
1526
- _labelCtx = c.getContext("2d");
1302
+ function measurePill(text, d, k, c = measurer()) {
1303
+ const px = PILL.labelSize * d.labelScale * k;
1304
+ const padX = PILL.labelPadding.x, border = PILL.labelBorderWidth;
1305
+ const room = PILL.labelMaxWidth - padX * 2 - border * 2;
1306
+ let shown = text, tw = text.length * px * 0.5;
1307
+ if (c) {
1308
+ c.font = pillFont(d, px);
1309
+ tw = c.measureText(text).width;
1310
+ if (tw > room) {
1311
+ let lo = 0, hi = text.length;
1312
+ while (lo < hi) {
1313
+ const mid = lo + hi + 1 >> 1;
1314
+ if (c.measureText(text.slice(0, mid).trimEnd() + "\u2026").width <= room) lo = mid;
1315
+ else hi = mid - 1;
1316
+ }
1317
+ shown = text.slice(0, lo).trimEnd() + "\u2026";
1318
+ tw = c.measureText(shown).width;
1319
+ }
1527
1320
  }
1528
- if (!_labelCtx) return Math.ceil(text.length * fontSize * 0.65 + 2 * LABEL_PAD_H);
1529
- _labelCtx.font = `${LABEL_FONT_WEIGHT} ${fontSize}px ${LABEL_FONT_FAMILY}`;
1530
- return Math.ceil(_labelCtx.measureText(text).width + 2 * LABEL_PAD_H);
1321
+ return {
1322
+ text: shown,
1323
+ x: 0,
1324
+ y: 0,
1325
+ side: "bottom",
1326
+ w: Math.min(PILL.labelMaxWidth, tw + padX * 2 + border * 2),
1327
+ h: px * PILL.labelLineHeight + PILL.labelPadding.top + PILL.labelPadding.bottom + border * 2
1328
+ };
1531
1329
  }
1532
- function isLeftTurn(p1, p2, p3) {
1533
- return (p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x) > 0;
1330
+ function ideaBox(n) {
1331
+ const { rx, ry } = (0, import_core11.nodeRadii)(n);
1332
+ const rings = (0, import_core11.ringsFor)(n.branchCount);
1333
+ const s = rings.length ? import_core11.RINGS[rings.length - 1].scale : 1;
1334
+ return { x0: n.x - rx * s, x1: n.x + rx * s, y0: n.y - ry * s, y1: n.y + ry * s };
1534
1335
  }
1535
- function convexHull(points) {
1536
- if (points.length < 3) return points;
1537
- let start = 0;
1538
- for (let i = 1; i < points.length; i++) {
1539
- if (points[i].y < points[start].y || points[i].y === points[start].y && points[i].x < points[start].x) start = i;
1540
- }
1541
- const sorted = [...points];
1542
- const sp = sorted.splice(start, 1)[0];
1543
- sorted.sort((a, b) => {
1544
- const da = Math.atan2(a.y - sp.y, a.x - sp.x);
1545
- const db = Math.atan2(b.y - sp.y, b.x - sp.x);
1546
- if (da !== db) return da - db;
1547
- return Math.hypot(a.x - sp.x, a.y - sp.y) - Math.hypot(b.x - sp.x, b.y - sp.y);
1336
+ function layoutContainers(containers, nodes, connections, d) {
1337
+ if (containers.length === 0) return [];
1338
+ const ideas = [];
1339
+ for (const n of nodes.values()) ideas.push(ideaBox(n));
1340
+ const lines = connections.flatMap((c) => {
1341
+ const a = nodes.get(c.from), b = nodes.get(c.to);
1342
+ if (!a || !b) return [];
1343
+ const g = (0, import_core11.connectorGeometry)(a, b, d.metrics.edgeGap);
1344
+ return [(0, import_core11.connectorSamples)(g.a, g.mid, g.b)];
1548
1345
  });
1549
- const hull = [sp, sorted[0]];
1550
- for (let i = 1; i < sorted.length; i++) {
1551
- while (hull.length > 1 && !isLeftTurn(hull[hull.length - 2], hull[hull.length - 1], sorted[i])) hull.pop();
1552
- hull.push(sorted[i]);
1346
+ const placed = [];
1347
+ const out = [];
1348
+ for (const c of containers) {
1349
+ const members = c.nodeIds.map((id) => nodes.get(id)).filter((n) => !!n);
1350
+ const hull = (0, import_core11.containerHull)(members.map((n) => ({ x: n.x, y: n.y, ...(0, import_core11.nodeRadii)(n) })));
1351
+ if (!hull) continue;
1352
+ let pill = null;
1353
+ if (c.label) {
1354
+ const size = measurePill(c.label, d, 1);
1355
+ const at = (0, import_core11.placeContainerLabel)(hull, size, { ideas, connectors: lines, labels: placed });
1356
+ pill = { ...size, x: at.x, y: at.y, side: at.side };
1357
+ placed.push({ x0: at.x - size.w / 2, x1: at.x + size.w / 2, y0: at.y - size.h / 2, y1: at.y + size.h / 2 });
1358
+ }
1359
+ out.push({ id: c.id, hull, pill });
1553
1360
  }
1554
- return hull;
1361
+ return out;
1555
1362
  }
1556
- function chaikinSmooth(pts, passes = 6) {
1557
- let cur = pts;
1558
- for (let iter = 0; iter < passes; iter++) {
1559
- const next = [];
1560
- const n = cur.length;
1561
- for (let i = 0; i < n; i++) {
1562
- const a = cur[i], b = cur[(i + 1) % n];
1563
- next.push({ x: 0.75 * a.x + 0.25 * b.x, y: 0.75 * a.y + 0.25 * b.y });
1564
- next.push({ x: 0.25 * a.x + 0.75 * b.x, y: 0.25 * a.y + 0.75 * b.y });
1565
- }
1566
- cur = next;
1567
- }
1568
- return cur.flatMap((p) => [p.x, p.y]);
1363
+ function drawHull(c, h, d) {
1364
+ c.save();
1365
+ c.strokeStyle = d.tokens["line-strong"];
1366
+ c.lineWidth = PILL.strokeWidth;
1367
+ c.setLineDash([...PILL.dash]);
1368
+ c.beginPath();
1369
+ c.roundRect(h.x0, h.y0, h.x1 - h.x0, h.y1 - h.y0, h.radius);
1370
+ c.stroke();
1371
+ c.restore();
1569
1372
  }
1570
- function hullEdgeYAtX(points, targetX, isBottom) {
1571
- const n = points.length;
1572
- let result = isBottom ? -Infinity : Infinity;
1573
- for (let i = 0; i < n; i += 2) {
1574
- const x1 = points[i], y1 = points[i + 1];
1575
- const x2 = points[(i + 2) % n], y2 = points[(i + 3) % n];
1576
- if (x1 < targetX && x2 >= targetX || x2 < targetX && x1 >= targetX) {
1577
- const t = (targetX - x1) / (x2 - x1);
1578
- const y = y1 + t * (y2 - y1);
1579
- if (isBottom) result = Math.max(result, y);
1580
- else result = Math.min(result, y);
1581
- }
1582
- }
1583
- if (!isFinite(result)) {
1584
- for (let i = 1; i < n; i += 2) {
1585
- if (isBottom) result = Math.max(result, points[i]);
1586
- else result = Math.min(result, points[i]);
1587
- }
1588
- }
1589
- return result;
1373
+ function drawPill(c, p, d, zoom) {
1374
+ const k = (0, import_core11.labelGrowth)(zoom).idea;
1375
+ const size = k === 1 ? p : measurePill(p.text, d, k, c);
1376
+ const contrast = d.theme.startsWith("contrast");
1377
+ const bw = contrast ? PILL.contrastLabelBorderWidth : PILL.labelBorderWidth;
1378
+ c.save();
1379
+ c.beginPath();
1380
+ c.roundRect(p.x - size.w / 2 + bw / 2, p.y - size.h / 2 + bw / 2, size.w - bw, size.h - bw, (size.h - bw) / 2);
1381
+ c.fillStyle = d.tokens[PILL.labelFill];
1382
+ c.fill();
1383
+ c.lineWidth = bw;
1384
+ c.strokeStyle = d.tokens[contrast ? PILL.contrastLabelBorder : PILL.labelBorder];
1385
+ c.stroke();
1386
+ c.font = pillFont(d, PILL.labelSize * d.labelScale * k);
1387
+ c.fillStyle = d.tokens[PILL.labelInk];
1388
+ c.textAlign = "center";
1389
+ c.textBaseline = "middle";
1390
+ c.fillText(size.text, p.x, p.y + (PILL.labelPadding.top - PILL.labelPadding.bottom) / 2);
1391
+ c.restore();
1590
1392
  }
1591
- function calculateHull(containerNodes) {
1592
- if (containerNodes.length === 0) return null;
1593
- const NUM_SAMPLES = 48;
1594
- const samples = [];
1595
- for (const node of containerNodes) {
1596
- const baseRX = node.width / 2;
1597
- const baseRY = Math.max(import_core11.NODE_HEIGHT * 1.6 / 2, baseRX * 0.74);
1598
- const halfW = baseRX * MAX_BLOB_EXTENT_X + CONTAINER_PADDING;
1599
- const halfH = baseRY * MAX_BLOB_EXTENT_Y + CONTAINER_PADDING;
1600
- for (let j = 0; j < NUM_SAMPLES; j++) {
1601
- const angle = j / NUM_SAMPLES * Math.PI * 2;
1602
- samples.push({ x: node.x + halfW * Math.cos(angle), y: node.y + halfH * Math.sin(angle) });
1603
- }
1604
- }
1605
- const hull = convexHull(samples);
1606
- const points = chaikinSmooth(hull, 6);
1607
- const xs = containerNodes.map((n) => n.x);
1608
- const ys = containerNodes.map((n) => n.y);
1609
- const spanX = Math.max(...xs) - Math.min(...xs);
1610
- const spanY = Math.max(...ys) - Math.min(...ys);
1611
- const useTopEdge = spanY > spanX * 2;
1612
- let hullMinX = Infinity, hullMaxX = -Infinity;
1613
- for (let i = 0; i < points.length; i += 2) {
1614
- if (points[i] < hullMinX) hullMinX = points[i];
1615
- if (points[i] > hullMaxX) hullMaxX = points[i];
1616
- }
1617
- const anchorX = (hullMinX + hullMaxX) / 2;
1618
- const anchorEdgeY = hullEdgeYAtX(points, anchorX, !useTopEdge);
1619
- return { points, anchorX, anchorEdgeY };
1620
- }
1621
- var KonvaContainer = (0, import_react9.memo)(function KonvaContainer2({
1622
- nodes,
1623
- label = ""
1624
- }) {
1625
- const zoomLevel = useViewportStore((s) => s.zoomLevel);
1626
- const hullData = (0, import_react9.useMemo)(() => calculateHull(nodes), [nodes]);
1627
- if (!hullData) return null;
1628
- const refNode = nodes[0];
1629
- const screenScale = refNode ? computeNodeTextScreenScale2(refNode.width, zoomLevel) : 1;
1630
- const labelFontSize = LABEL_FONT_SIZE * screenScale;
1631
- const labelBoxHeight = Math.ceil(labelFontSize * LABEL_LINE_HEIGHT + 2 * LABEL_PAD_V);
1632
- const boxWidth = label ? measureLabelBoxWidth(label, labelFontSize) : 0;
1633
- const boxX = hullData.anchorX - boxWidth / 2;
1634
- const boxY = hullData.anchorEdgeY - labelBoxHeight / 2;
1635
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(KGroup, { listening: false, children: [
1636
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1637
- KLine,
1638
- {
1639
- points: hullData.points,
1640
- stroke: "#747270",
1641
- strokeWidth: 1,
1642
- dash: CONTAINER_DASH,
1643
- closed: true,
1644
- tension: 0,
1645
- listening: false,
1646
- perfectDrawEnabled: false
1647
- }
1648
- ),
1649
- label.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1650
- KGroup,
1651
- {
1652
- x: boxX + boxWidth / 2,
1653
- y: boxY + labelBoxHeight / 2,
1654
- offsetX: boxWidth / 2,
1655
- offsetY: labelBoxHeight / 2,
1656
- scaleX: 1 / zoomLevel,
1657
- scaleY: 1 / zoomLevel,
1658
- visible: zoomLevel >= import_core11.TEXT_LABEL_HIDE_BELOW_ZOOM,
1659
- listening: false,
1660
- children: [
1661
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1662
- KRect,
1663
- {
1664
- width: boxWidth,
1665
- height: labelBoxHeight,
1666
- fill: LABEL_BG,
1667
- cornerRadius: LABEL_BORDER_RADIUS,
1668
- shadowEnabled: false,
1669
- perfectDrawEnabled: false
1670
- }
1671
- ),
1672
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1673
- KText,
1674
- {
1675
- x: LABEL_PAD_H,
1676
- y: LABEL_PAD_V,
1677
- width: boxWidth - 2 * LABEL_PAD_H,
1678
- height: labelBoxHeight - 2 * LABEL_PAD_V,
1679
- text: label,
1680
- fontSize: labelFontSize,
1681
- fontFamily: LABEL_FONT_FAMILY,
1682
- fontStyle: LABEL_FONT_WEIGHT,
1683
- fill: "rgba(0,0,0,1)",
1684
- align: "center",
1685
- verticalAlign: "middle",
1686
- lineHeight: LABEL_LINE_HEIGHT,
1687
- perfectDrawEnabled: false,
1688
- listening: false
1689
- }
1690
- )
1691
- ]
1692
- }
1693
- )
1694
- ] });
1695
- });
1696
- var KonvaContainer_default = KonvaContainer;
1697
1393
 
1698
1394
  // src/components/KonvaContainerLayer.tsx
1699
- var import_jsx_runtime7 = require("react/jsx-runtime");
1700
- function KonvaContainerLayer({ nodes, containers, isDark = false }) {
1701
- const layerRef = (0, import_react10.useRef)(null);
1395
+ var import_jsx_runtime4 = require("react/jsx-runtime");
1396
+ function useContainerLayout(containers, connections = [], design = DEFAULT_DESIGN) {
1397
+ const fonts = useFontsEpoch();
1702
1398
  const storeNodes = useNodeStore((s) => s.nodes);
1703
- (0, import_react10.useLayoutEffect)(() => {
1399
+ return (0, import_react8.useMemo)(
1400
+ () => layoutContainers(containers, storeNodes, connections, design),
1401
+ // fonts: the names are measured, so measure again once the web fonts are in.
1402
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1403
+ [containers, connections, storeNodes, design, fonts]
1404
+ );
1405
+ }
1406
+ function KonvaContainerLayer({ containers, connections = [], design = DEFAULT_DESIGN }) {
1407
+ const layerRef = (0, import_react8.useRef)(null);
1408
+ const placed = useContainerLayout(containers, connections, design);
1409
+ (0, import_react8.useLayoutEffect)(() => {
1704
1410
  layerRef.current?.batchDraw();
1705
- }, [containers, isDark, storeNodes]);
1411
+ }, [placed, design]);
1706
1412
  if (containers.length === 0) return null;
1707
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(KLayer, { ref: layerRef, listening: false, children: containers.map((c) => {
1708
- const groupNodes = c.nodeIds.map((id) => storeNodes.get(id)).filter((n) => !!n);
1709
- if (groupNodes.length === 0) return null;
1710
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1711
- KonvaContainer_default,
1712
- {
1713
- nodes: groupNodes,
1714
- label: c.label,
1715
- isDark
1716
- },
1717
- c.id
1718
- );
1719
- }) });
1413
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(KLayer, { ref: layerRef, listening: false, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1414
+ KShape,
1415
+ {
1416
+ listening: false,
1417
+ perfectDrawEnabled: false,
1418
+ sceneFunc: (ctx2) => {
1419
+ for (const p of placed) drawHull(ctx2._context, p.hull, design);
1420
+ }
1421
+ }
1422
+ ) });
1423
+ }
1424
+
1425
+ // src/components/KonvaContainerLabelLayer.tsx
1426
+ var import_react9 = require("react");
1427
+ var import_jsx_runtime5 = require("react/jsx-runtime");
1428
+ function KonvaContainerLabelLayer({ containers, connections = [], design = DEFAULT_DESIGN }) {
1429
+ const layerRef = (0, import_react9.useRef)(null);
1430
+ const placed = useContainerLayout(containers, connections, design);
1431
+ (0, import_react9.useLayoutEffect)(() => {
1432
+ layerRef.current?.batchDraw();
1433
+ }, [placed, design]);
1434
+ if (!placed.some((p) => p.pill)) return null;
1435
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(KLayer, { ref: layerRef, listening: false, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1436
+ KShape,
1437
+ {
1438
+ listening: false,
1439
+ perfectDrawEnabled: false,
1440
+ sceneFunc: (ctx2, shape) => {
1441
+ const zoom = shape.getStage()?.scaleX() || 1;
1442
+ for (const p of placed) if (p.pill) drawPill(ctx2._context, p.pill, design, zoom);
1443
+ }
1444
+ }
1445
+ ) });
1720
1446
  }
1721
1447
 
1722
1448
  // src/components/NavigationStack.tsx
1723
1449
  var import_react11 = require("react");
1724
- var import_jsx_runtime8 = require("react/jsx-runtime");
1725
- var PASTEL_FILLS = [
1726
- "#b6eea7",
1727
- "#d5bcfe",
1728
- "#a1c4fc",
1729
- "#a6f5d8",
1730
- "#ffc9de",
1731
- "#c4e8ff",
1732
- "#f0c4ff",
1733
- "#ffe0a3"
1734
- ];
1735
- var ORGANIC_PATHS_SMALL = [
1736
- "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",
1737
- "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"
1738
- ];
1739
- 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";
1740
- var CIRCLE_SIZE = 14;
1741
- function hexToRgba(hex, opacity) {
1742
- const h = hex.replace("#", "");
1743
- const r = parseInt(h.substring(0, 2), 16);
1744
- const g = parseInt(h.substring(2, 4), 16);
1745
- const b = parseInt(h.substring(4, 6), 16);
1746
- return `rgba(${r},${g},${b},${opacity})`;
1747
- }
1748
- var OrganicCircle = (0, import_react11.memo)(function OrganicCircle2({
1749
- size,
1750
- fill,
1751
- stroke,
1752
- strokeWidth,
1753
- variant,
1754
- isActive
1755
- }) {
1756
- const path = isActive ? ORGANIC_PATH_ACTIVE : ORGANIC_PATHS_SMALL[variant % ORGANIC_PATHS_SMALL.length];
1757
- const vb = isActive ? "4 3 20 20" : "2 1 18 18";
1758
- const vbSz = isActive ? 20 : 18;
1759
- const sw = strokeWidth * vbSz / size;
1760
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("svg", { width: size, height: size, viewBox: vb, fill: "none", style: { display: "block" }, children: [
1761
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: path, fill, fillOpacity: 1 }),
1762
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: path, fill: "none", stroke, strokeWidth: sw, strokeLinecap: "round" })
1450
+
1451
+ // src/components/Overlays.tsx
1452
+ var import_react10 = __toESM(require("react"), 1);
1453
+ var import_core12 = require("@visualli/core");
1454
+ var import_jsx_runtime6 = require("react/jsx-runtime");
1455
+ var Icon = ({ children, size = 20 }) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("svg", { className: "vi-icon", width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children });
1456
+ var MinusIcon = () => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Icon, { children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("line", { x1: "5", y1: "12", x2: "19", y2: "12" }) });
1457
+ var PlusIcon = () => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(Icon, { children: [
1458
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("line", { x1: "12", y1: "5", x2: "12", y2: "19" }),
1459
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("line", { x1: "5", y1: "12", x2: "19", y2: "12" })
1460
+ ] });
1461
+ var FitIcon = () => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(Icon, { children: [
1462
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "M8 3H5a2 2 0 0 0-2 2v3" }),
1463
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "M21 8V5a2 2 0 0 0-2-2h-3" }),
1464
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "M3 16v3a2 2 0 0 0 2 2h3" }),
1465
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "M16 21h3a2 2 0 0 0 2-2v-3" })
1466
+ ] });
1467
+ var ChevronLeftIcon = () => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Icon, { size: 14, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("polyline", { points: "15 6 9 12 15 18" }) });
1468
+ var ChevronIcon = () => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Icon, { size: 14, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("polyline", { points: "9 6 15 12 9 18" }) });
1469
+ var ExternalIcon = () => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(Icon, { size: 14, children: [
1470
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" }),
1471
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("polyline", { points: "15 3 21 3 21 9" }),
1472
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("line", { x1: "10", y1: "14", x2: "21", y2: "3" })
1473
+ ] });
1474
+ var HomeIcon = () => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(Icon, { size: import_core12.TRAIL.homeIconSize, children: [
1475
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" }),
1476
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("polyline", { points: "9 22 9 12 15 12 15 22" })
1477
+ ] });
1478
+ var CloseIcon = () => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(Icon, { size: 18, children: [
1479
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
1480
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
1481
+ ] });
1482
+ var ZoomControls = (0, import_react10.memo)(function ZoomControls2({ onFit, touch = false }) {
1483
+ const size = touch ? "vi-iconbtn--md" : "vi-iconbtn--sm";
1484
+ const zoom = useViewportStore((s) => s.zoomLevel);
1485
+ const setZoom = useViewportStore((s) => s.setZoom);
1486
+ const fitted = useViewportStore((s) => Math.abs(s.zoomLevel - s.fitScale) < 1e-6 && Math.abs(s.centerX - s.fitCenterX) < 0.5 && Math.abs(s.centerY - s.fitCenterY) < 0.5);
1487
+ const fit = useViewportStore((s) => s.fitScale);
1488
+ const zoomIn = (0, import_react10.useCallback)(() => setZoom(zoom * import_core12.VIEW.zoomStep), [zoom, setZoom]);
1489
+ const zoomOut = (0, import_react10.useCallback)(() => setZoom(zoom / import_core12.VIEW.zoomStep), [zoom, setZoom]);
1490
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "vi-ctrls", role: "toolbar", "aria-label": "Canvas", "data-help": "zoom-controls", children: [
1491
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: `vi-iconbtn ${size}`, "aria-label": "Zoom out", onClick: zoomOut, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MinusIcon, {}) }),
1492
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "vi-ctrls__pct", "aria-live": "polite", children: [
1493
+ Math.round(zoom / fit * 100),
1494
+ "%"
1495
+ ] }),
1496
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: `vi-iconbtn ${size}`, "aria-label": "Zoom in", onClick: zoomIn, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(PlusIcon, {}) }),
1497
+ onFit && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
1498
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "vi-ctrls__div", "aria-hidden": "true" }),
1499
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: `vi-iconbtn ${size}`, "aria-label": "Fit map to view", disabled: fitted, onClick: onFit, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(FitIcon, {}) })
1500
+ ] })
1763
1501
  ] });
1764
1502
  });
1765
- var NavigationStack = (0, import_react11.memo)(function NavigationStack2({
1766
- stack,
1767
- onNavigateBack,
1768
- isDark = false,
1769
- top = "16px",
1770
- left = "16px"
1771
- }) {
1772
- const [hoveredIndex, setHoveredIndex] = (0, import_react11.useState)(null);
1503
+ function paintVars(p) {
1504
+ if (p.custom) return { fill: p.custom.fill, ring: p.custom.ring ?? "var(--edge)" };
1505
+ const t = p.topic ?? import_core12.TRAIL.rootTopic;
1506
+ return { fill: `var(--topic-${t})`, ring: `var(--topic-${t}-ring)` };
1507
+ }
1508
+ var TrailDot = ({ level, paint, current }) => {
1509
+ const { fill, ring } = paintVars(paint);
1510
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("svg", { width: import_core12.TRAIL.dotBox, height: import_core12.TRAIL.dotBox, viewBox: `${-import_core12.TRAIL.dotBox / 2} ${-import_core12.TRAIL.dotBox / 2} ${import_core12.TRAIL.dotBox} ${import_core12.TRAIL.dotBox}`, "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: (0, import_core12.blobPath)((0, import_core12.shapeOfLevel)(level), import_core12.TRAIL.dotRadius, import_core12.TRAIL.dotRadius), fill, stroke: ring, strokeWidth: current ? import_core12.TRAIL.currentStroke : import_core12.TRAIL.stroke }) });
1511
+ };
1512
+ var DepthTrail = (0, import_react10.memo)(function DepthTrail2({ stack, onNavigateBack }) {
1773
1513
  if (stack.length === 0) return null;
1774
- const visitedStroke = isDark ? "#908e8c" : "#747270";
1775
- const visitedText = isDark ? "#908e8c" : "#747270";
1776
- const activeStroke = isDark ? "#e8e6e4" : "#1a1a18";
1777
- const activeText = isDark ? "#e8e6e4" : "#1a1a18";
1778
- const connectorColor = isDark ? "#908e8c" : "#747270";
1779
- const connectorMarginLeft = 10.5;
1780
- const currentIndex = stack.length - 1;
1781
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1782
- "div",
1514
+ const current = stack.length - 1;
1515
+ const iconOnly = import_core12.TRAIL.rootLabel === "icon";
1516
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("nav", { className: "vi-trail", "aria-label": "Depth trail", "data-help": "navigation-stack", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("ol", { children: stack.map((e, i) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("li", { className: i === current ? "is-current" : void 0, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1517
+ "button",
1783
1518
  {
1784
- "data-help": "navigation-stack",
1785
- style: {
1786
- position: "absolute",
1787
- top,
1788
- left,
1789
- zIndex: 30,
1790
- pointerEvents: "auto"
1791
- },
1792
- children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { style: { display: "flex", flexDirection: "column", padding: "8px" }, children: stack.flatMap((entry, index) => {
1793
- const isCurrent = index === currentIndex;
1794
- const isClickable = index < currentIndex;
1795
- const isHovered = hoveredIndex === index;
1796
- const pastelFill = PASTEL_FILLS[index % PASTEL_FILLS.length];
1797
- const rowBg = isCurrent ? hexToRgba(pastelFill, 0.6) : isHovered && isClickable ? hexToRgba(pastelFill, 0.3) : "transparent";
1798
- const items = [];
1799
- if (index > 0) {
1800
- items.push(
1801
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1802
- "div",
1803
- {
1804
- style: {
1805
- width: "1px",
1806
- height: "20px",
1807
- marginLeft: `${connectorMarginLeft}px`,
1808
- marginTop: "-6px",
1809
- marginBottom: "-6px",
1810
- background: connectorColor,
1811
- alignSelf: "flex-start",
1812
- flexShrink: 0,
1813
- position: "relative",
1814
- zIndex: 1
1815
- }
1816
- },
1817
- `conn-${index}`
1818
- )
1819
- );
1820
- }
1821
- items.push(
1822
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1823
- "div",
1824
- {
1825
- style: {
1826
- position: "relative",
1827
- zIndex: 2,
1828
- alignSelf: "flex-start",
1829
- display: "flex",
1830
- alignItems: "center",
1831
- gap: "4px",
1832
- height: "22px",
1833
- padding: "4px",
1834
- borderRadius: "8px",
1835
- cursor: isClickable ? "pointer" : "default",
1836
- background: rowBg,
1837
- transition: "background 0.15s"
1838
- },
1839
- onMouseEnter: () => setHoveredIndex(index),
1840
- onMouseLeave: () => setHoveredIndex(null),
1841
- onClick: (e) => {
1842
- e.stopPropagation();
1843
- if (isClickable) onNavigateBack(index);
1844
- },
1845
- children: [
1846
- /* @__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 }) }),
1847
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1848
- "span",
1849
- {
1850
- style: {
1851
- fontFamily: "'Nunito', sans-serif",
1852
- fontSize: isCurrent || isHovered && isClickable ? "11px" : "9px",
1853
- fontWeight: isCurrent ? 600 : 400,
1854
- color: isCurrent ? activeText : visitedText,
1855
- lineHeight: 1,
1856
- whiteSpace: "nowrap",
1857
- transition: "color 0.15s, font-size 0.15s"
1858
- },
1859
- children: entry.label
1860
- }
1861
- )
1862
- ]
1863
- },
1864
- `${entry.layerId}-${index}`
1865
- )
1866
- );
1867
- return items;
1868
- }) })
1519
+ type: "button",
1520
+ disabled: i === current,
1521
+ "aria-current": i === current ? "location" : void 0,
1522
+ onClick: () => onNavigateBack(i),
1523
+ ...i === 0 && iconOnly ? { "aria-label": e.label, title: e.label } : {},
1524
+ children: [
1525
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(TrailDot, { level: e.level, paint: e.paint, current: i === current }),
1526
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: i === 0 ? iconOnly ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(HomeIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
1527
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(HomeIcon, {}),
1528
+ " ",
1529
+ e.label
1530
+ ] }) : e.label })
1531
+ ]
1869
1532
  }
1870
- );
1533
+ ) }, `${e.layerId}-${i}`)) }) });
1871
1534
  });
1872
- var NavigationStack_default = NavigationStack;
1873
-
1874
- // src/components/ZoomControls.tsx
1875
- var import_react12 = require("react");
1876
- var import_core12 = require("@visualli/core");
1877
- var import_jsx_runtime9 = require("react/jsx-runtime");
1878
- 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: [
1879
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("circle", { cx: "11", cy: "11", r: "8" }),
1880
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("line", { x1: "21", y1: "21", x2: "16.65", y2: "16.65" }),
1881
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("line", { x1: "11", y1: "8", x2: "11", y2: "14" }),
1882
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("line", { x1: "8", y1: "11", x2: "14", y2: "11" })
1883
- ] });
1884
- 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: [
1885
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("circle", { cx: "11", cy: "11", r: "8" }),
1886
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("line", { x1: "21", y1: "21", x2: "16.65", y2: "16.65" }),
1887
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("line", { x1: "8", y1: "11", x2: "14", y2: "11" })
1535
+ var escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1536
+ function splitTerms(text, anchors) {
1537
+ const words = anchors.filter((a) => a.word).sort((a, b) => b.word.length - a.word.length);
1538
+ if (!words.length) return [text];
1539
+ const re = new RegExp(`(${words.map((a) => escapeRe(a.word)).join("|")})`, "giu");
1540
+ const byWord = new Map(words.map((a) => [a.word.toLowerCase(), a]));
1541
+ const isWordChar = (ch) => !!ch && /[\p{L}\p{N}]/u.test(ch);
1542
+ const out = [];
1543
+ let last = 0;
1544
+ for (const m of text.matchAll(re)) {
1545
+ const start = m.index, end = start + m[0].length;
1546
+ if (isWordChar(text[start - 1]) || isWordChar(text[end])) continue;
1547
+ if (start > last) out.push(text.slice(last, start));
1548
+ out.push({ ...byWord.get(m[0].toLowerCase()), word: m[0] });
1549
+ last = end;
1550
+ }
1551
+ if (last < text.length) out.push(text.slice(last));
1552
+ return out;
1553
+ }
1554
+ var TermCard = ({ anchor, inSheet = false }) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: `vi-anchor-card${inSheet ? " is-in-sheet" : ""}`, role: "note", children: [
1555
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "vi-anchor-card__eyebrow", children: "Term" }),
1556
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "vi-anchor-card__word", style: { display: "block" }, children: anchor.word }),
1557
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "vi-anchor-card__desc", children: anchor.description }),
1558
+ anchor.knowMoreUrl && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "vi-anchor-card__acts", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("a", { className: "vi-fact__link", href: anchor.knowMoreUrl, target: "_blank", rel: "noopener noreferrer", children: [
1559
+ import_core12.TERM.learnMoreLabel,
1560
+ " ",
1561
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ExternalIcon, {})
1562
+ ] }) })
1888
1563
  ] });
1889
- var ZoomControls = (0, import_react12.memo)(function ZoomControls2({ isDark = false }) {
1890
- const zoomLevel = useViewportStore((s) => s.zoomLevel);
1891
- const setZoom = useViewportStore((s) => s.setZoom);
1892
- const handleZoomIn = (0, import_react12.useCallback)(() => setZoom(Math.min(import_core12.ZOOM_MAX, zoomLevel * 1.2)), [zoomLevel, setZoom]);
1893
- const handleZoomOut = (0, import_react12.useCallback)(() => setZoom(Math.max(import_core12.ZOOM_MIN, zoomLevel / 1.2)), [zoomLevel, setZoom]);
1894
- const surfaceBg = isDark ? "var(--bg-surface, #1C1C1A)" : "var(--bg-surface, #FAF8F4)";
1895
- const border = isDark ? "var(--border-color, #333330)" : "var(--border-color, #DDD9D0)";
1896
- const primary = isDark ? "var(--text-primary, #F0EDE6)" : "var(--text-primary, #1A1A18)";
1897
- const muted = isDark ? "var(--text-muted, #8E8C88)" : "var(--text-muted, #747270)";
1898
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
1899
- "div",
1564
+ function TermAnchor({ anchor }) {
1565
+ const [open, setOpen] = (0, import_react10.useState)(false);
1566
+ const [shift, setShift] = (0, import_react10.useState)(0);
1567
+ const pinned = (0, import_react10.useRef)(false);
1568
+ const timer = (0, import_react10.useRef)(void 0);
1569
+ const wrap = (0, import_react10.useRef)(null), btn = (0, import_react10.useRef)(null), card = (0, import_react10.useRef)(null);
1570
+ const show = (pin) => {
1571
+ clearTimeout(timer.current);
1572
+ if (pin) pinned.current = true;
1573
+ setOpen(true);
1574
+ };
1575
+ const hide = () => {
1576
+ clearTimeout(timer.current);
1577
+ pinned.current = false;
1578
+ setOpen(false);
1579
+ };
1580
+ (0, import_react10.useEffect)(() => () => clearTimeout(timer.current), []);
1581
+ (0, import_react10.useLayoutEffect)(() => {
1582
+ if (!open || !card.current) {
1583
+ setShift(0);
1584
+ return;
1585
+ }
1586
+ const r = card.current.getBoundingClientRect(), vw = document.documentElement.clientWidth, m = import_core12.TERM.viewportMargin;
1587
+ setShift(r.right > vw - m ? vw - m - r.right : r.left < m ? m - r.left : 0);
1588
+ }, [open]);
1589
+ (0, import_react10.useEffect)(() => {
1590
+ if (!open) return;
1591
+ const onKey = (e) => {
1592
+ if (e.key === "Escape") {
1593
+ e.stopPropagation();
1594
+ hide();
1595
+ btn.current?.focus();
1596
+ }
1597
+ };
1598
+ const onPress = (e) => {
1599
+ if (!wrap.current?.contains(e.target)) hide();
1600
+ };
1601
+ document.addEventListener("keydown", onKey, true);
1602
+ document.addEventListener("pointerdown", onPress, true);
1603
+ return () => {
1604
+ document.removeEventListener("keydown", onKey, true);
1605
+ document.removeEventListener("pointerdown", onPress, true);
1606
+ };
1607
+ }, [open]);
1608
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1609
+ "span",
1900
1610
  {
1901
- style: {
1902
- position: "relative",
1903
- background: surfaceBg,
1904
- border: `0.5px solid ${border}`,
1905
- borderRadius: "8px",
1906
- overflow: "hidden",
1907
- width: "65px",
1908
- height: "32px",
1909
- flexShrink: 0
1611
+ ref: wrap,
1612
+ className: `vi-anchor${open ? " is-open" : ""}`,
1613
+ "data-semantic-tooltip": "true",
1614
+ onPointerEnter: (e) => {
1615
+ if (!(0, import_core12.hoverOpensTerm)(e.pointerType)) return;
1616
+ clearTimeout(timer.current);
1617
+ timer.current = setTimeout(() => setOpen(true), import_core12.TERM.hoverOpen);
1618
+ },
1619
+ onPointerLeave: (e) => {
1620
+ if (!(0, import_core12.hoverOpensTerm)(e.pointerType) || pinned.current) return;
1621
+ clearTimeout(timer.current);
1622
+ timer.current = setTimeout(() => setOpen(false), import_core12.TERM.hoverClose);
1623
+ },
1624
+ onBlur: (e) => {
1625
+ if (!wrap.current?.contains(e.relatedTarget)) hide();
1910
1626
  },
1911
- role: "group",
1912
- "aria-label": "Zoom controls",
1913
1627
  children: [
1914
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1628
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1915
1629
  "button",
1916
1630
  {
1917
- onClick: handleZoomIn,
1918
- disabled: zoomLevel >= import_core12.ZOOM_MAX,
1919
- style: {
1920
- position: "absolute",
1921
- top: 0,
1922
- left: 0,
1923
- width: "32px",
1924
- height: "100%",
1925
- display: "flex",
1926
- alignItems: "center",
1927
- justifyContent: "center",
1928
- border: "none",
1929
- background: "transparent",
1930
- cursor: zoomLevel >= import_core12.ZOOM_MAX ? "not-allowed" : "pointer",
1931
- color: zoomLevel >= import_core12.ZOOM_MAX ? muted : primary,
1932
- transition: "background 0.15s"
1933
- },
1934
- onMouseEnter: (e) => {
1935
- if (zoomLevel < import_core12.ZOOM_MAX) e.currentTarget.style.background = isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.05)";
1936
- },
1937
- onMouseLeave: (e) => {
1938
- e.currentTarget.style.background = "transparent";
1631
+ ref: btn,
1632
+ type: "button",
1633
+ className: "vi-term",
1634
+ "aria-expanded": open,
1635
+ onClick: () => {
1636
+ if (open && pinned.current) {
1637
+ hide();
1638
+ return;
1639
+ }
1640
+ show(true);
1641
+ setTimeout(() => card.current?.querySelector("a, button")?.focus(), 0);
1939
1642
  },
1940
- "aria-label": "Zoom in",
1941
- children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ZoomInIcon, {})
1643
+ children: anchor.word
1942
1644
  }
1943
1645
  ),
1944
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: { position: "absolute", top: 0, bottom: 0, left: "32px", width: "0.5px", background: border } }),
1945
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1946
- "button",
1947
- {
1948
- onClick: handleZoomOut,
1949
- disabled: zoomLevel <= import_core12.ZOOM_MIN,
1950
- style: {
1951
- position: "absolute",
1952
- top: 0,
1953
- right: 0,
1954
- width: "33px",
1955
- height: "100%",
1956
- display: "flex",
1957
- alignItems: "center",
1958
- justifyContent: "center",
1959
- border: "none",
1960
- background: "transparent",
1961
- cursor: zoomLevel <= import_core12.ZOOM_MIN ? "not-allowed" : "pointer",
1962
- color: zoomLevel <= import_core12.ZOOM_MIN ? muted : primary,
1963
- transition: "background 0.15s"
1964
- },
1965
- onMouseEnter: (e) => {
1966
- if (zoomLevel > import_core12.ZOOM_MIN) e.currentTarget.style.background = isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.05)";
1967
- },
1968
- onMouseLeave: (e) => {
1969
- e.currentTarget.style.background = "transparent";
1970
- },
1971
- "aria-label": "Zoom out",
1972
- children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ZoomOutIcon, {})
1973
- }
1974
- )
1646
+ open && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { ref: card, className: "vi-anchor__pop", style: shift ? { transform: `translateX(${shift}px)` } : void 0, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(TermCard, { anchor }) })
1975
1647
  ]
1976
1648
  }
1977
1649
  );
1650
+ }
1651
+ var PeekCard = (0, import_react10.memo)(function PeekCard2({ node, anchors, onStepInside, onClose, renderContent }) {
1652
+ const parts = (0, import_react10.useMemo)(() => splitTerms(node.description ?? "", anchors), [node.description, anchors]);
1653
+ const { fill, ring } = paintVars(node);
1654
+ const style = { ["--vi-fact-fill"]: fill, ["--vi-fact-ring"]: ring };
1655
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "vi-fact vi-peek", style, role: "dialog", "aria-label": node.title, "data-node-tooltip": "true", children: [
1656
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "vi-fact__head", children: [
1657
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "vi-fact__title", children: node.title }),
1658
+ onClose && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "vi-iconbtn vi-iconbtn--sm vi-fact__close", "aria-label": "Close", onClick: onClose, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CloseIcon, {}) })
1659
+ ] }),
1660
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "vi-fact__body", children: renderContent ? renderContent(node.description ?? "") : parts.map((p, i) => typeof p === "string" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react10.default.Fragment, { children: p }, i) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(TermAnchor, { anchor: p }, i)) }),
1661
+ onStepInside && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "vi-fact__foot", children: [
1662
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", {}),
1663
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "vi-fact__acts", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("button", { type: "button", className: "vi-fact__step", onClick: onStepInside, children: [
1664
+ "Step inside ",
1665
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ChevronIcon, {})
1666
+ ] }) })
1667
+ ] })
1668
+ ] });
1978
1669
  });
1979
- var ZoomControls_default = ZoomControls;
1980
-
1981
- // src/components/SketchyBoxKonva.tsx
1982
- var import_react13 = require("react");
1983
- var import_konva = __toESM(require("konva"), 1);
1984
- var import_jsx_runtime10 = require("react/jsx-runtime");
1985
- 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";
1986
- 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";
1987
- var BACK_W = 486;
1988
- var BACK_H = 178;
1989
- var FRONT_W = 490;
1990
- var FRONT_H = 168;
1991
- var BACK_OFFSET_Y = 8;
1992
- var FRONT_OFFSET_X = 5.65;
1993
- var SHADOW_PAD = 12;
1994
- var SketchyBoxKonva = ({
1995
- children,
1996
- fill,
1997
- stroke,
1998
- backStroke,
1999
- padding = "1rem 1.25rem"
2000
- }) => {
2001
- const wrapperRef = (0, import_react13.useRef)(null);
2002
- const konvaContainerRef = (0, import_react13.useRef)(null);
2003
- const stageRef = (0, import_react13.useRef)(null);
2004
- (0, import_react13.useEffect)(() => {
2005
- const wrapper = wrapperRef.current;
2006
- const konvaEl = konvaContainerRef.current;
2007
- if (!wrapper || !konvaEl) return;
2008
- function draw(W, H) {
2009
- if (!konvaEl || W <= 0 || H <= 0) return;
2010
- if (stageRef.current) {
2011
- stageRef.current.destroy();
2012
- stageRef.current = null;
2013
- }
2014
- const stage = new import_konva.default.Stage({ container: konvaEl, width: W, height: H });
2015
- const layer = new import_konva.default.Layer({ listening: false });
2016
- const contentH = H - SHADOW_PAD;
2017
- layer.add(new import_konva.default.Path({
2018
- x: 0,
2019
- y: BACK_OFFSET_Y,
2020
- data: BACK_PATH,
2021
- scaleX: W / BACK_W,
2022
- scaleY: contentH / BACK_H,
2023
- fill: "#ffffff",
2024
- stroke: backStroke,
2025
- strokeWidth: 2,
2026
- strokeScaleEnabled: false,
2027
- listening: false
2028
- }));
2029
- layer.add(new import_konva.default.Path({
2030
- x: FRONT_OFFSET_X,
2031
- y: 0,
2032
- data: FRONT_PATH,
2033
- scaleX: (W - FRONT_OFFSET_X) / FRONT_W,
2034
- scaleY: contentH / FRONT_H,
2035
- fill,
2036
- stroke,
2037
- strokeWidth: 3,
2038
- strokeScaleEnabled: false,
2039
- listening: false
2040
- }));
2041
- stage.add(layer);
2042
- stageRef.current = stage;
2043
- }
2044
- draw(wrapper.offsetWidth, wrapper.offsetHeight);
2045
- const ro = new ResizeObserver((entries) => {
2046
- const el = entries[0].target;
2047
- draw(el.offsetWidth, el.offsetHeight);
2048
- });
2049
- ro.observe(wrapper);
2050
- return () => {
2051
- ro.disconnect();
2052
- stageRef.current?.destroy();
2053
- stageRef.current = null;
2054
- };
2055
- }, [fill, stroke, backStroke]);
2056
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { ref: wrapperRef, style: { position: "relative", paddingBottom: `${SHADOW_PAD}px` }, children: [
2057
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1670
+ var PeekSheet = (0, import_react10.memo)(function PeekSheet2({ node, anchors, onStepInside, onClose, renderContent, expanded, onToggleExpanded, height }) {
1671
+ const [term, setTerm] = (0, import_react10.useState)(null);
1672
+ const parts = (0, import_react10.useMemo)(() => splitTerms(node.description ?? "", anchors), [node.description, anchors]);
1673
+ const startY = (0, import_react10.useRef)(null);
1674
+ const swiped = (0, import_react10.useRef)(false);
1675
+ const { fill, ring } = paintVars(node);
1676
+ const style = {
1677
+ ["--vi-fact-fill"]: fill,
1678
+ ["--vi-fact-ring"]: ring,
1679
+ ["--vi-sheet-h"]: `${height}px`,
1680
+ zIndex: "var(--z-card)"
1681
+ };
1682
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: `vi-fact vi-fact--sheet vi-peek${expanded ? " is-expanded" : ""}`, style, role: "dialog", "aria-label": node.title, "data-node-tooltip": "true", children: [
1683
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2058
1684
  "div",
2059
1685
  {
2060
- ref: konvaContainerRef,
2061
- style: { position: "absolute", inset: 0, pointerEvents: "none", zIndex: 0, overflow: "hidden" }
1686
+ className: "vi-fact__grip",
1687
+ onPointerDown: (e) => {
1688
+ startY.current = e.clientY;
1689
+ swiped.current = false;
1690
+ },
1691
+ onPointerUp: (e) => {
1692
+ if (startY.current === null) return;
1693
+ const dy = e.clientY - startY.current;
1694
+ startY.current = null;
1695
+ if (dy < -import_core12.SHEET.expandDrag) {
1696
+ swiped.current = true;
1697
+ if (!expanded) onToggleExpanded();
1698
+ } else if (dy > import_core12.SHEET.dismissDrag) {
1699
+ swiped.current = true;
1700
+ if (expanded) onToggleExpanded();
1701
+ else onClose();
1702
+ }
1703
+ },
1704
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "vi-fact__handle", "aria-label": expanded ? "Collapse" : "Expand", "aria-expanded": expanded, onClick: () => {
1705
+ if (!swiped.current) onToggleExpanded();
1706
+ swiped.current = false;
1707
+ } })
2062
1708
  }
2063
1709
  ),
2064
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { style: { position: "relative", padding, zIndex: 1 }, children })
1710
+ term ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
1711
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("button", { type: "button", className: "vi-fact__back", onClick: () => setTerm(null), children: [
1712
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ChevronLeftIcon, {}),
1713
+ " Back"
1714
+ ] }),
1715
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(TermCard, { anchor: term, inSheet: true })
1716
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
1717
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "vi-fact__head", children: [
1718
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "vi-fact__title", children: node.title }),
1719
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "vi-iconbtn vi-iconbtn--md vi-fact__close", "aria-label": "Close", onClick: onClose, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CloseIcon, {}) })
1720
+ ] }),
1721
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "vi-fact__body", children: renderContent ? renderContent(node.description ?? "") : parts.map((p, i) => typeof p === "string" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react10.default.Fragment, { children: p }, i) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "vi-anchor", "data-semantic-tooltip": "true", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "vi-term", onClick: () => setTerm(p), children: p.word }) }, i)) }),
1722
+ onStepInside && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "vi-fact__foot", children: [
1723
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", {}),
1724
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "vi-fact__acts", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("button", { type: "button", className: "vi-fact__step", onClick: onStepInside, children: [
1725
+ "Step inside ",
1726
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ChevronIcon, {})
1727
+ ] }) })
1728
+ ] })
1729
+ ] })
2065
1730
  ] });
2066
- };
2067
- var SketchyBoxKonva_default = SketchyBoxKonva;
1731
+ });
2068
1732
 
2069
- // src/VisualliCanvas.tsx
2070
- var import_jsx_runtime11 = require("react/jsx-runtime");
2071
- function hexToRgba2(hex, alpha) {
2072
- hex = hex.replace(/^#/, "");
2073
- let r, g, b;
2074
- if (hex.length === 3) {
2075
- r = parseInt(hex[0] + hex[0], 16);
2076
- g = parseInt(hex[1] + hex[1], 16);
2077
- b = parseInt(hex[2] + hex[2], 16);
2078
- } else if (hex.length === 6) {
2079
- r = parseInt(hex.substring(0, 2), 16);
2080
- g = parseInt(hex.substring(2, 4), 16);
2081
- b = parseInt(hex.substring(4, 6), 16);
2082
- } else {
2083
- return `rgba(240, 237, 230, ${alpha})`;
2084
- }
2085
- return `rgba(${r}, ${g}, ${b}, ${alpha})`;
1733
+ // src/components/NavigationStack.tsx
1734
+ var import_jsx_runtime7 = require("react/jsx-runtime");
1735
+ var NavigationStack = (0, import_react11.memo)(function NavigationStack2({ stack, onNavigateBack, top, left }) {
1736
+ const entries = (0, import_react11.useMemo)(
1737
+ () => stack.map((e) => ({ layerId: e.layerId, label: e.label, level: e.layer.level, paint: e.paint ?? {} })),
1738
+ [stack]
1739
+ );
1740
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "vi-map__trail", style: top || left ? { top, left } : void 0, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(DepthTrail, { stack: entries, onNavigateBack }) });
1741
+ });
1742
+ var NavigationStack_default = NavigationStack;
1743
+
1744
+ // src/components/A11yLayer.tsx
1745
+ var import_react12 = require("react");
1746
+ var import_core13 = require("@visualli/core");
1747
+ var import_jsx_runtime8 = require("react/jsx-runtime");
1748
+ var MAX_MIRRORED = 150;
1749
+ var A11yLayer = (0, import_react12.memo)(function A11yLayer2({ nodes, label, hasChildLayer, onActivate, onFocusNode, announcement }) {
1750
+ const cx = useViewportStore((s) => s.centerX);
1751
+ const cy = useViewportStore((s) => s.centerY);
1752
+ const zoom = useViewportStore((s) => s.zoomLevel);
1753
+ const w = useViewportStore((s) => s.canvasWidth);
1754
+ const h = useViewportStore((s) => s.canvasHeight);
1755
+ const [view, setView] = (0, import_react12.useState)({ cx, cy, zoom, w, h });
1756
+ (0, import_react12.useEffect)(() => {
1757
+ const t = setTimeout(() => setView({ cx, cy, zoom, w, h }), 120);
1758
+ return () => clearTimeout(t);
1759
+ }, [cx, cy, zoom, w, h]);
1760
+ const items = (0, import_react12.useMemo)(() => {
1761
+ const halfW = view.w / 2 / view.zoom, halfH = view.h / 2 / view.zoom;
1762
+ const bounds = { minX: view.cx - halfW, maxX: view.cx + halfW, minY: view.cy - halfH, maxY: view.cy + halfH };
1763
+ const visible = nodes.filter((n) => (0, import_core13.intersectsViewport)(n, bounds));
1764
+ const picked = visible.length > MAX_MIRRORED ? visible.map((n) => ({ n, d: (n.x - view.cx) ** 2 + (n.y - view.cy) ** 2 })).sort((a, b) => a.d - b.d).slice(0, MAX_MIRRORED).map((e) => e.n) : visible;
1765
+ const row = (n) => Math.round((0, import_core13.nodeBounds)(n).minY / 150);
1766
+ return [...picked].sort((a, b) => row(a) - row(b) || a.x - b.x);
1767
+ }, [nodes, view]);
1768
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
1769
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "vi-sr", role: "list", "aria-label": label, children: items.map((n) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { role: "listitem", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1770
+ "button",
1771
+ {
1772
+ type: "button",
1773
+ "data-node-id": n.id,
1774
+ "aria-label": `${n.title}. Depth ${n.level + 1}.${hasChildLayer(n.id) ? " Has more inside; press Enter to step inside." : ""}`,
1775
+ onFocus: () => onFocusNode(n.id),
1776
+ onBlur: () => onFocusNode(null),
1777
+ onClick: () => onActivate(n)
1778
+ }
1779
+ ) }, n.id)) }),
1780
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "vi-sr", role: "status", "aria-live": "polite", children: announcement })
1781
+ ] });
1782
+ });
1783
+
1784
+ // src/design/useDesign.ts
1785
+ var import_react13 = require("react");
1786
+ function useMedia(query) {
1787
+ const subscribe = (cb) => {
1788
+ if (typeof window === "undefined" || !window.matchMedia) return () => {
1789
+ };
1790
+ const mq = window.matchMedia(query);
1791
+ mq.addEventListener("change", cb);
1792
+ return () => mq.removeEventListener("change", cb);
1793
+ };
1794
+ const get = () => typeof window !== "undefined" && !!window.matchMedia && window.matchMedia(query).matches;
1795
+ return (0, import_react13.useSyncExternalStore)(subscribe, get, () => false);
1796
+ }
1797
+ function useReaderEnv() {
1798
+ return {
1799
+ prefersDark: useMedia("(prefers-color-scheme: dark)"),
1800
+ forcedColors: useMedia("(forced-colors: active)"),
1801
+ prefersReducedMotion: useMedia("(prefers-reduced-motion: reduce)")
1802
+ };
1803
+ }
1804
+ function useDesign(props) {
1805
+ const env = useReaderEnv();
1806
+ const { theme, isDark, respectForcedColors } = props;
1807
+ const { readableType, largerText, reducedMotion } = props.comfort ?? {};
1808
+ return (0, import_react13.useMemo)(
1809
+ () => resolveDesign({ theme, isDark, respectForcedColors, comfort: { readableType, largerText, reducedMotion } }, env),
1810
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1811
+ [theme, isDark, respectForcedColors, readableType, largerText, reducedMotion, env.prefersDark, env.forcedColors, env.prefersReducedMotion]
1812
+ );
1813
+ }
1814
+
1815
+ // src/design/measure.ts
1816
+ var import_core14 = require("@visualli/core");
1817
+ var ctx;
1818
+ var cache2 = /* @__PURE__ */ new Map();
1819
+ var MAX_CACHED = 8192;
1820
+ function ideaMeasure(d) {
1821
+ if (ctx === void 0) ctx = typeof document !== "undefined" ? document.createElement("canvas").getContext("2d") : null;
1822
+ const c = ctx;
1823
+ if (!c) return import_core14.approximateMeasure;
1824
+ const weight = import_core14.TYPE_STYLES["node-label"].weight;
1825
+ const tracking = d.comfort.readableType ? 0.02 : 0;
1826
+ return (text, px) => {
1827
+ const font = `${weight} ${px}px ${d.fontHand}`;
1828
+ const key2 = `${fontsEpoch()}|${font}|${text}`;
1829
+ let w = cache2.get(key2);
1830
+ if (w === void 0) {
1831
+ c.font = font;
1832
+ w = c.measureText(text).width + text.length * px * tracking;
1833
+ if (cache2.size >= MAX_CACHED) cache2.clear();
1834
+ cache2.set(key2, w);
1835
+ }
1836
+ return w;
1837
+ };
2086
1838
  }
1839
+
1840
+ // src/VisualliCanvas.tsx
1841
+ var import_jsx_runtime9 = require("react/jsx-runtime");
2087
1842
  function resolveDocument(props, fileText) {
2088
1843
  if (props.preParsedVisualli) return props.preParsedVisualli;
2089
1844
  const rawContent = fileText ?? props.visualliString;
2090
1845
  if (rawContent) {
2091
1846
  try {
2092
- return (0, import_core13.parseVisualliFile)(rawContent);
1847
+ return (0, import_core15.parseVisualliFile)(rawContent);
2093
1848
  } catch {
2094
1849
  return null;
2095
1850
  }
@@ -2108,6 +1863,13 @@ function layerLabel(doc, layer, layerId) {
2108
1863
  }
2109
1864
  return layer.description ?? layerId;
2110
1865
  }
1866
+ function layerPaint(doc, layer) {
1867
+ const parentLayer = layer.parentLayerId ? doc.layers.get(layer.parentLayerId) : null;
1868
+ const index = parentLayer ? parentLayer.nodes.findIndex((n) => n.id === layer.parentNodeId) : -1;
1869
+ if (!parentLayer || index < 0) return { topic: import_core15.TRAIL.rootTopic };
1870
+ const { topic, custom } = (0, import_core15.ideaColor)(parentLayer.nodes[index].data.color, index);
1871
+ return topic ? { topic } : { custom };
1872
+ }
2111
1873
  function getRootLayerId(doc) {
2112
1874
  for (const [id, layer] of doc.layers) {
2113
1875
  if (!layer.parentLayerId) return id;
@@ -2115,7 +1877,22 @@ function getRootLayerId(doc) {
2115
1877
  return doc.layers.keys().next().value ?? null;
2116
1878
  }
2117
1879
  function VisualliCanvas(props) {
2118
- const { isDark = false, chromaticImmersion = false, onNodeClick, onLayerChange, onNodeHover, renderOverlay, renderNodeContent, navigationStackTop = "16px", navigationStackLeft = "16px", className = "", style } = props;
1880
+ const { chromaticImmersion = false, onNodeClick, onLayerChange, onNodeHover, renderOverlay, renderNodeContent, navigationStackTop, navigationStackLeft, className = "", style } = props;
1881
+ const onNodeHoverRef = (0, import_react14.useRef)(onNodeHover);
1882
+ onNodeHoverRef.current = onNodeHover;
1883
+ const design = useDesign({ theme: props.theme ?? (props.isDark === void 0 ? "light" : void 0), isDark: props.isDark, comfort: props.comfort, respectForcedColors: props.respectForcedColors });
1884
+ const isDark = design.isDark;
1885
+ const [fontSheet] = (0, import_react14.useState)(() => ensureDesignSystemStyles({ fontBaseUrl: props.fontBaseUrl, loadWebFonts: props.loadWebFonts }));
1886
+ const [fontsReady, setFontsReady] = (0, import_react14.useState)(false);
1887
+ (0, import_react14.useEffect)(() => {
1888
+ let live = true;
1889
+ loadCanvasFonts(fontSheet).then(() => {
1890
+ if (live) setFontsReady(true);
1891
+ });
1892
+ return () => {
1893
+ live = false;
1894
+ };
1895
+ }, [fontSheet]);
2119
1896
  const [fileText, setFileText] = (0, import_react14.useState)(void 0);
2120
1897
  (0, import_react14.useEffect)(() => {
2121
1898
  if (!props.visualliFile) {
@@ -2147,6 +1924,7 @@ function VisualliCanvas(props) {
2147
1924
  const [navStack, setNavStack] = (0, import_react14.useState)([]);
2148
1925
  const [currentLayerId, setCurrentLayerId] = (0, import_react14.useState)(null);
2149
1926
  const parentViewports = (0, import_react14.useRef)([]);
1927
+ const stepTargetRef = (0, import_react14.useRef)(null);
2150
1928
  const [isTransitioning, setIsTransitioning] = (0, import_react14.useState)(false);
2151
1929
  const isTransitioningRef = (0, import_react14.useRef)(false);
2152
1930
  (0, import_react14.useEffect)(() => {
@@ -2155,21 +1933,22 @@ function VisualliCanvas(props) {
2155
1933
  if (!rootId) return;
2156
1934
  const rootLayer = doc.layers.get(rootId);
2157
1935
  setCurrentLayerId(rootId);
2158
- setNavStack([{ layerId: rootId, layer: rootLayer, label: "Home" }]);
1936
+ setNavStack([{ layerId: rootId, layer: rootLayer, label: doc.meta?.title || "Home", paint: { topic: import_core15.TRAIL.rootTopic } }]);
2159
1937
  parentViewports.current = [];
2160
1938
  }, [doc]);
1939
+ const sizing = (0, import_react14.useMemo)(() => ({ measure: ideaMeasure(design), labelScale: design.labelScale }), [design.fontHand, design.labelScale, design.comfort.readableType, fontsReady]);
2161
1940
  const { flatNodes, connections, containers } = (0, import_react14.useMemo)(() => {
2162
1941
  if (!doc || !currentLayerId) return { flatNodes: [], connections: [], containers: [] };
2163
1942
  try {
2164
1943
  return {
2165
- flatNodes: (0, import_core13.getNodesForLayer)(doc, currentLayerId),
2166
- connections: (0, import_core5.getConnectionsForLayer)(doc, currentLayerId),
2167
- containers: (0, import_core5.getContainersForLayer)(doc, currentLayerId)
1944
+ flatNodes: (0, import_core15.getNodesForLayer)(doc, currentLayerId, sizing),
1945
+ connections: (0, import_core3.getConnectionsForLayer)(doc, currentLayerId),
1946
+ containers: (0, import_core3.getContainersForLayer)(doc, currentLayerId)
2168
1947
  };
2169
1948
  } catch {
2170
1949
  return { flatNodes: [], connections: [], containers: [] };
2171
1950
  }
2172
- }, [doc, currentLayerId]);
1951
+ }, [doc, currentLayerId, sizing]);
2173
1952
  const viewport = useViewportStore((s) => ({
2174
1953
  centerX: s.centerX,
2175
1954
  centerY: s.centerY,
@@ -2189,52 +1968,48 @@ function VisualliCanvas(props) {
2189
1968
  setNodes(map);
2190
1969
  clearSel();
2191
1970
  }, [flatNodes, setNodes, clearSel]);
1971
+ const clock = (0, import_react14.useMemo)(() => new RevealClock(), []);
1972
+ const instantReveal = design.comfort.reducedMotion || props.reveal === "instant";
1973
+ (0, import_react14.useEffect)(() => {
1974
+ if (!fontsReady || flatNodes.length === 0) return;
1975
+ clock.start(flatNodes, connections.length, instantReveal);
1976
+ const r1 = requestAnimationFrame(() => {
1977
+ r2 = requestAnimationFrame(arrive);
1978
+ });
1979
+ let r2 = 0;
1980
+ return () => {
1981
+ cancelAnimationFrame(r1);
1982
+ cancelAnimationFrame(r2);
1983
+ };
1984
+ }, [flatNodes, fontsReady]);
1985
+ (0, import_react14.useEffect)(() => clock.subscribe((running) => {
1986
+ const el = containerRef.current;
1987
+ if (el) el.dataset.viReveal = running ? "running" : "idle";
1988
+ }), [clock]);
1989
+ (0, import_react14.useEffect)(() => () => clock.stop(), [clock]);
2192
1990
  const containerRef = (0, import_react14.useRef)(null);
2193
1991
  const stageRef = (0, import_react14.useRef)(null);
2194
1992
  const canvasWrapperRef = (0, import_react14.useRef)(null);
2195
- const baselineTransformRef = (0, import_react14.useRef)(null);
2196
- const konvaContentDivRef = (0, import_react14.useRef)(null);
2197
- const lastAnimViewportRef = (0, import_react14.useRef)(null);
2198
- const zoomOutTargetRef = (0, import_react14.useRef)(null);
2199
1993
  const canvasSizeRef = (0, import_react14.useRef)({ width: 0, height: 0 });
2200
1994
  const dragMoveRafRef = (0, import_react14.useRef)(null);
2201
1995
  const dragMoveLatestRef = (0, import_react14.useRef)(null);
2202
1996
  const dragStartPosRef = (0, import_react14.useRef)(/* @__PURE__ */ new Map());
2203
- const baseBgColor = isDark ? "#141412" : "#F0EDE6";
2204
- const chromaticBgColor = (0, import_react14.useMemo)(() => {
2205
- if (!chromaticImmersion || !doc || !currentLayerId) return baseBgColor;
2206
- const currentLayer = doc.layers.get(currentLayerId);
2207
- if (!currentLayer) return baseBgColor;
2208
- if (currentLayer.level === 0 || !currentLayer.parentNodeId) return baseBgColor;
2209
- const parentLayer = currentLayer.parentLayerId ? doc.layers.get(currentLayer.parentLayerId) : null;
2210
- if (parentLayer) {
2211
- const parentNode = parentLayer.nodes.find((n) => n.id === currentLayer.parentNodeId);
2212
- if (parentNode) {
2213
- const nodeColor = parentNode.data.color || baseBgColor;
2214
- return hexToRgba2(nodeColor, 0.15);
2215
- }
2216
- }
2217
- return baseBgColor;
2218
- }, [chromaticImmersion, doc, currentLayerId, baseBgColor]);
2219
- const bgColor = chromaticImmersion ? chromaticBgColor : baseBgColor;
1997
+ const immersionFill = (0, import_react14.useMemo)(() => {
1998
+ if (!chromaticImmersion || !doc || !currentLayerId) return null;
1999
+ const layer = doc.layers.get(currentLayerId);
2000
+ if (!layer || layer.level === 0 || !layer.parentNodeId) return null;
2001
+ return paintVars(layerPaint(doc, layer)).fill;
2002
+ }, [chromaticImmersion, doc, currentLayerId]);
2220
2003
  const spatialIndexRef = (0, import_react14.useRef)(null);
2221
2004
  (0, import_react14.useEffect)(() => {
2222
2005
  if (flatNodes.length === 0) {
2223
2006
  spatialIndexRef.current?.clear();
2224
2007
  return;
2225
2008
  }
2226
- const idx = new import_core13.RBushSpatialIndex();
2009
+ const idx = new import_core15.RBushSpatialIndex();
2227
2010
  idx.bulkLoad(flatNodes.map((n) => {
2228
- const blobHalfH = Math.max(import_core13.NODE_HEIGHT * 1.6 / 2, n.width / 2 * 0.74);
2229
- return {
2230
- nodeId: n.id,
2231
- bounds: {
2232
- minX: n.x - n.width / 2,
2233
- minY: n.y - blobHalfH,
2234
- maxX: n.x + n.width / 2,
2235
- maxY: n.y + blobHalfH
2236
- }
2237
- };
2011
+ const { rx, ry } = (0, import_core15.nodeRadii)(n);
2012
+ return { nodeId: n.id, bounds: { minX: n.x - rx, minY: n.y - ry, maxX: n.x + rx, maxY: n.y + ry } };
2238
2013
  }));
2239
2014
  spatialIndexRef.current = idx;
2240
2015
  return () => {
@@ -2246,6 +2021,7 @@ function VisualliCanvas(props) {
2246
2021
  handleMouseDown: rendererHandleMouseDown,
2247
2022
  handleMouseMove: rendererHandleMouseMove,
2248
2023
  handleMouseUp: rendererHandleMouseUp,
2024
+ cancelPan: rendererCancelPan,
2249
2025
  canvasWidth,
2250
2026
  canvasHeight
2251
2027
  } = useKonvaRenderer({ containerRef, stageRef });
@@ -2285,20 +2061,20 @@ function VisualliCanvas(props) {
2285
2061
  if (node) {
2286
2062
  const oldPos = dragStartPosRef.current.get(nodeId);
2287
2063
  if (oldPos) {
2288
- const oldHalfH = Math.max(import_core13.NODE_HEIGHT * 1.6 / 2, node.width / 2 * 0.74);
2064
+ const { rx: rx2, ry: ry2 } = (0, import_core15.nodeRadii)(node);
2289
2065
  spatialIndexRef.current.remove(nodeId, {
2290
- minX: oldPos.x - node.width / 2,
2291
- minY: oldPos.y - oldHalfH,
2292
- maxX: oldPos.x + node.width / 2,
2293
- maxY: oldPos.y + oldHalfH
2066
+ minX: oldPos.x - rx2,
2067
+ minY: oldPos.y - ry2,
2068
+ maxX: oldPos.x + rx2,
2069
+ maxY: oldPos.y + ry2
2294
2070
  });
2295
2071
  }
2296
- const newHalfH = Math.max(import_core13.NODE_HEIGHT * 1.6 / 2, node.width / 2 * 0.74);
2072
+ const { rx, ry } = (0, import_core15.nodeRadii)(node);
2297
2073
  spatialIndexRef.current.insert(nodeId, {
2298
- minX: x - node.width / 2,
2299
- minY: y - newHalfH,
2300
- maxX: x + node.width / 2,
2301
- maxY: y + newHalfH
2074
+ minX: x - rx,
2075
+ minY: y - ry,
2076
+ maxX: x + rx,
2077
+ maxY: y + ry
2302
2078
  });
2303
2079
  }
2304
2080
  }
@@ -2311,12 +2087,11 @@ function VisualliCanvas(props) {
2311
2087
  if (isTransitioningRef.current) return;
2312
2088
  const cw = canvasSizeRef.current.width || containerRef.current?.getBoundingClientRect().width || 800;
2313
2089
  const ch = canvasSizeRef.current.height || containerRef.current?.getBoundingClientRect().height || 600;
2314
- const { centerX, centerY } = (0, import_core5.calculateFitView)(flatNodes, cw, ch);
2315
- const isRootLayer = navStack.length <= 1;
2316
- const zoomLevel = isRootLayer ? 1 : (0, import_core5.calculateFitView)(flatNodes, cw, ch).zoomLevel;
2090
+ const { centerX, centerY, zoomLevel } = (0, import_core3.calculateFitView)(flatNodes, cw, ch, containers);
2091
+ useViewportStore.getState().setFit(zoomLevel, centerX, centerY);
2317
2092
  setCenter(centerX, centerY);
2318
2093
  setZoom(zoomLevel);
2319
- }, [flatNodes, navStack.length, setCenter, setZoom]);
2094
+ }, [flatNodes, containers, setCenter, setZoom]);
2320
2095
  fitToScreenRef.current = fitToScreen;
2321
2096
  (0, import_react14.useEffect)(() => {
2322
2097
  fitToScreen();
@@ -2328,14 +2103,22 @@ function VisualliCanvas(props) {
2328
2103
  fitToScreen();
2329
2104
  }, [canvasWidth, fitToScreen]);
2330
2105
  const navBackTargetRef = (0, import_react14.useRef)(-1);
2331
- const { zoomIntoLayer, zoomOutToParent, isTransitioning: isAnimating } = useKonvaLayerTransition({
2106
+ const { stepInside: diveInto, backOut, arrive, isTransitioning: isAnimating } = useLayerChoreography({
2332
2107
  stageRef,
2333
2108
  canvasWrapperRef,
2109
+ reducedMotion: design.comfort.reducedMotion || props.reveal === "instant",
2334
2110
  onSwapLayer: (childLayerId) => {
2335
2111
  const childLayer = doc?.layers.get(childLayerId);
2336
2112
  if (!childLayer || !doc) return;
2337
2113
  setCurrentLayerId(childLayerId);
2338
- setNavStack((prev) => [...prev, { layerId: childLayerId, layer: childLayer, label: layerLabel(doc, childLayer, childLayerId) }]);
2114
+ const target = stepTargetRef.current;
2115
+ stepTargetRef.current = null;
2116
+ setNavStack((prev) => [...prev, {
2117
+ layerId: childLayerId,
2118
+ layer: childLayer,
2119
+ label: target?.label || layerLabel(doc, childLayer, childLayerId),
2120
+ paint: target?.paint ?? layerPaint(doc, childLayer)
2121
+ }]);
2339
2122
  onLayerChange?.(childLayerId, childLayer);
2340
2123
  },
2341
2124
  onSwapBack: () => {
@@ -2355,96 +2138,6 @@ function VisualliCanvas(props) {
2355
2138
  requestAnimationFrame(() => {
2356
2139
  fitToScreenRef.current();
2357
2140
  });
2358
- },
2359
- // GPU CSS-transform callbacks (matching visualli.ai exactly):
2360
- onViewportFrame: (vp) => {
2361
- lastAnimViewportRef.current = vp;
2362
- const baseline = baselineTransformRef.current;
2363
- const contentDiv = konvaContentDivRef.current;
2364
- const { width, height } = canvasSizeRef.current;
2365
- if (baseline && contentDiv) {
2366
- const targetX = width / 2 - vp.centerX * vp.zoomLevel;
2367
- const targetY = height / 2 - vp.centerY * vp.zoomLevel;
2368
- const ratio = vp.zoomLevel / baseline.scaleX;
2369
- const tx = targetX - baseline.x * ratio;
2370
- const ty = targetY - baseline.y * ratio;
2371
- contentDiv.style.transform = `translate(${tx}px, ${ty}px) scale(${ratio})`;
2372
- } else {
2373
- const stage = stageRef.current;
2374
- if (stage) {
2375
- stage.x(width / 2 - vp.centerX * vp.zoomLevel);
2376
- stage.y(height / 2 - vp.centerY * vp.zoomLevel);
2377
- stage.scaleX(vp.zoomLevel);
2378
- stage.scaleY(vp.zoomLevel);
2379
- stage.batchDraw();
2380
- }
2381
- }
2382
- },
2383
- onViewportFinal: (vp) => {
2384
- if (konvaContentDivRef.current) konvaContentDivRef.current.style.transform = "";
2385
- const stage = stageRef.current;
2386
- if (stage) {
2387
- const { width, height } = canvasSizeRef.current;
2388
- stage.x(width / 2 - vp.centerX * vp.zoomLevel);
2389
- stage.y(height / 2 - vp.centerY * vp.zoomLevel);
2390
- stage.scaleX(vp.zoomLevel);
2391
- stage.scaleY(vp.zoomLevel);
2392
- stage.getLayers().forEach((l) => l.drawScene());
2393
- }
2394
- setCenter(vp.centerX, vp.centerY);
2395
- setZoom(vp.zoomLevel);
2396
- },
2397
- onPhaseChange: (phase) => {
2398
- const stage = stageRef.current;
2399
- const contentDiv = konvaContentDivRef.current;
2400
- if (stage && contentDiv && (phase === "swap" || phase === "zoom-back")) {
2401
- contentDiv.style.transform = "";
2402
- const vp = phase === "zoom-back" ? zoomOutTargetRef.current : lastAnimViewportRef.current;
2403
- if (vp) {
2404
- const { width, height } = canvasSizeRef.current;
2405
- const nx = width / 2 - vp.centerX * vp.zoomLevel;
2406
- const ny = height / 2 - vp.centerY * vp.zoomLevel;
2407
- stage.x(nx);
2408
- stage.y(ny);
2409
- stage.scaleX(vp.zoomLevel);
2410
- stage.scaleY(vp.zoomLevel);
2411
- stage.getLayers().forEach((l) => l.drawScene());
2412
- baselineTransformRef.current = { x: nx, y: ny, scaleX: vp.zoomLevel, scaleY: vp.zoomLevel };
2413
- }
2414
- }
2415
- },
2416
- onTransitionLifecycle: (phase) => {
2417
- const stage = stageRef.current;
2418
- if (phase === "start") {
2419
- if (canvasWrapperRef.current) canvasWrapperRef.current.style.willChange = "opacity, transform";
2420
- if (stage) {
2421
- baselineTransformRef.current = { x: stage.x(), y: stage.y(), scaleX: stage.scaleX(), scaleY: stage.scaleY() };
2422
- const container = stage.container();
2423
- const cd = container?.querySelector(".konvajs-content");
2424
- if (cd) {
2425
- konvaContentDivRef.current = cd;
2426
- cd.style.transformOrigin = "0 0";
2427
- cd.style.willChange = "transform";
2428
- }
2429
- stage.getLayers().forEach((l) => {
2430
- l.listening(false);
2431
- });
2432
- }
2433
- } else {
2434
- if (canvasWrapperRef.current) canvasWrapperRef.current.style.willChange = "auto";
2435
- if (konvaContentDivRef.current) {
2436
- konvaContentDivRef.current.style.willChange = "auto";
2437
- konvaContentDivRef.current = null;
2438
- }
2439
- baselineTransformRef.current = null;
2440
- zoomOutTargetRef.current = null;
2441
- if (stage) {
2442
- stage.getLayers().forEach((l) => {
2443
- l.listening(true);
2444
- });
2445
- stage.batchDraw();
2446
- }
2447
- }
2448
2141
  }
2449
2142
  });
2450
2143
  const handleNavigate = (0, import_react14.useCallback)((nodeId) => {
@@ -2454,7 +2147,7 @@ function VisualliCanvas(props) {
2454
2147
  if (isTransitioningRef.current || isAnimating()) {
2455
2148
  return;
2456
2149
  }
2457
- const childLayer = (0, import_core5.getChildLayerForNode)(doc, nodeId, currentLayerId);
2150
+ const childLayer = (0, import_core3.getChildLayerForNode)(doc, nodeId, currentLayerId);
2458
2151
  if (!childLayer) {
2459
2152
  return;
2460
2153
  }
@@ -2464,7 +2157,7 @@ function VisualliCanvas(props) {
2464
2157
  }
2465
2158
  const childNodes = (() => {
2466
2159
  try {
2467
- return (0, import_core13.getNodesForLayer)(doc, childLayerId);
2160
+ return (0, import_core15.getNodesForLayer)(doc, childLayerId, sizing);
2468
2161
  } catch {
2469
2162
  return [];
2470
2163
  }
@@ -2476,20 +2169,13 @@ function VisualliCanvas(props) {
2476
2169
  if (!node) return;
2477
2170
  const cw = canvasSizeRef.current.width || 800;
2478
2171
  const ch = canvasSizeRef.current.height || 600;
2479
- const fitVp = (0, import_core5.calculateFitView)(flatNodes, cw, ch);
2480
- const isRootNow = navStack.length <= 1;
2481
- parentViewports.current.push({ centerX: fitVp.centerX, centerY: fitVp.centerY, zoomLevel: isRootNow ? 0.85 : fitVp.zoomLevel });
2172
+ const fitVp = (0, import_core3.calculateFitView)(flatNodes, cw, ch, containers);
2173
+ parentViewports.current.push({ centerX: fitVp.centerX, centerY: fitVp.centerY, zoomLevel: fitVp.zoomLevel });
2174
+ stepTargetRef.current = { paint: { topic: node.topic, custom: node.custom }, label: node.title };
2482
2175
  setIsTransitioning(true);
2483
2176
  isTransitioningRef.current = true;
2484
- zoomIntoLayer(
2485
- doc,
2486
- node,
2487
- childLayerId,
2488
- childNodes,
2489
- node.color ?? (0, import_core13.getThemeBackground)(isDark, "secondary"),
2490
- node.color ?? (0, import_core13.getThemeBackground)(isDark, "primary")
2491
- );
2492
- }, [doc, currentLayerId, nodes, isDark, isAnimating, zoomIntoLayer]);
2177
+ diveInto(node, childLayerId, childNodes);
2178
+ }, [doc, currentLayerId, nodes, isAnimating, diveInto, sizing, flatNodes, containers]);
2493
2179
  const handleNavigateBack = (0, import_react14.useCallback)((targetIndex) => {
2494
2180
  if (targetIndex >= navStack.length - 1) return;
2495
2181
  if (isTransitioningRef.current || isAnimating()) return;
@@ -2498,45 +2184,8 @@ function VisualliCanvas(props) {
2498
2184
  navBackTargetRef.current = targetIndex;
2499
2185
  setIsTransitioning(true);
2500
2186
  isTransitioningRef.current = true;
2501
- zoomOutToParent(savedVp, (0, import_core13.getThemeBackground)(isDark, "primary"), (0, import_core13.getThemeBackground)(isDark, "secondary"));
2502
- }, [navStack.length, isDark, isAnimating, zoomOutToParent]);
2503
- const baseZoomRef = (0, import_react14.useRef)(1);
2504
- const lastZoomTransitionRef = (0, import_react14.useRef)(0);
2505
- const ZOOM_COOLDOWN = 1e3;
2506
- (0, import_react14.useEffect)(() => {
2507
- baseZoomRef.current = 1;
2508
- }, [currentLayerId]);
2509
- (0, import_react14.useEffect)(() => {
2510
- if (isTransitioningRef.current || isAnimating()) return;
2511
- const now = Date.now();
2512
- if (now - lastZoomTransitionRef.current < ZOOM_COOLDOWN) return;
2513
- const relative = viewport.zoomLevel / baseZoomRef.current;
2514
- if (relative >= import_core13.ZOOM_NAV_IN_THRESHOLD) {
2515
- let closest = null;
2516
- let minDist = Infinity;
2517
- for (const node of nodes.values()) {
2518
- const dx = node.x - viewport.centerX;
2519
- const dy = node.y - viewport.centerY;
2520
- const d = dx * dx + dy * dy;
2521
- if (d < minDist) {
2522
- minDist = d;
2523
- closest = node;
2524
- }
2525
- }
2526
- if (closest) {
2527
- const childLayer = doc ? (0, import_core5.getChildLayerForNode)(doc, closest.id, currentLayerId ?? "") : null;
2528
- if (childLayer) {
2529
- lastZoomTransitionRef.current = now;
2530
- handleNavigate(closest.id);
2531
- baseZoomRef.current = 1;
2532
- }
2533
- }
2534
- } else if (relative < import_core13.ZOOM_NAV_OUT_THRESHOLD && navStack.length > 1) {
2535
- lastZoomTransitionRef.current = now;
2536
- handleNavigateBack(navStack.length - 2);
2537
- baseZoomRef.current = 1;
2538
- }
2539
- }, [viewport.zoomLevel, viewport.centerX, viewport.centerY, nodes, doc, currentLayerId, navStack, isAnimating, handleNavigate, handleNavigateBack]);
2187
+ backOut(savedVp);
2188
+ }, [navStack.length, isAnimating, backOut]);
2540
2189
  const toWorldCoords = (0, import_react14.useCallback)((stage, pointer) => {
2541
2190
  const sp = stage.position();
2542
2191
  const sc = stage.scaleX();
@@ -2599,8 +2248,45 @@ function VisualliCanvas(props) {
2599
2248
  window.removeEventListener("keydown", onKey);
2600
2249
  };
2601
2250
  }, [canvasContextMenu]);
2251
+ const selectedNodeId = useSelectionStore((st) => st.selectedId);
2252
+ const [focusedNodeId, setFocusedNodeId] = (0, import_react14.useState)(null);
2253
+ const anchors = (0, import_react14.useMemo)(() => doc ? (0, import_core15.getSemanticAnchors)(doc) : [], [doc]);
2254
+ const [autoTouch, setAutoTouch] = (0, import_react14.useState)(() => typeof window !== "undefined" && !!window.matchMedia && window.matchMedia("(hover: none)").matches);
2255
+ const touchMode = props.layout === "touch" ? true : props.layout === "pointer" ? false : autoTouch;
2256
+ const touchModeRef = (0, import_react14.useRef)(touchMode);
2257
+ touchModeRef.current = touchMode;
2258
+ const lastTouchAtRef = (0, import_react14.useRef)(0);
2259
+ const emptyTapRef = (0, import_react14.useRef)(null);
2260
+ const [pinnedId, setPinnedId] = (0, import_react14.useState)(null);
2261
+ const [sheetExpanded, setSheetExpanded] = (0, import_react14.useState)(false);
2262
+ const isEmulatedMouse = (evt) => evt.type.startsWith("mouse") && performance.now() - lastTouchAtRef.current < 700;
2263
+ const clientOf = (evt) => {
2264
+ const t = "touches" in evt && (evt.touches[0] ?? evt.changedTouches[0]) || evt;
2265
+ return { x: t.clientX, y: t.clientY };
2266
+ };
2267
+ const pinnedIdRef = (0, import_react14.useRef)(null);
2268
+ pinnedIdRef.current = pinnedId;
2269
+ const sheetHeight = Math.min(import_core15.SHEET.maxHeight, Math.round((viewport.canvasHeight || canvasSizeRef.current.height || 600) * import_core15.SHEET.heightRatio));
2270
+ const openSheet = (node) => {
2271
+ setPinnedId(node.id);
2272
+ setSheetExpanded(false);
2273
+ setHoveredNode(null);
2274
+ const vp = useViewportStore.getState();
2275
+ const ch = canvasSizeRef.current.height || vp.canvasHeight;
2276
+ const sh = Math.min(import_core15.SHEET.maxHeight, Math.round(ch * import_core15.SHEET.heightRatio));
2277
+ const screenY = (node.y - vp.centerY) * vp.zoomLevel + ch / 2;
2278
+ const reach = (0, import_core15.nodeRadii)(node).ry * vp.zoomLevel;
2279
+ if (screenY + reach > ch - sh - 12 || screenY - reach < 0) vp.setCenter(vp.centerX, node.y + sh / (2 * vp.zoomLevel));
2280
+ };
2281
+ (0, import_react14.useEffect)(() => {
2282
+ setPinnedId(null);
2283
+ }, [currentLayerId]);
2602
2284
  const handleStageMouseDown = (0, import_react14.useCallback)((e) => {
2603
- if (e.evt.button !== 0) {
2285
+ if (isEmulatedMouse(e.evt)) return;
2286
+ const isTouch = e.evt.type.startsWith("touch");
2287
+ if (isTouch) lastTouchAtRef.current = performance.now();
2288
+ if (props.layout === void 0 || props.layout === "auto") setAutoTouch(isTouch);
2289
+ if (!isTouch && e.evt.button !== 0) {
2604
2290
  rendererHandleMouseDown(e);
2605
2291
  return;
2606
2292
  }
@@ -2622,7 +2308,7 @@ function VisualliCanvas(props) {
2622
2308
  select(nodeId);
2623
2309
  stageActiveNodeIdRef.current = nodeId;
2624
2310
  stageDragNodeOffsetRef.current = { x: worldX - node.x, y: worldY - node.y };
2625
- stageMouseDownPosRef.current = { x: e.evt.clientX, y: e.evt.clientY };
2311
+ stageMouseDownPosRef.current = clientOf(e.evt);
2626
2312
  stageDragCommittedRef.current = false;
2627
2313
  setPressedNodeId(nodeId);
2628
2314
  setHoveredNode(null);
@@ -2630,10 +2316,12 @@ function VisualliCanvas(props) {
2630
2316
  }
2631
2317
  }
2632
2318
  setHoveredNode(null);
2319
+ emptyTapRef.current = clientOf(e.evt);
2633
2320
  isCanvasPanningRef.current = true;
2634
2321
  rendererHandleMouseDown(e);
2635
- }, [hitTestNode, toWorldCoords, nodes, select, rendererHandleMouseDown]);
2322
+ }, [hitTestNode, toWorldCoords, nodes, select, rendererHandleMouseDown, props.layout]);
2636
2323
  const handleStageMouseMove = (0, import_react14.useCallback)((e) => {
2324
+ if (isEmulatedMouse(e.evt)) return;
2637
2325
  const stage = e.target.getStage();
2638
2326
  if (!stage) {
2639
2327
  rendererHandleMouseMove(e);
@@ -2646,7 +2334,8 @@ function VisualliCanvas(props) {
2646
2334
  }
2647
2335
  const { worldX, worldY } = toWorldCoords(stage, pointer);
2648
2336
  if (stageActiveNodeIdRef.current && stageMouseDownPosRef.current) {
2649
- const moved = Math.hypot(e.evt.clientX - stageMouseDownPosRef.current.x, e.evt.clientY - stageMouseDownPosRef.current.y);
2337
+ const cur = clientOf(e.evt);
2338
+ const moved = Math.hypot(cur.x - stageMouseDownPosRef.current.x, cur.y - stageMouseDownPosRef.current.y);
2650
2339
  if (!stageDragCommittedRef.current && moved >= 5) {
2651
2340
  stageDragCommittedRef.current = true;
2652
2341
  isDraggingRef.current = true;
@@ -2658,7 +2347,6 @@ function VisualliCanvas(props) {
2658
2347
  if (stageDragCommittedRef.current) {
2659
2348
  const offset = stageDragNodeOffsetRef.current;
2660
2349
  handleDragMove(stageActiveNodeIdRef.current, worldX - offset.x, worldY - offset.y);
2661
- stage.container().style.cursor = "grabbing";
2662
2350
  return;
2663
2351
  }
2664
2352
  return;
@@ -2674,17 +2362,14 @@ function VisualliCanvas(props) {
2674
2362
  if (hovered !== pointerNodeIdRef.current) {
2675
2363
  pointerNodeIdRef.current = hovered;
2676
2364
  setPointerNodeId(hovered);
2677
- onNodeHover?.(hovered);
2678
- }
2679
- if (hovered) {
2680
- stage.container().style.cursor = "pointer";
2681
- } else {
2682
- stage.container().style.cursor = "grab";
2365
+ onNodeHoverRef.current?.(hovered);
2683
2366
  }
2367
+ stage.container().style.cursor = hovered ? "pointer" : "";
2684
2368
  }
2685
2369
  rendererHandleMouseMove(e);
2686
2370
  }, [toWorldCoords, hitTestNode, handleDragStart, handleDragMove, rendererHandleMouseMove]);
2687
2371
  const handleStageMouseUp = (0, import_react14.useCallback)((e) => {
2372
+ if (isEmulatedMouse(e.evt)) return;
2688
2373
  setPressedNodeId(null);
2689
2374
  if (stageActiveNodeIdRef.current) {
2690
2375
  const nodeId = stageActiveNodeIdRef.current;
@@ -2701,8 +2386,13 @@ function VisualliCanvas(props) {
2701
2386
  } else {
2702
2387
  const node = nodes.get(nodeId);
2703
2388
  if (node) {
2704
- const childLayer = doc ? (0, import_core5.getChildLayerForNode)(doc, nodeId, currentLayerId ?? "") : null;
2705
- if (childLayer) {
2389
+ const childLayer = doc ? (0, import_core3.getChildLayerForNode)(doc, nodeId, currentLayerId ?? "") : null;
2390
+ if (touchModeRef.current && childLayer && pinnedIdRef.current === nodeId) {
2391
+ setPinnedId(null);
2392
+ handleNavigate(nodeId);
2393
+ } else if (touchModeRef.current && (node.description || childLayer)) {
2394
+ openSheet(node);
2395
+ } else if (childLayer) {
2706
2396
  handleNavigate(nodeId);
2707
2397
  }
2708
2398
  onNodeClick?.(node);
@@ -2715,6 +2405,12 @@ function VisualliCanvas(props) {
2715
2405
  stageDragCommittedRef.current = false;
2716
2406
  return;
2717
2407
  }
2408
+ const tap = emptyTapRef.current;
2409
+ emptyTapRef.current = null;
2410
+ if (tap && pinnedIdRef.current) {
2411
+ const end = clientOf(e.evt);
2412
+ if (!Number.isFinite(end.x) || Math.hypot(end.x - tap.x, end.y - tap.y) < 6) setPinnedId(null);
2413
+ }
2718
2414
  isCanvasPanningRef.current = false;
2719
2415
  rendererHandleMouseUp(e);
2720
2416
  }, [toWorldCoords, handleDragEnd, nodes, handleNavigate, onNodeClick, rendererHandleMouseUp]);
@@ -2729,31 +2425,75 @@ function VisualliCanvas(props) {
2729
2425
  const localY = rect ? e.evt.clientY - rect.top : e.evt.clientY;
2730
2426
  setCanvasContextMenu({ x: localX, y: localY });
2731
2427
  }, [navStack.length]);
2732
- const handleStageTouchStart = (0, import_react14.useCallback)((e) => handleStageMouseDown(e), [handleStageMouseDown]);
2733
- const handleStageTouchMove = (0, import_react14.useCallback)((e) => handleStageMouseMove(e), [handleStageMouseMove]);
2734
- const handleStageTouchEnd = (0, import_react14.useCallback)((e) => handleStageMouseUp(e), [handleStageMouseUp]);
2735
- const handleWheel = (0, import_react14.useCallback)((e) => {
2736
- const isZoomOut = e.evt.deltaY > 0;
2737
- if (!isZoomOut) {
2738
- rendererHandleWheel(e);
2739
- return;
2428
+ const touchPointsRef = (0, import_react14.useRef)(/* @__PURE__ */ new Map());
2429
+ const pinchRef = (0, import_react14.useRef)(null);
2430
+ const pinchEndedAtRef = (0, import_react14.useRef)(-Infinity);
2431
+ const [pinching, setPinching] = (0, import_react14.useState)(false);
2432
+ const localOf = (e) => {
2433
+ const r = containerRef.current.getBoundingClientRect();
2434
+ return { x: e.clientX - r.left, y: e.clientY - r.top };
2435
+ };
2436
+ const onPinchDown = (e) => {
2437
+ if (!import_core15.GESTURE.pinchZoom || !import_core15.GESTURE.pointers.includes(e.pointerType)) return;
2438
+ touchPointsRef.current.set(e.pointerId, localOf(e));
2439
+ if (touchPointsRef.current.size !== 2 || isTransitioningRef.current || isAnimating()) return;
2440
+ const [a, b] = [...touchPointsRef.current.values()];
2441
+ const vp = useViewportStore.getState();
2442
+ pinchRef.current = { d0: Math.hypot(a.x - b.x, a.y - b.y), m0: { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 }, z0: vp.zoomLevel, c0: { x: vp.centerX, y: vp.centerY } };
2443
+ rendererCancelPan();
2444
+ isCanvasPanningRef.current = false;
2445
+ emptyTapRef.current = null;
2446
+ stageActiveNodeIdRef.current = null;
2447
+ stageDragCommittedRef.current = false;
2448
+ isDraggingRef.current = false;
2449
+ setPressedNodeId(null);
2450
+ setPinching(true);
2451
+ };
2452
+ const onPinchMove = (e) => {
2453
+ if (!touchPointsRef.current.has(e.pointerId)) return;
2454
+ touchPointsRef.current.set(e.pointerId, localOf(e));
2455
+ const p = pinchRef.current;
2456
+ if (!p || touchPointsRef.current.size < 2) return;
2457
+ const [a, b] = [...touchPointsRef.current.values()];
2458
+ const fit = useViewportStore.getState().fitScale;
2459
+ const z = (0, import_core15.pinchZoom)(p.z0 / fit, p.d0, Math.hypot(a.x - b.x, a.y - b.y)) * fit;
2460
+ const m = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
2461
+ const cw = canvasSizeRef.current.width || containerRef.current?.clientWidth || 800;
2462
+ const ch = canvasSizeRef.current.height || containerRef.current?.clientHeight || 600;
2463
+ const t = (0, import_core15.zoomAround)({ x: cw / 2 - p.c0.x * p.z0, y: ch / 2 - p.c0.y * p.z0 }, p.z0, z, p.m0, import_core15.GESTURE.twoFingerPan ? m : p.m0);
2464
+ setZoom(z);
2465
+ setCenter((cw / 2 - t.x) / z, (ch / 2 - t.y) / z);
2466
+ };
2467
+ const onPinchUp = (e) => {
2468
+ if (!touchPointsRef.current.delete(e.pointerId)) return;
2469
+ if (pinchRef.current && touchPointsRef.current.size < 2) {
2470
+ pinchRef.current = null;
2471
+ pinchEndedAtRef.current = performance.now();
2472
+ setPinching(false);
2740
2473
  }
2741
- const liveZoom = useViewportStore.getState().zoomLevel;
2742
- const projected = Math.max(import_core13.ZOOM_MIN, liveZoom * (1 - e.evt.deltaY / 1e3));
2743
- if (navStack.length > 1 && projected <= import_core13.ZOOM_NAV_OUT_THRESHOLD && !isTransitioningRef.current && !isAnimating()) {
2744
- e.evt.preventDefault();
2745
- const now = Date.now();
2746
- if (now - lastZoomTransitionRef.current >= ZOOM_COOLDOWN) {
2747
- lastZoomTransitionRef.current = now;
2748
- handleNavigateBack(navStack.length - 2);
2749
- baseZoomRef.current = 1;
2750
- }
2474
+ };
2475
+ const inPinch = () => pinchRef.current !== null || touchPointsRef.current.size > 1 || performance.now() - pinchEndedAtRef.current < 300;
2476
+ const handleStageTouchStart = (0, import_react14.useCallback)((e) => {
2477
+ if (inPinch() || e.evt.touches.length > 1) return;
2478
+ handleStageMouseDown(e);
2479
+ }, [handleStageMouseDown]);
2480
+ const handleStageTouchMove = (0, import_react14.useCallback)((e) => {
2481
+ if (inPinch() || e.evt.touches.length > 1) return;
2482
+ handleStageMouseMove(e);
2483
+ }, [handleStageMouseMove]);
2484
+ const handleStageTouchEnd = (0, import_react14.useCallback)((e) => {
2485
+ if (inPinch()) {
2486
+ isCanvasPanningRef.current = false;
2751
2487
  return;
2752
2488
  }
2489
+ handleStageMouseUp(e);
2490
+ }, [handleStageMouseUp]);
2491
+ const handleWheel = (0, import_react14.useCallback)((e) => {
2753
2492
  rendererHandleWheel(e);
2754
- }, [rendererHandleWheel, navStack, setZoom, isAnimating, handleNavigateBack]);
2493
+ }, [rendererHandleWheel]);
2755
2494
  const lastHoverCheckRef = (0, import_react14.useRef)(0);
2756
2495
  const handleCanvasMouseMove = (0, import_react14.useCallback)((e) => {
2496
+ if (touchModeRef.current || isEmulatedMouse(e.nativeEvent)) return;
2757
2497
  if (!containerRef.current || isDraggingRef.current || isCanvasPanningRef.current) {
2758
2498
  setHoveredNode(null);
2759
2499
  return;
@@ -2789,7 +2529,7 @@ function VisualliCanvas(props) {
2789
2529
  if (clearTooltipTimerRef.current) clearTimeout(clearTooltipTimerRef.current);
2790
2530
  clearTooltipTimerRef.current = setTimeout(() => {
2791
2531
  if (!tooltipHoverRef.current && !keepTooltipOpenRef.current) setHoveredNode(null);
2792
- }, 150);
2532
+ }, import_core15.PEEK.hoverClose);
2793
2533
  } else {
2794
2534
  if (clearTooltipTimerRef.current) {
2795
2535
  clearTimeout(clearTooltipTimerRef.current);
@@ -2813,28 +2553,77 @@ function VisualliCanvas(props) {
2813
2553
  if (dragMoveRafRef.current !== null) cancelAnimationFrame(dragMoveRafRef.current);
2814
2554
  };
2815
2555
  }, []);
2816
- const handleZoomIn = (0, import_react14.useCallback)(() => setZoom(Math.min(viewport.zoomLevel * 1.2, 5)), [viewport.zoomLevel, setZoom]);
2817
- const handleZoomOut = (0, import_react14.useCallback)(() => {
2818
- setZoom(Math.max(viewport.zoomLevel / 1.2, import_core13.ZOOM_MIN));
2819
- }, [viewport.zoomLevel, setZoom]);
2556
+ const activateNode = (0, import_react14.useCallback)((node) => {
2557
+ const childLayer = doc ? (0, import_core3.getChildLayerForNode)(doc, node.id, currentLayerId ?? "") : null;
2558
+ if (touchModeRef.current && childLayer && pinnedIdRef.current === node.id) {
2559
+ setPinnedId(null);
2560
+ handleNavigate(node.id);
2561
+ } else if (touchModeRef.current && (node.description || childLayer)) openSheet(node);
2562
+ else if (childLayer) handleNavigate(node.id);
2563
+ onNodeClick?.(node);
2564
+ }, [doc, currentLayerId, handleNavigate, onNodeClick]);
2565
+ const stepInside = (0, import_react14.useCallback)((nodeId) => {
2566
+ setHoveredNode(null);
2567
+ handleNavigate(nodeId);
2568
+ }, [handleNavigate]);
2569
+ (0, import_react14.useEffect)(() => {
2570
+ if (!hoveredNode) tooltipHoverRef.current = false;
2571
+ }, [hoveredNode]);
2572
+ const handleKeyDown = (0, import_react14.useCallback)((e) => {
2573
+ if (e.target.closest('input, textarea, [contenteditable="true"]')) return;
2574
+ const cmd = e.metaKey || e.ctrlKey;
2575
+ if (cmd && !e.altKey) {
2576
+ if (e.key === "+" || e.key === "=") {
2577
+ e.preventDefault();
2578
+ setZoom(viewport.zoomLevel * import_core15.VIEW.zoomStep);
2579
+ } else if (e.key === "-" || e.key === "_") {
2580
+ e.preventDefault();
2581
+ setZoom(viewport.zoomLevel / import_core15.VIEW.zoomStep);
2582
+ } else if (e.key === "0") {
2583
+ e.preventDefault();
2584
+ fitToScreen();
2585
+ }
2586
+ return;
2587
+ }
2588
+ if (e.altKey || e.shiftKey) return;
2589
+ if (e.key === "Escape" && pinnedIdRef.current) {
2590
+ e.preventDefault();
2591
+ setPinnedId(null);
2592
+ } else if ((e.key === "Escape" || e.key === "Backspace") && navStack.length > 1) {
2593
+ e.preventDefault();
2594
+ handleNavigateBack(navStack.length - 2);
2595
+ }
2596
+ }, [navStack.length, handleNavigateBack, setZoom, viewport.zoomLevel, fitToScreen]);
2820
2597
  if (!doc) {
2821
- 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" }) });
2598
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: `vi-map ${className}`, ...design.attrs, style: { display: "flex", alignItems: "center", justifyContent: "center", ...style }, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "body-sm", style: { color: "var(--ink-muted)" }, children: "No document" }) });
2822
2599
  }
2823
2600
  const hasBackOption = navStack.length > 1;
2824
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
2601
+ const currentLayerTitle = navStack[navStack.length - 1]?.label ?? "";
2602
+ const peekNode = hoveredNode ? nodes.get(hoveredNode.nodeId) : void 0;
2603
+ const pinnedNode = pinnedId ? nodes.get(pinnedId) : void 0;
2604
+ const pinnedHasChild = !!(pinnedNode && (0, import_core3.getChildLayerForNode)(doc, pinnedNode.id, currentLayerId ?? ""));
2605
+ const compact = (viewport.canvasWidth || canvasSizeRef.current.width || 1024) < 560;
2606
+ const medium = !compact && (viewport.canvasWidth || canvasSizeRef.current.width || 1024) < 900;
2607
+ const peekHasChild = !!(peekNode && doc && (0, import_core3.getChildLayerForNode)(doc, peekNode.id, currentLayerId ?? ""));
2608
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
2825
2609
  "div",
2826
2610
  {
2827
2611
  ref: containerRef,
2828
- className,
2829
- style: {
2830
- position: "relative",
2831
- overflow: "hidden",
2832
- userSelect: "none",
2833
- background: bgColor,
2834
- width: "100%",
2835
- height: "100%",
2836
- ...style
2612
+ className: `vi-map${touchMode ? " is-touch" : ""}${compact ? " is-compact" : ""}${medium ? " is-medium" : ""}${pinching ? " is-pinching" : ""}${props.app ? " is-app" : ""} ${className}`,
2613
+ ...design.attrs,
2614
+ "data-reveal": instantReveal ? "instant" : void 0,
2615
+ role: "group",
2616
+ "aria-label": `${doc.meta?.title ?? "Map"}${currentLayerTitle ? `, ${currentLayerTitle}` : ""}`,
2617
+ onKeyDown: handleKeyDown,
2618
+ tabIndex: -1,
2619
+ onPointerDownCapture: (e) => {
2620
+ onPinchDown(e);
2621
+ if (!e.target.closest("button, a, input")) containerRef.current?.focus({ preventScroll: true });
2837
2622
  },
2623
+ onPointerMoveCapture: onPinchMove,
2624
+ onPointerUpCapture: onPinchUp,
2625
+ onPointerCancelCapture: onPinchUp,
2626
+ style: { position: "relative", overflow: "hidden", userSelect: "none", width: "100%", height: "100%", outline: "none", ...(0, import_core15.motionVars)(), ...style },
2838
2627
  onMouseMove: handleCanvasMouseMove,
2839
2628
  onMouseLeave: () => {
2840
2629
  if (!tooltipHoverRef.current && !keepTooltipOpenRef.current) {
@@ -2842,7 +2631,8 @@ function VisualliCanvas(props) {
2842
2631
  }
2843
2632
  },
2844
2633
  children: [
2845
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { ref: canvasWrapperRef, style: { position: "relative", zIndex: 1, pointerEvents: "auto" }, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
2634
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "vi-map__immersion", style: { background: immersionFill ?? "transparent" }, "aria-hidden": "true" }),
2635
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { ref: canvasWrapperRef, style: { position: "relative", zIndex: 1, pointerEvents: "auto" }, children: fontsReady && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
2846
2636
  KonvaStage_default,
2847
2637
  {
2848
2638
  ref: stageRef,
@@ -2855,56 +2645,59 @@ function VisualliCanvas(props) {
2855
2645
  onTouchMove: handleStageTouchMove,
2856
2646
  onTouchEnd: handleStageTouchEnd,
2857
2647
  children: [
2858
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(KonvaContainerLayer, { nodes: flatNodes, containers, isDark }),
2859
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2860
- KonvaEdgeLayer,
2861
- {
2862
- nodes: flatNodes,
2863
- connections,
2864
- isDark,
2865
- isDragging: isDraggingState
2866
- }
2867
- ),
2868
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2648
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(KonvaContainerLayer, { nodes: flatNodes, containers, connections, design }),
2649
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(KonvaEdgeLayer, { nodes: flatNodes, connections, design, isDragging: isDraggingState, clock }),
2650
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2869
2651
  KonvaNodeLayer,
2870
2652
  {
2653
+ design,
2654
+ clock,
2871
2655
  isTransitioning,
2872
2656
  isDragging: isDraggingState,
2873
2657
  hoveredNodeId: pointerNodeId,
2874
- pressedNodeId
2658
+ pressedNodeId,
2659
+ selectedNodeId,
2660
+ focusedNodeId,
2661
+ touch: touchMode,
2662
+ peekedNodeId: touchMode ? pinnedId : null
2875
2663
  }
2876
- )
2664
+ ),
2665
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(KonvaContainerLabelLayer, { containers, connections, design })
2877
2666
  ]
2878
2667
  }
2879
2668
  ) }),
2880
- canvasContextMenu && hasBackOption && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2669
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2670
+ A11yLayer,
2671
+ {
2672
+ nodes: flatNodes,
2673
+ label: `Ideas in ${currentLayerTitle || "this map"}`,
2674
+ hasChildLayer: (id) => !!(0, import_core3.getChildLayerForNode)(doc, id, currentLayerId ?? ""),
2675
+ onActivate: activateNode,
2676
+ onFocusNode: setFocusedNodeId,
2677
+ announcement: hasBackOption ? `Now inside ${currentLayerTitle}` : void 0
2678
+ }
2679
+ ),
2680
+ canvasContextMenu && hasBackOption && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2881
2681
  "div",
2882
2682
  {
2883
2683
  role: "menu",
2884
2684
  "aria-label": "Canvas options",
2885
2685
  style: {
2886
2686
  position: "absolute",
2887
- zIndex: 60,
2687
+ zIndex: "var(--z-menu)",
2888
2688
  pointerEvents: "auto",
2889
2689
  left: `${Math.min(canvasContextMenu.x, Math.max(8, (canvasSizeRef.current.width || 800) - 152))}px`,
2890
2690
  top: `${Math.min(canvasContextMenu.y, Math.max(8, (canvasSizeRef.current.height || 600) - 72))}px`
2891
2691
  },
2892
2692
  onContextMenu: (e) => e.preventDefault(),
2893
2693
  onPointerDown: (e) => e.stopPropagation(),
2894
- children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2694
+ children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2895
2695
  "button",
2896
2696
  {
2897
2697
  type: "button",
2898
- style: {
2899
- padding: "6px 16px",
2900
- borderRadius: "10px",
2901
- border: `1px solid ${isDark ? "#333330" : "#DDD9D0"}`,
2902
- background: isDark ? "#1C1C1A" : "#FAF8F4",
2903
- color: isDark ? "#F0EDE6" : "#1A1A18",
2904
- cursor: "pointer",
2905
- fontSize: 13,
2906
- boxShadow: "0 4px 12px rgba(0,0,0,0.15)"
2907
- },
2698
+ role: "menuitem",
2699
+ className: "vi-iconbtn vi-iconbtn--surface",
2700
+ style: { width: "auto", padding: "6px var(--space-4)", boxShadow: "var(--shadow-pop)" },
2908
2701
  onClick: () => {
2909
2702
  setCanvasContextMenu(null);
2910
2703
  handleNavigateBack(navStack.length - 2);
@@ -2914,32 +2707,41 @@ function VisualliCanvas(props) {
2914
2707
  )
2915
2708
  }
2916
2709
  ),
2917
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(NavigationStack_default, { stack: navStack, onNavigateBack: handleNavigateBack, isDark, top: navigationStackTop, left: navigationStackLeft }),
2918
- /* @__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 }) }),
2919
- renderOverlay?.({ isDark, containerWidth: canvasSizeRef.current.width || 0, containerHeight: canvasSizeRef.current.height || 0 }),
2920
- hoveredNode && hoveredNodePosition && viewport.zoomLevel < 3 && viewport.zoomLevel >= import_core13.TEXT_LABEL_HIDE_BELOW_ZOOM && (() => {
2921
- const nodeData = nodes.get(hoveredNode.nodeId);
2922
- const borderColor = nodeData?.color ? (0, import_core13.darkenHexColor)(nodeData.color, 0.13) : "#5BA8D4";
2923
- const VP_PAD = 8;
2924
- const HALF_W = 160;
2925
- const APPROX_H = 90;
2926
- const containerW = canvasSizeRef.current.width || 800;
2927
- const safeLeft = Math.max(HALF_W + VP_PAD, Math.min(hoveredNodePosition.screenX, containerW - HALF_W - VP_PAD));
2928
- const rawTop = hoveredNodePosition.screenY - 60 * hoveredNodePosition.zoom;
2929
- const safeTop = Math.max(VP_PAD + APPROX_H, rawTop);
2930
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2710
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(NavigationStack_default, { stack: navStack, onNavigateBack: handleNavigateBack, top: navigationStackTop, left: navigationStackLeft }),
2711
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2712
+ "div",
2713
+ {
2714
+ className: "vi-map__ctrls",
2715
+ style: props.controlsPosition === "bottom-right" ? { top: "auto", bottom: pinnedNode && touchMode ? `calc(var(--space-4) + ${sheetHeight}px)` : "var(--space-4)" } : void 0,
2716
+ children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ZoomControls, { onFit: fitToScreen, touch: touchMode })
2717
+ }
2718
+ ),
2719
+ renderOverlay?.({ isDark, theme: design.theme, containerWidth: canvasSizeRef.current.width || 0, containerHeight: canvasSizeRef.current.height || 0 }),
2720
+ touchMode && pinnedNode && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2721
+ PeekSheet,
2722
+ {
2723
+ node: pinnedNode,
2724
+ anchors,
2725
+ height: sheetHeight,
2726
+ expanded: sheetExpanded,
2727
+ onToggleExpanded: () => setSheetExpanded((v) => !v),
2728
+ onClose: () => setPinnedId(null),
2729
+ onStepInside: pinnedHasChild ? () => {
2730
+ setPinnedId(null);
2731
+ handleNavigate(pinnedNode.id);
2732
+ } : void 0,
2733
+ renderContent: renderNodeContent ? (summary) => renderNodeContent({ summary, nodeId: pinnedNode.id, nodeColor: pinnedNode.color, zoom: viewport.zoomLevel }) : void 0
2734
+ }
2735
+ ),
2736
+ !touchMode && hoveredNode && hoveredNodePosition && peekNode && (() => {
2737
+ const rings = (0, import_core15.ringsFor)(peekNode.branchCount);
2738
+ const half = (0, import_core15.nodeRadii)(peekNode).rx * (rings.length ? rings[rings.length - 1].scale : 1) * hoveredNodePosition.zoom;
2739
+ const at = (0, import_core15.peekPosition)({ x: hoveredNodePosition.screenX, y: hoveredNodePosition.screenY, half }, { w: canvasSizeRef.current.width || 800, h: canvasSizeRef.current.height || 600 });
2740
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2931
2741
  "div",
2932
2742
  {
2933
- "data-node-tooltip": "true",
2934
- style: {
2935
- position: "absolute",
2936
- zIndex: 50,
2937
- left: safeLeft,
2938
- top: safeTop,
2939
- transform: "translate(-50%, -100%)",
2940
- transformOrigin: "bottom center",
2941
- pointerEvents: "auto"
2942
- },
2743
+ className: "vi-map__fact",
2744
+ style: { left: at.left, top: at.top, pointerEvents: "auto", zIndex: "var(--z-card)" },
2943
2745
  onMouseEnter: () => {
2944
2746
  tooltipHoverRef.current = true;
2945
2747
  if (clearTooltipTimerRef.current) {
@@ -2952,22 +2754,17 @@ function VisualliCanvas(props) {
2952
2754
  if (clearTooltipTimerRef.current) clearTimeout(clearTooltipTimerRef.current);
2953
2755
  clearTooltipTimerRef.current = setTimeout(() => {
2954
2756
  if (!keepTooltipOpenRef.current) setHoveredNode(null);
2955
- }, 100);
2757
+ }, import_core15.PEEK.hoverClose);
2956
2758
  },
2957
- 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: {
2958
- fontFamily: "'Playpen Sans', cursive",
2959
- fontSize: `${DESCRIPTION_TEXT_BASE_FONT_PX2}px`,
2960
- fontWeight: 300,
2961
- color: "#000000",
2962
- textAlign: "center",
2963
- lineHeight: 1.6,
2964
- margin: 0
2965
- }, children: renderNodeContent ? renderNodeContent({
2966
- summary: hoveredNode.summary,
2967
- nodeId: hoveredNode.nodeId,
2968
- nodeColor: nodeData?.color,
2969
- zoom: viewport.zoomLevel
2970
- }) : hoveredNode.summary }) }) })
2759
+ children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2760
+ PeekCard,
2761
+ {
2762
+ node: peekNode,
2763
+ anchors,
2764
+ onStepInside: peekHasChild ? () => stepInside(peekNode.id) : void 0,
2765
+ renderContent: renderNodeContent ? (summary) => renderNodeContent({ summary, nodeId: peekNode.id, nodeColor: peekNode.color, zoom: viewport.zoomLevel }) : void 0
2766
+ }
2767
+ )
2971
2768
  }
2972
2769
  );
2973
2770
  })()
@@ -2977,91 +2774,29 @@ function VisualliCanvas(props) {
2977
2774
  }
2978
2775
 
2979
2776
  // src/VisualliRenderer.tsx
2980
- var import_jsx_runtime12 = require("react/jsx-runtime");
2981
- function useSystemDark() {
2982
- const mq = typeof window !== "undefined" ? window.matchMedia("(prefers-color-scheme: dark)") : null;
2983
- const [dark, setDark] = (0, import_react15.useState)(mq?.matches ?? false);
2984
- (0, import_react15.useEffect)(() => {
2985
- if (!mq) return;
2986
- const handler = (e) => setDark(e.matches);
2987
- mq.addEventListener("change", handler);
2988
- return () => mq.removeEventListener("change", handler);
2989
- }, [mq]);
2990
- return dark;
2991
- }
2992
- function useFontInjection() {
2993
- (0, import_react15.useEffect)(() => {
2994
- if (typeof document === "undefined") return;
2995
- const links = [
2996
- "https://fonts.googleapis.com",
2997
- "https://fonts.gstatic.com",
2998
- "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",
2999
- "https://fonts.googleapis.com/css2?family=Playpen+Sans:wght@300;400;600;800&family=Story+Script:wght@400&display=swap"
3000
- ];
3001
- links.forEach((href, i) => {
3002
- const id = `visualli-font-${i}`;
3003
- if (!document.getElementById(id)) {
3004
- const link = document.createElement("link");
3005
- link.id = id;
3006
- link.rel = i < 2 ? "preconnect" : "stylesheet";
3007
- link.href = href;
3008
- if (i === 1) link.crossOrigin = "anonymous";
3009
- document.head.appendChild(link);
3010
- }
3011
- });
3012
- }, []);
3013
- }
3014
- function EmptyState({ isDark }) {
3015
- const bg = isDark ? "#131311" : "#F5F3EF";
3016
- const fg = isDark ? "rgba(240,237,230,0.35)" : "rgba(26,26,24,0.35)";
3017
- const bd = isDark ? "#2A2A28" : "#DDD9D0";
3018
- const code = { fontFamily: "monospace", fontSize: 12, padding: "1px 4px", borderRadius: 4, background: isDark ? "#222220" : "#E8E4DC", border: `1px solid ${bd}` };
3019
- 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: [
3020
- /* @__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: [
3021
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("rect", { x: "10", y: "6", width: "28", height: "36", rx: "3" }),
3022
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("line", { x1: "16", y1: "16", x2: "32", y2: "16" }),
3023
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("line", { x1: "16", y1: "22", x2: "32", y2: "22" }),
3024
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("line", { x1: "16", y1: "28", x2: "26", y2: "28" })
3025
- ] }),
3026
- /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { style: { textAlign: "center", lineHeight: 1.7 }, children: [
3027
- /* @__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" }),
3028
- /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("p", { style: { margin: "4px 0 0", fontSize: 13 }, children: [
3029
- "Pass a ",
3030
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("code", { style: code, children: "visualliFile" }),
3031
- " or ",
3032
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("code", { style: code, children: "visualliString" }),
3033
- " prop."
3034
- ] })
3035
- ] })
2777
+ var import_jsx_runtime10 = require("react/jsx-runtime");
2778
+ var stateBox = { display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", width: "100%", height: "100%", gap: "var(--space-3)", padding: "var(--space-7)", boxSizing: "border-box", textAlign: "center", color: "var(--ink-muted)", background: "var(--canvas)" };
2779
+ function StateNote({ topic, title, children, role }) {
2780
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "vi-map", style: stateBox, role, "aria-live": role === "status" ? "polite" : void 0, children: [
2781
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("svg", { width: "64", height: "52", viewBox: "-32 -26 64 52", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("path", { d: (0, import_core16.blobPath)((0, import_core16.shapeForLevel)(0), 26, 20), fill: `var(--topic-${topic})`, stroke: `var(--topic-${topic}-ring)`, strokeWidth: "2" }) }),
2782
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { className: "label", style: { margin: 0, color: "var(--ink)" }, children: title }),
2783
+ children && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "body-sm", style: { margin: 0, maxWidth: 480, overflowWrap: "anywhere" }, children })
3036
2784
  ] });
3037
2785
  }
3038
- function LoadingState({ isDark }) {
3039
- const bg = isDark ? "#131311" : "#F5F3EF";
3040
- const fg = isDark ? "rgba(240,237,230,0.5)" : "rgba(26,26,24,0.5)";
3041
- const dot = isDark ? "rgba(240,237,230,0.6)" : "rgba(26,26,24,0.45)";
3042
- 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: [
3043
- /* @__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)) }),
3044
- /* @__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)}}` }),
3045
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { style: { margin: 0, fontSize: 14 }, children: "Loading .visualli file\u2026" })
2786
+ function EmptyState() {
2787
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(StateNote, { topic: "stone", title: "Nothing to show yet", children: [
2788
+ "Pass a ",
2789
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("code", { style: { fontFamily: "var(--font-mono)" }, children: "visualliFile" }),
2790
+ " or ",
2791
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("code", { style: { fontFamily: "var(--font-mono)" }, children: "visualliString" }),
2792
+ "."
3046
2793
  ] });
3047
2794
  }
3048
- function ErrorState({ message, isDark }) {
3049
- const bg = isDark ? "#131311" : "#F5F3EF";
3050
- const bd = isDark ? "#3a1f1f" : "#F0CECE";
3051
- const card = isDark ? "#1e1212" : "#FFF5F5";
3052
- const head = isDark ? "#F87171" : "#DC2626";
3053
- const body = isDark ? "rgba(248,113,113,0.8)" : "rgba(180,40,40,0.8)";
3054
- 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: [
3055
- /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 10, marginBottom: 12 }, children: [
3056
- /* @__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: [
3057
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("circle", { cx: "12", cy: "12", r: "10" }),
3058
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("line", { x1: "12", y1: "8", x2: "12", y2: "12" }),
3059
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("line", { x1: "12", y1: "16", x2: "12.01", y2: "16" })
3060
- ] }),
3061
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { style: { fontSize: 15, fontWeight: 700, color: head }, children: "Failed to parse .visualli file" })
3062
- ] }),
3063
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { style: { margin: 0, fontSize: 13, color: body, lineHeight: 1.6, wordBreak: "break-word", fontFamily: "monospace" }, children: message })
3064
- ] }) });
2795
+ function LoadingState() {
2796
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(StateNote, { topic: "teal", title: "Opening the map\u2026", role: "status" });
2797
+ }
2798
+ function ErrorState({ message }) {
2799
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(StateNote, { topic: "berry", title: "This map couldn't be opened", role: "alert", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("code", { style: { fontFamily: "var(--font-mono)" }, children: message }) });
3065
2800
  }
3066
2801
  var WORKER_SCRIPT = `
3067
2802
  self.onmessage = function(e) {
@@ -3073,6 +2808,7 @@ self.onmessage = function(e) {
3073
2808
  layers: new Map(),
3074
2809
  layersByLevel: new Map(),
3075
2810
  rootLayer: null,
2811
+ extensions: {},
3076
2812
  };
3077
2813
  for (var i = 0; i < lines.length; i++) {
3078
2814
  var line = lines[i].trim();
@@ -3088,6 +2824,8 @@ self.onmessage = function(e) {
3088
2824
  if (!doc.layersByLevel.has(obj.level)) doc.layersByLevel.set(obj.level, []);
3089
2825
  doc.layersByLevel.get(obj.level).push(obj);
3090
2826
  if (obj.level === 0) doc.rootLayer = obj;
2827
+ } else if (obj.type === 'extension' && typeof obj.id === 'string') {
2828
+ doc.extensions[obj.id] = Array.isArray(obj.data) ? obj.data : [];
3091
2829
  }
3092
2830
  }
3093
2831
  if (!doc.meta) throw new Error('Missing required meta section');
@@ -3140,7 +2878,7 @@ function useAsyncParse(content, useWorker) {
3140
2878
  Promise.resolve().then(() => {
3141
2879
  if (cancelled) return;
3142
2880
  try {
3143
- const doc = (0, import_core14.parseVisualliFile)(content);
2881
+ const doc = (0, import_core16.parseVisualliFile)(content);
3144
2882
  if (!cancelled) setState({ status: "ready", doc });
3145
2883
  } catch (err) {
3146
2884
  if (!cancelled) setState({ status: "error", message: err instanceof Error ? err.message : String(err) });
@@ -3208,16 +2946,25 @@ function VisualliRenderer({
3208
2946
  visualliString,
3209
2947
  visualliFile,
3210
2948
  theme = "light",
2949
+ comfort,
2950
+ respectForcedColors,
2951
+ layout,
2952
+ controlsPosition,
2953
+ app,
3211
2954
  width = "100%",
3212
2955
  height = "100%",
3213
2956
  useWorker = true,
3214
2957
  chromaticImmersion = false,
2958
+ fontBaseUrl,
2959
+ loadWebFonts,
3215
2960
  className,
3216
2961
  style
3217
2962
  }) {
3218
- useFontInjection();
3219
- const systemDark = useSystemDark();
3220
- const isDark = theme === "dark" ? true : theme === "light" ? false : systemDark;
2963
+ const design = useDesign({ theme, comfort, respectForcedColors });
2964
+ (0, import_react15.useState)(() => {
2965
+ ensureDesignSystemStyles({ fontBaseUrl, loadWebFonts });
2966
+ return true;
2967
+ });
3221
2968
  const cssWidth = typeof width === "number" ? `${width}px` : width;
3222
2969
  const cssHeight = typeof height === "number" ? `${height}px` : height;
3223
2970
  const { text: fileText, error: fileReadError, loading: fileReading } = useFileText(visualliFile);
@@ -3228,21 +2975,29 @@ function VisualliRenderer({
3228
2975
  return null;
3229
2976
  }, [parseState]);
3230
2977
  const wrapperStyle = { width: cssWidth, height: cssHeight, overflow: "hidden", ...style };
2978
+ const attrs = design.attrs;
3231
2979
  if (!visualliString && !visualliFile) {
3232
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className, style: wrapperStyle, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(EmptyState, { isDark }) });
2980
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className, ...attrs, style: wrapperStyle, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(EmptyState, {}) });
3233
2981
  }
3234
2982
  if (fileReading || parseState.status === "loading") {
3235
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className, style: wrapperStyle, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(LoadingState, { isDark }) });
2983
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className, ...attrs, style: wrapperStyle, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(LoadingState, {}) });
3236
2984
  }
3237
2985
  if (fileReadError || parseState.status === "error") {
3238
2986
  const msg = fileReadError ?? (parseState.status === "error" ? parseState.message : "Unknown error");
3239
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className, style: wrapperStyle, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ErrorState, { message: msg, isDark }) });
2987
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className, ...attrs, style: wrapperStyle, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ErrorState, { message: msg }) });
3240
2988
  }
3241
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className, style: wrapperStyle, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2989
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className, style: wrapperStyle, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
3242
2990
  VisualliCanvas,
3243
2991
  {
3244
2992
  preParsedVisualli: resolvedDoc ?? void 0,
3245
- isDark,
2993
+ theme,
2994
+ comfort,
2995
+ respectForcedColors,
2996
+ layout,
2997
+ controlsPosition,
2998
+ app,
2999
+ fontBaseUrl,
3000
+ loadWebFonts,
3246
3001
  chromaticImmersion,
3247
3002
  style: { width: "100%", height: "100%" }
3248
3003
  }
@@ -3251,19 +3006,131 @@ function VisualliRenderer({
3251
3006
 
3252
3007
  // src/context/VisualliContext.tsx
3253
3008
  var import_react16 = require("react");
3254
- var import_jsx_runtime13 = require("react/jsx-runtime");
3009
+ var import_jsx_runtime11 = require("react/jsx-runtime");
3255
3010
  var VisualliContext = (0, import_react16.createContext)(null);
3256
3011
  function VisualliProvider({
3257
3012
  children
3258
3013
  }) {
3259
3014
  const value = {};
3260
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(VisualliContext.Provider, { value, children });
3015
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(VisualliContext.Provider, { value, children });
3261
3016
  }
3262
3017
  function useVisualli() {
3263
- const ctx = (0, import_react16.useContext)(VisualliContext);
3264
- if (!ctx) {
3018
+ const ctx2 = (0, import_react16.useContext)(VisualliContext);
3019
+ if (!ctx2) {
3265
3020
  return {};
3266
3021
  }
3267
- return ctx;
3022
+ return ctx2;
3023
+ }
3024
+
3025
+ // src/hooks/useViewportNodes.ts
3026
+ var import_react17 = require("react");
3027
+ var import_core17 = require("@visualli/core");
3028
+ var SPATIAL_INDEX_THRESHOLD2 = 200;
3029
+ function useViewportNodes(nodes, levelFilter, skipCulling = false, isDragging = false) {
3030
+ const { centerX, centerY, zoomLevel, canvasWidth, canvasHeight } = useViewportStore();
3031
+ const indexRef = (0, import_react17.useRef)(null);
3032
+ const nodesHashRef = (0, import_react17.useRef)(0);
3033
+ const nodesHash = nodes.length;
3034
+ if (nodesHash !== nodesHashRef.current) {
3035
+ nodesHashRef.current = nodesHash;
3036
+ indexRef.current = null;
3037
+ }
3038
+ return (0, import_react17.useMemo)(() => {
3039
+ if (nodes.length === 0) return [];
3040
+ if (!indexRef.current && nodes.length >= SPATIAL_INDEX_THRESHOLD2 && !skipCulling && !isDragging) {
3041
+ indexRef.current = new import_core17.RBushSpatialIndex();
3042
+ indexRef.current.bulkLoad(nodes.map((n) => ({
3043
+ nodeId: n.id,
3044
+ bounds: { minX: n.x, minY: n.y, maxX: n.x + n.width, maxY: n.y + n.height }
3045
+ })));
3046
+ }
3047
+ return (0, import_core17.getViewportVisibleNodes)({
3048
+ nodes,
3049
+ viewport: { centerX, centerY, zoomLevel, rotation: 0, visibleBounds: { minX: 0, minY: 0, maxX: 0, maxY: 0 } },
3050
+ canvasWidth,
3051
+ canvasHeight,
3052
+ levelFilter,
3053
+ skipCulling,
3054
+ isDragging,
3055
+ spatialIndex: indexRef.current,
3056
+ spatialIndexThreshold: SPATIAL_INDEX_THRESHOLD2
3057
+ });
3058
+ }, [nodes, levelFilter, skipCulling, isDragging, centerX, centerY, zoomLevel, canvasWidth, canvasHeight]);
3268
3059
  }
3060
+
3061
+ // src/index.ts
3062
+ var import_core19 = require("@visualli/core");
3063
+
3064
+ // src/components/KonvaNode.tsx
3065
+ var import_react18 = __toESM(require("react"), 1);
3066
+ var import_jsx_runtime12 = require("react/jsx-runtime");
3067
+ var KonvaNode = import_react18.default.memo(function KonvaNode2({
3068
+ node,
3069
+ zoomLevel,
3070
+ isExternallyHovered = false,
3071
+ isExternallyPressed = false,
3072
+ isSelected = false,
3073
+ isDragging = false,
3074
+ design = DEFAULT_DESIGN
3075
+ }) {
3076
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
3077
+ KShape,
3078
+ {
3079
+ listening: false,
3080
+ perfectDrawEnabled: false,
3081
+ sceneFunc: (ctx2) => {
3082
+ drawIdea(ctx2._context, node, design, zoomLevel, { ...IDLE, hovered: isExternallyHovered, pressed: isExternallyPressed, selected: isSelected }, {
3083
+ shadows: !isDragging
3084
+ });
3085
+ }
3086
+ }
3087
+ );
3088
+ });
3089
+ var KonvaNode_default = KonvaNode;
3090
+
3091
+ // src/components/KonvaEdge.tsx
3092
+ var import_react19 = require("react");
3093
+ var import_core18 = require("@visualli/core");
3094
+ var import_jsx_runtime13 = require("react/jsx-runtime");
3095
+ var KonvaEdge = (0, import_react19.memo)(function KonvaEdge2({ sourceNode: from, targetNode: to, connection, zoomLevel = 1, design = DEFAULT_DESIGN }) {
3096
+ const g = (0, import_react19.useMemo)(
3097
+ () => prepareConnector((0, import_core18.connectorGeometry)(from, to, design.metrics.edgeGap)),
3098
+ // eslint-disable-next-line react-hooks/exhaustive-deps
3099
+ [from.x, from.y, from.width, from.level, from.branchCount, to.x, to.y, to.width, to.level, to.branchCount, design.metrics.edgeGap]
3100
+ );
3101
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
3102
+ KShape,
3103
+ {
3104
+ listening: false,
3105
+ perfectDrawEnabled: false,
3106
+ sceneFunc: (ctx2) => {
3107
+ drawConnector(ctx2._context, g, design, zoomLevel, connection?.style === "dashed", connection?.label);
3108
+ }
3109
+ }
3110
+ );
3111
+ });
3112
+ var KonvaEdge_default = KonvaEdge;
3113
+
3114
+ // src/components/KonvaContainer.tsx
3115
+ var import_react20 = require("react");
3116
+ var import_jsx_runtime14 = require("react/jsx-runtime");
3117
+ var KonvaContainer = (0, import_react20.memo)(function KonvaContainer2({ nodes, label = "", zoomLevel = 1, design = DEFAULT_DESIGN }) {
3118
+ const placed = (0, import_react20.useMemo)(() => {
3119
+ const map = new Map(nodes.map((n) => [n.id, n]));
3120
+ return layoutContainers([{ id: "c", label: label || void 0, nodeIds: nodes.map((n) => n.id) }], map, [], design)[0] ?? null;
3121
+ }, [nodes, label, design]);
3122
+ if (!placed) return null;
3123
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
3124
+ KShape,
3125
+ {
3126
+ listening: false,
3127
+ perfectDrawEnabled: false,
3128
+ sceneFunc: (ctx2) => {
3129
+ drawHull(ctx2._context, placed.hull, design);
3130
+ if (placed.pill) drawPill(ctx2._context, placed.pill, design, zoomLevel);
3131
+ }
3132
+ }
3133
+ );
3134
+ });
3135
+ var KonvaContainer_default = KonvaContainer;
3269
3136
  //# sourceMappingURL=index.cjs.map