@visualli/react 0.1.7 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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.js CHANGED
@@ -1,22 +1,28 @@
1
1
  'use client';
2
2
 
3
3
  // src/VisualliRenderer.tsx
4
- import { useEffect as useEffect7, useRef as useRef10, useState as useState4, useMemo as useMemo8 } from "react";
5
- import { parseVisualliFile as parseVisualliFile2 } from "@visualli/core";
4
+ import { useEffect as useEffect8, useRef as useRef11, useState as useState5, useMemo as useMemo10 } from "react";
5
+ import { blobPath as blobPath3, parseVisualliFile as parseVisualliFile2, shapeForLevel } from "@visualli/core";
6
6
 
7
7
  // src/VisualliCanvas.tsx
8
- import { useRef as useRef9, useState as useState3, useCallback as useCallback4, useEffect as useEffect6, useMemo as useMemo7 } from "react";
8
+ import { useRef as useRef10, useState as useState4, useCallback as useCallback4, useEffect as useEffect7, useMemo as useMemo9 } from "react";
9
9
  import {
10
10
  parseVisualliFile,
11
11
  getNodesForLayer,
12
- getThemeBackground,
13
- darkenHexColor as darkenHexColor2,
12
+ getSemanticAnchors,
13
+ ideaColor,
14
+ motionVars,
15
+ nodeRadii as nodeRadii3,
16
+ ringsFor as ringsFor3,
17
+ peekPosition,
18
+ TRAIL as TRAIL2,
14
19
  RBushSpatialIndex as RBushSpatialIndex2,
15
- NODE_HEIGHT as NODE_HEIGHT5,
16
- TEXT_LABEL_HIDE_BELOW_ZOOM as TEXT_LABEL_HIDE_BELOW_ZOOM3,
17
- ZOOM_NAV_IN_THRESHOLD,
18
- ZOOM_NAV_OUT_THRESHOLD,
19
- ZOOM_MIN as ZOOM_MIN2
20
+ GESTURE,
21
+ pinchZoom,
22
+ zoomAround,
23
+ VIEW as VIEW2,
24
+ PEEK,
25
+ SHEET as SHEET2
20
26
  } from "@visualli/core";
21
27
 
22
28
  // src/stores/useNodeStore.ts
@@ -125,13 +131,16 @@ var useViewportStore = create2((set, get) => {
125
131
  ...init,
126
132
  canvasWidth: DEFAULT_CANVAS_WIDTH,
127
133
  canvasHeight: DEFAULT_CANVAS_HEIGHT,
134
+ fitScale: 1,
135
+ fitCenterX: 0,
136
+ fitCenterY: 0,
128
137
  pan: (dx, dy) => set((s) => {
129
138
  const updated = recalculateViewportBounds(panViewport(dx, dy, getVP(s)), s.canvasWidth, s.canvasHeight);
130
139
  return { centerX: updated.centerX, centerY: updated.centerY, visibleBounds: updated.visibleBounds };
131
140
  }),
132
141
  zoom: (delta, pivotX, pivotY) => set((s) => {
133
142
  const updated = recalculateViewportBounds(
134
- zoomViewport(delta, getVP(s), pivotX, pivotY, s.canvasWidth, s.canvasHeight),
143
+ zoomViewport(delta, getVP(s), pivotX, pivotY, s.canvasWidth, s.canvasHeight, s.fitScale),
135
144
  s.canvasWidth,
136
145
  s.canvasHeight
137
146
  );
@@ -142,10 +151,11 @@ var useViewportStore = create2((set, get) => {
142
151
  return { centerX: updated.centerX, centerY: updated.centerY, visibleBounds: updated.visibleBounds };
143
152
  }),
144
153
  setZoom: (level) => set((s) => {
145
- const clamped = clampZoom(level);
154
+ const clamped = clampZoom(level, s.fitScale);
146
155
  const updated = recalculateViewportBounds({ ...getVP(s), zoomLevel: clamped }, s.canvasWidth, s.canvasHeight);
147
156
  return { zoomLevel: updated.zoomLevel, visibleBounds: updated.visibleBounds };
148
157
  }),
158
+ setFit: (scale, centerX, centerY) => set({ fitScale: scale > 0 ? scale : 1, fitCenterX: centerX, fitCenterY: centerY }),
149
159
  updateCanvasSize: (w, h) => set((s) => {
150
160
  const updated = recalculateViewportBounds(getVP(s), w, h);
151
161
  return { canvasWidth: w, canvasHeight: h, visibleBounds: updated.visibleBounds };
@@ -165,11 +175,11 @@ var useViewportStore = create2((set, get) => {
165
175
  // src/stores/stores.ts
166
176
  import { create as create3 } from "zustand";
167
177
  import { selectionStore, renderConfigStore } from "@visualli/core";
168
- var useSelectionStore = create3((set, get, store) => {
178
+ var useSelectionStore = create3((set) => {
169
179
  selectionStore.subscribe((state) => set(state));
170
180
  return selectionStore.getState();
171
181
  });
172
- var useRenderConfigStore = create3((set, get, store) => {
182
+ var useRenderConfigStore = create3((set) => {
173
183
  renderConfigStore.subscribe((state) => set(state));
174
184
  return renderConfigStore.getState();
175
185
  });
@@ -178,13 +188,10 @@ var useRenderConfigStore = create3((set, get, store) => {
178
188
  import { useEffect, useCallback, useRef, useState } from "react";
179
189
  function useKonvaRenderer({
180
190
  containerRef,
181
- stageRef,
182
191
  enabled = true
183
192
  }) {
184
193
  const pan = useViewportStore((s) => s.pan);
185
194
  const zoom = useViewportStore((s) => s.zoom);
186
- const setZoom = useViewportStore((s) => s.setZoom);
187
- const setCenter = useViewportStore((s) => s.setCenter);
188
195
  const updateCanvasSize = useViewportStore((s) => s.updateCanvasSize);
189
196
  const autoAdjust = useRenderConfigStore((s) => s.autoAdjustQuality);
190
197
  const [canvasWidth, setCanvasWidth] = useState(0);
@@ -242,7 +249,8 @@ function useKonvaRenderer({
242
249
  window.addEventListener("resize", updateSize);
243
250
  }
244
251
  return () => {
245
- ro ? ro.disconnect() : window.removeEventListener("resize", updateSize);
252
+ if (ro) ro.disconnect();
253
+ else window.removeEventListener("resize", updateSize);
246
254
  if (rafId) cancelAnimationFrame(rafId);
247
255
  };
248
256
  }, [enabled, containerRef, updateCanvasSize]);
@@ -262,17 +270,10 @@ function useKonvaRenderer({
262
270
  isPanningRef.current = true;
263
271
  lastPanPointRef.current = { x: e.evt.clientX, y: e.evt.clientY };
264
272
  panAccumRef.current = { x: 0, y: 0 };
265
- const stage = e.target.getStage();
266
- if (stage) stage.container().style.cursor = "grabbing";
267
273
  }
268
274
  }, []);
269
275
  const handleMouseMove = useCallback((e) => {
270
- if (!isPanningRef.current) {
271
- const isOverEmpty = e.target === e.target.getStage();
272
- const stage2 = e.target.getStage();
273
- if (stage2) stage2.container().style.cursor = isOverEmpty ? "grab" : "default";
274
- return;
275
- }
276
+ if (!isPanningRef.current) return;
276
277
  e.evt.preventDefault();
277
278
  const deltaX = e.evt.clientX - lastPanPointRef.current.x;
278
279
  const deltaY = e.evt.clientY - lastPanPointRef.current.y;
@@ -286,7 +287,7 @@ function useKonvaRenderer({
286
287
  panAccumRef.current.y += deltaY;
287
288
  lastPanPointRef.current = { x: e.evt.clientX, y: e.evt.clientY };
288
289
  }, []);
289
- const handleMouseUp = useCallback((e) => {
290
+ const handleMouseUp = useCallback(() => {
290
291
  if (isPanningRef.current) {
291
292
  isPanningRef.current = false;
292
293
  const { x, y } = panAccumRef.current;
@@ -294,181 +295,23 @@ function useKonvaRenderer({
294
295
  pan(x, y);
295
296
  panAccumRef.current = { x: 0, y: 0 };
296
297
  }
297
- const stage = e.target.getStage();
298
- if (stage) stage.container().style.cursor = "grab";
299
298
  }
300
299
  }, [pan]);
301
- return { handleWheel, handleMouseDown, handleMouseMove, handleMouseUp, canvasWidth, canvasHeight };
300
+ const cancelPan = useCallback(() => {
301
+ if (!isPanningRef.current) return;
302
+ isPanningRef.current = false;
303
+ const { x, y } = panAccumRef.current;
304
+ if (x !== 0 || y !== 0) {
305
+ pan(x, y);
306
+ panAccumRef.current = { x: 0, y: 0 };
307
+ }
308
+ }, [pan]);
309
+ return { handleWheel, handleMouseDown, handleMouseMove, handleMouseUp, cancelPan, canvasWidth, canvasHeight };
302
310
  }
303
311
 
304
- // src/hooks/useKonvaLayerTransition.ts
305
- import { useCallback as useCallback2, useRef as useRef2, useEffect as useEffect2 } from "react";
306
-
307
- // src/animations/konvaLayerTransition.ts
308
- import { easeInOutSine, easeOutCubic, easeZoomTransition } from "@visualli/core";
309
- import { ANIMATION_PHASES } from "@visualli/core";
310
- function parseColor(color) {
311
- if (color.startsWith("#")) {
312
- const h = color.slice(1);
313
- return { r: parseInt(h.slice(0, 2), 16), g: parseInt(h.slice(2, 4), 16), b: parseInt(h.slice(4, 6), 16) };
314
- }
315
- const m = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
316
- return m ? { r: +m[1], g: +m[2], b: +m[3] } : { r: 255, g: 255, b: 255 };
317
- }
318
- function interpolateRgb(a, b, t) {
319
- const e = easeInOutSine(t);
320
- 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})`;
321
- }
322
- function lerp(a, b, t) {
323
- return a + (b - a) * t;
324
- }
325
- var KonvaLayerTransitionAnimator = class {
326
- constructor() {
327
- this.rafId = null;
328
- this.timers = [];
329
- this._active = false;
330
- }
331
- get active() {
332
- return this._active;
333
- }
334
- cancel() {
335
- if (this.rafId !== null) {
336
- cancelAnimationFrame(this.rafId);
337
- this.rafId = null;
338
- }
339
- this.timers.forEach(clearTimeout);
340
- this.timers = [];
341
- this._active = false;
342
- }
343
- // ── Zoom into child layer ──────────────────────────────────────────────────
344
- animateZoomIntoLayer(current, targetNode, childCenter, fromColor, toColor, cb, targetZoom = 1, canvasWidth = 800, canvasHeight = 600) {
345
- if (this._active) return;
346
- this._active = true;
347
- const ZOOM_DURATION = ANIMATION_PHASES.ZOOM_IN.ZOOM_TO_TARGET;
348
- const FADE_OUT_DELAY = ANIMATION_PHASES.ZOOM_IN.FADE_OUT_START;
349
- const FADE_OUT_DURATION = ANIMATION_PHASES.ZOOM_IN.FADE_OUT;
350
- const SWAP_AT_MS = ANIMATION_PHASES.ZOOM_IN.CONTENT_SWAP;
351
- const FADE_IN_DURATION = ANIMATION_PHASES.ZOOM_IN.FADE_IN;
352
- const SETTLE_FACTOR = 0.94;
353
- const z0 = current.zoomLevel;
354
- const dx = targetNode.x - current.centerX;
355
- const dy = targetNode.y - current.centerY;
356
- const vmin = Math.min(canvasWidth, canvasHeight);
357
- const fillZoom = Math.max(4, vmin * 1.5 / Math.max(targetNode.width, 100));
358
- const fromRgb = parseColor(fromColor);
359
- const toRgb = parseColor(toColor);
360
- const state = { viewport: { centerX: 0, centerY: 0, zoomLevel: 0 }, opacity: 1, backgroundColor: fromColor };
361
- const start = performance.now();
362
- let swapped = false;
363
- let fadeInStart = 0;
364
- cb.onPhaseChange?.("zoom");
365
- const tick = (now) => {
366
- if (!this._active) return;
367
- const elapsed = now - start;
368
- if (!swapped) {
369
- if (elapsed >= SWAP_AT_MS) {
370
- swapped = true;
371
- fadeInStart = now;
372
- state.viewport = { centerX: childCenter.x, centerY: childCenter.y, zoomLevel: targetZoom * SETTLE_FACTOR };
373
- state.opacity = 0;
374
- state.backgroundColor = toColor;
375
- cb.onUpdate(state);
376
- cb.onPhaseChange?.("swap");
377
- this.rafId = requestAnimationFrame(tick);
378
- return;
379
- }
380
- const rawT = Math.min(elapsed / ZOOM_DURATION, 1);
381
- const zoomT = easeZoomTransition(rawT);
382
- const zl = lerp(z0, fillZoom, zoomT);
383
- state.viewport = {
384
- centerX: targetNode.x - dx * z0 * (1 - zoomT) / zl,
385
- centerY: targetNode.y - dy * z0 * (1 - zoomT) / zl,
386
- zoomLevel: zl
387
- };
388
- state.opacity = elapsed > FADE_OUT_DELAY ? Math.max(0, 1 - easeOutCubic(Math.min((elapsed - FADE_OUT_DELAY) / FADE_OUT_DURATION, 1))) : 1;
389
- state.backgroundColor = interpolateRgb(fromRgb, toRgb, rawT);
390
- cb.onUpdate(state);
391
- } else {
392
- const fi = now - fadeInStart;
393
- const ft = Math.min(fi / FADE_IN_DURATION, 1);
394
- const eased = easeOutCubic(ft);
395
- state.viewport = { centerX: childCenter.x, centerY: childCenter.y, zoomLevel: lerp(targetZoom * SETTLE_FACTOR, targetZoom, eased) };
396
- state.opacity = eased;
397
- state.backgroundColor = toColor;
398
- cb.onUpdate(state);
399
- if (ft >= 1) {
400
- cb.onPhaseChange?.("fade-in");
401
- this.rafId = null;
402
- this._complete(cb.onComplete);
403
- return;
404
- }
405
- }
406
- this.rafId = requestAnimationFrame(tick);
407
- };
408
- this.rafId = requestAnimationFrame(tick);
409
- }
410
- // ── Zoom out to parent layer ───────────────────────────────────────────────
411
- animateZoomOutToParent(current, parent, fromColor, toColor, cb) {
412
- if (this._active) return;
413
- this._active = true;
414
- const FADE_OUT = ANIMATION_PHASES.ZOOM_OUT.FADE_OUT;
415
- const FADE_IN = ANIMATION_PHASES.ZOOM_OUT.FADE_IN;
416
- const OPACITY_SETTLE = 66;
417
- const fromRgb = parseColor(fromColor);
418
- const toRgb = parseColor(toColor);
419
- const state = { viewport: { centerX: 0, centerY: 0, zoomLevel: 0 }, opacity: 1, backgroundColor: fromColor };
420
- const start = performance.now();
421
- let swapped = false;
422
- let fadeInStart = 0;
423
- cb.onPhaseChange?.("fade-out");
424
- const tick = (now) => {
425
- if (!this._active) return;
426
- const elapsed = now - start;
427
- if (!swapped) {
428
- if (elapsed >= FADE_OUT) {
429
- swapped = true;
430
- fadeInStart = now;
431
- state.viewport = { centerX: parent.centerX, centerY: parent.centerY, zoomLevel: parent.zoomLevel * 5 };
432
- state.opacity = 0;
433
- state.backgroundColor = toColor;
434
- cb.onUpdate(state);
435
- cb.onPhaseChange?.("zoom-back");
436
- this.rafId = requestAnimationFrame(tick);
437
- return;
438
- }
439
- const rawT = Math.min(elapsed / FADE_OUT, 1);
440
- const t = easeOutCubic(rawT);
441
- state.viewport = { centerX: current.centerX, centerY: current.centerY, zoomLevel: lerp(current.zoomLevel, current.zoomLevel * 0.6, t) };
442
- state.opacity = 1 - t;
443
- state.backgroundColor = interpolateRgb(fromRgb, toRgb, rawT);
444
- cb.onUpdate(state);
445
- } else {
446
- const fi = now - fadeInStart;
447
- const rawT = Math.min(fi / FADE_IN, 1);
448
- const t = easeZoomTransition(rawT);
449
- state.viewport = { centerX: parent.centerX, centerY: parent.centerY, zoomLevel: lerp(parent.zoomLevel * 5, parent.zoomLevel, t) };
450
- state.opacity = fi < OPACITY_SETTLE ? 0 : easeZoomTransition(Math.min((fi - OPACITY_SETTLE) / (FADE_IN - OPACITY_SETTLE), 1));
451
- state.backgroundColor = toColor;
452
- cb.onUpdate(state);
453
- if (rawT >= 1) {
454
- cb.onPhaseChange?.("fade-in");
455
- this.rafId = null;
456
- this._complete(cb.onComplete);
457
- return;
458
- }
459
- }
460
- this.rafId = requestAnimationFrame(tick);
461
- };
462
- this.rafId = requestAnimationFrame(tick);
463
- }
464
- _complete(onComplete) {
465
- this.timers.forEach(clearTimeout);
466
- this.timers = [];
467
- this._active = false;
468
- onComplete();
469
- }
470
- };
471
- var konvaLayerTransitionAnimator = new KonvaLayerTransitionAnimator();
312
+ // src/hooks/useLayerChoreography.ts
313
+ import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2 } from "react";
314
+ import { MOTION, cssEase, swapDelay } from "@visualli/core";
472
315
 
473
316
  // src/utils/layerNavigation.ts
474
317
  import {
@@ -481,152 +324,272 @@ import {
481
324
  calculateFitView
482
325
  } from "@visualli/core";
483
326
 
484
- // src/hooks/useKonvaLayerTransition.ts
485
- function useKonvaLayerTransition(opts) {
486
- const {
487
- stageRef,
488
- canvasWrapperRef,
489
- bgRef,
490
- onSwapLayer,
491
- onSwapBack,
492
- onComplete,
493
- onViewportFrame,
494
- onViewportFinal,
495
- onPhaseChange: onPhaseChangeExt,
496
- onTransitionLifecycle
497
- } = opts;
498
- const setZoom = useViewportStore((s) => s.setZoom);
499
- const setCenter = useViewportStore((s) => s.setCenter);
500
- const getState = useViewportStore.getState;
501
- const swappedRef = useRef2(false);
502
- const canvasSizeRef = useRef2({ width: 800, height: 600 });
503
- useEffect2(() => {
504
- const update = () => {
505
- const vp = useViewportStore.getState();
506
- canvasSizeRef.current = { width: vp.canvasWidth || 800, height: vp.canvasHeight || 600 };
507
- };
508
- update();
509
- const unsubscribe = useViewportStore.subscribe(update);
510
- return unsubscribe;
327
+ // src/hooks/useLayerChoreography.ts
328
+ function useLayerChoreography(o) {
329
+ const { stageRef, canvasWrapperRef } = o;
330
+ const opts = useRef2(o);
331
+ opts.current = o;
332
+ const busy = useRef2(false);
333
+ const waiting = useRef2(false);
334
+ const anims = useRef2([]);
335
+ const timers = useRef2([]);
336
+ const after = useRef2([]);
337
+ const clear = useCallback2(() => {
338
+ timers.current.forEach(clearTimeout);
339
+ timers.current = [];
340
+ after.current.forEach(cancelAnimationFrame);
341
+ after.current = [];
342
+ anims.current.forEach((a) => a.cancel());
343
+ anims.current = [];
511
344
  }, []);
512
345
  const applyViewport = useCallback2((vp) => {
513
346
  const stage = stageRef.current;
514
- if (!stage) return;
515
- const { width, height } = canvasSizeRef.current;
516
- stage.x(width / 2 - vp.centerX * vp.zoomLevel);
517
- stage.y(height / 2 - vp.centerY * vp.zoomLevel);
518
- stage.scaleX(vp.zoomLevel);
519
- stage.scaleY(vp.zoomLevel);
520
- stage.getLayers().forEach((l) => l.drawScene());
521
- setCenter(vp.centerX, vp.centerY);
522
- setZoom(vp.zoomLevel);
523
- }, [stageRef, setCenter, setZoom]);
524
- const applyOpacity = useCallback2((opacity) => {
525
- if (canvasWrapperRef.current) {
526
- canvasWrapperRef.current.style.opacity = String(opacity);
527
- }
528
- }, [canvasWrapperRef]);
529
- const applyBgColor = useCallback2((color) => {
530
- if (bgRef?.current) {
531
- bgRef.current.style.backgroundColor = color;
532
- }
533
- }, [bgRef]);
534
- const zoomIntoLayer = useCallback2((_doc, targetNode, childLayerId, childNodes, fromColor, toColor) => {
535
- if (konvaLayerTransitionAnimator.active) return;
536
- const vp = getState();
347
+ if (stage) {
348
+ stage.x(stage.width() / 2 - vp.centerX * vp.zoomLevel);
349
+ stage.y(stage.height() / 2 - vp.centerY * vp.zoomLevel);
350
+ stage.scaleX(vp.zoomLevel);
351
+ stage.scaleY(vp.zoomLevel);
352
+ stage.getLayers().forEach((l) => l.drawScene());
353
+ }
354
+ const s = useViewportStore.getState();
355
+ s.setCenter(vp.centerX, vp.centerY);
356
+ s.setZoom(vp.zoomLevel);
357
+ }, [stageRef]);
358
+ const lifecycle = useCallback2((phase) => {
537
359
  const stage = stageRef.current;
538
- const w = stage?.width() ?? 800;
539
- const h = stage?.height() ?? 600;
540
- const fit = calculateFitView(childNodes, w, h);
541
- swappedRef.current = false;
542
- onTransitionLifecycle?.("start");
543
- konvaLayerTransitionAnimator.animateZoomIntoLayer(
544
- { centerX: vp.centerX, centerY: vp.centerY, zoomLevel: vp.zoomLevel },
545
- { x: targetNode.x, y: targetNode.y, width: targetNode.width, height: targetNode.height },
546
- { x: fit.centerX, y: fit.centerY },
547
- fromColor,
548
- toColor,
549
- {
550
- onUpdate: ({ viewport, opacity, backgroundColor }) => {
551
- if (onViewportFrame) {
552
- onViewportFrame(viewport);
553
- } else {
554
- applyViewport(viewport);
555
- }
556
- applyOpacity(opacity);
557
- applyBgColor(backgroundColor);
558
- },
559
- onComplete: () => {
560
- const finalVp = { centerX: fit.centerX, centerY: fit.centerY, zoomLevel: fit.zoomLevel };
561
- if (onViewportFinal) {
562
- onViewportFinal(finalVp);
563
- } else {
564
- applyViewport(finalVp);
565
- }
566
- applyOpacity(1);
567
- onComplete();
568
- onTransitionLifecycle?.("end");
569
- },
570
- onPhaseChange: (phase) => {
571
- onPhaseChangeExt?.(phase);
572
- if (phase === "swap" && !swappedRef.current) {
573
- swappedRef.current = true;
574
- onSwapLayer(childLayerId);
575
- }
576
- }
577
- },
578
- fit.zoomLevel,
579
- w,
580
- h
581
- );
582
- }, [stageRef, getState, applyViewport, applyOpacity, applyBgColor, onSwapLayer, onComplete, onViewportFrame, onViewportFinal, onPhaseChangeExt, onTransitionLifecycle]);
583
- const zoomOutToParent = useCallback2((parentViewport, fromColor, toColor) => {
584
- if (konvaLayerTransitionAnimator.active) return;
585
- const vp = getState();
586
- swappedRef.current = false;
587
- onTransitionLifecycle?.("start");
588
- konvaLayerTransitionAnimator.animateZoomOutToParent(
589
- { centerX: vp.centerX, centerY: vp.centerY, zoomLevel: vp.zoomLevel },
590
- parentViewport,
591
- fromColor,
592
- toColor,
593
- {
594
- onUpdate: ({ viewport, opacity, backgroundColor }) => {
595
- if (onViewportFrame) {
596
- onViewportFrame(viewport);
597
- } else {
598
- applyViewport(viewport);
599
- }
600
- applyOpacity(opacity);
601
- applyBgColor(backgroundColor);
602
- },
603
- onComplete: () => {
604
- if (onViewportFinal) {
605
- onViewportFinal(parentViewport);
606
- } else {
607
- applyViewport(parentViewport);
608
- }
609
- applyOpacity(1);
610
- onComplete();
611
- onTransitionLifecycle?.("end");
612
- },
613
- onPhaseChange: (phase) => {
614
- onPhaseChangeExt?.(phase);
615
- if (phase === "zoom-back" && !swappedRef.current) {
616
- swappedRef.current = true;
617
- onSwapBack();
618
- }
619
- }
360
+ stage?.getLayers().forEach((l) => {
361
+ l.listening(phase === "end");
362
+ });
363
+ if (phase === "end") stage?.batchDraw();
364
+ opts.current.onLifecycle?.(phase);
365
+ }, [stageRef]);
366
+ const leave = useCallback2((wrap, dir, origin, vp, swap) => {
367
+ busy.current = true;
368
+ waiting.current = false;
369
+ lifecycle("start");
370
+ const reduced = opts.current.reducedMotion;
371
+ if (wrap && !reduced && typeof wrap.animate === "function") {
372
+ const m = MOTION;
373
+ wrap.style.transformOrigin = origin;
374
+ wrap.style.willChange = "transform, opacity";
375
+ if (dir === "in") {
376
+ anims.current.push(
377
+ wrap.animate([{ transform: "scale(1)" }, { transform: `scale(${m.dive.scale})` }], { duration: m.dive.duration, easing: cssEase(m.dive.ease), fill: "forwards" }),
378
+ wrap.animate([{ opacity: 1 }, { opacity: 0 }], { delay: m.dive.fadeDelay, duration: m.dive.fade, easing: "ease", fill: "both" })
379
+ );
380
+ } else {
381
+ anims.current.push(
382
+ wrap.animate([{ transform: "scale(1)" }, { transform: `scale(${m.surface.scale})` }], { duration: m.surface.duration, easing: cssEase(m.surface.ease), fill: "forwards" }),
383
+ wrap.animate([{ opacity: 1 }, { opacity: 0 }], { duration: m.surface.fade, easing: "ease", fill: "both" })
384
+ );
385
+ }
386
+ }
387
+ timers.current.push(setTimeout(() => {
388
+ waiting.current = true;
389
+ swap();
390
+ applyViewport(vp);
391
+ timers.current.push(setTimeout(() => arriveRef.current(), 500));
392
+ }, swapDelay(dir, reduced)));
393
+ }, [applyViewport, lifecycle]);
394
+ const arriveRef = useRef2(() => {
395
+ });
396
+ const arrive = useCallback2(() => {
397
+ if (!waiting.current) return;
398
+ waiting.current = false;
399
+ const wrap = canvasWrapperRef.current;
400
+ const reduced = opts.current.reducedMotion;
401
+ anims.current.forEach((a) => a.cancel());
402
+ anims.current = [];
403
+ let settle = reduced ? MOTION.reduced.fade : MOTION.settle.duration;
404
+ if (wrap && !reduced && typeof wrap.animate === "function") {
405
+ const a = wrap.animate([{ transform: `scale(${MOTION.settle.fromScale})` }, { transform: "none" }], { duration: MOTION.settle.duration, easing: cssEase(MOTION.settle.ease) });
406
+ anims.current.push(a);
407
+ settle = MOTION.settle.duration;
408
+ }
409
+ timers.current.push(setTimeout(() => {
410
+ anims.current.forEach((a) => a.cancel());
411
+ anims.current = [];
412
+ if (wrap) {
413
+ wrap.style.willChange = "auto";
414
+ wrap.style.transformOrigin = "";
620
415
  }
621
- );
622
- }, [getState, applyViewport, applyOpacity, applyBgColor, onSwapBack, onComplete, onViewportFrame, onViewportFinal, onPhaseChangeExt, onTransitionLifecycle]);
623
- const isTransitioning = useCallback2(() => konvaLayerTransitionAnimator.active, []);
416
+ busy.current = false;
417
+ opts.current.onComplete();
418
+ lifecycle("end");
419
+ }, settle));
420
+ }, [canvasWrapperRef, lifecycle]);
421
+ arriveRef.current = arrive;
422
+ const stepInside = useCallback2((targetNode, childLayerId, childNodes) => {
423
+ if (busy.current) return;
424
+ const stage = stageRef.current;
425
+ const zoom = stage?.scaleX() ?? 1;
426
+ const origin = stage ? `${targetNode.x * zoom + stage.x()}px ${targetNode.y * zoom + stage.y()}px` : "50% 50%";
427
+ const fit = calculateFitView(childNodes, stage?.width() ?? 800, stage?.height() ?? 600);
428
+ leave(canvasWrapperRef.current, "in", origin, { centerX: fit.centerX, centerY: fit.centerY, zoomLevel: fit.zoomLevel }, () => opts.current.onSwapLayer(childLayerId));
429
+ }, [stageRef, canvasWrapperRef, leave]);
430
+ const backOut = useCallback2((parent) => {
431
+ if (busy.current) return;
432
+ leave(canvasWrapperRef.current, "out", "50% 50%", parent, () => opts.current.onSwapBack());
433
+ }, [canvasWrapperRef, leave]);
434
+ const isTransitioning = useCallback2(() => busy.current, []);
624
435
  useEffect2(() => () => {
625
- konvaLayerTransitionAnimator.cancel();
626
- }, []);
627
- return { zoomIntoLayer, zoomOutToParent, isTransitioning };
436
+ clear();
437
+ busy.current = false;
438
+ }, [clear]);
439
+ return { stepInside, backOut, arrive, isTransitioning };
628
440
  }
629
441
 
442
+ // src/design/choreography.ts
443
+ import { MOTION as MOTION2, easeBezier, bloomOffset, layerReveal } from "@visualli/core";
444
+ var clamp01 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
445
+ var RevealClock = class {
446
+ constructor() {
447
+ this.t0 = -Infinity;
448
+ this.end = 0;
449
+ this.plan = layerReveal(0, 0);
450
+ this.reduced = false;
451
+ this.center = { x: 0, y: 0 };
452
+ this.order = /* @__PURE__ */ new Map();
453
+ this.listeners = /* @__PURE__ */ new Set();
454
+ this.timer = null;
455
+ }
456
+ /** Begin a layer's arrival. `reduced` (reduced motion or instant reveal): everything fades in together. */
457
+ start(nodes, edgeCount, reduced, now = performance.now()) {
458
+ this.order.clear();
459
+ nodes.forEach((n, i) => this.order.set(n.id, i));
460
+ this.plan = layerReveal(nodes.length, edgeCount);
461
+ this.reduced = reduced || this.plan.instant;
462
+ reduced = this.reduced;
463
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
464
+ for (const n of nodes) {
465
+ minX = Math.min(minX, n.x);
466
+ maxX = Math.max(maxX, n.x);
467
+ minY = Math.min(minY, n.y);
468
+ maxY = Math.max(maxY, n.y);
469
+ }
470
+ this.center = nodes.length ? { x: (minX + maxX) / 2, y: (minY + maxY) / 2 } : { x: 0, y: 0 };
471
+ this.t0 = now;
472
+ this.end = reduced ? MOTION2.reduced.fade : this.plan.total;
473
+ this.emit(true);
474
+ if (this.timer) clearTimeout(this.timer);
475
+ this.timer = setTimeout(() => {
476
+ this.timer = null;
477
+ this.emit(false);
478
+ }, this.end + 50);
479
+ }
480
+ /** Jump to the finished state (e.g. on unmount). */
481
+ stop() {
482
+ this.t0 = -Infinity;
483
+ if (this.timer) {
484
+ clearTimeout(this.timer);
485
+ this.timer = null;
486
+ }
487
+ this.emit(false);
488
+ }
489
+ /** True while the layer is still arriving. */
490
+ active(now = performance.now()) {
491
+ return now - this.t0 < this.end;
492
+ }
493
+ /** Called with true when an arrival starts and false when it has finished. Returns the unsubscribe function. */
494
+ subscribe(fn) {
495
+ this.listeners.add(fn);
496
+ return () => {
497
+ this.listeners.delete(fn);
498
+ };
499
+ }
500
+ emit(running) {
501
+ this.listeners.forEach((fn) => fn(running));
502
+ }
503
+ /** Fills `out` with idea `id`'s arrival look. Returns false when it has arrived (nothing to apply). */
504
+ node(id, x, y, now, out) {
505
+ const el = now - this.t0;
506
+ if (el >= this.end) return false;
507
+ const i = this.order.get(id);
508
+ if (i === void 0) return false;
509
+ if (this.reduced) {
510
+ out.alpha = clamp01(el / MOTION2.reduced.fade);
511
+ out.scale = 1;
512
+ out.dx = 0;
513
+ out.dy = 0;
514
+ return true;
515
+ }
516
+ const r = MOTION2.reveal;
517
+ const t = el - this.plan.ideaDelay(i);
518
+ if (t >= r.duration) return false;
519
+ const e = t <= 0 ? 0 : easeBezier(r.ease, t / r.duration);
520
+ const from = bloomOffset({ x, y }, this.center);
521
+ out.alpha = clamp01(e);
522
+ out.scale = r.fromScale + (1 - r.fromScale) * e;
523
+ out.dx = from.x * (1 - e);
524
+ out.dy = from.y * (1 - e);
525
+ return true;
526
+ }
527
+ /** Fills `out` with connector `i`'s arrival look. Returns false when it has arrived. */
528
+ edge(i, dashed, now, out) {
529
+ const el = now - this.t0;
530
+ if (el >= this.end) return false;
531
+ if (this.reduced) {
532
+ out.alpha = clamp01(el / MOTION2.reduced.fade);
533
+ out.draw = 1;
534
+ return true;
535
+ }
536
+ const c = MOTION2.connector;
537
+ const t = el - this.plan.connectorDelay(i);
538
+ if (t >= c.draw && t >= c.fade) return false;
539
+ const group = t <= 0 ? 0 : easeBezier(c.ease, clamp01(t / c.fade));
540
+ const line = t <= 0 ? 0 : easeBezier(c.ease, clamp01(t / c.draw));
541
+ out.alpha = dashed ? group * line : group;
542
+ out.draw = dashed ? 1 : line;
543
+ return true;
544
+ }
545
+ };
546
+ var HoverTweens = class {
547
+ constructor() {
548
+ this.tweens = /* @__PURE__ */ new Map();
549
+ this.raised = /* @__PURE__ */ new Set();
550
+ this.cache = { lift: 0, ring: 0 };
551
+ }
552
+ /** Set which ideas are raised now. Returns true when anything started moving. */
553
+ set(next, reduced, now = performance.now()) {
554
+ let changed = false;
555
+ const ids = /* @__PURE__ */ new Set([...this.raised, ...next]);
556
+ for (const id of ids) {
557
+ const was = this.raised.has(id), is = next.has(id);
558
+ if (was === is) continue;
559
+ const cur = this.progress(id, now);
560
+ this.tweens.set(id, { fromLift: cur.lift, fromRing: cur.ring, to: is ? 1 : 0, t0: reduced ? -Infinity : now });
561
+ changed = true;
562
+ }
563
+ this.raised = new Set(next);
564
+ return changed;
565
+ }
566
+ /** Lift (bloom ease) and ring (standard ease) progress of an idea, 0 at rest and 1 fully raised. */
567
+ progress(id, now) {
568
+ const tw = this.tweens.get(id);
569
+ const out = this.cache;
570
+ if (!tw) {
571
+ out.lift = out.ring = this.raised.has(id) ? 1 : 0;
572
+ return out;
573
+ }
574
+ const u = clamp01((now - tw.t0) / MOTION2.hover.duration);
575
+ out.lift = tw.fromLift + (tw.to - tw.fromLift) * easeBezier(MOTION2.hover.liftEase, u);
576
+ out.ring = tw.fromRing + (tw.to - tw.fromRing) * easeBezier(MOTION2.hover.ringEase, u);
577
+ if (u >= 1) {
578
+ this.tweens.delete(id);
579
+ out.lift = out.ring = tw.to;
580
+ }
581
+ return out;
582
+ }
583
+ /** True while any idea is still moving. */
584
+ active(now = performance.now()) {
585
+ for (const tw of this.tweens.values()) if (now - tw.t0 < MOTION2.hover.duration) return true;
586
+ return false;
587
+ }
588
+ has(id) {
589
+ return this.tweens.has(id) || this.raised.has(id);
590
+ }
591
+ };
592
+
630
593
  // src/components/KonvaStage.tsx
631
594
  import { useRef as useRef3, useEffect as useEffect3, forwardRef } from "react";
632
595
 
@@ -646,12 +609,124 @@ import {
646
609
  } from "react-konva";
647
610
  var KStage = Stage;
648
611
  var KLayer = Layer;
649
- var KGroup = Group;
650
612
  var KShape = Shape;
651
- var KRect = Rect;
652
- var KText = Text;
653
- var KLine = Line;
654
- var KTextPath = TextPath;
613
+
614
+ // src/components/KonvaStage.tsx
615
+ import Konva from "konva";
616
+
617
+ // src/design/runtime.ts
618
+ import { useSyncExternalStore } from "react";
619
+ import { TOKENS, TYPE_STYLES } from "@visualli/core";
620
+
621
+ // src/generated/specCss.ts
622
+ var SPEC_TOKENS_CSS = '/* Spec tokens for all 8 themes \u2014 generated from the Visualli design system (v0.2.1). 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.vi-text-heading { font-family: var(--font-ui); font-size: 17px; line-height: 24px; font-weight: 700; }\n.vi-text-body { font-family: var(--font-ui); font-size: 15px; line-height: 22px; font-weight: 400; }\n.vi-text-body-sm { font-family: var(--font-ui); font-size: 13px; line-height: 18px; font-weight: 400; }\n.vi-text-label { font-family: var(--font-ui); font-size: 13px; line-height: 16px; font-weight: 600; letter-spacing: 0.01em; }\n.vi-text-caption { font-family: var(--font-ui); font-size: 12px; line-height: 16px; font-weight: 400; }\n.vi-text-node-root { font-family: var(--font-hand); font-size: 30px; line-height: 34px; font-weight: 700; }\n.vi-text-node-label { font-family: var(--font-hand); font-size: 22px; line-height: 26px; font-weight: 700; }\n.vi-text-edge-label { font-family: var(--font-note); font-size: 18px; line-height: 20px; font-weight: 500; }\n.vi-text-annotation { font-family: var(--font-note); font-size: 24px; line-height: 26px; font-weight: 600; }\n';
623
+ 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.1). 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; }\n[data-theme$="dark"] :where([class^="vi-"], [class*=" vi-"]) { 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; }\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.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:where([class^="vi-"], [class*=" vi-"]):where(button, a, input, textarea, [tabindex]):focus-visible, :where([class^="vi-"], [class*=" vi-"]) :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"] :where([class^="vi-"], [class*=" vi-"]), [data-motion="reduced"] :where([class^="vi-"], [class*=" vi-"])::before, [data-motion="reduced"] :where([class^="vi-"], [class*=" vi-"])::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';
624
+ 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";
625
+ var SPEC_DESIGN_SYSTEM_VERSION = "0.2.1";
626
+ var SPEC_BUNDLED_FONTS = [
627
+ {
628
+ "family": "Caveat",
629
+ "file": "Caveat-Variable.ttf",
630
+ "weight": "400 700"
631
+ }
632
+ ];
633
+
634
+ // src/design/runtime.ts
635
+ var STYLE_ID = "visualli-design-system";
636
+ var LAYOUT_CSS = `
637
+ .vi-map__ctrls { transition: bottom var(--duration-quick) var(--ease-standard); } /* controlsPosition 'bottom-right': rising above the sheet */
638
+ .vi-map.is-compact .vi-trail li:not(:first-child):not(.is-current):not(:nth-last-child(2)) { display: none; }
639
+ .vi-map.is-compact .vi-trail button span { max-width: 40vw; max-width: 40cqw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
640
+ .vi-map.is-compact .vi-fact:not(.vi-fact--sheet) { width: min(300px, calc(100cqw - 2 * var(--space-4))); }
641
+ .vi-map.is-touch .vi-trail button { min-height: 32px; }
642
+ `;
643
+ var FONTS_ID = "visualli-design-system-fonts";
644
+ var CAVEAT_ID = "visualli-design-system-caveat";
645
+ var SDK_VERSION = true ? "0.2.1" : "latest";
646
+ var webFontsSheet = Promise.resolve();
647
+ function ensureDesignSystemStyles(assets = {}) {
648
+ if (typeof document === "undefined") return Promise.resolve();
649
+ if (!document.getElementById(STYLE_ID)) {
650
+ const style = document.createElement("style");
651
+ style.id = STYLE_ID;
652
+ style.setAttribute("data-design-system", SPEC_DESIGN_SYSTEM_VERSION);
653
+ style.textContent = `${SPEC_TOKENS_CSS}
654
+ ${SPEC_COMPONENT_CSS}
655
+ ${LAYOUT_CSS}`;
656
+ document.head.appendChild(style);
657
+ }
658
+ if (assets.loadWebFonts !== false && SPEC_FONTS_URL && !document.getElementById(FONTS_ID)) {
659
+ const link = document.createElement("link");
660
+ link.id = FONTS_ID;
661
+ link.rel = "stylesheet";
662
+ link.href = SPEC_FONTS_URL;
663
+ webFontsSheet = new Promise((resolve) => {
664
+ link.addEventListener("load", () => resolve(), { once: true });
665
+ link.addEventListener("error", () => resolve(), { once: true });
666
+ });
667
+ document.head.appendChild(link);
668
+ }
669
+ if (!document.getElementById(CAVEAT_ID)) {
670
+ const base = (assets.fontBaseUrl ?? `https://cdn.jsdelivr.net/npm/@visualli/react@${SDK_VERSION}/fonts`).replace(/\/$/, "");
671
+ const style = document.createElement("style");
672
+ style.id = CAVEAT_ID;
673
+ 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");
674
+ document.head.appendChild(style);
675
+ }
676
+ return webFontsSheet;
677
+ }
678
+ var firstFamily = (stack) => stack.split(",")[0].trim();
679
+ var CANVAS_FONT_FACES = ["node-label", "edge-label", "body"].map((style) => {
680
+ const t = TYPE_STYLES[style];
681
+ return `${t.weight} ${t.size}px ${firstFamily(TOKENS.light[t.family])}`;
682
+ });
683
+ var fontsEpochValue = 0;
684
+ var fontListeners = /* @__PURE__ */ new Set();
685
+ var fontsEpoch = () => fontsEpochValue;
686
+ function useFontsEpoch() {
687
+ return useSyncExternalStore(onFontsChange, fontsEpoch, () => 0);
688
+ }
689
+ var onFontsChange = (cb) => {
690
+ fontListeners.add(cb);
691
+ return () => fontListeners.delete(cb);
692
+ };
693
+ function loadCanvasFonts(sheet = Promise.resolve(), timeoutMs = 3e3) {
694
+ if (typeof document === "undefined" || !("fonts" in document)) return Promise.resolve();
695
+ const fonts = document.fonts;
696
+ const loads = sheet.then(() => Promise.all(CANVAS_FONT_FACES.map((f) => fonts.load(f).catch(() => [])))).then(() => fonts.ready).then(() => void 0);
697
+ const timeout = new Promise((r) => setTimeout(r, timeoutMs));
698
+ return Promise.race([loads, timeout]).then(() => {
699
+ fontsEpochValue++;
700
+ fontListeners.forEach((cb) => cb());
701
+ });
702
+ }
703
+ if (typeof document !== "undefined" && "fonts" in document) {
704
+ document.fonts.addEventListener?.("loadingdone", () => {
705
+ fontsEpochValue++;
706
+ fontListeners.forEach((cb) => cb());
707
+ });
708
+ }
709
+ var currentPixelRatio = () => typeof window === "undefined" ? 1 : Math.min(Math.max(window.devicePixelRatio || 1, 1), 3);
710
+ function watchPixelRatio(cb) {
711
+ if (typeof window === "undefined" || !window.matchMedia) return () => {
712
+ };
713
+ let mql;
714
+ let stopped = false;
715
+ const arm = () => {
716
+ mql = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
717
+ mql.addEventListener("change", onChange, { once: true });
718
+ };
719
+ const onChange = () => {
720
+ if (stopped) return;
721
+ cb(currentPixelRatio());
722
+ arm();
723
+ };
724
+ arm();
725
+ return () => {
726
+ stopped = true;
727
+ mql.removeEventListener("change", onChange);
728
+ };
729
+ }
655
730
 
656
731
  // src/components/KonvaStage.tsx
657
732
  import { jsx } from "react/jsx-runtime";
@@ -677,10 +752,22 @@ var KonvaStage = forwardRef(function KonvaStage2({
677
752
  }
678
753
  }, []);
679
754
  useEffect3(() => {
755
+ const stage = internalRef.current;
680
756
  return () => {
681
- internalRef.current?.destroy();
757
+ stage?.destroy();
682
758
  };
683
759
  }, []);
760
+ Konva.pixelRatio = currentPixelRatio();
761
+ useEffect3(() => watchPixelRatio((ratio) => {
762
+ Konva.pixelRatio = ratio;
763
+ const stage = internalRef.current;
764
+ if (!stage) return;
765
+ stage.getLayers().forEach((l) => {
766
+ l.getCanvas().setPixelRatio(ratio);
767
+ l.getHitCanvas().setPixelRatio(ratio);
768
+ });
769
+ stage.batchDraw();
770
+ }), []);
684
771
  const w = canvasWidth || 800;
685
772
  const h = canvasHeight || 600;
686
773
  const stageX = w / 2 - centerX * zoomLevel;
@@ -704,7 +791,6 @@ var KonvaStage = forwardRef(function KonvaStage2({
704
791
  onTouchMove,
705
792
  onTouchEnd,
706
793
  draggable: false,
707
- pixelRatio: 1,
708
794
  children
709
795
  }
710
796
  );
@@ -712,1360 +798,1025 @@ var KonvaStage = forwardRef(function KonvaStage2({
712
798
  var KonvaStage_default = KonvaStage;
713
799
 
714
800
  // src/components/KonvaNodeLayer.tsx
715
- import { useRef as useRef5, useLayoutEffect, useEffect as useEffect4, useMemo as useMemo3 } from "react";
801
+ import { useRef as useRef5, useLayoutEffect, useMemo as useMemo2 } from "react";
802
+ import { CANVAS_STYLE as CANVAS_STYLE3, RBushSpatialIndex, layerBlooms, shadowsShown, intersectsViewport, nodeBounds } from "@visualli/core";
716
803
 
717
- // src/hooks/useViewportNodes.ts
718
- import { useMemo, useRef as useRef4 } from "react";
719
- import { RBushSpatialIndex, getViewportVisibleNodes } from "@visualli/core";
720
- var SPATIAL_INDEX_THRESHOLD = 200;
721
- function useViewportNodes(nodes, levelFilter, skipCulling = false, isDragging = false) {
722
- const { centerX, centerY, zoomLevel, canvasWidth, canvasHeight } = useViewportStore();
723
- const indexRef = useRef4(null);
724
- const nodesHashRef = useRef4(0);
725
- const nodesHash = nodes.length;
726
- if (nodesHash !== nodesHashRef.current) {
727
- nodesHashRef.current = nodesHash;
728
- indexRef.current = null;
804
+ // src/design/design.ts
805
+ import {
806
+ CANVAS_STYLE,
807
+ TYPE_STYLES as TYPE_STYLES2,
808
+ isContrastTheme,
809
+ isDarkTheme,
810
+ labelScale,
811
+ metricsFor,
812
+ resolveComfort,
813
+ resolveTheme,
814
+ TOKENS as TOKENS2
815
+ } from "@visualli/core";
816
+ function parseShadow(css) {
817
+ const m = /^(-?[\d.]+)(?:px)?\s+(-?[\d.]+)(?:px)?\s+([\d.]+)(?:px)?\s+(.+)$/.exec(css.trim());
818
+ return m ? { x: +m[1], y: +m[2], blur: +m[3], color: m[4].trim() } : null;
819
+ }
820
+ var cache = /* @__PURE__ */ new Map();
821
+ function makeDesign(theme, comfortIn) {
822
+ const key2 = `${theme}|${+comfortIn.readableType}${+comfortIn.largerText}${+comfortIn.reducedMotion}`;
823
+ const hit = cache.get(key2);
824
+ if (hit) return hit;
825
+ const tokens = TOKENS2[theme];
826
+ const ui = tokens["font-ui"];
827
+ const d = {
828
+ theme,
829
+ comfort: comfortIn,
830
+ tokens,
831
+ metrics: metricsFor(comfortIn),
832
+ isDark: isDarkTheme(theme),
833
+ fontHand: comfortIn.readableType ? ui : tokens["font-hand"],
834
+ fontNote: comfortIn.readableType ? ui : tokens["font-note"],
835
+ fontUi: ui,
836
+ labelScale: labelScale(comfortIn),
837
+ nodeShadow: isContrastTheme(theme) ? null : parseShadow(isDarkTheme(theme) ? CANVAS_STYLE.node.shadowDark : CANVAS_STYLE.node.shadowLight),
838
+ attrs: {
839
+ "data-theme": theme,
840
+ ...comfortIn.readableType ? { "data-type": "readable" } : {},
841
+ ...comfortIn.largerText ? { "data-scale": "lg" } : {},
842
+ ...comfortIn.reducedMotion ? { "data-motion": "reduced" } : {}
843
+ },
844
+ edgeLabel: comfortIn.readableType ? { weight: CANVAS_STYLE.edge.readableLabelWeight, size: CANVAS_STYLE.edge.readableLabelSize } : { weight: CANVAS_STYLE.edge.labelWeight, size: CANVAS_STYLE.edge.labelSize }
845
+ };
846
+ cache.set(key2, d);
847
+ return d;
848
+ }
849
+ var DEFAULT_DESIGN = makeDesign("light", { readableType: false, largerText: false, reducedMotion: false });
850
+ function resolveDesign(props, env) {
851
+ const input = props.theme ?? (props.isDark === void 0 ? void 0 : props.isDark ? "dark" : "light");
852
+ let theme = resolveTheme(input, { prefersDark: env.prefersDark, forcedColors: env.forcedColors });
853
+ if (env.forcedColors && props.respectForcedColors !== false && !isContrastTheme(theme)) {
854
+ theme = isDarkTheme(theme) ? "contrast-dark" : "contrast-light";
729
855
  }
730
- return useMemo(() => {
731
- if (nodes.length === 0) return [];
732
- if (!indexRef.current && nodes.length >= SPATIAL_INDEX_THRESHOLD && !skipCulling && !isDragging) {
733
- indexRef.current = new RBushSpatialIndex();
734
- indexRef.current.bulkLoad(nodes.map((n) => ({
735
- nodeId: n.id,
736
- bounds: { minX: n.x, minY: n.y, maxX: n.x + n.width, maxY: n.y + n.height }
737
- })));
738
- }
739
- return getViewportVisibleNodes({
740
- nodes,
741
- viewport: { centerX, centerY, zoomLevel, rotation: 0, visibleBounds: { minX: 0, minY: 0, maxX: 0, maxY: 0 } },
742
- canvasWidth,
743
- canvasHeight,
744
- levelFilter,
745
- skipCulling,
746
- isDragging,
747
- spatialIndex: indexRef.current,
748
- spatialIndexThreshold: SPATIAL_INDEX_THRESHOLD
749
- });
750
- }, [nodes, levelFilter, skipCulling, isDragging, centerX, centerY, zoomLevel, canvasWidth, canvasHeight]);
856
+ return makeDesign(theme, resolveComfort(props.comfort, { prefersReducedMotion: env.prefersReducedMotion }));
751
857
  }
752
858
 
753
- // src/components/KonvaNode.tsx
754
- import React2, { useMemo as useMemo2 } from "react";
755
- import { CANVAS_COLORS, NODE_HEIGHT, TEXT_LABEL_HIDE_BELOW_ZOOM, darkenHexColor } from "@visualli/core";
756
-
757
- // src/config/index.ts
859
+ // src/design/drawing.ts
758
860
  import {
759
- ALL_BLOB_SHAPES as ALL_BLOB_SHAPES2,
760
- ACTIVE_BLOB_TYPES,
761
- BLOB_LAYER_CONFIG,
762
- NODE_LAYER_CONFIG,
763
- getBlobTypeForLayer,
764
- BLOB_TEXT_OFFSETS,
765
- drawBlobPath,
766
- NODE_TEXT_BASE_FONT_PX,
767
- DESCRIPTION_TEXT_BASE_FONT_PX,
768
- CONTAINER_LABEL_BASE_FONT_PX,
769
- EDGE_LABEL_BASE_FONT_PX,
770
- OVERLAY_FONT_UPPER_BOOST_PX,
771
- computeNodeTextWorldScale,
772
- computeNodeTextScreenScale,
773
- computeOverlayScale,
774
- computeEdgeLabelScale
861
+ CANVAS_STYLE as CANVAS_STYLE2,
862
+ IDEA,
863
+ ideaDetail,
864
+ TYPE_STYLES as TYPE_STYLES3,
865
+ blobPath,
866
+ ideaStyle,
867
+ labelGrowth,
868
+ nodeRadii,
869
+ ringsFor,
870
+ shapeOfLevel
775
871
  } from "@visualli/core";
776
-
777
- // src/config/blobShapes.ts
778
- var ALL_BLOB_SHAPES = [
779
- // Shape 0 · Blob 1 (root / L0)
780
- [
781
- { x: 1.12, y: 0 },
782
- { x: 0.86, y: 0.56 },
783
- { x: 0.46, y: 0.94 },
784
- { x: -0.08, y: 1.06 },
785
- { x: -0.68, y: 0.82 },
786
- { x: -0.96, y: 0.08 },
787
- { x: -0.55, y: -0.82 },
788
- { x: 0.04, y: -1.02 },
789
- { x: 0.72, y: -0.8 },
790
- { x: 1.08, y: -0.38 }
791
- ],
792
- // Shape 1 · Blob 2
793
- [
794
- { x: -0.489, y: -0.625 },
795
- { x: -0.384, y: -0.716 },
796
- { x: -0.285, y: -0.792 },
797
- { x: -0.189, y: -0.855 },
798
- { x: -0.09, y: -0.903 },
799
- { x: 0.014, y: -0.932 },
800
- { x: 0.129, y: -0.94 },
801
- { x: 0.255, y: -0.924 },
802
- { x: 0.412, y: -0.876 },
803
- { x: 0.516, y: -0.832 },
804
- { x: 0.612, y: -0.78 },
805
- { x: 0.695, y: -0.725 },
806
- { x: 0.767, y: -0.666 },
807
- { x: 0.83, y: -0.606 },
808
- { x: 0.885, y: -0.546 },
809
- { x: 0.933, y: -0.486 },
810
- { x: 0.979, y: -0.425 },
811
- { x: 1.006, y: -0.385 },
812
- { x: 1.034, y: -0.334 },
813
- { x: 1.058, y: -0.276 },
814
- { x: 1.078, y: -0.209 },
815
- { x: 1.092, y: -0.133 },
816
- { x: 1.101, y: -0.046 },
817
- { x: 1.102, y: 0.052 },
818
- { x: 1.094, y: 0.173 },
819
- { x: 1.083, y: 0.244 },
820
- { x: 1.063, y: 0.324 },
821
- { x: 1.034, y: 0.401 },
822
- { x: 0.996, y: 0.477 },
823
- { x: 0.948, y: 0.549 },
824
- { x: 0.889, y: 0.617 },
825
- { x: 0.82, y: 0.681 },
826
- { x: 0.735, y: 0.741 },
827
- { x: 0.658, y: 0.783 },
828
- { x: 0.569, y: 0.822 },
829
- { x: 0.474, y: 0.855 },
830
- { x: 0.372, y: 0.882 },
831
- { x: 0.265, y: 0.904 },
832
- { x: 0.156, y: 0.92 },
833
- { x: 0.046, y: 0.931 },
834
- { x: -0.069, y: 0.938 },
835
- { x: -0.155, y: 0.935 },
836
- { x: -0.25, y: 0.919 },
837
- { x: -0.346, y: 0.89 },
838
- { x: -0.443, y: 0.851 },
839
- { x: -0.539, y: 0.8 },
840
- { x: -0.633, y: 0.738 },
841
- { x: -0.723, y: 0.667 },
842
- { x: -0.808, y: 0.585 },
843
- { x: -0.882, y: 0.501 },
844
- { x: -0.944, y: 0.421 },
845
- { x: -0.994, y: 0.343 },
846
- { x: -1.034, y: 0.267 },
847
- { x: -1.064, y: 0.192 },
848
- { x: -1.085, y: 0.12 },
849
- { x: -1.097, y: 0.042 },
850
- { x: -1.102, y: 1e-3 },
851
- { x: -1.09, y: -0.243 },
852
- { x: -1.05, y: -0.365 },
853
- { x: -0.99, y: -0.441 },
854
- { x: -0.911, y: -0.479 },
855
- { x: -0.819, y: -0.499 },
856
- { x: -0.715, y: -0.517 },
857
- { x: -0.605, y: -0.551 }
858
- ],
859
- // Shape 2 · Blob 3
860
- [
861
- { x: 3e-3, y: -0.97 },
862
- { x: 0.163, y: -0.984 },
863
- { x: 0.286, y: -1.028 },
864
- { x: 0.389, y: -1.096 },
865
- { x: 0.488, y: -1.174 },
866
- { x: 0.597, y: -1.21 },
867
- { x: 0.728, y: -1.165 },
868
- { x: 0.867, y: -1.036 },
869
- { x: 0.996, y: -0.839 },
870
- { x: 1.098, y: -0.59 },
871
- { x: 1.158, y: -0.304 },
872
- { x: 1.153, y: 0.012 },
873
- { x: 1.08, y: 0.336 },
874
- { x: 0.944, y: 0.639 },
875
- { x: 0.756, y: 0.889 },
876
- { x: 0.484, y: 1.072 },
877
- { x: 0.339, y: 1.079 },
878
- { x: 0.219, y: 1.051 },
879
- { x: 0.131, y: 0.994 },
880
- { x: 0.059, y: 0.939 },
881
- { x: 8e-3, y: 0.915 },
882
- { x: -0.105, y: 0.931 },
883
- { x: -0.176, y: 0.965 },
884
- { x: -0.249, y: 1.005 },
885
- { x: -0.345, y: 1.039 },
886
- { x: -0.497, y: 1.055 },
887
- { x: -0.672, y: 1.032 },
888
- { x: -0.817, y: 0.952 },
889
- { x: -0.945, y: 0.81 },
890
- { x: -1.054, y: 0.6 },
891
- { x: -1.16, y: 0.302 },
892
- { x: -1.187, y: -0.011 },
893
- { x: -1.165, y: -0.326 },
894
- { x: -1.097, y: -0.599 },
895
- { x: -0.991, y: -0.838 },
896
- { x: -0.852, y: -0.912 },
897
- { x: -0.695, y: -0.958 },
898
- { x: -0.53, y: -0.988 },
899
- { x: -0.357, y: -0.992 },
900
- { x: -0.18, y: -0.983 }
901
- ],
902
- // Shape 3 · Blob 4
903
- [
904
- { x: -0.734, y: -1.127 },
905
- { x: -0.656, y: -1.146 },
906
- { x: -0.567, y: -1.142 },
907
- { x: -0.468, y: -1.115 },
908
- { x: -0.358, y: -1.079 },
909
- { x: -0.238, y: -1.034 },
910
- { x: -0.107, y: -0.989 },
911
- { x: 0.035, y: -0.953 },
912
- { x: 0.188, y: -0.929 },
913
- { x: 0.325, y: -0.916 },
914
- { x: 0.484, y: -0.896 },
915
- { x: 0.65, y: -0.854 },
916
- { x: 0.811, y: -0.77 },
917
- { x: 0.955, y: -0.631 },
918
- { x: 1.068, y: -0.422 },
919
- { x: 1.138, y: -0.126 },
920
- { x: 1.152, y: 0.272 },
921
- { x: 1.121, y: 0.451 },
922
- { x: 1.046, y: 0.602 },
923
- { x: 0.928, y: 0.727 },
924
- { x: 0.767, y: 0.833 },
925
- { x: 0.565, y: 0.925 },
926
- { x: 0.323, y: 1.001 },
927
- { x: 0.042, y: 1.075 },
928
- { x: -0.277, y: 1.142 },
929
- { x: -0.368, y: 1.147 },
930
- { x: -0.475, y: 1.136 },
931
- { x: -0.589, y: 1.104 },
932
- { x: -0.704, y: 1.05 },
933
- { x: -0.815, y: 0.973 },
934
- { x: -0.913, y: 0.871 },
935
- { x: -0.993, y: 0.746 },
936
- { x: -1.048, y: 0.593 },
937
- { x: -1.109, y: 0.298 },
938
- { x: -1.145, y: 0.018 },
939
- { x: -1.154, y: -0.243 },
940
- { x: -1.135, y: -0.482 },
941
- { x: -1.085, y: -0.695 },
942
- { x: -1.003, y: -0.874 },
943
- { x: -0.886, y: -1.02 }
944
- ],
945
- // Shape 4 · Blob 5 (inverted Blob 4)
946
- [
947
- { x: -0.734, y: 1.127 },
948
- { x: -0.656, y: 1.146 },
949
- { x: -0.567, y: 1.142 },
950
- { x: -0.468, y: 1.115 },
951
- { x: -0.358, y: 1.079 },
952
- { x: -0.238, y: 1.034 },
953
- { x: -0.107, y: 0.989 },
954
- { x: 0.035, y: 0.953 },
955
- { x: 0.188, y: 0.929 },
956
- { x: 0.325, y: 0.916 },
957
- { x: 0.484, y: 0.896 },
958
- { x: 0.65, y: 0.854 },
959
- { x: 0.811, y: 0.77 },
960
- { x: 0.955, y: 0.631 },
961
- { x: 1.068, y: 0.422 },
962
- { x: 1.138, y: 0.126 },
963
- { x: 1.152, y: -0.272 },
964
- { x: 1.121, y: -0.451 },
965
- { x: 1.046, y: -0.602 },
966
- { x: 0.928, y: -0.727 },
967
- { x: 0.767, y: -0.833 },
968
- { x: 0.565, y: -0.925 },
969
- { x: 0.323, y: -1.001 },
970
- { x: 0.042, y: -1.075 },
971
- { x: -0.277, y: -1.142 },
972
- { x: -0.368, y: -1.147 },
973
- { x: -0.475, y: -1.136 },
974
- { x: -0.589, y: -1.104 },
975
- { x: -0.704, y: -1.05 },
976
- { x: -0.815, y: -0.973 },
977
- { x: -0.913, y: -0.871 },
978
- { x: -0.993, y: -0.746 },
979
- { x: -1.048, y: -0.593 },
980
- { x: -1.109, y: -0.298 },
981
- { x: -1.145, y: -0.018 },
982
- { x: -1.154, y: 0.243 },
983
- { x: -1.135, y: 0.482 },
984
- { x: -1.085, y: 0.695 },
985
- { x: -1.003, y: 0.874 },
986
- { x: -0.886, y: 1.02 }
987
- ],
988
- // Shape 5 · Blob 6
989
- [
990
- { x: 1e-3, y: -1.006 },
991
- { x: 0.134, y: -1.001 },
992
- { x: 0.262, y: -1.024 },
993
- { x: 0.382, y: -1.073 },
994
- { x: 0.498, y: -1.107 },
995
- { x: 0.614, y: -1.105 },
996
- { x: 0.733, y: -1.049 },
997
- { x: 0.85, y: -0.932 },
998
- { x: 0.953, y: -0.755 },
999
- { x: 1.03, y: -0.529 },
1000
- { x: 1.067, y: -0.265 },
1001
- { x: 1.054, y: 0.018 },
1002
- { x: 0.986, y: 0.3 },
1003
- { x: 0.868, y: 0.557 },
1004
- { x: 0.715, y: 0.766 },
1005
- { x: 0.55, y: 0.909 },
1006
- { x: 0.396, y: 0.984 },
1007
- { x: 0.262, y: 1.005 },
1008
- { x: 0.148, y: 1.003 },
1009
- { x: 0.037, y: 1.008 },
1010
- { x: -0.083, y: 1.04 },
1011
- { x: -0.22, y: 1.118 },
1012
- { x: -0.36, y: 1.192 },
1013
- { x: -0.5, y: 1.234 },
1014
- { x: -0.65, y: 1.175 },
1015
- { x: -0.81, y: 1.045 },
1016
- { x: -0.99, y: 0.855 },
1017
- { x: -1.14, y: 0.61 },
1018
- { x: -1.24, y: 0.33 },
1019
- { x: -1.27, y: 0.04 },
1020
- { x: -1.275, y: -0.25 },
1021
- { x: -1.225, y: -0.5 },
1022
- { x: -1.08, y: -0.72 },
1023
- { x: -0.89, y: -0.92 },
1024
- { x: -0.734, y: -1.053 },
1025
- { x: -0.614, y: -1.112 },
1026
- { x: -0.498, y: -1.127 },
1027
- { x: -0.381, y: -1.11 },
1028
- { x: -0.261, y: -1.095 },
1029
- { x: -0.133, y: -1.058 },
1030
- { x: 1e-3, y: -1.01 },
1031
- { x: 0.134, y: -1.001 }
1032
- ]
1033
- ];
1034
- function buildBlobPathData(_shape, cx, cy, width, height) {
1035
- const rx = width / 2;
1036
- const ry = height / 2;
1037
- const blobType = 0;
1038
- const srcPts = ALL_BLOB_SHAPES[blobType];
1039
- const pts = srcPts.map((p) => ({ x: cx + p.x * rx, y: cy + p.y * ry }));
1040
- const mid = (a, b) => ({ x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 });
1041
- const last = pts[pts.length - 1];
1042
- const first = pts[0];
1043
- const s = mid(last, first);
1044
- let d = `M ${s.x} ${s.y}`;
1045
- for (let i = 0; i < pts.length; i++) {
1046
- const curr = pts[i];
1047
- const next = pts[(i + 1) % pts.length];
1048
- const m = mid(curr, next);
1049
- d += ` Q ${curr.x} ${curr.y} ${m.x} ${m.y}`;
872
+ var DEG = Math.PI / 180;
873
+ var MAX_CACHED_PATHS = 4096;
874
+ var pathCache = /* @__PURE__ */ new Map();
875
+ function blobPath2D(shape, rx, ry) {
876
+ const key2 = `${shape}|${rx}|${ry}`;
877
+ let p = pathCache.get(key2);
878
+ if (!p) {
879
+ if (pathCache.size >= MAX_CACHED_PATHS) pathCache.delete(pathCache.keys().next().value);
880
+ p = new Path2D(blobPath(shape, rx, ry));
881
+ pathCache.set(key2, p);
882
+ }
883
+ return p;
884
+ }
885
+ var NO_DASH = [];
886
+ var NO_RINGS = [];
887
+ var dashCache = /* @__PURE__ */ new Map();
888
+ function dashOf(spec) {
889
+ if (!spec) return NO_DASH;
890
+ let d = dashCache.get(spec);
891
+ if (!d) {
892
+ d = spec.split(/\s+/).map(Number);
893
+ dashCache.set(spec, d);
1050
894
  }
1051
- d += " Z";
1052
895
  return d;
1053
896
  }
1054
-
1055
- // src/components/KonvaNode.tsx
1056
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
1057
- var KonvaNode = React2.memo(function KonvaNode2({
1058
- node,
1059
- zoomLevel,
1060
- isExternallyHovered = false,
1061
- isExternallyPressed = false,
1062
- isDragging = false
1063
- }) {
1064
- const borderColor = useMemo2(() => darkenHexColor(node.color, 0.13), [node.color]);
1065
- const layerCount = useMemo2(() => {
1066
- const count = node.branchCount ?? 0;
1067
- return Math.min(count, 3);
1068
- }, [node.branchCount]);
1069
- const baseRadiusX = node.width / 2;
1070
- const baseRadiusY = Math.max(NODE_HEIGHT * 1.6 / 2, baseRadiusX * 0.74);
1071
- const calculateFontSize = useMemo2(() => {
1072
- return 30;
1073
- }, [node.title]);
1074
- const textNaturalWidth = node.width - 40;
1075
- const textNaturalHeight = NODE_HEIGHT * 1.2;
1076
- const maxScale = computeNodeTextWorldScale(node.width, zoomLevel);
1077
- const blobType = getBlobTypeForLayer(node.level);
1078
- const textOffsetX = (BLOB_TEXT_OFFSETS[blobType] ?? 0) * baseRadiusX;
1079
- const effectiveHover = isExternallyHovered;
1080
- const isPressed = isExternallyPressed;
1081
- const topFill = effectiveHover || isPressed ? borderColor : node.color;
1082
- const topStroke = isPressed ? node.color : borderColor;
1083
- const backgroundLayers = useMemo2(() => {
1084
- if (layerCount === 0) return [];
1085
- const startIdx = BLOB_LAYER_CONFIG.length - layerCount;
1086
- const layers = [];
1087
- for (let i = startIdx; i < BLOB_LAYER_CONFIG.length; i++) {
1088
- const config = BLOB_LAYER_CONFIG[i];
1089
- layers.push({
1090
- ...config,
1091
- radiusX: baseRadiusX * config.scaleUp,
1092
- radiusY: baseRadiusY * config.scaleUp
1093
- });
1094
- }
1095
- return layers;
1096
- }, [layerCount, baseRadiusX, baseRadiusY]);
1097
- return /* @__PURE__ */ jsxs(KGroup, { x: node.x, y: node.y, listening: false, children: [
1098
- backgroundLayers.map((layer, idx) => /* @__PURE__ */ jsx2(
1099
- KShape,
1100
- {
1101
- rotation: layer.rotation,
1102
- opacity: layer.opacity,
1103
- sceneFunc: (ctx, shape) => {
1104
- drawBlobPath(ctx, layer.radiusX, layer.radiusY, blobType);
1105
- ctx.fillStrokeShape(shape);
1106
- },
1107
- fill: node.color,
1108
- stroke: borderColor,
1109
- strokeWidth: layer.strokeWidth,
1110
- dash: layer.dash ? [...layer.dash] : void 0,
1111
- listening: false,
1112
- perfectDrawEnabled: false
1113
- },
1114
- `blob-layer-${idx}`
1115
- )),
1116
- /* @__PURE__ */ jsx2(
1117
- KShape,
1118
- {
1119
- rotation: NODE_LAYER_CONFIG.rotation,
1120
- opacity: NODE_LAYER_CONFIG.opacity,
1121
- sceneFunc: (ctx, shape) => {
1122
- drawBlobPath(ctx, baseRadiusX, baseRadiusY, blobType);
1123
- ctx.fillStrokeShape(shape);
1124
- },
1125
- hitFunc: (ctx, shape) => {
1126
- drawBlobPath(ctx, baseRadiusX, baseRadiusY, blobType);
1127
- ctx.fillStrokeShape(shape);
1128
- },
1129
- fill: topFill,
1130
- stroke: topStroke,
1131
- strokeWidth: NODE_LAYER_CONFIG.strokeWidth,
1132
- shadowEnabled: !isDragging,
1133
- shadowColor: "rgba(255,255,255,1)",
1134
- shadowBlur: effectiveHover || isPressed ? 40 : 15,
1135
- shadowOpacity: effectiveHover || isPressed ? 0.4 : 0.15,
1136
- shadowOffsetX: 0,
1137
- shadowOffsetY: 0,
1138
- perfectDrawEnabled: false
897
+ var lineCache = /* @__PURE__ */ new Map();
898
+ var MAX_CACHED_LAYOUTS = 8192;
899
+ function layoutLabel(c, text, font, maxWidth, maxLines = Infinity) {
900
+ const key2 = `${fontsEpoch()}|${font}|${maxWidth}|${text}`;
901
+ const hit = lineCache.get(key2);
902
+ if (hit) return hit;
903
+ c.font = font;
904
+ const words = text.split(/\s+/).filter(Boolean);
905
+ const lines = [];
906
+ let cur = "";
907
+ const fit = (s) => c.measureText(s).width <= maxWidth;
908
+ for (const w of words) {
909
+ const next = cur ? `${cur} ${w}` : w;
910
+ if (fit(next)) {
911
+ cur = next;
912
+ continue;
913
+ }
914
+ if (cur) lines.push(cur);
915
+ let rest = w;
916
+ while (!fit(rest) && rest.length > 1) {
917
+ let n = rest.length - 1;
918
+ while (n > 1 && !fit(rest.slice(0, n))) n--;
919
+ lines.push(rest.slice(0, n));
920
+ rest = rest.slice(n);
921
+ }
922
+ cur = rest;
923
+ }
924
+ if (cur) lines.push(cur);
925
+ if (lines.length > maxLines) {
926
+ lines.length = maxLines;
927
+ let last = lines[maxLines - 1] + "\u2026";
928
+ while (last.length > 1 && !fit(last)) last = last.slice(0, -2) + "\u2026";
929
+ lines[maxLines - 1] = last;
930
+ }
931
+ if (lineCache.size >= MAX_CACHED_LAYOUTS) lineCache.clear();
932
+ lineCache.set(key2, lines);
933
+ return lines;
934
+ }
935
+ var IDLE = { hovered: false, pressed: false, selected: false, focused: false, dimmed: false };
936
+ var ideaColors = (d, n) => ideaStyle(d.theme, n);
937
+ var ideaLabelPx = (n, d) => IDEA.labelSize[n.kind ?? "node"] * d.labelScale;
938
+ function drawIdea(c, node, d, zoom, st, opt) {
939
+ const { rx, ry } = nodeRadii(node);
940
+ const { fill, ring } = ideaColors(d, node);
941
+ const screenW = node.width * zoom;
942
+ const base = opt.base;
943
+ const detail = ideaDetail(screenW, st.selected || st.focused || st.hovered);
944
+ if (detail === "speck") {
945
+ const a = (st.dimmed ? opt.dimOpacity ?? CANVAS_STYLE2.node.dimmedOpacityFocus : 1) * (st.arrival ? st.arrival.alpha : 1);
946
+ if (a !== 1) c.globalAlpha = a;
947
+ c.fillStyle = fill;
948
+ c.fillRect(node.x - rx, node.y - ry, rx * 2, ry * 2);
949
+ if (a !== 1) c.globalAlpha = 1;
950
+ return;
951
+ }
952
+ const detailed = detail === "full";
953
+ const shape = shapeOfLevel(node.level);
954
+ const rings = detailed ? ringsFor(node.branchCount) : NO_RINGS;
955
+ const raised = st.hovered || st.pressed || st.selected;
956
+ const lift = st.lift ?? (raised ? 1 : 0);
957
+ const ringT = st.ring ?? (raised ? 1 : 0);
958
+ const arr = st.arrival;
959
+ const y = node.y + lift * CANVAS_STYLE2.node.hoverLift + (arr ? arr.dy : 0);
960
+ const x = node.x + (arr ? arr.dx : 0);
961
+ let k = opt.k;
962
+ if (k === void 0) {
963
+ const m = c.getTransform();
964
+ k = Math.hypot(m.a, m.b);
965
+ }
966
+ 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);
967
+ else {
968
+ c.save();
969
+ c.translate(x, y);
970
+ }
971
+ if (arr) c.scale(arr.scale, arr.scale);
972
+ c.lineJoin = "round";
973
+ const baseAlpha = (st.dimmed ? opt.dimOpacity ?? CANVAS_STYLE2.node.dimmedOpacityFocus : 1) * (arr ? arr.alpha : 1);
974
+ c.globalAlpha = baseAlpha;
975
+ if (rings.length) {
976
+ const ringSpin = ringT * CANVAS_STYLE2.node.hoverRingsRotate;
977
+ const ringGrow = 1 + ringT * (CANVAS_STYLE2.node.hoverRingsScale - 1);
978
+ c.fillStyle = fill;
979
+ c.strokeStyle = ring;
980
+ let dashed = false;
981
+ for (let i = rings.length - 1; i >= 0; i--) {
982
+ const r = rings[i];
983
+ const angle = (r.rotate + ringSpin) * DEG;
984
+ c.rotate(angle);
985
+ c.globalAlpha = baseAlpha * r.opacity;
986
+ const path = blobPath2D(shape, rx * r.scale * ringGrow, ry * r.scale * ringGrow);
987
+ c.fill(path);
988
+ c.lineWidth = r.width;
989
+ if (r.dash) {
990
+ c.setLineDash(dashOf(r.dash));
991
+ dashed = true;
992
+ } else if (dashed) {
993
+ c.setLineDash(NO_DASH);
994
+ dashed = false;
1139
995
  }
1140
- ),
1141
- /* @__PURE__ */ jsx2(
1142
- KGroup,
1143
- {
1144
- clipFunc: (ctx) => {
1145
- drawBlobPath(ctx, baseRadiusX, baseRadiusY, blobType);
1146
- },
1147
- visible: zoomLevel >= TEXT_LABEL_HIDE_BELOW_ZOOM,
1148
- listening: false,
1149
- children: /* @__PURE__ */ jsx2(
1150
- KText,
1151
- {
1152
- x: textOffsetX,
1153
- y: 0,
1154
- offsetX: textNaturalWidth / 2,
1155
- offsetY: textNaturalHeight / 2,
1156
- width: textNaturalWidth,
1157
- height: textNaturalHeight,
1158
- scaleX: maxScale,
1159
- scaleY: maxScale,
1160
- text: node.title,
1161
- fontSize: calculateFontSize,
1162
- fontFamily: "'Story Script', cursive",
1163
- fontStyle: "normal",
1164
- fill: CANVAS_COLORS.node.titleColor,
1165
- align: "center",
1166
- verticalAlign: "middle",
1167
- wrap: "word",
1168
- ellipsis: false,
1169
- listening: false,
1170
- perfectDrawEnabled: false
1171
- }
1172
- )
996
+ c.stroke(path);
997
+ c.rotate(-angle);
998
+ }
999
+ if (dashed) c.setLineDash(NO_DASH);
1000
+ c.globalAlpha = baseAlpha;
1001
+ }
1002
+ const body = blobPath2D(shape, rx, ry);
1003
+ c.fillStyle = fill;
1004
+ const shadow = detailed && opt.shadows ? d.nodeShadow : null;
1005
+ if (shadow) {
1006
+ c.shadowColor = shadow.color;
1007
+ c.shadowBlur = shadow.blur * k;
1008
+ c.shadowOffsetX = shadow.x * k;
1009
+ c.shadowOffsetY = shadow.y * k;
1010
+ }
1011
+ c.fill(body);
1012
+ if (shadow) {
1013
+ c.shadowColor = "transparent";
1014
+ c.shadowBlur = 0;
1015
+ c.shadowOffsetX = 0;
1016
+ c.shadowOffsetY = 0;
1017
+ }
1018
+ c.strokeStyle = st.focused ? d.tokens["focus"] : st.selected ? d.tokens["ink"] : ring;
1019
+ c.lineWidth = st.selected || st.focused ? CANVAS_STYLE2.node.selectedStrokeWidth : d.metrics.nodeStroke;
1020
+ c.stroke(body);
1021
+ if (detailed && node.title) {
1022
+ const px = ideaLabelPx(node, d);
1023
+ const font = `${TYPE_STYLES3["node-label"].weight} ${px}px ${d.fontHand}`;
1024
+ const lines = layoutLabel(c, node.title, font, node.width - IDEA.labelInset);
1025
+ const s = labelGrowth(zoom).idea;
1026
+ const lh = px * IDEA.labelLineHeight;
1027
+ c.scale(s, s);
1028
+ c.font = font;
1029
+ c.fillStyle = d.tokens["node-ink"];
1030
+ c.textAlign = "center";
1031
+ c.textBaseline = "middle";
1032
+ if (d.comfort.readableType && "letterSpacing" in c) c.letterSpacing = `${(px * 0.02).toFixed(2)}px`;
1033
+ const y0 = -((lines.length - 1) * lh) / 2;
1034
+ for (let i = 0; i < lines.length; i++) c.fillText(lines[i], 0, y0 + i * lh);
1035
+ }
1036
+ if (base) c.globalAlpha = 1;
1037
+ else c.restore();
1038
+ }
1039
+ function cubicLength(d) {
1040
+ const v = d.match(/-?\d+(?:\.\d+)?/g)?.map(Number);
1041
+ if (!v || v.length < 8) return 0;
1042
+ const [x0, y0, x1, y1, x2, y2, x3, y3] = v;
1043
+ let len = 0, px = x0, py = y0;
1044
+ for (let i = 1; i <= 16; i++) {
1045
+ const t = i / 16, u = 1 - t;
1046
+ const x = u * u * u * x0 + 3 * u * u * t * x1 + 3 * u * t * t * x2 + t * t * t * x3;
1047
+ const y = u * u * u * y0 + 3 * u * u * t * y1 + 3 * u * t * t * y2 + t * t * t * y3;
1048
+ len += Math.hypot(x - px, y - py);
1049
+ px = x;
1050
+ py = y;
1051
+ }
1052
+ return len;
1053
+ }
1054
+ function prepareConnector(g) {
1055
+ return { ...g, path: new Path2D(g.d), arrowPath: new Path2D(g.arrow), len: cubicLength(g.d) };
1056
+ }
1057
+ var CONNECTOR_MIN_SCREEN_PX = 3;
1058
+ var LABEL_MIN_SCREEN_PX = 6;
1059
+ function drawConnector(c, g, d, zoom, dashed, label, arrival) {
1060
+ if (Math.hypot(g.b.x - g.a.x, g.b.y - g.a.y) * zoom < CONNECTOR_MIN_SCREEN_PX) return;
1061
+ c.save();
1062
+ if (arrival) c.globalAlpha = arrival.alpha;
1063
+ c.strokeStyle = d.tokens["edge"];
1064
+ c.lineWidth = d.metrics.edgeWidth;
1065
+ c.lineCap = "round";
1066
+ c.lineJoin = "round";
1067
+ if (dashed) c.setLineDash([...CANVAS_STYLE2.edge.dash]);
1068
+ else if (arrival && arrival.draw < 1) c.setLineDash([g.len * arrival.draw, g.len * 2]);
1069
+ if (!(arrival && arrival.draw <= 1e-3)) c.stroke(g.path);
1070
+ c.setLineDash([]);
1071
+ c.stroke(g.arrowPath);
1072
+ const labelPx = label ? d.edgeLabel.size * labelGrowth(zoom).connector : 0;
1073
+ if (label && labelPx * zoom >= LABEL_MIN_SCREEN_PX) {
1074
+ const px = labelPx;
1075
+ c.font = `${d.edgeLabel.weight} ${px}px ${d.fontNote}`;
1076
+ c.textAlign = "center";
1077
+ c.textBaseline = "middle";
1078
+ c.lineWidth = CANVAS_STYLE2.edge.labelHaloWidth;
1079
+ c.strokeStyle = d.tokens["canvas"];
1080
+ c.strokeText(label, g.mid.x, g.mid.y);
1081
+ c.fillStyle = d.tokens["edge-label"];
1082
+ c.fillText(label, g.mid.x, g.mid.y);
1083
+ }
1084
+ c.restore();
1085
+ }
1086
+
1087
+ // src/components/useFrames.ts
1088
+ import { useEffect as useEffect4, useMemo, useRef as useRef4 } from "react";
1089
+ function useFrames(layerRef, busy) {
1090
+ const raf = useRef4(null);
1091
+ const busyRef = useRef4(busy);
1092
+ busyRef.current = busy;
1093
+ useEffect4(() => () => {
1094
+ if (raf.current !== null) cancelAnimationFrame(raf.current);
1095
+ raf.current = null;
1096
+ }, []);
1097
+ return useMemo(() => {
1098
+ const tick = () => {
1099
+ raf.current = null;
1100
+ if (!busyRef.current()) return;
1101
+ layerRef.current?.batchDraw();
1102
+ raf.current = requestAnimationFrame(tick);
1103
+ };
1104
+ return {
1105
+ run: () => {
1106
+ layerRef.current?.batchDraw();
1107
+ if (raf.current === null) raf.current = requestAnimationFrame(tick);
1173
1108
  }
1174
- )
1175
- ] });
1176
- });
1177
- var KonvaNode_default = KonvaNode;
1109
+ };
1110
+ }, [layerRef]);
1111
+ }
1178
1112
 
1179
1113
  // src/components/KonvaNodeLayer.tsx
1180
- import { jsx as jsx3 } from "react/jsx-runtime";
1114
+ import { jsx as jsx2 } from "react/jsx-runtime";
1115
+ var SPATIAL_INDEX_THRESHOLD = 200;
1116
+ function stageViewRect(stage) {
1117
+ const s = stage.scaleX() || 1;
1118
+ return { minX: -stage.x() / s, minY: -stage.y() / s, maxX: (stage.width() - stage.x()) / s, maxY: (stage.height() - stage.y()) / s };
1119
+ }
1181
1120
  function KonvaNodeLayer({
1182
1121
  nodes: propNodes,
1183
- isTransitioning = false,
1184
1122
  isDragging = false,
1185
1123
  hoveredNodeId = null,
1186
- pressedNodeId = null
1124
+ pressedNodeId = null,
1125
+ selectedNodeId = null,
1126
+ focusedNodeId = null,
1127
+ design = DEFAULT_DESIGN,
1128
+ clock,
1129
+ touch = false,
1130
+ peekedNodeId = null
1187
1131
  }) {
1188
1132
  const layerRef = useRef5(null);
1189
- const zoomLevel = useViewportStore((s) => s.zoomLevel);
1133
+ const fonts = useFontsEpoch();
1190
1134
  const storeNodes = useNodeStore((s) => s.nodes);
1191
- const allNodes = useMemo3(
1192
- () => propNodes ?? Array.from(storeNodes.values()),
1193
- [propNodes, storeNodes]
1194
- );
1195
- const visibleNodes = useViewportNodes(allNodes, void 0, isTransitioning, isDragging);
1135
+ const nodes = useMemo2(() => propNodes ?? Array.from(storeNodes.values()), [propNodes, storeNodes]);
1136
+ const index = useMemo2(() => {
1137
+ if (nodes.length < SPATIAL_INDEX_THRESHOLD || isDragging) return null;
1138
+ const idx = new RBushSpatialIndex();
1139
+ idx.bulkLoad(nodes.map((n) => ({ nodeId: n.id, bounds: nodeBounds(n) })));
1140
+ return idx;
1141
+ }, [nodes, isDragging]);
1142
+ const byId = useMemo2(() => index ? new Map(nodes.map((n) => [n.id, n])) : null, [index, nodes]);
1143
+ const highlighted = hoveredNodeId ?? peekedNodeId ?? selectedNodeId;
1144
+ const N = CANVAS_STYLE3.node;
1145
+ const focusTheme = design.theme.startsWith("focus");
1146
+ const dimOpacity = focusTheme ? touch ? N.dimmedOpacityFocusTouch : N.dimmedOpacityFocus : touch ? N.dimmedOpacityTouch : N.dimmedOpacity;
1147
+ const dimOthers = highlighted !== null && dimOpacity !== 1;
1148
+ const tweens = useRef5(new HoverTweens()).current;
1149
+ const frames = useFrames(layerRef, () => clock?.active() || tweens.active());
1150
+ useLayoutEffect(() => {
1151
+ const raised = /* @__PURE__ */ new Set();
1152
+ if (hoveredNodeId) raised.add(hoveredNodeId);
1153
+ if (pressedNodeId) raised.add(pressedNodeId);
1154
+ if (selectedNodeId) raised.add(selectedNodeId);
1155
+ if (tweens.set(raised, design.comfort.reducedMotion || !layerBlooms(nodes.length))) frames.run();
1156
+ }, [hoveredNodeId, pressedNodeId, selectedNodeId, design.comfort.reducedMotion, layerBlooms(nodes.length), tweens, frames]);
1157
+ useLayoutEffect(() => clock?.subscribe(() => frames.run()), [clock, frames]);
1196
1158
  useLayoutEffect(() => {
1197
1159
  layerRef.current?.batchDraw();
1198
- }, [visibleNodes, hoveredNodeId, pressedNodeId, isDragging, zoomLevel]);
1199
- useEffect4(() => {
1200
- layerRef.current?.batchDraw();
1201
- }, [isTransitioning]);
1202
- return /* @__PURE__ */ jsx3(KLayer, { ref: layerRef, name: "nodes", children: visibleNodes.map((n) => /* @__PURE__ */ jsx3(
1203
- KonvaNode_default,
1204
- {
1205
- node: n,
1206
- zoomLevel,
1207
- isExternallyHovered: hoveredNodeId === n.id,
1208
- isExternallyPressed: pressedNodeId === n.id,
1209
- isDragging
1210
- },
1211
- n.id
1212
- )) });
1213
- }
1214
-
1215
- // src/components/KonvaEdgeLayer.tsx
1216
- import { useRef as useRef6, useLayoutEffect as useLayoutEffect2, useMemo as useMemo5 } from "react";
1217
-
1218
- // src/components/KonvaEdge.tsx
1219
- import { useMemo as useMemo4, memo } from "react";
1220
- import { NODE_HEIGHT as NODE_HEIGHT2 } from "@visualli/core";
1221
- import { Fragment, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
1222
- var getOutermostScaleUp = (branchCount) => {
1223
- const layerCount = Math.min(branchCount ?? 0, 3);
1224
- if (layerCount === 0) return 1;
1225
- const scaleUps = [1.3, 1.2, 1.1];
1226
- return scaleUps[scaleUps.length - layerCount];
1227
- };
1228
- function getEllipseIntersection(cx, cy, rx, ry, angle) {
1229
- return {
1230
- x: cx + rx * Math.cos(angle),
1231
- y: cy + ry * Math.sin(angle)
1160
+ }, [nodes, index, design, fonts, hoveredNodeId, pressedNodeId, selectedNodeId, focusedNodeId, isDragging, touch, peekedNodeId]);
1161
+ const sceneFunc = (ctx2, shape) => {
1162
+ const stage = shape.getStage();
1163
+ if (!stage) return;
1164
+ const zoom = stage.scaleX() || 1;
1165
+ const view = stageViewRect(stage);
1166
+ let visible;
1167
+ if (isDragging) visible = nodes;
1168
+ else if (index && byId) visible = index.query(view).map((id) => byId.get(id)).filter(Boolean);
1169
+ else visible = nodes.filter((n) => intersectsViewport(n, view));
1170
+ const c = ctx2._context;
1171
+ const m = c.getTransform();
1172
+ const opt = { shadows: !isDragging && shadowsShown(visible.length), k: Math.hypot(m.a, m.b), base: m, dimOpacity };
1173
+ const st = { ...IDLE };
1174
+ const now = performance.now();
1175
+ const arrival = { alpha: 1, scale: 1, dx: 0, dy: 0 };
1176
+ const arriving = clock?.active(now) ?? false;
1177
+ for (let i = 0; i < visible.length; i++) {
1178
+ const n = visible[i];
1179
+ st.hovered = n.id === hoveredNodeId;
1180
+ st.pressed = n.id === pressedNodeId;
1181
+ st.selected = n.id === selectedNodeId;
1182
+ st.focused = n.id === focusedNodeId;
1183
+ st.dimmed = dimOthers && n.id !== highlighted;
1184
+ if (tweens.has(n.id)) {
1185
+ const p = tweens.progress(n.id, now);
1186
+ st.lift = p.lift;
1187
+ st.ring = p.ring;
1188
+ } else {
1189
+ st.lift = void 0;
1190
+ st.ring = void 0;
1191
+ }
1192
+ st.arrival = arriving && clock.node(n.id, n.x, n.y, now, arrival) ? arrival : null;
1193
+ drawIdea(c, n, design, zoom, st, opt);
1194
+ }
1195
+ c.setTransform(m);
1196
+ c.globalAlpha = 1;
1232
1197
  };
1198
+ return /* @__PURE__ */ jsx2(KLayer, { ref: layerRef, name: "nodes", listening: false, children: /* @__PURE__ */ jsx2(KShape, { sceneFunc, listening: false, perfectDrawEnabled: false }) });
1233
1199
  }
1234
- var EDGE_SPACING = 25;
1235
- var STROKE_COLOR = "#000000";
1236
- var STROKE_COLOR_DARK = "#ffffff";
1237
- var STROKE_WIDTH = 2.5;
1238
- var KonvaEdge = memo(function KonvaEdge2({
1239
- sourceNode: from,
1240
- targetNode: to,
1241
- connection,
1242
- isDark = false,
1243
- zoomLevel = 1
1244
- }) {
1245
- const label = connection?.label;
1246
- const points = useMemo4(() => {
1247
- const fromBaseX = from.width / 2;
1248
- const fromBaseY = Math.max(NODE_HEIGHT2 * 1.6 / 2, fromBaseX * 0.74);
1249
- const fromScale = getOutermostScaleUp(from.branchCount);
1250
- const fromRadiusX = fromBaseX * fromScale;
1251
- const fromRadiusY = fromBaseY * fromScale;
1252
- const toBaseX = to.width / 2;
1253
- const toBaseY = Math.max(NODE_HEIGHT2 * 1.6 / 2, toBaseX * 0.74);
1254
- const toScale = getOutermostScaleUp(to.branchCount);
1255
- const toRadiusX = toBaseX * toScale;
1256
- const toRadiusY = toBaseY * toScale;
1257
- const angle = Math.atan2(to.y - from.y, to.x - from.x);
1258
- const fromEdge = getEllipseIntersection(from.x, from.y, fromRadiusX, fromRadiusY, angle);
1259
- const toEdge = getEllipseIntersection(to.x, to.y, toRadiusX, toRadiusY, angle + Math.PI);
1260
- const fromX = fromEdge.x + EDGE_SPACING * Math.cos(angle);
1261
- const fromY = fromEdge.y + EDGE_SPACING * Math.sin(angle);
1262
- const toX = toEdge.x - EDGE_SPACING * Math.cos(angle);
1263
- const toY = toEdge.y - EDGE_SPACING * Math.sin(angle);
1264
- const midX = (fromX + toX) / 2;
1265
- const midY = (fromY + toY) / 2;
1266
- const distance = Math.sqrt((toX - fromX) ** 2 + (toY - fromY) ** 2);
1267
- const arcHeight = Math.min(45, 45 / (1 + Math.max(0, (distance - 200) / 400)));
1268
- const perpX = -(toY - fromY) / distance;
1269
- const perpY = (toX - fromX) / distance;
1270
- const offsetMidX = midX + perpX * arcHeight;
1271
- const offsetMidY = midY + perpY * arcHeight;
1272
- const ctrl1X = fromX + (offsetMidX - fromX) * 0.66;
1273
- const ctrl1Y = fromY + (offsetMidY - fromY) * 0.66;
1274
- const ctrl2X = toX + (offsetMidX - toX) * 0.66;
1275
- const ctrl2Y = toY + (offsetMidY - toY) * 0.66;
1276
- const NUM_SEGMENTS = 20;
1277
- const pts = [];
1278
- for (let i = 0; i <= NUM_SEGMENTS; i++) {
1279
- const t = i / NUM_SEGMENTS;
1280
- const mt = 1 - t;
1281
- const mt2 = mt * mt;
1282
- const mt3 = mt2 * mt;
1283
- const t2 = t * t;
1284
- const t3 = t2 * t;
1285
- pts.push(
1286
- mt3 * fromX + 3 * mt2 * t * ctrl1X + 3 * mt * t2 * ctrl2X + t3 * toX,
1287
- mt3 * fromY + 3 * mt2 * t * ctrl1Y + 3 * mt * t2 * ctrl2Y + t3 * toY
1288
- );
1289
- }
1290
- return pts;
1291
- }, [from.x, from.y, from.width, from.branchCount, to.x, to.y, to.width, to.branchCount]);
1292
- const arrowHeadPoints = useMemo4(() => {
1293
- const len = points.length;
1294
- const tipX = points[len - 2];
1295
- const tipY = points[len - 1];
1296
- const prevX = points[len - 4];
1297
- const prevY = points[len - 3];
1298
- const angle = Math.atan2(tipY - prevY, tipX - prevX);
1299
- const armLength = 11;
1300
- const armAngle = Math.PI / 5.5;
1301
- return [
1302
- tipX - armLength * Math.cos(angle - armAngle),
1303
- tipY - armLength * Math.sin(angle - armAngle),
1304
- tipX,
1305
- tipY,
1306
- tipX - armLength * Math.cos(angle + armAngle),
1307
- tipY - armLength * Math.sin(angle + armAngle)
1308
- ];
1309
- }, [points]);
1310
- const { pathData, fontSize } = useMemo4(() => {
1311
- if (!label) return { pathData: null, fontSize: 12 };
1312
- const fromBaseX = from.width / 2;
1313
- const fromBaseY = Math.max(NODE_HEIGHT2 * 1.6 / 2, fromBaseX * 0.74);
1314
- const fromScale = getOutermostScaleUp(from.branchCount);
1315
- const fromRadiusX = fromBaseX * fromScale;
1316
- const fromRadiusY = fromBaseY * fromScale;
1317
- const toBaseX = to.width / 2;
1318
- const toBaseY = Math.max(NODE_HEIGHT2 * 1.6 / 2, toBaseX * 0.74);
1319
- const toScale = getOutermostScaleUp(to.branchCount);
1320
- const toRadiusX = toBaseX * toScale;
1321
- const toRadiusY = toBaseY * toScale;
1322
- const angle = Math.atan2(to.y - from.y, to.x - from.x);
1323
- const fromEdge = getEllipseIntersection(from.x, from.y, fromRadiusX, fromRadiusY, angle);
1324
- const toEdge = getEllipseIntersection(to.x, to.y, toRadiusX, toRadiusY, angle + Math.PI);
1325
- const fromX = fromEdge.x + EDGE_SPACING * Math.cos(angle);
1326
- const fromY = fromEdge.y + EDGE_SPACING * Math.sin(angle);
1327
- const toX = toEdge.x - EDGE_SPACING * Math.cos(angle);
1328
- const toY = toEdge.y - EDGE_SPACING * Math.sin(angle);
1329
- const midX = (fromX + toX) / 2;
1330
- const midY = (fromY + toY) / 2;
1331
- const distance = Math.sqrt((toX - fromX) ** 2 + (toY - fromY) ** 2);
1332
- const arcHeight = Math.min(45, 45 / (1 + Math.max(0, (distance - 200) / 400)));
1333
- const perpX = -(toY - fromY) / distance;
1334
- const perpY = (toX - fromX) / distance;
1335
- const offsetMidX = midX + perpX * arcHeight;
1336
- const offsetMidY = midY + perpY * arcHeight;
1337
- const ctrl1X = fromX + (offsetMidX - fromX) * 0.66;
1338
- const ctrl1Y = fromY + (offsetMidY - fromY) * 0.66;
1339
- const ctrl2X = toX + (offsetMidX - toX) * 0.66;
1340
- const ctrl2Y = toY + (offsetMidY - toY) * 0.66;
1341
- const TEXT_OFFSET = 15;
1342
- const fxo = fromX + perpX * TEXT_OFFSET;
1343
- const fyo = fromY + perpY * TEXT_OFFSET;
1344
- const txo = toX + perpX * TEXT_OFFSET;
1345
- const tyo = toY + perpY * TEXT_OFFSET;
1346
- const c1xo = ctrl1X + perpX * TEXT_OFFSET;
1347
- const c1yo = ctrl1Y + perpY * TEXT_OFFSET;
1348
- const c2xo = ctrl2X + perpX * TEXT_OFFSET;
1349
- const c2yo = ctrl2Y + perpY * TEXT_OFFSET;
1350
- const baseFontSize = 14;
1351
- const zoomFactor = Math.max(1, 1 / zoomLevel);
1352
- const scaledFontSize = Math.max(12, Math.min(24, baseFontSize * (from.width / 180) * zoomFactor));
1353
- let d = "";
1354
- if (fromX > toX) {
1355
- d = `M ${txo},${tyo} C ${c2xo},${c2yo} ${c1xo},${c1yo} ${fxo},${fyo}`;
1356
- } else {
1357
- d = `M ${fxo},${fyo} C ${c1xo},${c1yo} ${c2xo},${c2yo} ${txo},${tyo}`;
1358
- }
1359
- return { pathData: d, fontSize: scaledFontSize };
1360
- }, [from.x, from.y, from.width, from.branchCount, to.x, to.y, to.width, to.branchCount, label, zoomLevel]);
1361
- const strokeColor = isDark ? STROKE_COLOR_DARK : STROKE_COLOR;
1362
- return /* @__PURE__ */ jsxs2(KGroup, { children: [
1363
- /* @__PURE__ */ jsx4(
1364
- KLine,
1365
- {
1366
- points,
1367
- stroke: strokeColor,
1368
- strokeWidth: STROKE_WIDTH,
1369
- tension: 0,
1370
- opacity: 0.9,
1371
- lineCap: "round",
1372
- lineJoin: "round",
1373
- listening: false,
1374
- perfectDrawEnabled: false
1375
- }
1376
- ),
1377
- /* @__PURE__ */ jsx4(
1378
- KLine,
1379
- {
1380
- points: arrowHeadPoints,
1381
- stroke: strokeColor,
1382
- strokeWidth: STROKE_WIDTH,
1383
- lineCap: "round",
1384
- lineJoin: "round",
1385
- opacity: 0.9,
1386
- listening: false,
1387
- perfectDrawEnabled: false
1388
- }
1389
- ),
1390
- label && pathData && /* @__PURE__ */ jsxs2(Fragment, { children: [
1391
- /* @__PURE__ */ jsx4(
1392
- KTextPath,
1393
- {
1394
- data: pathData,
1395
- text: label,
1396
- fontSize,
1397
- fontFamily: "'Playpen Sans', cursive",
1398
- fontStyle: "normal",
1399
- fill: "#ffffff",
1400
- align: "center",
1401
- letterSpacing: 0.5,
1402
- listening: false,
1403
- opacity: 0.9
1404
- }
1405
- ),
1406
- /* @__PURE__ */ jsx4(
1407
- KTextPath,
1408
- {
1409
- data: pathData,
1410
- text: label,
1411
- fontSize,
1412
- fontFamily: "'Playpen Sans', cursive",
1413
- fontStyle: "normal",
1414
- fill: "#6b6860",
1415
- align: "center",
1416
- letterSpacing: 0.5,
1417
- listening: false,
1418
- opacity: 0.9
1419
- }
1420
- )
1421
- ] })
1422
- ] });
1423
- });
1424
- var KonvaEdge_default = KonvaEdge;
1425
1200
 
1426
1201
  // src/components/KonvaEdgeLayer.tsx
1427
- import { jsx as jsx5 } from "react/jsx-runtime";
1428
- function KonvaEdgeLayer({
1429
- nodes,
1430
- connections,
1431
- currentLevel,
1432
- isDark = false,
1433
- isDragging = false
1434
- }) {
1202
+ import { useRef as useRef6, useLayoutEffect as useLayoutEffect2, useMemo as useMemo3 } from "react";
1203
+ import { connectorGeometry } from "@visualli/core";
1204
+ import { jsx as jsx3 } from "react/jsx-runtime";
1205
+ var key = (n) => `${n.x},${n.y},${n.width},${n.level},${n.branchCount ?? 0}`;
1206
+ var MARGIN = 80;
1207
+ function KonvaEdgeLayer({ connections, isDragging = false, design = DEFAULT_DESIGN, clock }) {
1435
1208
  const layerRef = useRef6(null);
1436
- const zoomLevel = useViewportStore((s) => s.zoomLevel);
1209
+ const fonts = useFontsEpoch();
1437
1210
  const storeNodes = useNodeStore((s) => s.nodes);
1438
- const visibleNodes = useViewportNodes(nodes, currentLevel, false, isDragging);
1439
- const visibleSet = useMemo5(() => new Set(visibleNodes.map((n) => n.id)), [visibleNodes]);
1211
+ const cache3 = useRef6(/* @__PURE__ */ new Map());
1212
+ const edges = useMemo3(() => {
1213
+ const out = [];
1214
+ const seen = /* @__PURE__ */ new Set();
1215
+ connections.forEach((conn, i) => {
1216
+ const src = storeNodes.get(conn.from);
1217
+ const tgt = storeNodes.get(conn.to);
1218
+ if (!src || !tgt) return;
1219
+ const id = `${conn.from}>${conn.to}#${i}`;
1220
+ seen.add(id);
1221
+ const k = `${key(src)}|${key(tgt)}`;
1222
+ let hit = cache3.current.get(id);
1223
+ if (!hit || hit.k !== k) {
1224
+ hit = { k, g: prepareConnector(connectorGeometry(src, tgt, design.metrics.edgeGap)) };
1225
+ cache3.current.set(id, hit);
1226
+ }
1227
+ out.push({ g: hit.g, dashed: conn.style === "dashed", label: conn.label });
1228
+ });
1229
+ for (const id of cache3.current.keys()) if (!seen.has(id)) cache3.current.delete(id);
1230
+ return out;
1231
+ }, [connections, storeNodes, design.metrics.edgeGap]);
1232
+ const frames = useFrames(layerRef, () => clock?.active() ?? false);
1233
+ useLayoutEffect2(() => clock?.subscribe(() => frames.run()), [clock, frames]);
1440
1234
  useLayoutEffect2(() => {
1441
1235
  layerRef.current?.batchDraw();
1442
- }, [connections, isDark, isDragging, visibleSet, storeNodes]);
1443
- return /* @__PURE__ */ jsx5(KLayer, { ref: layerRef, listening: false, children: connections.map((conn, idx) => {
1444
- const src = storeNodes.get(conn.from);
1445
- const tgt = storeNodes.get(conn.to);
1446
- if (!src || !tgt) return null;
1447
- if (!isDragging && !visibleSet.has(src.id) && !visibleSet.has(tgt.id)) return null;
1448
- return /* @__PURE__ */ jsx5(
1449
- KonvaEdge_default,
1450
- {
1451
- sourceNode: src,
1452
- targetNode: tgt,
1453
- connection: conn,
1454
- isDark,
1455
- zoomLevel
1456
- },
1457
- `${conn.from}-${conn.to}-${idx}`
1458
- );
1459
- }) });
1236
+ }, [edges, design, fonts, isDragging]);
1237
+ const sceneFunc = (ctx2, shape) => {
1238
+ const stage = shape.getStage();
1239
+ if (!stage) return;
1240
+ const zoom = stage.scaleX() || 1;
1241
+ const v = stageViewRect(stage);
1242
+ const c = ctx2._context;
1243
+ const now = performance.now();
1244
+ const arriving = clock?.active(now) ?? false;
1245
+ const arrival = { alpha: 1, draw: 1 };
1246
+ for (let i = 0; i < edges.length; i++) {
1247
+ const e = edges[i];
1248
+ const { a, b, mid } = e.g;
1249
+ const minX = Math.min(a.x, b.x, mid.x) - MARGIN, maxX = Math.max(a.x, b.x, mid.x) + MARGIN;
1250
+ const minY = Math.min(a.y, b.y, mid.y) - MARGIN, maxY = Math.max(a.y, b.y, mid.y) + MARGIN;
1251
+ if (maxX < v.minX || minX > v.maxX || maxY < v.minY || minY > v.maxY) continue;
1252
+ drawConnector(c, e.g, design, zoom, e.dashed, e.label, arriving && clock.edge(i, e.dashed, now, arrival) ? arrival : null);
1253
+ }
1254
+ };
1255
+ return /* @__PURE__ */ jsx3(KLayer, { ref: layerRef, listening: false, children: /* @__PURE__ */ jsx3(KShape, { sceneFunc, listening: false, perfectDrawEnabled: false }) });
1460
1256
  }
1461
1257
 
1462
1258
  // src/components/KonvaContainerLayer.tsx
1463
- import { useRef as useRef7, useLayoutEffect as useLayoutEffect3 } from "react";
1259
+ import { useRef as useRef7, useLayoutEffect as useLayoutEffect3, useMemo as useMemo4 } from "react";
1464
1260
 
1465
- // src/components/KonvaContainer.tsx
1466
- import { memo as memo2, useMemo as useMemo6 } from "react";
1467
- import { NODE_HEIGHT as NODE_HEIGHT4, TEXT_LABEL_HIDE_BELOW_ZOOM as TEXT_LABEL_HIDE_BELOW_ZOOM2 } from "@visualli/core";
1468
-
1469
- // src/config/textScaling.ts
1470
- import { NODE_HEIGHT as NODE_HEIGHT3 } from "@visualli/core";
1471
- var DESCRIPTION_TEXT_BASE_FONT_PX2 = 12;
1472
- var CONTAINER_LABEL_BASE_FONT_PX2 = 24;
1473
- var NODE_TEXT_SAFE_BLOB_FILL_RATIO = 0.88;
1474
- function computeNodeTextWorldScale2(nodeWidth, zoomLevel) {
1475
- const safeZoom = Math.max(zoomLevel, 1e-4);
1476
- const baseWidth = 300;
1477
- const baseRadiusX = baseWidth / 2;
1478
- const baseRadiusY = Math.max(NODE_HEIGHT3 * 1.6 / 2, baseRadiusX * 0.74);
1479
- const textNaturalWidth = baseWidth - 40;
1480
- const textNaturalHeight = NODE_HEIGHT3 * 1.2;
1481
- const maxScaleX = baseRadiusX * 2 * NODE_TEXT_SAFE_BLOB_FILL_RATIO / Math.max(textNaturalWidth, 1);
1482
- const maxScaleY = baseRadiusY * 2 * NODE_TEXT_SAFE_BLOB_FILL_RATIO / Math.max(textNaturalHeight, 1);
1483
- const fitScale = Math.max(0.01, Math.min(maxScaleX, maxScaleY));
1484
- return Math.min(1 / safeZoom, fitScale);
1485
- }
1486
- function computeNodeTextScreenScale2(nodeWidth, zoomLevel) {
1487
- return computeNodeTextWorldScale2(nodeWidth, zoomLevel) * zoomLevel;
1261
+ // src/design/containers.ts
1262
+ import {
1263
+ CANVAS_STYLE as CANVAS_STYLE4,
1264
+ RINGS,
1265
+ connectorGeometry as connectorGeometry2,
1266
+ connectorSamples,
1267
+ containerHull,
1268
+ labelGrowth as labelGrowth2,
1269
+ nodeRadii as nodeRadii2,
1270
+ placeContainerLabel,
1271
+ ringsFor as ringsFor2
1272
+ } from "@visualli/core";
1273
+ var PILL = CANVAS_STYLE4.group;
1274
+ var pillFont = (d, px) => `${PILL.labelWeight} ${px}px ${d.fontNote}`;
1275
+ var measureCtx = null;
1276
+ function measurer() {
1277
+ if (!measureCtx && typeof document !== "undefined") measureCtx = document.createElement("canvas").getContext("2d");
1278
+ return measureCtx;
1488
1279
  }
1489
-
1490
- // src/components/KonvaContainer.tsx
1491
- import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
1492
- var CONTAINER_PADDING = 50;
1493
- var CONTAINER_DASH = [10, 5];
1494
- var LABEL_BG = "#ddd9d0";
1495
- var LABEL_BORDER_RADIUS = 20;
1496
- var LABEL_FONT_SIZE = CONTAINER_LABEL_BASE_FONT_PX2;
1497
- var LABEL_FONT_WEIGHT = "800";
1498
- var LABEL_FONT_FAMILY = "'Playpen Sans', Quicksand, sans-serif";
1499
- var LABEL_PAD_H = 20;
1500
- var LABEL_PAD_V = 10;
1501
- var LABEL_LINE_HEIGHT = 1.2;
1502
- var MAX_BLOB_EXTENT_X = 1.275;
1503
- var MAX_BLOB_EXTENT_Y = 1.234;
1504
- var _labelCtx = null;
1505
- function measureLabelBoxWidth(text, fontSize) {
1506
- if (!_labelCtx) {
1507
- const c = document.createElement("canvas");
1508
- _labelCtx = c.getContext("2d");
1280
+ function measurePill(text, d, k, c = measurer()) {
1281
+ const px = PILL.labelSize * d.labelScale * k;
1282
+ const padX = PILL.labelPadding.x, border = PILL.labelBorderWidth;
1283
+ const room = PILL.labelMaxWidth - padX * 2 - border * 2;
1284
+ let shown = text, tw = text.length * px * 0.5;
1285
+ if (c) {
1286
+ c.font = pillFont(d, px);
1287
+ tw = c.measureText(text).width;
1288
+ if (tw > room) {
1289
+ let lo = 0, hi = text.length;
1290
+ while (lo < hi) {
1291
+ const mid = lo + hi + 1 >> 1;
1292
+ if (c.measureText(text.slice(0, mid).trimEnd() + "\u2026").width <= room) lo = mid;
1293
+ else hi = mid - 1;
1294
+ }
1295
+ shown = text.slice(0, lo).trimEnd() + "\u2026";
1296
+ tw = c.measureText(shown).width;
1297
+ }
1509
1298
  }
1510
- if (!_labelCtx) return Math.ceil(text.length * fontSize * 0.65 + 2 * LABEL_PAD_H);
1511
- _labelCtx.font = `${LABEL_FONT_WEIGHT} ${fontSize}px ${LABEL_FONT_FAMILY}`;
1512
- return Math.ceil(_labelCtx.measureText(text).width + 2 * LABEL_PAD_H);
1299
+ return {
1300
+ text: shown,
1301
+ x: 0,
1302
+ y: 0,
1303
+ side: "bottom",
1304
+ w: Math.min(PILL.labelMaxWidth, tw + padX * 2 + border * 2),
1305
+ h: px * PILL.labelLineHeight + PILL.labelPadding.top + PILL.labelPadding.bottom + border * 2
1306
+ };
1513
1307
  }
1514
- function isLeftTurn(p1, p2, p3) {
1515
- return (p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x) > 0;
1308
+ function ideaBox(n) {
1309
+ const { rx, ry } = nodeRadii2(n);
1310
+ const rings = ringsFor2(n.branchCount);
1311
+ const s = rings.length ? RINGS[rings.length - 1].scale : 1;
1312
+ return { x0: n.x - rx * s, x1: n.x + rx * s, y0: n.y - ry * s, y1: n.y + ry * s };
1516
1313
  }
1517
- function convexHull(points) {
1518
- if (points.length < 3) return points;
1519
- let start = 0;
1520
- for (let i = 1; i < points.length; i++) {
1521
- if (points[i].y < points[start].y || points[i].y === points[start].y && points[i].x < points[start].x) start = i;
1522
- }
1523
- const sorted = [...points];
1524
- const sp = sorted.splice(start, 1)[0];
1525
- sorted.sort((a, b) => {
1526
- const da = Math.atan2(a.y - sp.y, a.x - sp.x);
1527
- const db = Math.atan2(b.y - sp.y, b.x - sp.x);
1528
- if (da !== db) return da - db;
1529
- return Math.hypot(a.x - sp.x, a.y - sp.y) - Math.hypot(b.x - sp.x, b.y - sp.y);
1314
+ function layoutContainers(containers, nodes, connections, d) {
1315
+ if (containers.length === 0) return [];
1316
+ const ideas = [];
1317
+ for (const n of nodes.values()) ideas.push(ideaBox(n));
1318
+ const lines = connections.flatMap((c) => {
1319
+ const a = nodes.get(c.from), b = nodes.get(c.to);
1320
+ if (!a || !b) return [];
1321
+ const g = connectorGeometry2(a, b, d.metrics.edgeGap);
1322
+ return [connectorSamples(g.a, g.mid, g.b)];
1530
1323
  });
1531
- const hull = [sp, sorted[0]];
1532
- for (let i = 1; i < sorted.length; i++) {
1533
- while (hull.length > 1 && !isLeftTurn(hull[hull.length - 2], hull[hull.length - 1], sorted[i])) hull.pop();
1534
- hull.push(sorted[i]);
1324
+ const placed = [];
1325
+ const out = [];
1326
+ for (const c of containers) {
1327
+ const members = c.nodeIds.map((id) => nodes.get(id)).filter((n) => !!n);
1328
+ const hull = containerHull(members.map((n) => ({ x: n.x, y: n.y, ...nodeRadii2(n) })));
1329
+ if (!hull) continue;
1330
+ let pill = null;
1331
+ if (c.label) {
1332
+ const size = measurePill(c.label, d, 1);
1333
+ const at = placeContainerLabel(hull, size, { ideas, connectors: lines, labels: placed });
1334
+ pill = { ...size, x: at.x, y: at.y, side: at.side };
1335
+ 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 });
1336
+ }
1337
+ out.push({ id: c.id, hull, pill });
1535
1338
  }
1536
- return hull;
1339
+ return out;
1537
1340
  }
1538
- function chaikinSmooth(pts, passes = 6) {
1539
- let cur = pts;
1540
- for (let iter = 0; iter < passes; iter++) {
1541
- const next = [];
1542
- const n = cur.length;
1543
- for (let i = 0; i < n; i++) {
1544
- const a = cur[i], b = cur[(i + 1) % n];
1545
- next.push({ x: 0.75 * a.x + 0.25 * b.x, y: 0.75 * a.y + 0.25 * b.y });
1546
- next.push({ x: 0.25 * a.x + 0.75 * b.x, y: 0.25 * a.y + 0.75 * b.y });
1547
- }
1548
- cur = next;
1549
- }
1550
- return cur.flatMap((p) => [p.x, p.y]);
1551
- }
1552
- function hullEdgeYAtX(points, targetX, isBottom) {
1553
- const n = points.length;
1554
- let result = isBottom ? -Infinity : Infinity;
1555
- for (let i = 0; i < n; i += 2) {
1556
- const x1 = points[i], y1 = points[i + 1];
1557
- const x2 = points[(i + 2) % n], y2 = points[(i + 3) % n];
1558
- if (x1 < targetX && x2 >= targetX || x2 < targetX && x1 >= targetX) {
1559
- const t = (targetX - x1) / (x2 - x1);
1560
- const y = y1 + t * (y2 - y1);
1561
- if (isBottom) result = Math.max(result, y);
1562
- else result = Math.min(result, y);
1563
- }
1564
- }
1565
- if (!isFinite(result)) {
1566
- for (let i = 1; i < n; i += 2) {
1567
- if (isBottom) result = Math.max(result, points[i]);
1568
- else result = Math.min(result, points[i]);
1569
- }
1570
- }
1571
- return result;
1341
+ function drawHull(c, h, d) {
1342
+ c.save();
1343
+ c.strokeStyle = d.tokens["line-strong"];
1344
+ c.lineWidth = PILL.strokeWidth;
1345
+ c.setLineDash([...PILL.dash]);
1346
+ c.beginPath();
1347
+ c.roundRect(h.x0, h.y0, h.x1 - h.x0, h.y1 - h.y0, h.radius);
1348
+ c.stroke();
1349
+ c.restore();
1572
1350
  }
1573
- function calculateHull(containerNodes) {
1574
- if (containerNodes.length === 0) return null;
1575
- const NUM_SAMPLES = 48;
1576
- const samples = [];
1577
- for (const node of containerNodes) {
1578
- const baseRX = node.width / 2;
1579
- const baseRY = Math.max(NODE_HEIGHT4 * 1.6 / 2, baseRX * 0.74);
1580
- const halfW = baseRX * MAX_BLOB_EXTENT_X + CONTAINER_PADDING;
1581
- const halfH = baseRY * MAX_BLOB_EXTENT_Y + CONTAINER_PADDING;
1582
- for (let j = 0; j < NUM_SAMPLES; j++) {
1583
- const angle = j / NUM_SAMPLES * Math.PI * 2;
1584
- samples.push({ x: node.x + halfW * Math.cos(angle), y: node.y + halfH * Math.sin(angle) });
1585
- }
1586
- }
1587
- const hull = convexHull(samples);
1588
- const points = chaikinSmooth(hull, 6);
1589
- const xs = containerNodes.map((n) => n.x);
1590
- const ys = containerNodes.map((n) => n.y);
1591
- const spanX = Math.max(...xs) - Math.min(...xs);
1592
- const spanY = Math.max(...ys) - Math.min(...ys);
1593
- const useTopEdge = spanY > spanX * 2;
1594
- let hullMinX = Infinity, hullMaxX = -Infinity;
1595
- for (let i = 0; i < points.length; i += 2) {
1596
- if (points[i] < hullMinX) hullMinX = points[i];
1597
- if (points[i] > hullMaxX) hullMaxX = points[i];
1598
- }
1599
- const anchorX = (hullMinX + hullMaxX) / 2;
1600
- const anchorEdgeY = hullEdgeYAtX(points, anchorX, !useTopEdge);
1601
- return { points, anchorX, anchorEdgeY };
1351
+ function drawPill(c, p, d, zoom) {
1352
+ const k = labelGrowth2(zoom).idea;
1353
+ const size = k === 1 ? p : measurePill(p.text, d, k, c);
1354
+ const contrast = d.theme.startsWith("contrast");
1355
+ const bw = contrast ? PILL.contrastLabelBorderWidth : PILL.labelBorderWidth;
1356
+ c.save();
1357
+ c.beginPath();
1358
+ 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);
1359
+ c.fillStyle = d.tokens[PILL.labelFill];
1360
+ c.fill();
1361
+ c.lineWidth = bw;
1362
+ c.strokeStyle = d.tokens[contrast ? PILL.contrastLabelBorder : PILL.labelBorder];
1363
+ c.stroke();
1364
+ c.font = pillFont(d, PILL.labelSize * d.labelScale * k);
1365
+ c.fillStyle = d.tokens[PILL.labelInk];
1366
+ c.textAlign = "center";
1367
+ c.textBaseline = "middle";
1368
+ c.fillText(size.text, p.x, p.y + (PILL.labelPadding.top - PILL.labelPadding.bottom) / 2);
1369
+ c.restore();
1602
1370
  }
1603
- var KonvaContainer = memo2(function KonvaContainer2({
1604
- nodes,
1605
- label = ""
1606
- }) {
1607
- const zoomLevel = useViewportStore((s) => s.zoomLevel);
1608
- const hullData = useMemo6(() => calculateHull(nodes), [nodes]);
1609
- if (!hullData) return null;
1610
- const refNode = nodes[0];
1611
- const screenScale = refNode ? computeNodeTextScreenScale2(refNode.width, zoomLevel) : 1;
1612
- const labelFontSize = LABEL_FONT_SIZE * screenScale;
1613
- const labelBoxHeight = Math.ceil(labelFontSize * LABEL_LINE_HEIGHT + 2 * LABEL_PAD_V);
1614
- const boxWidth = label ? measureLabelBoxWidth(label, labelFontSize) : 0;
1615
- const boxX = hullData.anchorX - boxWidth / 2;
1616
- const boxY = hullData.anchorEdgeY - labelBoxHeight / 2;
1617
- return /* @__PURE__ */ jsxs3(KGroup, { listening: false, children: [
1618
- /* @__PURE__ */ jsx6(
1619
- KLine,
1620
- {
1621
- points: hullData.points,
1622
- stroke: "#747270",
1623
- strokeWidth: 1,
1624
- dash: CONTAINER_DASH,
1625
- closed: true,
1626
- tension: 0,
1627
- listening: false,
1628
- perfectDrawEnabled: false
1629
- }
1630
- ),
1631
- label.length > 0 && /* @__PURE__ */ jsxs3(
1632
- KGroup,
1633
- {
1634
- x: boxX + boxWidth / 2,
1635
- y: boxY + labelBoxHeight / 2,
1636
- offsetX: boxWidth / 2,
1637
- offsetY: labelBoxHeight / 2,
1638
- scaleX: 1 / zoomLevel,
1639
- scaleY: 1 / zoomLevel,
1640
- visible: zoomLevel >= TEXT_LABEL_HIDE_BELOW_ZOOM2,
1641
- listening: false,
1642
- children: [
1643
- /* @__PURE__ */ jsx6(
1644
- KRect,
1645
- {
1646
- width: boxWidth,
1647
- height: labelBoxHeight,
1648
- fill: LABEL_BG,
1649
- cornerRadius: LABEL_BORDER_RADIUS,
1650
- shadowEnabled: false,
1651
- perfectDrawEnabled: false
1652
- }
1653
- ),
1654
- /* @__PURE__ */ jsx6(
1655
- KText,
1656
- {
1657
- x: LABEL_PAD_H,
1658
- y: LABEL_PAD_V,
1659
- width: boxWidth - 2 * LABEL_PAD_H,
1660
- height: labelBoxHeight - 2 * LABEL_PAD_V,
1661
- text: label,
1662
- fontSize: labelFontSize,
1663
- fontFamily: LABEL_FONT_FAMILY,
1664
- fontStyle: LABEL_FONT_WEIGHT,
1665
- fill: "rgba(0,0,0,1)",
1666
- align: "center",
1667
- verticalAlign: "middle",
1668
- lineHeight: LABEL_LINE_HEIGHT,
1669
- perfectDrawEnabled: false,
1670
- listening: false
1671
- }
1672
- )
1673
- ]
1674
- }
1675
- )
1676
- ] });
1677
- });
1678
- var KonvaContainer_default = KonvaContainer;
1679
1371
 
1680
1372
  // src/components/KonvaContainerLayer.tsx
1681
- import { jsx as jsx7 } from "react/jsx-runtime";
1682
- function KonvaContainerLayer({ nodes, containers, isDark = false }) {
1683
- const layerRef = useRef7(null);
1373
+ import { jsx as jsx4 } from "react/jsx-runtime";
1374
+ function useContainerLayout(containers, connections = [], design = DEFAULT_DESIGN) {
1375
+ const fonts = useFontsEpoch();
1684
1376
  const storeNodes = useNodeStore((s) => s.nodes);
1377
+ return useMemo4(
1378
+ () => layoutContainers(containers, storeNodes, connections, design),
1379
+ // fonts: the names are measured, so measure again once the web fonts are in.
1380
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1381
+ [containers, connections, storeNodes, design, fonts]
1382
+ );
1383
+ }
1384
+ function KonvaContainerLayer({ containers, connections = [], design = DEFAULT_DESIGN }) {
1385
+ const layerRef = useRef7(null);
1386
+ const placed = useContainerLayout(containers, connections, design);
1685
1387
  useLayoutEffect3(() => {
1686
1388
  layerRef.current?.batchDraw();
1687
- }, [containers, isDark, storeNodes]);
1389
+ }, [placed, design]);
1688
1390
  if (containers.length === 0) return null;
1689
- return /* @__PURE__ */ jsx7(KLayer, { ref: layerRef, listening: false, children: containers.map((c) => {
1690
- const groupNodes = c.nodeIds.map((id) => storeNodes.get(id)).filter((n) => !!n);
1691
- if (groupNodes.length === 0) return null;
1692
- return /* @__PURE__ */ jsx7(
1693
- KonvaContainer_default,
1694
- {
1695
- nodes: groupNodes,
1696
- label: c.label,
1697
- isDark
1698
- },
1699
- c.id
1700
- );
1701
- }) });
1391
+ return /* @__PURE__ */ jsx4(KLayer, { ref: layerRef, listening: false, children: /* @__PURE__ */ jsx4(
1392
+ KShape,
1393
+ {
1394
+ listening: false,
1395
+ perfectDrawEnabled: false,
1396
+ sceneFunc: (ctx2) => {
1397
+ for (const p of placed) drawHull(ctx2._context, p.hull, design);
1398
+ }
1399
+ }
1400
+ ) });
1702
1401
  }
1703
1402
 
1704
- // src/components/NavigationStack.tsx
1705
- import { memo as memo3, useState as useState2 } from "react";
1706
- import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
1707
- var PASTEL_FILLS = [
1708
- "#b6eea7",
1709
- "#d5bcfe",
1710
- "#a1c4fc",
1711
- "#a6f5d8",
1712
- "#ffc9de",
1713
- "#c4e8ff",
1714
- "#f0c4ff",
1715
- "#ffe0a3"
1716
- ];
1717
- var ORGANIC_PATHS_SMALL = [
1718
- "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",
1719
- "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"
1720
- ];
1721
- 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";
1722
- var CIRCLE_SIZE = 14;
1723
- function hexToRgba(hex, opacity) {
1724
- const h = hex.replace("#", "");
1725
- const r = parseInt(h.substring(0, 2), 16);
1726
- const g = parseInt(h.substring(2, 4), 16);
1727
- const b = parseInt(h.substring(4, 6), 16);
1728
- return `rgba(${r},${g},${b},${opacity})`;
1403
+ // src/components/KonvaContainerLabelLayer.tsx
1404
+ import { useRef as useRef8, useLayoutEffect as useLayoutEffect4 } from "react";
1405
+ import { jsx as jsx5 } from "react/jsx-runtime";
1406
+ function KonvaContainerLabelLayer({ containers, connections = [], design = DEFAULT_DESIGN }) {
1407
+ const layerRef = useRef8(null);
1408
+ const placed = useContainerLayout(containers, connections, design);
1409
+ useLayoutEffect4(() => {
1410
+ layerRef.current?.batchDraw();
1411
+ }, [placed, design]);
1412
+ if (!placed.some((p) => p.pill)) return null;
1413
+ return /* @__PURE__ */ jsx5(KLayer, { ref: layerRef, listening: false, children: /* @__PURE__ */ jsx5(
1414
+ KShape,
1415
+ {
1416
+ listening: false,
1417
+ perfectDrawEnabled: false,
1418
+ sceneFunc: (ctx2, shape) => {
1419
+ const zoom = shape.getStage()?.scaleX() || 1;
1420
+ for (const p of placed) if (p.pill) drawPill(ctx2._context, p.pill, design, zoom);
1421
+ }
1422
+ }
1423
+ ) });
1729
1424
  }
1730
- var OrganicCircle = memo3(function OrganicCircle2({
1731
- size,
1732
- fill,
1733
- stroke,
1734
- strokeWidth,
1735
- variant,
1736
- isActive
1737
- }) {
1738
- const path = isActive ? ORGANIC_PATH_ACTIVE : ORGANIC_PATHS_SMALL[variant % ORGANIC_PATHS_SMALL.length];
1739
- const vb = isActive ? "4 3 20 20" : "2 1 18 18";
1740
- const vbSz = isActive ? 20 : 18;
1741
- const sw = strokeWidth * vbSz / size;
1742
- return /* @__PURE__ */ jsxs4("svg", { width: size, height: size, viewBox: vb, fill: "none", style: { display: "block" }, children: [
1743
- /* @__PURE__ */ jsx8("path", { d: path, fill, fillOpacity: 1 }),
1744
- /* @__PURE__ */ jsx8("path", { d: path, fill: "none", stroke, strokeWidth: sw, strokeLinecap: "round" })
1425
+
1426
+ // src/components/NavigationStack.tsx
1427
+ import { memo as memo2, useMemo as useMemo6 } from "react";
1428
+
1429
+ // src/components/Overlays.tsx
1430
+ import React6, { memo, useCallback as useCallback3, useEffect as useEffect5, useLayoutEffect as useLayoutEffect5, useMemo as useMemo5, useRef as useRef9, useState as useState2 } from "react";
1431
+ import { SHEET, TERM, TRAIL, VIEW, blobPath as blobPath2, hoverOpensTerm, shapeOfLevel as shapeOfLevel2 } from "@visualli/core";
1432
+ import { Fragment, jsx as jsx6, jsxs } from "react/jsx-runtime";
1433
+ var Icon = ({ children, size = 20 }) => /* @__PURE__ */ jsx6("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 });
1434
+ var MinusIcon = () => /* @__PURE__ */ jsx6(Icon, { children: /* @__PURE__ */ jsx6("line", { x1: "5", y1: "12", x2: "19", y2: "12" }) });
1435
+ var PlusIcon = () => /* @__PURE__ */ jsxs(Icon, { children: [
1436
+ /* @__PURE__ */ jsx6("line", { x1: "12", y1: "5", x2: "12", y2: "19" }),
1437
+ /* @__PURE__ */ jsx6("line", { x1: "5", y1: "12", x2: "19", y2: "12" })
1438
+ ] });
1439
+ var FitIcon = () => /* @__PURE__ */ jsxs(Icon, { children: [
1440
+ /* @__PURE__ */ jsx6("path", { d: "M8 3H5a2 2 0 0 0-2 2v3" }),
1441
+ /* @__PURE__ */ jsx6("path", { d: "M21 8V5a2 2 0 0 0-2-2h-3" }),
1442
+ /* @__PURE__ */ jsx6("path", { d: "M3 16v3a2 2 0 0 0 2 2h3" }),
1443
+ /* @__PURE__ */ jsx6("path", { d: "M16 21h3a2 2 0 0 0 2-2v-3" })
1444
+ ] });
1445
+ var ChevronLeftIcon = () => /* @__PURE__ */ jsx6(Icon, { size: 14, children: /* @__PURE__ */ jsx6("polyline", { points: "15 6 9 12 15 18" }) });
1446
+ var ChevronIcon = () => /* @__PURE__ */ jsx6(Icon, { size: 14, children: /* @__PURE__ */ jsx6("polyline", { points: "9 6 15 12 9 18" }) });
1447
+ var ExternalIcon = () => /* @__PURE__ */ jsxs(Icon, { size: 14, children: [
1448
+ /* @__PURE__ */ jsx6("path", { d: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" }),
1449
+ /* @__PURE__ */ jsx6("polyline", { points: "15 3 21 3 21 9" }),
1450
+ /* @__PURE__ */ jsx6("line", { x1: "10", y1: "14", x2: "21", y2: "3" })
1451
+ ] });
1452
+ var HomeIcon = () => /* @__PURE__ */ jsxs(Icon, { size: TRAIL.homeIconSize, children: [
1453
+ /* @__PURE__ */ jsx6("path", { d: "m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" }),
1454
+ /* @__PURE__ */ jsx6("polyline", { points: "9 22 9 12 15 12 15 22" })
1455
+ ] });
1456
+ var CloseIcon = () => /* @__PURE__ */ jsxs(Icon, { size: 18, children: [
1457
+ /* @__PURE__ */ jsx6("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
1458
+ /* @__PURE__ */ jsx6("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
1459
+ ] });
1460
+ var ZoomControls = memo(function ZoomControls2({ onFit, touch = false }) {
1461
+ const size = touch ? "vi-iconbtn--md" : "vi-iconbtn--sm";
1462
+ const zoom = useViewportStore((s) => s.zoomLevel);
1463
+ const setZoom = useViewportStore((s) => s.setZoom);
1464
+ 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);
1465
+ const fit = useViewportStore((s) => s.fitScale);
1466
+ const zoomIn = useCallback3(() => setZoom(zoom * VIEW.zoomStep), [zoom, setZoom]);
1467
+ const zoomOut = useCallback3(() => setZoom(zoom / VIEW.zoomStep), [zoom, setZoom]);
1468
+ return /* @__PURE__ */ jsxs("div", { className: "vi-ctrls", role: "toolbar", "aria-label": "Canvas", "data-help": "zoom-controls", children: [
1469
+ /* @__PURE__ */ jsx6("button", { type: "button", className: `vi-iconbtn ${size}`, "aria-label": "Zoom out", onClick: zoomOut, children: /* @__PURE__ */ jsx6(MinusIcon, {}) }),
1470
+ /* @__PURE__ */ jsxs("span", { className: "vi-ctrls__pct", "aria-live": "polite", children: [
1471
+ Math.round(zoom / fit * 100),
1472
+ "%"
1473
+ ] }),
1474
+ /* @__PURE__ */ jsx6("button", { type: "button", className: `vi-iconbtn ${size}`, "aria-label": "Zoom in", onClick: zoomIn, children: /* @__PURE__ */ jsx6(PlusIcon, {}) }),
1475
+ onFit && /* @__PURE__ */ jsxs(Fragment, { children: [
1476
+ /* @__PURE__ */ jsx6("span", { className: "vi-ctrls__div", "aria-hidden": "true" }),
1477
+ /* @__PURE__ */ jsx6("button", { type: "button", className: `vi-iconbtn ${size}`, "aria-label": "Fit map to view", disabled: fitted, onClick: onFit, children: /* @__PURE__ */ jsx6(FitIcon, {}) })
1478
+ ] })
1745
1479
  ] });
1746
1480
  });
1747
- var NavigationStack = memo3(function NavigationStack2({
1748
- stack,
1749
- onNavigateBack,
1750
- isDark = false,
1751
- top = "16px",
1752
- left = "16px"
1753
- }) {
1754
- const [hoveredIndex, setHoveredIndex] = useState2(null);
1481
+ function paintVars(p) {
1482
+ if (p.custom) return { fill: p.custom.fill, ring: p.custom.ring ?? "var(--edge)" };
1483
+ const t = p.topic ?? TRAIL.rootTopic;
1484
+ return { fill: `var(--topic-${t})`, ring: `var(--topic-${t}-ring)` };
1485
+ }
1486
+ var TrailDot = ({ level, paint, current }) => {
1487
+ const { fill, ring } = paintVars(paint);
1488
+ return /* @__PURE__ */ jsx6("svg", { width: TRAIL.dotBox, height: TRAIL.dotBox, viewBox: `${-TRAIL.dotBox / 2} ${-TRAIL.dotBox / 2} ${TRAIL.dotBox} ${TRAIL.dotBox}`, "aria-hidden": "true", children: /* @__PURE__ */ jsx6("path", { d: blobPath2(shapeOfLevel2(level), TRAIL.dotRadius, TRAIL.dotRadius), fill, stroke: ring, strokeWidth: current ? TRAIL.currentStroke : TRAIL.stroke }) });
1489
+ };
1490
+ var DepthTrail = memo(function DepthTrail2({ stack, onNavigateBack }) {
1755
1491
  if (stack.length === 0) return null;
1756
- const visitedStroke = isDark ? "#908e8c" : "#747270";
1757
- const visitedText = isDark ? "#908e8c" : "#747270";
1758
- const activeStroke = isDark ? "#e8e6e4" : "#1a1a18";
1759
- const activeText = isDark ? "#e8e6e4" : "#1a1a18";
1760
- const connectorColor = isDark ? "#908e8c" : "#747270";
1761
- const connectorMarginLeft = 10.5;
1762
- const currentIndex = stack.length - 1;
1763
- return /* @__PURE__ */ jsx8(
1764
- "div",
1492
+ const current = stack.length - 1;
1493
+ const iconOnly = TRAIL.rootLabel === "icon";
1494
+ return /* @__PURE__ */ jsx6("nav", { className: "vi-trail", "aria-label": "Depth trail", "data-help": "navigation-stack", children: /* @__PURE__ */ jsx6("ol", { children: stack.map((e, i) => /* @__PURE__ */ jsx6("li", { className: i === current ? "is-current" : void 0, children: /* @__PURE__ */ jsxs(
1495
+ "button",
1765
1496
  {
1766
- "data-help": "navigation-stack",
1767
- style: {
1768
- position: "absolute",
1769
- top,
1770
- left,
1771
- zIndex: 30,
1772
- pointerEvents: "auto"
1773
- },
1774
- children: /* @__PURE__ */ jsx8("div", { style: { display: "flex", flexDirection: "column", padding: "8px" }, children: stack.flatMap((entry, index) => {
1775
- const isCurrent = index === currentIndex;
1776
- const isClickable = index < currentIndex;
1777
- const isHovered = hoveredIndex === index;
1778
- const pastelFill = PASTEL_FILLS[index % PASTEL_FILLS.length];
1779
- const rowBg = isCurrent ? hexToRgba(pastelFill, 0.6) : isHovered && isClickable ? hexToRgba(pastelFill, 0.3) : "transparent";
1780
- const items = [];
1781
- if (index > 0) {
1782
- items.push(
1783
- /* @__PURE__ */ jsx8(
1784
- "div",
1785
- {
1786
- style: {
1787
- width: "1px",
1788
- height: "20px",
1789
- marginLeft: `${connectorMarginLeft}px`,
1790
- marginTop: "-6px",
1791
- marginBottom: "-6px",
1792
- background: connectorColor,
1793
- alignSelf: "flex-start",
1794
- flexShrink: 0,
1795
- position: "relative",
1796
- zIndex: 1
1797
- }
1798
- },
1799
- `conn-${index}`
1800
- )
1801
- );
1802
- }
1803
- items.push(
1804
- /* @__PURE__ */ jsxs4(
1805
- "div",
1806
- {
1807
- style: {
1808
- position: "relative",
1809
- zIndex: 2,
1810
- alignSelf: "flex-start",
1811
- display: "flex",
1812
- alignItems: "center",
1813
- gap: "4px",
1814
- height: "22px",
1815
- padding: "4px",
1816
- borderRadius: "8px",
1817
- cursor: isClickable ? "pointer" : "default",
1818
- background: rowBg,
1819
- transition: "background 0.15s"
1820
- },
1821
- onMouseEnter: () => setHoveredIndex(index),
1822
- onMouseLeave: () => setHoveredIndex(null),
1823
- onClick: (e) => {
1824
- e.stopPropagation();
1825
- if (isClickable) onNavigateBack(index);
1826
- },
1827
- children: [
1828
- /* @__PURE__ */ jsx8("div", { style: { width: CIRCLE_SIZE, height: CIRCLE_SIZE, display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0, zIndex: 4 }, children: isCurrent ? /* @__PURE__ */ jsx8(OrganicCircle, { size: CIRCLE_SIZE, fill: pastelFill, stroke: activeStroke, strokeWidth: 1.5, variant: index, isActive: true }) : /* @__PURE__ */ jsx8(OrganicCircle, { size: CIRCLE_SIZE, fill: pastelFill, stroke: visitedStroke, strokeWidth: 1, variant: index }) }),
1829
- /* @__PURE__ */ jsx8(
1830
- "span",
1831
- {
1832
- style: {
1833
- fontFamily: "'Nunito', sans-serif",
1834
- fontSize: isCurrent || isHovered && isClickable ? "11px" : "9px",
1835
- fontWeight: isCurrent ? 600 : 400,
1836
- color: isCurrent ? activeText : visitedText,
1837
- lineHeight: 1,
1838
- whiteSpace: "nowrap",
1839
- transition: "color 0.15s, font-size 0.15s"
1840
- },
1841
- children: entry.label
1842
- }
1843
- )
1844
- ]
1845
- },
1846
- `${entry.layerId}-${index}`
1847
- )
1848
- );
1849
- return items;
1850
- }) })
1497
+ type: "button",
1498
+ disabled: i === current,
1499
+ "aria-current": i === current ? "location" : void 0,
1500
+ onClick: () => onNavigateBack(i),
1501
+ ...i === 0 && iconOnly ? { "aria-label": e.label, title: e.label } : {},
1502
+ children: [
1503
+ /* @__PURE__ */ jsx6(TrailDot, { level: e.level, paint: e.paint, current: i === current }),
1504
+ /* @__PURE__ */ jsx6("span", { children: i === 0 ? iconOnly ? /* @__PURE__ */ jsx6(HomeIcon, {}) : /* @__PURE__ */ jsxs(Fragment, { children: [
1505
+ /* @__PURE__ */ jsx6(HomeIcon, {}),
1506
+ " ",
1507
+ e.label
1508
+ ] }) : e.label })
1509
+ ]
1851
1510
  }
1852
- );
1511
+ ) }, `${e.layerId}-${i}`)) }) });
1853
1512
  });
1854
- var NavigationStack_default = NavigationStack;
1855
-
1856
- // src/components/ZoomControls.tsx
1857
- import { useCallback as useCallback3, memo as memo4 } from "react";
1858
- import { ZOOM_MIN, ZOOM_MAX } from "@visualli/core";
1859
- import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
1860
- var ZoomInIcon = () => /* @__PURE__ */ jsxs5("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1861
- /* @__PURE__ */ jsx9("circle", { cx: "11", cy: "11", r: "8" }),
1862
- /* @__PURE__ */ jsx9("line", { x1: "21", y1: "21", x2: "16.65", y2: "16.65" }),
1863
- /* @__PURE__ */ jsx9("line", { x1: "11", y1: "8", x2: "11", y2: "14" }),
1864
- /* @__PURE__ */ jsx9("line", { x1: "8", y1: "11", x2: "14", y2: "11" })
1865
- ] });
1866
- var ZoomOutIcon = () => /* @__PURE__ */ jsxs5("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1867
- /* @__PURE__ */ jsx9("circle", { cx: "11", cy: "11", r: "8" }),
1868
- /* @__PURE__ */ jsx9("line", { x1: "21", y1: "21", x2: "16.65", y2: "16.65" }),
1869
- /* @__PURE__ */ jsx9("line", { x1: "8", y1: "11", x2: "14", y2: "11" })
1513
+ var escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1514
+ function splitTerms(text, anchors) {
1515
+ const words = anchors.filter((a) => a.word).sort((a, b) => b.word.length - a.word.length);
1516
+ if (!words.length) return [text];
1517
+ const re = new RegExp(`(${words.map((a) => escapeRe(a.word)).join("|")})`, "giu");
1518
+ const byWord = new Map(words.map((a) => [a.word.toLowerCase(), a]));
1519
+ const isWordChar = (ch) => !!ch && /[\p{L}\p{N}]/u.test(ch);
1520
+ const out = [];
1521
+ let last = 0;
1522
+ for (const m of text.matchAll(re)) {
1523
+ const start = m.index, end = start + m[0].length;
1524
+ if (isWordChar(text[start - 1]) || isWordChar(text[end])) continue;
1525
+ if (start > last) out.push(text.slice(last, start));
1526
+ out.push({ ...byWord.get(m[0].toLowerCase()), word: m[0] });
1527
+ last = end;
1528
+ }
1529
+ if (last < text.length) out.push(text.slice(last));
1530
+ return out;
1531
+ }
1532
+ var TermCard = ({ anchor, inSheet = false }) => /* @__PURE__ */ jsxs("span", { className: `vi-anchor-card${inSheet ? " is-in-sheet" : ""}`, role: "note", children: [
1533
+ /* @__PURE__ */ jsx6("span", { className: "vi-anchor-card__eyebrow", children: "Term" }),
1534
+ /* @__PURE__ */ jsx6("span", { className: "vi-anchor-card__word", style: { display: "block" }, children: anchor.word }),
1535
+ /* @__PURE__ */ jsx6("p", { className: "vi-anchor-card__desc", children: anchor.description }),
1536
+ anchor.knowMoreUrl && /* @__PURE__ */ jsx6("span", { className: "vi-anchor-card__acts", children: /* @__PURE__ */ jsxs("a", { className: "vi-fact__link", href: anchor.knowMoreUrl, target: "_blank", rel: "noopener noreferrer", children: [
1537
+ TERM.learnMoreLabel,
1538
+ " ",
1539
+ /* @__PURE__ */ jsx6(ExternalIcon, {})
1540
+ ] }) })
1870
1541
  ] });
1871
- var ZoomControls = memo4(function ZoomControls2({ isDark = false }) {
1872
- const zoomLevel = useViewportStore((s) => s.zoomLevel);
1873
- const setZoom = useViewportStore((s) => s.setZoom);
1874
- const handleZoomIn = useCallback3(() => setZoom(Math.min(ZOOM_MAX, zoomLevel * 1.2)), [zoomLevel, setZoom]);
1875
- const handleZoomOut = useCallback3(() => setZoom(Math.max(ZOOM_MIN, zoomLevel / 1.2)), [zoomLevel, setZoom]);
1876
- const surfaceBg = isDark ? "var(--bg-surface, #1C1C1A)" : "var(--bg-surface, #FAF8F4)";
1877
- const border = isDark ? "var(--border-color, #333330)" : "var(--border-color, #DDD9D0)";
1878
- const primary = isDark ? "var(--text-primary, #F0EDE6)" : "var(--text-primary, #1A1A18)";
1879
- const muted = isDark ? "var(--text-muted, #8E8C88)" : "var(--text-muted, #747270)";
1880
- return /* @__PURE__ */ jsxs5(
1881
- "div",
1542
+ function TermAnchor({ anchor }) {
1543
+ const [open, setOpen] = useState2(false);
1544
+ const [shift, setShift] = useState2(0);
1545
+ const pinned = useRef9(false);
1546
+ const timer = useRef9(void 0);
1547
+ const wrap = useRef9(null), btn = useRef9(null), card = useRef9(null);
1548
+ const show = (pin) => {
1549
+ clearTimeout(timer.current);
1550
+ if (pin) pinned.current = true;
1551
+ setOpen(true);
1552
+ };
1553
+ const hide = () => {
1554
+ clearTimeout(timer.current);
1555
+ pinned.current = false;
1556
+ setOpen(false);
1557
+ };
1558
+ useEffect5(() => () => clearTimeout(timer.current), []);
1559
+ useLayoutEffect5(() => {
1560
+ if (!open || !card.current) {
1561
+ setShift(0);
1562
+ return;
1563
+ }
1564
+ const r = card.current.getBoundingClientRect(), vw = document.documentElement.clientWidth, m = TERM.viewportMargin;
1565
+ setShift(r.right > vw - m ? vw - m - r.right : r.left < m ? m - r.left : 0);
1566
+ }, [open]);
1567
+ useEffect5(() => {
1568
+ if (!open) return;
1569
+ const onKey = (e) => {
1570
+ if (e.key === "Escape") {
1571
+ e.stopPropagation();
1572
+ hide();
1573
+ btn.current?.focus();
1574
+ }
1575
+ };
1576
+ const onPress = (e) => {
1577
+ if (!wrap.current?.contains(e.target)) hide();
1578
+ };
1579
+ document.addEventListener("keydown", onKey, true);
1580
+ document.addEventListener("pointerdown", onPress, true);
1581
+ return () => {
1582
+ document.removeEventListener("keydown", onKey, true);
1583
+ document.removeEventListener("pointerdown", onPress, true);
1584
+ };
1585
+ }, [open]);
1586
+ return /* @__PURE__ */ jsxs(
1587
+ "span",
1882
1588
  {
1883
- style: {
1884
- position: "relative",
1885
- background: surfaceBg,
1886
- border: `0.5px solid ${border}`,
1887
- borderRadius: "8px",
1888
- overflow: "hidden",
1889
- width: "65px",
1890
- height: "32px",
1891
- flexShrink: 0
1589
+ ref: wrap,
1590
+ className: `vi-anchor${open ? " is-open" : ""}`,
1591
+ "data-semantic-tooltip": "true",
1592
+ onPointerEnter: (e) => {
1593
+ if (!hoverOpensTerm(e.pointerType)) return;
1594
+ clearTimeout(timer.current);
1595
+ timer.current = setTimeout(() => setOpen(true), TERM.hoverOpen);
1892
1596
  },
1893
- role: "group",
1894
- "aria-label": "Zoom controls",
1895
- children: [
1896
- /* @__PURE__ */ jsx9(
1897
- "button",
1898
- {
1899
- onClick: handleZoomIn,
1900
- disabled: zoomLevel >= ZOOM_MAX,
1901
- style: {
1902
- position: "absolute",
1903
- top: 0,
1904
- left: 0,
1905
- width: "32px",
1906
- height: "100%",
1907
- display: "flex",
1908
- alignItems: "center",
1909
- justifyContent: "center",
1910
- border: "none",
1911
- background: "transparent",
1912
- cursor: zoomLevel >= ZOOM_MAX ? "not-allowed" : "pointer",
1913
- color: zoomLevel >= ZOOM_MAX ? muted : primary,
1914
- transition: "background 0.15s"
1915
- },
1916
- onMouseEnter: (e) => {
1917
- if (zoomLevel < ZOOM_MAX) e.currentTarget.style.background = isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.05)";
1918
- },
1919
- onMouseLeave: (e) => {
1920
- e.currentTarget.style.background = "transparent";
1921
- },
1922
- "aria-label": "Zoom in",
1923
- children: /* @__PURE__ */ jsx9(ZoomInIcon, {})
1924
- }
1925
- ),
1926
- /* @__PURE__ */ jsx9("div", { style: { position: "absolute", top: 0, bottom: 0, left: "32px", width: "0.5px", background: border } }),
1927
- /* @__PURE__ */ jsx9(
1597
+ onPointerLeave: (e) => {
1598
+ if (!hoverOpensTerm(e.pointerType) || pinned.current) return;
1599
+ clearTimeout(timer.current);
1600
+ timer.current = setTimeout(() => setOpen(false), TERM.hoverClose);
1601
+ },
1602
+ onBlur: (e) => {
1603
+ if (!wrap.current?.contains(e.relatedTarget)) hide();
1604
+ },
1605
+ children: [
1606
+ /* @__PURE__ */ jsx6(
1928
1607
  "button",
1929
1608
  {
1930
- onClick: handleZoomOut,
1931
- disabled: zoomLevel <= ZOOM_MIN,
1932
- style: {
1933
- position: "absolute",
1934
- top: 0,
1935
- right: 0,
1936
- width: "33px",
1937
- height: "100%",
1938
- display: "flex",
1939
- alignItems: "center",
1940
- justifyContent: "center",
1941
- border: "none",
1942
- background: "transparent",
1943
- cursor: zoomLevel <= ZOOM_MIN ? "not-allowed" : "pointer",
1944
- color: zoomLevel <= ZOOM_MIN ? muted : primary,
1945
- transition: "background 0.15s"
1946
- },
1947
- onMouseEnter: (e) => {
1948
- if (zoomLevel > ZOOM_MIN) e.currentTarget.style.background = isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.05)";
1949
- },
1950
- onMouseLeave: (e) => {
1951
- e.currentTarget.style.background = "transparent";
1609
+ ref: btn,
1610
+ type: "button",
1611
+ className: "vi-term",
1612
+ "aria-expanded": open,
1613
+ onClick: () => {
1614
+ if (open && pinned.current) {
1615
+ hide();
1616
+ return;
1617
+ }
1618
+ show(true);
1619
+ setTimeout(() => card.current?.querySelector("a, button")?.focus(), 0);
1952
1620
  },
1953
- "aria-label": "Zoom out",
1954
- children: /* @__PURE__ */ jsx9(ZoomOutIcon, {})
1621
+ children: anchor.word
1955
1622
  }
1956
- )
1623
+ ),
1624
+ open && /* @__PURE__ */ jsx6("span", { ref: card, className: "vi-anchor__pop", style: shift ? { transform: `translateX(${shift}px)` } : void 0, children: /* @__PURE__ */ jsx6(TermCard, { anchor }) })
1957
1625
  ]
1958
1626
  }
1959
1627
  );
1628
+ }
1629
+ var PeekCard = memo(function PeekCard2({ node, anchors, onStepInside, onClose, renderContent }) {
1630
+ const parts = useMemo5(() => splitTerms(node.description ?? "", anchors), [node.description, anchors]);
1631
+ const { fill, ring } = paintVars(node);
1632
+ const style = { ["--vi-fact-fill"]: fill, ["--vi-fact-ring"]: ring };
1633
+ return /* @__PURE__ */ jsxs("div", { className: "vi-fact vi-peek", style, role: "dialog", "aria-label": node.title, "data-node-tooltip": "true", children: [
1634
+ /* @__PURE__ */ jsxs("div", { className: "vi-fact__head", children: [
1635
+ /* @__PURE__ */ jsx6("div", { className: "vi-fact__title", children: node.title }),
1636
+ onClose && /* @__PURE__ */ jsx6("button", { type: "button", className: "vi-iconbtn vi-iconbtn--sm vi-fact__close", "aria-label": "Close", onClick: onClose, children: /* @__PURE__ */ jsx6(CloseIcon, {}) })
1637
+ ] }),
1638
+ /* @__PURE__ */ jsx6("div", { className: "vi-fact__body", children: renderContent ? renderContent(node.description ?? "") : parts.map((p, i) => typeof p === "string" ? /* @__PURE__ */ jsx6(React6.Fragment, { children: p }, i) : /* @__PURE__ */ jsx6(TermAnchor, { anchor: p }, i)) }),
1639
+ onStepInside && /* @__PURE__ */ jsxs("div", { className: "vi-fact__foot", children: [
1640
+ /* @__PURE__ */ jsx6("span", {}),
1641
+ /* @__PURE__ */ jsx6("div", { className: "vi-fact__acts", children: /* @__PURE__ */ jsxs("button", { type: "button", className: "vi-fact__step", onClick: onStepInside, children: [
1642
+ "Step inside ",
1643
+ /* @__PURE__ */ jsx6(ChevronIcon, {})
1644
+ ] }) })
1645
+ ] })
1646
+ ] });
1960
1647
  });
1961
- var ZoomControls_default = ZoomControls;
1962
-
1963
- // src/components/SketchyBoxKonva.tsx
1964
- import { useEffect as useEffect5, useRef as useRef8 } from "react";
1965
- import Konva from "konva";
1966
- import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
1967
- 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";
1968
- 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";
1969
- var BACK_W = 486;
1970
- var BACK_H = 178;
1971
- var FRONT_W = 490;
1972
- var FRONT_H = 168;
1973
- var BACK_OFFSET_Y = 8;
1974
- var FRONT_OFFSET_X = 5.65;
1975
- var SHADOW_PAD = 12;
1976
- var SketchyBoxKonva = ({
1977
- children,
1978
- fill,
1979
- stroke,
1980
- backStroke,
1981
- padding = "1rem 1.25rem"
1982
- }) => {
1983
- const wrapperRef = useRef8(null);
1984
- const konvaContainerRef = useRef8(null);
1985
- const stageRef = useRef8(null);
1986
- useEffect5(() => {
1987
- const wrapper = wrapperRef.current;
1988
- const konvaEl = konvaContainerRef.current;
1989
- if (!wrapper || !konvaEl) return;
1990
- function draw(W, H) {
1991
- if (!konvaEl || W <= 0 || H <= 0) return;
1992
- if (stageRef.current) {
1993
- stageRef.current.destroy();
1994
- stageRef.current = null;
1995
- }
1996
- const stage = new Konva.Stage({ container: konvaEl, width: W, height: H });
1997
- const layer = new Konva.Layer({ listening: false });
1998
- const contentH = H - SHADOW_PAD;
1999
- layer.add(new Konva.Path({
2000
- x: 0,
2001
- y: BACK_OFFSET_Y,
2002
- data: BACK_PATH,
2003
- scaleX: W / BACK_W,
2004
- scaleY: contentH / BACK_H,
2005
- fill: "#ffffff",
2006
- stroke: backStroke,
2007
- strokeWidth: 2,
2008
- strokeScaleEnabled: false,
2009
- listening: false
2010
- }));
2011
- layer.add(new Konva.Path({
2012
- x: FRONT_OFFSET_X,
2013
- y: 0,
2014
- data: FRONT_PATH,
2015
- scaleX: (W - FRONT_OFFSET_X) / FRONT_W,
2016
- scaleY: contentH / FRONT_H,
2017
- fill,
2018
- stroke,
2019
- strokeWidth: 3,
2020
- strokeScaleEnabled: false,
2021
- listening: false
2022
- }));
2023
- stage.add(layer);
2024
- stageRef.current = stage;
2025
- }
2026
- draw(wrapper.offsetWidth, wrapper.offsetHeight);
2027
- const ro = new ResizeObserver((entries) => {
2028
- const el = entries[0].target;
2029
- draw(el.offsetWidth, el.offsetHeight);
2030
- });
2031
- ro.observe(wrapper);
2032
- return () => {
2033
- ro.disconnect();
2034
- stageRef.current?.destroy();
2035
- stageRef.current = null;
2036
- };
2037
- }, [fill, stroke, backStroke]);
2038
- return /* @__PURE__ */ jsxs6("div", { ref: wrapperRef, style: { position: "relative", paddingBottom: `${SHADOW_PAD}px` }, children: [
2039
- /* @__PURE__ */ jsx10(
1648
+ var PeekSheet = memo(function PeekSheet2({ node, anchors, onStepInside, onClose, renderContent, expanded, onToggleExpanded, height }) {
1649
+ const [term, setTerm] = useState2(null);
1650
+ const parts = useMemo5(() => splitTerms(node.description ?? "", anchors), [node.description, anchors]);
1651
+ const startY = useRef9(null);
1652
+ const swiped = useRef9(false);
1653
+ const { fill, ring } = paintVars(node);
1654
+ const style = {
1655
+ ["--vi-fact-fill"]: fill,
1656
+ ["--vi-fact-ring"]: ring,
1657
+ ["--vi-sheet-h"]: `${height}px`,
1658
+ zIndex: "var(--z-card)"
1659
+ };
1660
+ return /* @__PURE__ */ 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: [
1661
+ /* @__PURE__ */ jsx6(
2040
1662
  "div",
2041
1663
  {
2042
- ref: konvaContainerRef,
2043
- style: { position: "absolute", inset: 0, pointerEvents: "none", zIndex: 0, overflow: "hidden" }
1664
+ className: "vi-fact__grip",
1665
+ onPointerDown: (e) => {
1666
+ startY.current = e.clientY;
1667
+ swiped.current = false;
1668
+ },
1669
+ onPointerUp: (e) => {
1670
+ if (startY.current === null) return;
1671
+ const dy = e.clientY - startY.current;
1672
+ startY.current = null;
1673
+ if (dy < -SHEET.expandDrag) {
1674
+ swiped.current = true;
1675
+ if (!expanded) onToggleExpanded();
1676
+ } else if (dy > SHEET.dismissDrag) {
1677
+ swiped.current = true;
1678
+ if (expanded) onToggleExpanded();
1679
+ else onClose();
1680
+ }
1681
+ },
1682
+ children: /* @__PURE__ */ jsx6("button", { type: "button", className: "vi-fact__handle", "aria-label": expanded ? "Collapse" : "Expand", "aria-expanded": expanded, onClick: () => {
1683
+ if (!swiped.current) onToggleExpanded();
1684
+ swiped.current = false;
1685
+ } })
2044
1686
  }
2045
1687
  ),
2046
- /* @__PURE__ */ jsx10("div", { style: { position: "relative", padding, zIndex: 1 }, children })
1688
+ term ? /* @__PURE__ */ jsxs(Fragment, { children: [
1689
+ /* @__PURE__ */ jsxs("button", { type: "button", className: "vi-fact__back", onClick: () => setTerm(null), children: [
1690
+ /* @__PURE__ */ jsx6(ChevronLeftIcon, {}),
1691
+ " Back"
1692
+ ] }),
1693
+ /* @__PURE__ */ jsx6(TermCard, { anchor: term, inSheet: true })
1694
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1695
+ /* @__PURE__ */ jsxs("div", { className: "vi-fact__head", children: [
1696
+ /* @__PURE__ */ jsx6("div", { className: "vi-fact__title", children: node.title }),
1697
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "vi-iconbtn vi-iconbtn--md vi-fact__close", "aria-label": "Close", onClick: onClose, children: /* @__PURE__ */ jsx6(CloseIcon, {}) })
1698
+ ] }),
1699
+ /* @__PURE__ */ jsx6("div", { className: "vi-fact__body", children: renderContent ? renderContent(node.description ?? "") : parts.map((p, i) => typeof p === "string" ? /* @__PURE__ */ jsx6(React6.Fragment, { children: p }, i) : /* @__PURE__ */ jsx6("span", { className: "vi-anchor", "data-semantic-tooltip": "true", children: /* @__PURE__ */ jsx6("button", { type: "button", className: "vi-term", onClick: () => setTerm(p), children: p.word }) }, i)) }),
1700
+ onStepInside && /* @__PURE__ */ jsxs("div", { className: "vi-fact__foot", children: [
1701
+ /* @__PURE__ */ jsx6("span", {}),
1702
+ /* @__PURE__ */ jsx6("div", { className: "vi-fact__acts", children: /* @__PURE__ */ jsxs("button", { type: "button", className: "vi-fact__step", onClick: onStepInside, children: [
1703
+ "Step inside ",
1704
+ /* @__PURE__ */ jsx6(ChevronIcon, {})
1705
+ ] }) })
1706
+ ] })
1707
+ ] })
2047
1708
  ] });
2048
- };
2049
- var SketchyBoxKonva_default = SketchyBoxKonva;
1709
+ });
2050
1710
 
2051
- // src/VisualliCanvas.tsx
2052
- import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
2053
- function hexToRgba2(hex, alpha) {
2054
- hex = hex.replace(/^#/, "");
2055
- let r, g, b;
2056
- if (hex.length === 3) {
2057
- r = parseInt(hex[0] + hex[0], 16);
2058
- g = parseInt(hex[1] + hex[1], 16);
2059
- b = parseInt(hex[2] + hex[2], 16);
2060
- } else if (hex.length === 6) {
2061
- r = parseInt(hex.substring(0, 2), 16);
2062
- g = parseInt(hex.substring(2, 4), 16);
2063
- b = parseInt(hex.substring(4, 6), 16);
2064
- } else {
2065
- return `rgba(240, 237, 230, ${alpha})`;
2066
- }
2067
- return `rgba(${r}, ${g}, ${b}, ${alpha})`;
1711
+ // src/components/NavigationStack.tsx
1712
+ import { jsx as jsx7 } from "react/jsx-runtime";
1713
+ var NavigationStack = memo2(function NavigationStack2({ stack, onNavigateBack, top, left }) {
1714
+ const entries = useMemo6(
1715
+ () => stack.map((e) => ({ layerId: e.layerId, label: e.label, level: e.layer.level, paint: e.paint ?? {} })),
1716
+ [stack]
1717
+ );
1718
+ return /* @__PURE__ */ jsx7("div", { className: "vi-map__trail", style: top || left ? { top, left } : void 0, children: /* @__PURE__ */ jsx7(DepthTrail, { stack: entries, onNavigateBack }) });
1719
+ });
1720
+ var NavigationStack_default = NavigationStack;
1721
+
1722
+ // src/components/A11yLayer.tsx
1723
+ import { memo as memo3, useEffect as useEffect6, useMemo as useMemo7, useState as useState3 } from "react";
1724
+ import { intersectsViewport as intersectsViewport2, nodeBounds as nodeBounds2 } from "@visualli/core";
1725
+ import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs2 } from "react/jsx-runtime";
1726
+ var MAX_MIRRORED = 150;
1727
+ var A11yLayer = memo3(function A11yLayer2({ nodes, label, hasChildLayer, onActivate, onFocusNode, announcement }) {
1728
+ const cx = useViewportStore((s) => s.centerX);
1729
+ const cy = useViewportStore((s) => s.centerY);
1730
+ const zoom = useViewportStore((s) => s.zoomLevel);
1731
+ const w = useViewportStore((s) => s.canvasWidth);
1732
+ const h = useViewportStore((s) => s.canvasHeight);
1733
+ const [view, setView] = useState3({ cx, cy, zoom, w, h });
1734
+ useEffect6(() => {
1735
+ const t = setTimeout(() => setView({ cx, cy, zoom, w, h }), 120);
1736
+ return () => clearTimeout(t);
1737
+ }, [cx, cy, zoom, w, h]);
1738
+ const items = useMemo7(() => {
1739
+ const halfW = view.w / 2 / view.zoom, halfH = view.h / 2 / view.zoom;
1740
+ const bounds = { minX: view.cx - halfW, maxX: view.cx + halfW, minY: view.cy - halfH, maxY: view.cy + halfH };
1741
+ const visible = nodes.filter((n) => intersectsViewport2(n, bounds));
1742
+ 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;
1743
+ const row = (n) => Math.round(nodeBounds2(n).minY / 150);
1744
+ return [...picked].sort((a, b) => row(a) - row(b) || a.x - b.x);
1745
+ }, [nodes, view]);
1746
+ return /* @__PURE__ */ jsxs2(Fragment2, { children: [
1747
+ /* @__PURE__ */ jsx8("div", { className: "vi-sr", role: "list", "aria-label": label, children: items.map((n) => /* @__PURE__ */ jsx8("div", { role: "listitem", children: /* @__PURE__ */ jsx8(
1748
+ "button",
1749
+ {
1750
+ type: "button",
1751
+ "data-node-id": n.id,
1752
+ "aria-label": `${n.title}. Depth ${n.level + 1}.${hasChildLayer(n.id) ? " Has more inside; press Enter to step inside." : ""}`,
1753
+ onFocus: () => onFocusNode(n.id),
1754
+ onBlur: () => onFocusNode(null),
1755
+ onClick: () => onActivate(n)
1756
+ }
1757
+ ) }, n.id)) }),
1758
+ /* @__PURE__ */ jsx8("div", { className: "vi-sr", role: "status", "aria-live": "polite", children: announcement })
1759
+ ] });
1760
+ });
1761
+
1762
+ // src/design/useDesign.ts
1763
+ import { useMemo as useMemo8, useSyncExternalStore as useSyncExternalStore2 } from "react";
1764
+ function useMedia(query) {
1765
+ const subscribe = (cb) => {
1766
+ if (typeof window === "undefined" || !window.matchMedia) return () => {
1767
+ };
1768
+ const mq = window.matchMedia(query);
1769
+ mq.addEventListener("change", cb);
1770
+ return () => mq.removeEventListener("change", cb);
1771
+ };
1772
+ const get = () => typeof window !== "undefined" && !!window.matchMedia && window.matchMedia(query).matches;
1773
+ return useSyncExternalStore2(subscribe, get, () => false);
2068
1774
  }
1775
+ function useReaderEnv() {
1776
+ return {
1777
+ prefersDark: useMedia("(prefers-color-scheme: dark)"),
1778
+ forcedColors: useMedia("(forced-colors: active)"),
1779
+ prefersReducedMotion: useMedia("(prefers-reduced-motion: reduce)")
1780
+ };
1781
+ }
1782
+ function useDesign(props) {
1783
+ const env = useReaderEnv();
1784
+ const { theme, isDark, respectForcedColors } = props;
1785
+ const { readableType, largerText, reducedMotion } = props.comfort ?? {};
1786
+ return useMemo8(
1787
+ () => resolveDesign({ theme, isDark, respectForcedColors, comfort: { readableType, largerText, reducedMotion } }, env),
1788
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1789
+ [theme, isDark, respectForcedColors, readableType, largerText, reducedMotion, env.prefersDark, env.forcedColors, env.prefersReducedMotion]
1790
+ );
1791
+ }
1792
+
1793
+ // src/design/measure.ts
1794
+ import { TYPE_STYLES as TYPE_STYLES4, approximateMeasure } from "@visualli/core";
1795
+ var ctx;
1796
+ var cache2 = /* @__PURE__ */ new Map();
1797
+ var MAX_CACHED = 8192;
1798
+ function ideaMeasure(d) {
1799
+ if (ctx === void 0) ctx = typeof document !== "undefined" ? document.createElement("canvas").getContext("2d") : null;
1800
+ const c = ctx;
1801
+ if (!c) return approximateMeasure;
1802
+ const weight = TYPE_STYLES4["node-label"].weight;
1803
+ const tracking = d.comfort.readableType ? 0.02 : 0;
1804
+ return (text, px) => {
1805
+ const font = `${weight} ${px}px ${d.fontHand}`;
1806
+ const key2 = `${fontsEpoch()}|${font}|${text}`;
1807
+ let w = cache2.get(key2);
1808
+ if (w === void 0) {
1809
+ c.font = font;
1810
+ w = c.measureText(text).width + text.length * px * tracking;
1811
+ if (cache2.size >= MAX_CACHED) cache2.clear();
1812
+ cache2.set(key2, w);
1813
+ }
1814
+ return w;
1815
+ };
1816
+ }
1817
+
1818
+ // src/VisualliCanvas.tsx
1819
+ import { jsx as jsx9, jsxs as jsxs3 } from "react/jsx-runtime";
2069
1820
  function resolveDocument(props, fileText) {
2070
1821
  if (props.preParsedVisualli) return props.preParsedVisualli;
2071
1822
  const rawContent = fileText ?? props.visualliString;
@@ -2090,6 +1841,13 @@ function layerLabel(doc, layer, layerId) {
2090
1841
  }
2091
1842
  return layer.description ?? layerId;
2092
1843
  }
1844
+ function layerPaint(doc, layer) {
1845
+ const parentLayer = layer.parentLayerId ? doc.layers.get(layer.parentLayerId) : null;
1846
+ const index = parentLayer ? parentLayer.nodes.findIndex((n) => n.id === layer.parentNodeId) : -1;
1847
+ if (!parentLayer || index < 0) return { topic: TRAIL2.rootTopic };
1848
+ const { topic, custom } = ideaColor(parentLayer.nodes[index].data.color, index);
1849
+ return topic ? { topic } : { custom };
1850
+ }
2093
1851
  function getRootLayerId(doc) {
2094
1852
  for (const [id, layer] of doc.layers) {
2095
1853
  if (!layer.parentLayerId) return id;
@@ -2097,9 +1855,24 @@ function getRootLayerId(doc) {
2097
1855
  return doc.layers.keys().next().value ?? null;
2098
1856
  }
2099
1857
  function VisualliCanvas(props) {
2100
- const { isDark = false, chromaticImmersion = false, onNodeClick, onLayerChange, onNodeHover, renderOverlay, renderNodeContent, navigationStackTop = "16px", navigationStackLeft = "16px", className = "", style } = props;
2101
- const [fileText, setFileText] = useState3(void 0);
2102
- useEffect6(() => {
1858
+ const { chromaticImmersion = false, onNodeClick, onLayerChange, onNodeHover, renderOverlay, renderNodeContent, navigationStackTop, navigationStackLeft, className = "", style } = props;
1859
+ const onNodeHoverRef = useRef10(onNodeHover);
1860
+ onNodeHoverRef.current = onNodeHover;
1861
+ const design = useDesign({ theme: props.theme ?? (props.isDark === void 0 ? "light" : void 0), isDark: props.isDark, comfort: props.comfort, respectForcedColors: props.respectForcedColors });
1862
+ const isDark = design.isDark;
1863
+ const [fontSheet] = useState4(() => ensureDesignSystemStyles({ fontBaseUrl: props.fontBaseUrl, loadWebFonts: props.loadWebFonts }));
1864
+ const [fontsReady, setFontsReady] = useState4(false);
1865
+ useEffect7(() => {
1866
+ let live = true;
1867
+ loadCanvasFonts(fontSheet).then(() => {
1868
+ if (live) setFontsReady(true);
1869
+ });
1870
+ return () => {
1871
+ live = false;
1872
+ };
1873
+ }, [fontSheet]);
1874
+ const [fileText, setFileText] = useState4(void 0);
1875
+ useEffect7(() => {
2103
1876
  if (!props.visualliFile) {
2104
1877
  setFileText(void 0);
2105
1878
  return;
@@ -2125,33 +1898,35 @@ function VisualliCanvas(props) {
2125
1898
  cancelled = true;
2126
1899
  };
2127
1900
  }, [props.visualliFile]);
2128
- const doc = useMemo7(() => resolveDocument(props, fileText), [props.preParsedVisualli, props.visualliString, props.visualliFile, fileText]);
2129
- const [navStack, setNavStack] = useState3([]);
2130
- const [currentLayerId, setCurrentLayerId] = useState3(null);
2131
- const parentViewports = useRef9([]);
2132
- const [isTransitioning, setIsTransitioning] = useState3(false);
2133
- const isTransitioningRef = useRef9(false);
2134
- useEffect6(() => {
1901
+ const doc = useMemo9(() => resolveDocument(props, fileText), [props.preParsedVisualli, props.visualliString, props.visualliFile, fileText]);
1902
+ const [navStack, setNavStack] = useState4([]);
1903
+ const [currentLayerId, setCurrentLayerId] = useState4(null);
1904
+ const parentViewports = useRef10([]);
1905
+ const stepTargetRef = useRef10(null);
1906
+ const [isTransitioning, setIsTransitioning] = useState4(false);
1907
+ const isTransitioningRef = useRef10(false);
1908
+ useEffect7(() => {
2135
1909
  if (!doc) return;
2136
1910
  const rootId = getRootLayerId(doc);
2137
1911
  if (!rootId) return;
2138
1912
  const rootLayer = doc.layers.get(rootId);
2139
1913
  setCurrentLayerId(rootId);
2140
- setNavStack([{ layerId: rootId, layer: rootLayer, label: "Home" }]);
1914
+ setNavStack([{ layerId: rootId, layer: rootLayer, label: doc.meta?.title || "Home", paint: { topic: TRAIL2.rootTopic } }]);
2141
1915
  parentViewports.current = [];
2142
1916
  }, [doc]);
2143
- const { flatNodes, connections, containers } = useMemo7(() => {
1917
+ const sizing = useMemo9(() => ({ measure: ideaMeasure(design), labelScale: design.labelScale }), [design.fontHand, design.labelScale, design.comfort.readableType, fontsReady]);
1918
+ const { flatNodes, connections, containers } = useMemo9(() => {
2144
1919
  if (!doc || !currentLayerId) return { flatNodes: [], connections: [], containers: [] };
2145
1920
  try {
2146
1921
  return {
2147
- flatNodes: getNodesForLayer(doc, currentLayerId),
1922
+ flatNodes: getNodesForLayer(doc, currentLayerId, sizing),
2148
1923
  connections: getConnectionsForLayer(doc, currentLayerId),
2149
1924
  containers: getContainersForLayer(doc, currentLayerId)
2150
1925
  };
2151
1926
  } catch {
2152
1927
  return { flatNodes: [], connections: [], containers: [] };
2153
1928
  }
2154
- }, [doc, currentLayerId]);
1929
+ }, [doc, currentLayerId, sizing]);
2155
1930
  const viewport = useViewportStore((s) => ({
2156
1931
  centerX: s.centerX,
2157
1932
  centerY: s.centerY,
@@ -2166,57 +1941,53 @@ function VisualliCanvas(props) {
2166
1941
  const updateNodePosition = useNodeStore((s) => s.updateNodePosition);
2167
1942
  const clearSel = useSelectionStore((s) => s.clear);
2168
1943
  const select = useSelectionStore((s) => s.select);
2169
- useEffect6(() => {
1944
+ useEffect7(() => {
2170
1945
  const map = new Map(flatNodes.map((n) => [n.id, n]));
2171
1946
  setNodes(map);
2172
1947
  clearSel();
2173
1948
  }, [flatNodes, setNodes, clearSel]);
2174
- const containerRef = useRef9(null);
2175
- const stageRef = useRef9(null);
2176
- const canvasWrapperRef = useRef9(null);
2177
- const baselineTransformRef = useRef9(null);
2178
- const konvaContentDivRef = useRef9(null);
2179
- const lastAnimViewportRef = useRef9(null);
2180
- const zoomOutTargetRef = useRef9(null);
2181
- const canvasSizeRef = useRef9({ width: 0, height: 0 });
2182
- const dragMoveRafRef = useRef9(null);
2183
- const dragMoveLatestRef = useRef9(null);
2184
- const dragStartPosRef = useRef9(/* @__PURE__ */ new Map());
2185
- const baseBgColor = isDark ? "#141412" : "#F0EDE6";
2186
- const chromaticBgColor = useMemo7(() => {
2187
- if (!chromaticImmersion || !doc || !currentLayerId) return baseBgColor;
2188
- const currentLayer = doc.layers.get(currentLayerId);
2189
- if (!currentLayer) return baseBgColor;
2190
- if (currentLayer.level === 0 || !currentLayer.parentNodeId) return baseBgColor;
2191
- const parentLayer = currentLayer.parentLayerId ? doc.layers.get(currentLayer.parentLayerId) : null;
2192
- if (parentLayer) {
2193
- const parentNode = parentLayer.nodes.find((n) => n.id === currentLayer.parentNodeId);
2194
- if (parentNode) {
2195
- const nodeColor = parentNode.data.color || baseBgColor;
2196
- return hexToRgba2(nodeColor, 0.15);
2197
- }
2198
- }
2199
- return baseBgColor;
2200
- }, [chromaticImmersion, doc, currentLayerId, baseBgColor]);
2201
- const bgColor = chromaticImmersion ? chromaticBgColor : baseBgColor;
2202
- const spatialIndexRef = useRef9(null);
2203
- useEffect6(() => {
1949
+ const clock = useMemo9(() => new RevealClock(), []);
1950
+ const instantReveal = design.comfort.reducedMotion || props.reveal === "instant";
1951
+ useEffect7(() => {
1952
+ if (!fontsReady || flatNodes.length === 0) return;
1953
+ clock.start(flatNodes, connections.length, instantReveal);
1954
+ const r1 = requestAnimationFrame(() => {
1955
+ r2 = requestAnimationFrame(arrive);
1956
+ });
1957
+ let r2 = 0;
1958
+ return () => {
1959
+ cancelAnimationFrame(r1);
1960
+ cancelAnimationFrame(r2);
1961
+ };
1962
+ }, [flatNodes, fontsReady]);
1963
+ useEffect7(() => clock.subscribe((running) => {
1964
+ const el = containerRef.current;
1965
+ if (el) el.dataset.viReveal = running ? "running" : "idle";
1966
+ }), [clock]);
1967
+ useEffect7(() => () => clock.stop(), [clock]);
1968
+ const containerRef = useRef10(null);
1969
+ const stageRef = useRef10(null);
1970
+ const canvasWrapperRef = useRef10(null);
1971
+ const canvasSizeRef = useRef10({ width: 0, height: 0 });
1972
+ const dragMoveRafRef = useRef10(null);
1973
+ const dragMoveLatestRef = useRef10(null);
1974
+ const dragStartPosRef = useRef10(/* @__PURE__ */ new Map());
1975
+ const immersionFill = useMemo9(() => {
1976
+ if (!chromaticImmersion || !doc || !currentLayerId) return null;
1977
+ const layer = doc.layers.get(currentLayerId);
1978
+ if (!layer || layer.level === 0 || !layer.parentNodeId) return null;
1979
+ return paintVars(layerPaint(doc, layer)).fill;
1980
+ }, [chromaticImmersion, doc, currentLayerId]);
1981
+ const spatialIndexRef = useRef10(null);
1982
+ useEffect7(() => {
2204
1983
  if (flatNodes.length === 0) {
2205
1984
  spatialIndexRef.current?.clear();
2206
1985
  return;
2207
1986
  }
2208
1987
  const idx = new RBushSpatialIndex2();
2209
1988
  idx.bulkLoad(flatNodes.map((n) => {
2210
- const blobHalfH = Math.max(NODE_HEIGHT5 * 1.6 / 2, n.width / 2 * 0.74);
2211
- return {
2212
- nodeId: n.id,
2213
- bounds: {
2214
- minX: n.x - n.width / 2,
2215
- minY: n.y - blobHalfH,
2216
- maxX: n.x + n.width / 2,
2217
- maxY: n.y + blobHalfH
2218
- }
2219
- };
1989
+ const { rx, ry } = nodeRadii3(n);
1990
+ return { nodeId: n.id, bounds: { minX: n.x - rx, minY: n.y - ry, maxX: n.x + rx, maxY: n.y + ry } };
2220
1991
  }));
2221
1992
  spatialIndexRef.current = idx;
2222
1993
  return () => {
@@ -2228,6 +1999,7 @@ function VisualliCanvas(props) {
2228
1999
  handleMouseDown: rendererHandleMouseDown,
2229
2000
  handleMouseMove: rendererHandleMouseMove,
2230
2001
  handleMouseUp: rendererHandleMouseUp,
2002
+ cancelPan: rendererCancelPan,
2231
2003
  canvasWidth,
2232
2004
  canvasHeight
2233
2005
  } = useKonvaRenderer({ containerRef, stageRef });
@@ -2267,57 +2039,64 @@ function VisualliCanvas(props) {
2267
2039
  if (node) {
2268
2040
  const oldPos = dragStartPosRef.current.get(nodeId);
2269
2041
  if (oldPos) {
2270
- const oldHalfH = Math.max(NODE_HEIGHT5 * 1.6 / 2, node.width / 2 * 0.74);
2042
+ const { rx: rx2, ry: ry2 } = nodeRadii3(node);
2271
2043
  spatialIndexRef.current.remove(nodeId, {
2272
- minX: oldPos.x - node.width / 2,
2273
- minY: oldPos.y - oldHalfH,
2274
- maxX: oldPos.x + node.width / 2,
2275
- maxY: oldPos.y + oldHalfH
2044
+ minX: oldPos.x - rx2,
2045
+ minY: oldPos.y - ry2,
2046
+ maxX: oldPos.x + rx2,
2047
+ maxY: oldPos.y + ry2
2276
2048
  });
2277
2049
  }
2278
- const newHalfH = Math.max(NODE_HEIGHT5 * 1.6 / 2, node.width / 2 * 0.74);
2050
+ const { rx, ry } = nodeRadii3(node);
2279
2051
  spatialIndexRef.current.insert(nodeId, {
2280
- minX: x - node.width / 2,
2281
- minY: y - newHalfH,
2282
- maxX: x + node.width / 2,
2283
- maxY: y + newHalfH
2052
+ minX: x - rx,
2053
+ minY: y - ry,
2054
+ maxX: x + rx,
2055
+ maxY: y + ry
2284
2056
  });
2285
2057
  }
2286
2058
  }
2287
2059
  dragStartPosRef.current.delete(nodeId);
2288
2060
  }, [updateNodePosition, nodes]);
2289
- const fitToScreenRef = useRef9(() => {
2061
+ const fitToScreenRef = useRef10(() => {
2290
2062
  });
2291
2063
  const fitToScreen = useCallback4(() => {
2292
2064
  if (flatNodes.length === 0) return;
2293
2065
  if (isTransitioningRef.current) return;
2294
2066
  const cw = canvasSizeRef.current.width || containerRef.current?.getBoundingClientRect().width || 800;
2295
2067
  const ch = canvasSizeRef.current.height || containerRef.current?.getBoundingClientRect().height || 600;
2296
- const { centerX, centerY } = calculateFitView(flatNodes, cw, ch);
2297
- const isRootLayer = navStack.length <= 1;
2298
- const zoomLevel = isRootLayer ? 1 : calculateFitView(flatNodes, cw, ch).zoomLevel;
2068
+ const { centerX, centerY, zoomLevel } = calculateFitView(flatNodes, cw, ch, containers);
2069
+ useViewportStore.getState().setFit(zoomLevel, centerX, centerY);
2299
2070
  setCenter(centerX, centerY);
2300
2071
  setZoom(zoomLevel);
2301
- }, [flatNodes, navStack.length, setCenter, setZoom]);
2072
+ }, [flatNodes, containers, setCenter, setZoom]);
2302
2073
  fitToScreenRef.current = fitToScreen;
2303
- useEffect6(() => {
2074
+ useEffect7(() => {
2304
2075
  fitToScreen();
2305
2076
  }, [currentLayerId]);
2306
- const didFitAfterMeasureRef = useRef9(false);
2307
- useEffect6(() => {
2077
+ const didFitAfterMeasureRef = useRef10(false);
2078
+ useEffect7(() => {
2308
2079
  if (!canvasWidth || didFitAfterMeasureRef.current) return;
2309
2080
  didFitAfterMeasureRef.current = true;
2310
2081
  fitToScreen();
2311
2082
  }, [canvasWidth, fitToScreen]);
2312
- const navBackTargetRef = useRef9(-1);
2313
- const { zoomIntoLayer, zoomOutToParent, isTransitioning: isAnimating } = useKonvaLayerTransition({
2083
+ const navBackTargetRef = useRef10(-1);
2084
+ const { stepInside: diveInto, backOut, arrive, isTransitioning: isAnimating } = useLayerChoreography({
2314
2085
  stageRef,
2315
2086
  canvasWrapperRef,
2087
+ reducedMotion: design.comfort.reducedMotion || props.reveal === "instant",
2316
2088
  onSwapLayer: (childLayerId) => {
2317
2089
  const childLayer = doc?.layers.get(childLayerId);
2318
2090
  if (!childLayer || !doc) return;
2319
2091
  setCurrentLayerId(childLayerId);
2320
- setNavStack((prev) => [...prev, { layerId: childLayerId, layer: childLayer, label: layerLabel(doc, childLayer, childLayerId) }]);
2092
+ const target = stepTargetRef.current;
2093
+ stepTargetRef.current = null;
2094
+ setNavStack((prev) => [...prev, {
2095
+ layerId: childLayerId,
2096
+ layer: childLayer,
2097
+ label: target?.label || layerLabel(doc, childLayer, childLayerId),
2098
+ paint: target?.paint ?? layerPaint(doc, childLayer)
2099
+ }]);
2321
2100
  onLayerChange?.(childLayerId, childLayer);
2322
2101
  },
2323
2102
  onSwapBack: () => {
@@ -2337,96 +2116,6 @@ function VisualliCanvas(props) {
2337
2116
  requestAnimationFrame(() => {
2338
2117
  fitToScreenRef.current();
2339
2118
  });
2340
- },
2341
- // GPU CSS-transform callbacks (matching visualli.ai exactly):
2342
- onViewportFrame: (vp) => {
2343
- lastAnimViewportRef.current = vp;
2344
- const baseline = baselineTransformRef.current;
2345
- const contentDiv = konvaContentDivRef.current;
2346
- const { width, height } = canvasSizeRef.current;
2347
- if (baseline && contentDiv) {
2348
- const targetX = width / 2 - vp.centerX * vp.zoomLevel;
2349
- const targetY = height / 2 - vp.centerY * vp.zoomLevel;
2350
- const ratio = vp.zoomLevel / baseline.scaleX;
2351
- const tx = targetX - baseline.x * ratio;
2352
- const ty = targetY - baseline.y * ratio;
2353
- contentDiv.style.transform = `translate(${tx}px, ${ty}px) scale(${ratio})`;
2354
- } else {
2355
- const stage = stageRef.current;
2356
- if (stage) {
2357
- stage.x(width / 2 - vp.centerX * vp.zoomLevel);
2358
- stage.y(height / 2 - vp.centerY * vp.zoomLevel);
2359
- stage.scaleX(vp.zoomLevel);
2360
- stage.scaleY(vp.zoomLevel);
2361
- stage.batchDraw();
2362
- }
2363
- }
2364
- },
2365
- onViewportFinal: (vp) => {
2366
- if (konvaContentDivRef.current) konvaContentDivRef.current.style.transform = "";
2367
- const stage = stageRef.current;
2368
- if (stage) {
2369
- const { width, height } = canvasSizeRef.current;
2370
- stage.x(width / 2 - vp.centerX * vp.zoomLevel);
2371
- stage.y(height / 2 - vp.centerY * vp.zoomLevel);
2372
- stage.scaleX(vp.zoomLevel);
2373
- stage.scaleY(vp.zoomLevel);
2374
- stage.getLayers().forEach((l) => l.drawScene());
2375
- }
2376
- setCenter(vp.centerX, vp.centerY);
2377
- setZoom(vp.zoomLevel);
2378
- },
2379
- onPhaseChange: (phase) => {
2380
- const stage = stageRef.current;
2381
- const contentDiv = konvaContentDivRef.current;
2382
- if (stage && contentDiv && (phase === "swap" || phase === "zoom-back")) {
2383
- contentDiv.style.transform = "";
2384
- const vp = phase === "zoom-back" ? zoomOutTargetRef.current : lastAnimViewportRef.current;
2385
- if (vp) {
2386
- const { width, height } = canvasSizeRef.current;
2387
- const nx = width / 2 - vp.centerX * vp.zoomLevel;
2388
- const ny = height / 2 - vp.centerY * vp.zoomLevel;
2389
- stage.x(nx);
2390
- stage.y(ny);
2391
- stage.scaleX(vp.zoomLevel);
2392
- stage.scaleY(vp.zoomLevel);
2393
- stage.getLayers().forEach((l) => l.drawScene());
2394
- baselineTransformRef.current = { x: nx, y: ny, scaleX: vp.zoomLevel, scaleY: vp.zoomLevel };
2395
- }
2396
- }
2397
- },
2398
- onTransitionLifecycle: (phase) => {
2399
- const stage = stageRef.current;
2400
- if (phase === "start") {
2401
- if (canvasWrapperRef.current) canvasWrapperRef.current.style.willChange = "opacity, transform";
2402
- if (stage) {
2403
- baselineTransformRef.current = { x: stage.x(), y: stage.y(), scaleX: stage.scaleX(), scaleY: stage.scaleY() };
2404
- const container = stage.container();
2405
- const cd = container?.querySelector(".konvajs-content");
2406
- if (cd) {
2407
- konvaContentDivRef.current = cd;
2408
- cd.style.transformOrigin = "0 0";
2409
- cd.style.willChange = "transform";
2410
- }
2411
- stage.getLayers().forEach((l) => {
2412
- l.listening(false);
2413
- });
2414
- }
2415
- } else {
2416
- if (canvasWrapperRef.current) canvasWrapperRef.current.style.willChange = "auto";
2417
- if (konvaContentDivRef.current) {
2418
- konvaContentDivRef.current.style.willChange = "auto";
2419
- konvaContentDivRef.current = null;
2420
- }
2421
- baselineTransformRef.current = null;
2422
- zoomOutTargetRef.current = null;
2423
- if (stage) {
2424
- stage.getLayers().forEach((l) => {
2425
- l.listening(true);
2426
- });
2427
- stage.batchDraw();
2428
- }
2429
- }
2430
2119
  }
2431
2120
  });
2432
2121
  const handleNavigate = useCallback4((nodeId) => {
@@ -2446,7 +2135,7 @@ function VisualliCanvas(props) {
2446
2135
  }
2447
2136
  const childNodes = (() => {
2448
2137
  try {
2449
- return getNodesForLayer(doc, childLayerId);
2138
+ return getNodesForLayer(doc, childLayerId, sizing);
2450
2139
  } catch {
2451
2140
  return [];
2452
2141
  }
@@ -2458,20 +2147,13 @@ function VisualliCanvas(props) {
2458
2147
  if (!node) return;
2459
2148
  const cw = canvasSizeRef.current.width || 800;
2460
2149
  const ch = canvasSizeRef.current.height || 600;
2461
- const fitVp = calculateFitView(flatNodes, cw, ch);
2462
- const isRootNow = navStack.length <= 1;
2463
- parentViewports.current.push({ centerX: fitVp.centerX, centerY: fitVp.centerY, zoomLevel: isRootNow ? 0.85 : fitVp.zoomLevel });
2150
+ const fitVp = calculateFitView(flatNodes, cw, ch, containers);
2151
+ parentViewports.current.push({ centerX: fitVp.centerX, centerY: fitVp.centerY, zoomLevel: fitVp.zoomLevel });
2152
+ stepTargetRef.current = { paint: { topic: node.topic, custom: node.custom }, label: node.title };
2464
2153
  setIsTransitioning(true);
2465
2154
  isTransitioningRef.current = true;
2466
- zoomIntoLayer(
2467
- doc,
2468
- node,
2469
- childLayerId,
2470
- childNodes,
2471
- node.color ?? getThemeBackground(isDark, "secondary"),
2472
- node.color ?? getThemeBackground(isDark, "primary")
2473
- );
2474
- }, [doc, currentLayerId, nodes, isDark, isAnimating, zoomIntoLayer]);
2155
+ diveInto(node, childLayerId, childNodes);
2156
+ }, [doc, currentLayerId, nodes, isAnimating, diveInto, sizing, flatNodes, containers]);
2475
2157
  const handleNavigateBack = useCallback4((targetIndex) => {
2476
2158
  if (targetIndex >= navStack.length - 1) return;
2477
2159
  if (isTransitioningRef.current || isAnimating()) return;
@@ -2480,45 +2162,8 @@ function VisualliCanvas(props) {
2480
2162
  navBackTargetRef.current = targetIndex;
2481
2163
  setIsTransitioning(true);
2482
2164
  isTransitioningRef.current = true;
2483
- zoomOutToParent(savedVp, getThemeBackground(isDark, "primary"), getThemeBackground(isDark, "secondary"));
2484
- }, [navStack.length, isDark, isAnimating, zoomOutToParent]);
2485
- const baseZoomRef = useRef9(1);
2486
- const lastZoomTransitionRef = useRef9(0);
2487
- const ZOOM_COOLDOWN = 1e3;
2488
- useEffect6(() => {
2489
- baseZoomRef.current = 1;
2490
- }, [currentLayerId]);
2491
- useEffect6(() => {
2492
- if (isTransitioningRef.current || isAnimating()) return;
2493
- const now = Date.now();
2494
- if (now - lastZoomTransitionRef.current < ZOOM_COOLDOWN) return;
2495
- const relative = viewport.zoomLevel / baseZoomRef.current;
2496
- if (relative >= ZOOM_NAV_IN_THRESHOLD) {
2497
- let closest = null;
2498
- let minDist = Infinity;
2499
- for (const node of nodes.values()) {
2500
- const dx = node.x - viewport.centerX;
2501
- const dy = node.y - viewport.centerY;
2502
- const d = dx * dx + dy * dy;
2503
- if (d < minDist) {
2504
- minDist = d;
2505
- closest = node;
2506
- }
2507
- }
2508
- if (closest) {
2509
- const childLayer = doc ? getChildLayerForNode(doc, closest.id, currentLayerId ?? "") : null;
2510
- if (childLayer) {
2511
- lastZoomTransitionRef.current = now;
2512
- handleNavigate(closest.id);
2513
- baseZoomRef.current = 1;
2514
- }
2515
- }
2516
- } else if (relative < ZOOM_NAV_OUT_THRESHOLD && navStack.length > 1) {
2517
- lastZoomTransitionRef.current = now;
2518
- handleNavigateBack(navStack.length - 2);
2519
- baseZoomRef.current = 1;
2520
- }
2521
- }, [viewport.zoomLevel, viewport.centerX, viewport.centerY, nodes, doc, currentLayerId, navStack, isAnimating, handleNavigate, handleNavigateBack]);
2165
+ backOut(savedVp);
2166
+ }, [navStack.length, isAnimating, backOut]);
2522
2167
  const toWorldCoords = useCallback4((stage, pointer) => {
2523
2168
  const sp = stage.position();
2524
2169
  const sc = stage.scaleX();
@@ -2540,22 +2185,22 @@ function VisualliCanvas(props) {
2540
2185
  }
2541
2186
  return targetId;
2542
2187
  }, [nodes]);
2543
- const stageActiveNodeIdRef = useRef9(null);
2544
- const stageDragNodeOffsetRef = useRef9({ x: 0, y: 0 });
2545
- const stageMouseDownPosRef = useRef9(null);
2546
- const stageDragCommittedRef = useRef9(false);
2547
- const stageLastPointerCheckRef = useRef9(0);
2548
- const pointerNodeIdRef = useRef9(null);
2549
- const isCanvasPanningRef = useRef9(false);
2550
- const isDraggingRef = useRef9(false);
2551
- const [pointerNodeId, setPointerNodeId] = useState3(null);
2552
- const [pressedNodeId, setPressedNodeId] = useState3(null);
2553
- const [isDraggingState, setIsDraggingState] = useState3(false);
2554
- const [hoveredNode, setHoveredNode] = useState3(null);
2555
- const tooltipHoverRef = useRef9(false);
2556
- const keepTooltipOpenRef = useRef9(false);
2557
- const clearTooltipTimerRef = useRef9(null);
2558
- const hoveredNodePosition = useMemo7(() => {
2188
+ const stageActiveNodeIdRef = useRef10(null);
2189
+ const stageDragNodeOffsetRef = useRef10({ x: 0, y: 0 });
2190
+ const stageMouseDownPosRef = useRef10(null);
2191
+ const stageDragCommittedRef = useRef10(false);
2192
+ const stageLastPointerCheckRef = useRef10(0);
2193
+ const pointerNodeIdRef = useRef10(null);
2194
+ const isCanvasPanningRef = useRef10(false);
2195
+ const isDraggingRef = useRef10(false);
2196
+ const [pointerNodeId, setPointerNodeId] = useState4(null);
2197
+ const [pressedNodeId, setPressedNodeId] = useState4(null);
2198
+ const [isDraggingState, setIsDraggingState] = useState4(false);
2199
+ const [hoveredNode, setHoveredNode] = useState4(null);
2200
+ const tooltipHoverRef = useRef10(false);
2201
+ const keepTooltipOpenRef = useRef10(false);
2202
+ const clearTooltipTimerRef = useRef10(null);
2203
+ const hoveredNodePosition = useMemo9(() => {
2559
2204
  if (!hoveredNode) return null;
2560
2205
  const node = nodes.get(hoveredNode.nodeId);
2561
2206
  if (!node) return null;
@@ -2567,8 +2212,8 @@ function VisualliCanvas(props) {
2567
2212
  zoom: viewport.zoomLevel
2568
2213
  };
2569
2214
  }, [hoveredNode, nodes, viewport]);
2570
- const [canvasContextMenu, setCanvasContextMenu] = useState3(null);
2571
- useEffect6(() => {
2215
+ const [canvasContextMenu, setCanvasContextMenu] = useState4(null);
2216
+ useEffect7(() => {
2572
2217
  if (!canvasContextMenu) return;
2573
2218
  const close = () => setCanvasContextMenu(null);
2574
2219
  const onKey = (e) => {
@@ -2581,8 +2226,45 @@ function VisualliCanvas(props) {
2581
2226
  window.removeEventListener("keydown", onKey);
2582
2227
  };
2583
2228
  }, [canvasContextMenu]);
2229
+ const selectedNodeId = useSelectionStore((st) => st.selectedId);
2230
+ const [focusedNodeId, setFocusedNodeId] = useState4(null);
2231
+ const anchors = useMemo9(() => doc ? getSemanticAnchors(doc) : [], [doc]);
2232
+ const [autoTouch, setAutoTouch] = useState4(() => typeof window !== "undefined" && !!window.matchMedia && window.matchMedia("(hover: none)").matches);
2233
+ const touchMode = props.layout === "touch" ? true : props.layout === "pointer" ? false : autoTouch;
2234
+ const touchModeRef = useRef10(touchMode);
2235
+ touchModeRef.current = touchMode;
2236
+ const lastTouchAtRef = useRef10(0);
2237
+ const emptyTapRef = useRef10(null);
2238
+ const [pinnedId, setPinnedId] = useState4(null);
2239
+ const [sheetExpanded, setSheetExpanded] = useState4(false);
2240
+ const isEmulatedMouse = (evt) => evt.type.startsWith("mouse") && performance.now() - lastTouchAtRef.current < 700;
2241
+ const clientOf = (evt) => {
2242
+ const t = "touches" in evt && (evt.touches[0] ?? evt.changedTouches[0]) || evt;
2243
+ return { x: t.clientX, y: t.clientY };
2244
+ };
2245
+ const pinnedIdRef = useRef10(null);
2246
+ pinnedIdRef.current = pinnedId;
2247
+ const sheetHeight = Math.min(SHEET2.maxHeight, Math.round((viewport.canvasHeight || canvasSizeRef.current.height || 600) * SHEET2.heightRatio));
2248
+ const openSheet = (node) => {
2249
+ setPinnedId(node.id);
2250
+ setSheetExpanded(false);
2251
+ setHoveredNode(null);
2252
+ const vp = useViewportStore.getState();
2253
+ const ch = canvasSizeRef.current.height || vp.canvasHeight;
2254
+ const sh = Math.min(SHEET2.maxHeight, Math.round(ch * SHEET2.heightRatio));
2255
+ const screenY = (node.y - vp.centerY) * vp.zoomLevel + ch / 2;
2256
+ const reach = nodeRadii3(node).ry * vp.zoomLevel;
2257
+ if (screenY + reach > ch - sh - 12 || screenY - reach < 0) vp.setCenter(vp.centerX, node.y + sh / (2 * vp.zoomLevel));
2258
+ };
2259
+ useEffect7(() => {
2260
+ setPinnedId(null);
2261
+ }, [currentLayerId]);
2584
2262
  const handleStageMouseDown = useCallback4((e) => {
2585
- if (e.evt.button !== 0) {
2263
+ if (isEmulatedMouse(e.evt)) return;
2264
+ const isTouch = e.evt.type.startsWith("touch");
2265
+ if (isTouch) lastTouchAtRef.current = performance.now();
2266
+ if (props.layout === void 0 || props.layout === "auto") setAutoTouch(isTouch);
2267
+ if (!isTouch && e.evt.button !== 0) {
2586
2268
  rendererHandleMouseDown(e);
2587
2269
  return;
2588
2270
  }
@@ -2604,7 +2286,7 @@ function VisualliCanvas(props) {
2604
2286
  select(nodeId);
2605
2287
  stageActiveNodeIdRef.current = nodeId;
2606
2288
  stageDragNodeOffsetRef.current = { x: worldX - node.x, y: worldY - node.y };
2607
- stageMouseDownPosRef.current = { x: e.evt.clientX, y: e.evt.clientY };
2289
+ stageMouseDownPosRef.current = clientOf(e.evt);
2608
2290
  stageDragCommittedRef.current = false;
2609
2291
  setPressedNodeId(nodeId);
2610
2292
  setHoveredNode(null);
@@ -2612,10 +2294,12 @@ function VisualliCanvas(props) {
2612
2294
  }
2613
2295
  }
2614
2296
  setHoveredNode(null);
2297
+ emptyTapRef.current = clientOf(e.evt);
2615
2298
  isCanvasPanningRef.current = true;
2616
2299
  rendererHandleMouseDown(e);
2617
- }, [hitTestNode, toWorldCoords, nodes, select, rendererHandleMouseDown]);
2300
+ }, [hitTestNode, toWorldCoords, nodes, select, rendererHandleMouseDown, props.layout]);
2618
2301
  const handleStageMouseMove = useCallback4((e) => {
2302
+ if (isEmulatedMouse(e.evt)) return;
2619
2303
  const stage = e.target.getStage();
2620
2304
  if (!stage) {
2621
2305
  rendererHandleMouseMove(e);
@@ -2628,7 +2312,8 @@ function VisualliCanvas(props) {
2628
2312
  }
2629
2313
  const { worldX, worldY } = toWorldCoords(stage, pointer);
2630
2314
  if (stageActiveNodeIdRef.current && stageMouseDownPosRef.current) {
2631
- const moved = Math.hypot(e.evt.clientX - stageMouseDownPosRef.current.x, e.evt.clientY - stageMouseDownPosRef.current.y);
2315
+ const cur = clientOf(e.evt);
2316
+ const moved = Math.hypot(cur.x - stageMouseDownPosRef.current.x, cur.y - stageMouseDownPosRef.current.y);
2632
2317
  if (!stageDragCommittedRef.current && moved >= 5) {
2633
2318
  stageDragCommittedRef.current = true;
2634
2319
  isDraggingRef.current = true;
@@ -2640,7 +2325,6 @@ function VisualliCanvas(props) {
2640
2325
  if (stageDragCommittedRef.current) {
2641
2326
  const offset = stageDragNodeOffsetRef.current;
2642
2327
  handleDragMove(stageActiveNodeIdRef.current, worldX - offset.x, worldY - offset.y);
2643
- stage.container().style.cursor = "grabbing";
2644
2328
  return;
2645
2329
  }
2646
2330
  return;
@@ -2656,17 +2340,14 @@ function VisualliCanvas(props) {
2656
2340
  if (hovered !== pointerNodeIdRef.current) {
2657
2341
  pointerNodeIdRef.current = hovered;
2658
2342
  setPointerNodeId(hovered);
2659
- onNodeHover?.(hovered);
2660
- }
2661
- if (hovered) {
2662
- stage.container().style.cursor = "pointer";
2663
- } else {
2664
- stage.container().style.cursor = "grab";
2343
+ onNodeHoverRef.current?.(hovered);
2665
2344
  }
2345
+ stage.container().style.cursor = hovered ? "pointer" : "";
2666
2346
  }
2667
2347
  rendererHandleMouseMove(e);
2668
2348
  }, [toWorldCoords, hitTestNode, handleDragStart, handleDragMove, rendererHandleMouseMove]);
2669
2349
  const handleStageMouseUp = useCallback4((e) => {
2350
+ if (isEmulatedMouse(e.evt)) return;
2670
2351
  setPressedNodeId(null);
2671
2352
  if (stageActiveNodeIdRef.current) {
2672
2353
  const nodeId = stageActiveNodeIdRef.current;
@@ -2684,7 +2365,12 @@ function VisualliCanvas(props) {
2684
2365
  const node = nodes.get(nodeId);
2685
2366
  if (node) {
2686
2367
  const childLayer = doc ? getChildLayerForNode(doc, nodeId, currentLayerId ?? "") : null;
2687
- if (childLayer) {
2368
+ if (touchModeRef.current && childLayer && pinnedIdRef.current === nodeId) {
2369
+ setPinnedId(null);
2370
+ handleNavigate(nodeId);
2371
+ } else if (touchModeRef.current && (node.description || childLayer)) {
2372
+ openSheet(node);
2373
+ } else if (childLayer) {
2688
2374
  handleNavigate(nodeId);
2689
2375
  }
2690
2376
  onNodeClick?.(node);
@@ -2697,6 +2383,12 @@ function VisualliCanvas(props) {
2697
2383
  stageDragCommittedRef.current = false;
2698
2384
  return;
2699
2385
  }
2386
+ const tap = emptyTapRef.current;
2387
+ emptyTapRef.current = null;
2388
+ if (tap && pinnedIdRef.current) {
2389
+ const end = clientOf(e.evt);
2390
+ if (!Number.isFinite(end.x) || Math.hypot(end.x - tap.x, end.y - tap.y) < 6) setPinnedId(null);
2391
+ }
2700
2392
  isCanvasPanningRef.current = false;
2701
2393
  rendererHandleMouseUp(e);
2702
2394
  }, [toWorldCoords, handleDragEnd, nodes, handleNavigate, onNodeClick, rendererHandleMouseUp]);
@@ -2711,31 +2403,75 @@ function VisualliCanvas(props) {
2711
2403
  const localY = rect ? e.evt.clientY - rect.top : e.evt.clientY;
2712
2404
  setCanvasContextMenu({ x: localX, y: localY });
2713
2405
  }, [navStack.length]);
2714
- const handleStageTouchStart = useCallback4((e) => handleStageMouseDown(e), [handleStageMouseDown]);
2715
- const handleStageTouchMove = useCallback4((e) => handleStageMouseMove(e), [handleStageMouseMove]);
2716
- const handleStageTouchEnd = useCallback4((e) => handleStageMouseUp(e), [handleStageMouseUp]);
2717
- const handleWheel = useCallback4((e) => {
2718
- const isZoomOut = e.evt.deltaY > 0;
2719
- if (!isZoomOut) {
2720
- rendererHandleWheel(e);
2721
- return;
2406
+ const touchPointsRef = useRef10(/* @__PURE__ */ new Map());
2407
+ const pinchRef = useRef10(null);
2408
+ const pinchEndedAtRef = useRef10(-Infinity);
2409
+ const [pinching, setPinching] = useState4(false);
2410
+ const localOf = (e) => {
2411
+ const r = containerRef.current.getBoundingClientRect();
2412
+ return { x: e.clientX - r.left, y: e.clientY - r.top };
2413
+ };
2414
+ const onPinchDown = (e) => {
2415
+ if (!GESTURE.pinchZoom || !GESTURE.pointers.includes(e.pointerType)) return;
2416
+ touchPointsRef.current.set(e.pointerId, localOf(e));
2417
+ if (touchPointsRef.current.size !== 2 || isTransitioningRef.current || isAnimating()) return;
2418
+ const [a, b] = [...touchPointsRef.current.values()];
2419
+ const vp = useViewportStore.getState();
2420
+ 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 } };
2421
+ rendererCancelPan();
2422
+ isCanvasPanningRef.current = false;
2423
+ emptyTapRef.current = null;
2424
+ stageActiveNodeIdRef.current = null;
2425
+ stageDragCommittedRef.current = false;
2426
+ isDraggingRef.current = false;
2427
+ setPressedNodeId(null);
2428
+ setPinching(true);
2429
+ };
2430
+ const onPinchMove = (e) => {
2431
+ if (!touchPointsRef.current.has(e.pointerId)) return;
2432
+ touchPointsRef.current.set(e.pointerId, localOf(e));
2433
+ const p = pinchRef.current;
2434
+ if (!p || touchPointsRef.current.size < 2) return;
2435
+ const [a, b] = [...touchPointsRef.current.values()];
2436
+ const fit = useViewportStore.getState().fitScale;
2437
+ const z = pinchZoom(p.z0 / fit, p.d0, Math.hypot(a.x - b.x, a.y - b.y)) * fit;
2438
+ const m = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
2439
+ const cw = canvasSizeRef.current.width || containerRef.current?.clientWidth || 800;
2440
+ const ch = canvasSizeRef.current.height || containerRef.current?.clientHeight || 600;
2441
+ const t = zoomAround({ x: cw / 2 - p.c0.x * p.z0, y: ch / 2 - p.c0.y * p.z0 }, p.z0, z, p.m0, GESTURE.twoFingerPan ? m : p.m0);
2442
+ setZoom(z);
2443
+ setCenter((cw / 2 - t.x) / z, (ch / 2 - t.y) / z);
2444
+ };
2445
+ const onPinchUp = (e) => {
2446
+ if (!touchPointsRef.current.delete(e.pointerId)) return;
2447
+ if (pinchRef.current && touchPointsRef.current.size < 2) {
2448
+ pinchRef.current = null;
2449
+ pinchEndedAtRef.current = performance.now();
2450
+ setPinching(false);
2722
2451
  }
2723
- const liveZoom = useViewportStore.getState().zoomLevel;
2724
- const projected = Math.max(ZOOM_MIN2, liveZoom * (1 - e.evt.deltaY / 1e3));
2725
- if (navStack.length > 1 && projected <= ZOOM_NAV_OUT_THRESHOLD && !isTransitioningRef.current && !isAnimating()) {
2726
- e.evt.preventDefault();
2727
- const now = Date.now();
2728
- if (now - lastZoomTransitionRef.current >= ZOOM_COOLDOWN) {
2729
- lastZoomTransitionRef.current = now;
2730
- handleNavigateBack(navStack.length - 2);
2731
- baseZoomRef.current = 1;
2732
- }
2452
+ };
2453
+ const inPinch = () => pinchRef.current !== null || touchPointsRef.current.size > 1 || performance.now() - pinchEndedAtRef.current < 300;
2454
+ const handleStageTouchStart = useCallback4((e) => {
2455
+ if (inPinch() || e.evt.touches.length > 1) return;
2456
+ handleStageMouseDown(e);
2457
+ }, [handleStageMouseDown]);
2458
+ const handleStageTouchMove = useCallback4((e) => {
2459
+ if (inPinch() || e.evt.touches.length > 1) return;
2460
+ handleStageMouseMove(e);
2461
+ }, [handleStageMouseMove]);
2462
+ const handleStageTouchEnd = useCallback4((e) => {
2463
+ if (inPinch()) {
2464
+ isCanvasPanningRef.current = false;
2733
2465
  return;
2734
2466
  }
2467
+ handleStageMouseUp(e);
2468
+ }, [handleStageMouseUp]);
2469
+ const handleWheel = useCallback4((e) => {
2735
2470
  rendererHandleWheel(e);
2736
- }, [rendererHandleWheel, navStack, setZoom, isAnimating, handleNavigateBack]);
2737
- const lastHoverCheckRef = useRef9(0);
2471
+ }, [rendererHandleWheel]);
2472
+ const lastHoverCheckRef = useRef10(0);
2738
2473
  const handleCanvasMouseMove = useCallback4((e) => {
2474
+ if (touchModeRef.current || isEmulatedMouse(e.nativeEvent)) return;
2739
2475
  if (!containerRef.current || isDraggingRef.current || isCanvasPanningRef.current) {
2740
2476
  setHoveredNode(null);
2741
2477
  return;
@@ -2771,7 +2507,7 @@ function VisualliCanvas(props) {
2771
2507
  if (clearTooltipTimerRef.current) clearTimeout(clearTooltipTimerRef.current);
2772
2508
  clearTooltipTimerRef.current = setTimeout(() => {
2773
2509
  if (!tooltipHoverRef.current && !keepTooltipOpenRef.current) setHoveredNode(null);
2774
- }, 150);
2510
+ }, PEEK.hoverClose);
2775
2511
  } else {
2776
2512
  if (clearTooltipTimerRef.current) {
2777
2513
  clearTimeout(clearTooltipTimerRef.current);
@@ -2779,7 +2515,7 @@ function VisualliCanvas(props) {
2779
2515
  }
2780
2516
  }
2781
2517
  }, [nodes, viewport, spatialIndexRef]);
2782
- useEffect6(() => {
2518
+ useEffect7(() => {
2783
2519
  const handleKeepTooltipOpen = (event) => {
2784
2520
  keepTooltipOpenRef.current = event.detail.keep;
2785
2521
  };
@@ -2788,35 +2524,84 @@ function VisualliCanvas(props) {
2788
2524
  window.removeEventListener("keepNodeTooltipOpen", handleKeepTooltipOpen);
2789
2525
  };
2790
2526
  }, []);
2791
- useEffect6(() => {
2527
+ useEffect7(() => {
2792
2528
  return () => {
2793
2529
  if (clearTooltipTimerRef.current) clearTimeout(clearTooltipTimerRef.current);
2794
2530
  spatialIndexRef.current?.clear();
2795
2531
  if (dragMoveRafRef.current !== null) cancelAnimationFrame(dragMoveRafRef.current);
2796
2532
  };
2797
2533
  }, []);
2798
- const handleZoomIn = useCallback4(() => setZoom(Math.min(viewport.zoomLevel * 1.2, 5)), [viewport.zoomLevel, setZoom]);
2799
- const handleZoomOut = useCallback4(() => {
2800
- setZoom(Math.max(viewport.zoomLevel / 1.2, ZOOM_MIN2));
2801
- }, [viewport.zoomLevel, setZoom]);
2534
+ const activateNode = useCallback4((node) => {
2535
+ const childLayer = doc ? getChildLayerForNode(doc, node.id, currentLayerId ?? "") : null;
2536
+ if (touchModeRef.current && childLayer && pinnedIdRef.current === node.id) {
2537
+ setPinnedId(null);
2538
+ handleNavigate(node.id);
2539
+ } else if (touchModeRef.current && (node.description || childLayer)) openSheet(node);
2540
+ else if (childLayer) handleNavigate(node.id);
2541
+ onNodeClick?.(node);
2542
+ }, [doc, currentLayerId, handleNavigate, onNodeClick]);
2543
+ const stepInside = useCallback4((nodeId) => {
2544
+ setHoveredNode(null);
2545
+ handleNavigate(nodeId);
2546
+ }, [handleNavigate]);
2547
+ useEffect7(() => {
2548
+ if (!hoveredNode) tooltipHoverRef.current = false;
2549
+ }, [hoveredNode]);
2550
+ const handleKeyDown = useCallback4((e) => {
2551
+ if (e.target.closest('input, textarea, [contenteditable="true"]')) return;
2552
+ const cmd = e.metaKey || e.ctrlKey;
2553
+ if (cmd && !e.altKey) {
2554
+ if (e.key === "+" || e.key === "=") {
2555
+ e.preventDefault();
2556
+ setZoom(viewport.zoomLevel * VIEW2.zoomStep);
2557
+ } else if (e.key === "-" || e.key === "_") {
2558
+ e.preventDefault();
2559
+ setZoom(viewport.zoomLevel / VIEW2.zoomStep);
2560
+ } else if (e.key === "0") {
2561
+ e.preventDefault();
2562
+ fitToScreen();
2563
+ }
2564
+ return;
2565
+ }
2566
+ if (e.altKey || e.shiftKey) return;
2567
+ if (e.key === "Escape" && pinnedIdRef.current) {
2568
+ e.preventDefault();
2569
+ setPinnedId(null);
2570
+ } else if ((e.key === "Escape" || e.key === "Backspace") && navStack.length > 1) {
2571
+ e.preventDefault();
2572
+ handleNavigateBack(navStack.length - 2);
2573
+ }
2574
+ }, [navStack.length, handleNavigateBack, setZoom, viewport.zoomLevel, fitToScreen]);
2802
2575
  if (!doc) {
2803
- return /* @__PURE__ */ jsx11("div", { className, style: { position: "relative", display: "flex", alignItems: "center", justifyContent: "center", background: bgColor, ...style }, children: /* @__PURE__ */ jsx11("span", { style: { color: isDark ? "rgba(255,255,255,0.4)" : "rgba(0,0,0,0.3)", fontSize: 14 }, children: "No document" }) });
2576
+ return /* @__PURE__ */ jsx9("div", { className: `vi-map ${className}`, ...design.attrs, style: { display: "flex", alignItems: "center", justifyContent: "center", ...style }, children: /* @__PURE__ */ jsx9("span", { className: "vi-text-body-sm", style: { color: "var(--ink-muted)" }, children: "No document" }) });
2804
2577
  }
2805
2578
  const hasBackOption = navStack.length > 1;
2806
- return /* @__PURE__ */ jsxs7(
2579
+ const currentLayerTitle = navStack[navStack.length - 1]?.label ?? "";
2580
+ const peekNode = hoveredNode ? nodes.get(hoveredNode.nodeId) : void 0;
2581
+ const pinnedNode = pinnedId ? nodes.get(pinnedId) : void 0;
2582
+ const pinnedHasChild = !!(pinnedNode && getChildLayerForNode(doc, pinnedNode.id, currentLayerId ?? ""));
2583
+ const compact = (viewport.canvasWidth || canvasSizeRef.current.width || 1024) < 560;
2584
+ const medium = !compact && (viewport.canvasWidth || canvasSizeRef.current.width || 1024) < 900;
2585
+ const peekHasChild = !!(peekNode && doc && getChildLayerForNode(doc, peekNode.id, currentLayerId ?? ""));
2586
+ return /* @__PURE__ */ jsxs3(
2807
2587
  "div",
2808
2588
  {
2809
2589
  ref: containerRef,
2810
- className,
2811
- style: {
2812
- position: "relative",
2813
- overflow: "hidden",
2814
- userSelect: "none",
2815
- background: bgColor,
2816
- width: "100%",
2817
- height: "100%",
2818
- ...style
2590
+ className: `vi-map${touchMode ? " is-touch" : ""}${compact ? " is-compact" : ""}${medium ? " is-medium" : ""}${pinching ? " is-pinching" : ""}${props.app ? " is-app" : ""} ${className}`,
2591
+ ...design.attrs,
2592
+ "data-reveal": instantReveal ? "instant" : void 0,
2593
+ role: "group",
2594
+ "aria-label": `${doc.meta?.title ?? "Map"}${currentLayerTitle ? `, ${currentLayerTitle}` : ""}`,
2595
+ onKeyDown: handleKeyDown,
2596
+ tabIndex: -1,
2597
+ onPointerDownCapture: (e) => {
2598
+ onPinchDown(e);
2599
+ if (!e.target.closest("button, a, input")) containerRef.current?.focus({ preventScroll: true });
2819
2600
  },
2601
+ onPointerMoveCapture: onPinchMove,
2602
+ onPointerUpCapture: onPinchUp,
2603
+ onPointerCancelCapture: onPinchUp,
2604
+ style: { position: "relative", overflow: "hidden", userSelect: "none", width: "100%", height: "100%", outline: "none", ...motionVars(), ...style },
2820
2605
  onMouseMove: handleCanvasMouseMove,
2821
2606
  onMouseLeave: () => {
2822
2607
  if (!tooltipHoverRef.current && !keepTooltipOpenRef.current) {
@@ -2824,7 +2609,8 @@ function VisualliCanvas(props) {
2824
2609
  }
2825
2610
  },
2826
2611
  children: [
2827
- /* @__PURE__ */ jsx11("div", { ref: canvasWrapperRef, style: { position: "relative", zIndex: 1, pointerEvents: "auto" }, children: /* @__PURE__ */ jsxs7(
2612
+ /* @__PURE__ */ jsx9("div", { className: "vi-map__immersion", style: { background: immersionFill ?? "transparent" }, "aria-hidden": "true" }),
2613
+ /* @__PURE__ */ jsx9("div", { ref: canvasWrapperRef, style: { position: "relative", zIndex: 1, pointerEvents: "auto" }, children: fontsReady && /* @__PURE__ */ jsxs3(
2828
2614
  KonvaStage_default,
2829
2615
  {
2830
2616
  ref: stageRef,
@@ -2837,56 +2623,59 @@ function VisualliCanvas(props) {
2837
2623
  onTouchMove: handleStageTouchMove,
2838
2624
  onTouchEnd: handleStageTouchEnd,
2839
2625
  children: [
2840
- /* @__PURE__ */ jsx11(KonvaContainerLayer, { nodes: flatNodes, containers, isDark }),
2841
- /* @__PURE__ */ jsx11(
2842
- KonvaEdgeLayer,
2843
- {
2844
- nodes: flatNodes,
2845
- connections,
2846
- isDark,
2847
- isDragging: isDraggingState
2848
- }
2849
- ),
2850
- /* @__PURE__ */ jsx11(
2626
+ /* @__PURE__ */ jsx9(KonvaContainerLayer, { nodes: flatNodes, containers, connections, design }),
2627
+ /* @__PURE__ */ jsx9(KonvaEdgeLayer, { nodes: flatNodes, connections, design, isDragging: isDraggingState, clock }),
2628
+ /* @__PURE__ */ jsx9(
2851
2629
  KonvaNodeLayer,
2852
2630
  {
2631
+ design,
2632
+ clock,
2853
2633
  isTransitioning,
2854
2634
  isDragging: isDraggingState,
2855
2635
  hoveredNodeId: pointerNodeId,
2856
- pressedNodeId
2636
+ pressedNodeId,
2637
+ selectedNodeId,
2638
+ focusedNodeId,
2639
+ touch: touchMode,
2640
+ peekedNodeId: touchMode ? pinnedId : null
2857
2641
  }
2858
- )
2642
+ ),
2643
+ /* @__PURE__ */ jsx9(KonvaContainerLabelLayer, { containers, connections, design })
2859
2644
  ]
2860
2645
  }
2861
2646
  ) }),
2862
- canvasContextMenu && hasBackOption && /* @__PURE__ */ jsx11(
2647
+ /* @__PURE__ */ jsx9(
2648
+ A11yLayer,
2649
+ {
2650
+ nodes: flatNodes,
2651
+ label: `Ideas in ${currentLayerTitle || "this map"}`,
2652
+ hasChildLayer: (id) => !!getChildLayerForNode(doc, id, currentLayerId ?? ""),
2653
+ onActivate: activateNode,
2654
+ onFocusNode: setFocusedNodeId,
2655
+ announcement: hasBackOption ? `Now inside ${currentLayerTitle}` : void 0
2656
+ }
2657
+ ),
2658
+ canvasContextMenu && hasBackOption && /* @__PURE__ */ jsx9(
2863
2659
  "div",
2864
2660
  {
2865
2661
  role: "menu",
2866
2662
  "aria-label": "Canvas options",
2867
2663
  style: {
2868
2664
  position: "absolute",
2869
- zIndex: 60,
2665
+ zIndex: "var(--z-menu)",
2870
2666
  pointerEvents: "auto",
2871
2667
  left: `${Math.min(canvasContextMenu.x, Math.max(8, (canvasSizeRef.current.width || 800) - 152))}px`,
2872
2668
  top: `${Math.min(canvasContextMenu.y, Math.max(8, (canvasSizeRef.current.height || 600) - 72))}px`
2873
2669
  },
2874
2670
  onContextMenu: (e) => e.preventDefault(),
2875
2671
  onPointerDown: (e) => e.stopPropagation(),
2876
- children: /* @__PURE__ */ jsx11(
2672
+ children: /* @__PURE__ */ jsx9(
2877
2673
  "button",
2878
2674
  {
2879
2675
  type: "button",
2880
- style: {
2881
- padding: "6px 16px",
2882
- borderRadius: "10px",
2883
- border: `1px solid ${isDark ? "#333330" : "#DDD9D0"}`,
2884
- background: isDark ? "#1C1C1A" : "#FAF8F4",
2885
- color: isDark ? "#F0EDE6" : "#1A1A18",
2886
- cursor: "pointer",
2887
- fontSize: 13,
2888
- boxShadow: "0 4px 12px rgba(0,0,0,0.15)"
2889
- },
2676
+ role: "menuitem",
2677
+ className: "vi-iconbtn vi-iconbtn--surface",
2678
+ style: { width: "auto", padding: "6px var(--space-4)", boxShadow: "var(--shadow-pop)" },
2890
2679
  onClick: () => {
2891
2680
  setCanvasContextMenu(null);
2892
2681
  handleNavigateBack(navStack.length - 2);
@@ -2896,32 +2685,41 @@ function VisualliCanvas(props) {
2896
2685
  )
2897
2686
  }
2898
2687
  ),
2899
- /* @__PURE__ */ jsx11(NavigationStack_default, { stack: navStack, onNavigateBack: handleNavigateBack, isDark, top: navigationStackTop, left: navigationStackLeft }),
2900
- /* @__PURE__ */ jsx11("div", { "data-help": "zoom-controls", style: { position: "absolute", top: "24px", right: "16px", zIndex: 50, pointerEvents: "auto" }, children: /* @__PURE__ */ jsx11(ZoomControls_default, { isDark }) }),
2901
- renderOverlay?.({ isDark, containerWidth: canvasSizeRef.current.width || 0, containerHeight: canvasSizeRef.current.height || 0 }),
2902
- hoveredNode && hoveredNodePosition && viewport.zoomLevel < 3 && viewport.zoomLevel >= TEXT_LABEL_HIDE_BELOW_ZOOM3 && (() => {
2903
- const nodeData = nodes.get(hoveredNode.nodeId);
2904
- const borderColor = nodeData?.color ? darkenHexColor2(nodeData.color, 0.13) : "#5BA8D4";
2905
- const VP_PAD = 8;
2906
- const HALF_W = 160;
2907
- const APPROX_H = 90;
2908
- const containerW = canvasSizeRef.current.width || 800;
2909
- const safeLeft = Math.max(HALF_W + VP_PAD, Math.min(hoveredNodePosition.screenX, containerW - HALF_W - VP_PAD));
2910
- const rawTop = hoveredNodePosition.screenY - 60 * hoveredNodePosition.zoom;
2911
- const safeTop = Math.max(VP_PAD + APPROX_H, rawTop);
2912
- return /* @__PURE__ */ jsx11(
2688
+ /* @__PURE__ */ jsx9(NavigationStack_default, { stack: navStack, onNavigateBack: handleNavigateBack, top: navigationStackTop, left: navigationStackLeft }),
2689
+ /* @__PURE__ */ jsx9(
2690
+ "div",
2691
+ {
2692
+ className: "vi-map__ctrls",
2693
+ style: props.controlsPosition === "bottom-right" ? { top: "auto", bottom: pinnedNode && touchMode ? `calc(var(--space-4) + ${sheetHeight}px)` : "var(--space-4)" } : void 0,
2694
+ children: /* @__PURE__ */ jsx9(ZoomControls, { onFit: fitToScreen, touch: touchMode })
2695
+ }
2696
+ ),
2697
+ renderOverlay?.({ isDark, theme: design.theme, containerWidth: canvasSizeRef.current.width || 0, containerHeight: canvasSizeRef.current.height || 0 }),
2698
+ touchMode && pinnedNode && /* @__PURE__ */ jsx9(
2699
+ PeekSheet,
2700
+ {
2701
+ node: pinnedNode,
2702
+ anchors,
2703
+ height: sheetHeight,
2704
+ expanded: sheetExpanded,
2705
+ onToggleExpanded: () => setSheetExpanded((v) => !v),
2706
+ onClose: () => setPinnedId(null),
2707
+ onStepInside: pinnedHasChild ? () => {
2708
+ setPinnedId(null);
2709
+ handleNavigate(pinnedNode.id);
2710
+ } : void 0,
2711
+ renderContent: renderNodeContent ? (summary) => renderNodeContent({ summary, nodeId: pinnedNode.id, nodeColor: pinnedNode.color, zoom: viewport.zoomLevel }) : void 0
2712
+ }
2713
+ ),
2714
+ !touchMode && hoveredNode && hoveredNodePosition && peekNode && (() => {
2715
+ const rings = ringsFor3(peekNode.branchCount);
2716
+ const half = nodeRadii3(peekNode).rx * (rings.length ? rings[rings.length - 1].scale : 1) * hoveredNodePosition.zoom;
2717
+ const at = peekPosition({ x: hoveredNodePosition.screenX, y: hoveredNodePosition.screenY, half }, { w: canvasSizeRef.current.width || 800, h: canvasSizeRef.current.height || 600 });
2718
+ return /* @__PURE__ */ jsx9(
2913
2719
  "div",
2914
2720
  {
2915
- "data-node-tooltip": "true",
2916
- style: {
2917
- position: "absolute",
2918
- zIndex: 50,
2919
- left: safeLeft,
2920
- top: safeTop,
2921
- transform: "translate(-50%, -100%)",
2922
- transformOrigin: "bottom center",
2923
- pointerEvents: "auto"
2924
- },
2721
+ className: "vi-map__fact",
2722
+ style: { left: at.left, top: at.top, pointerEvents: "auto", zIndex: "var(--z-card)" },
2925
2723
  onMouseEnter: () => {
2926
2724
  tooltipHoverRef.current = true;
2927
2725
  if (clearTooltipTimerRef.current) {
@@ -2934,22 +2732,17 @@ function VisualliCanvas(props) {
2934
2732
  if (clearTooltipTimerRef.current) clearTimeout(clearTooltipTimerRef.current);
2935
2733
  clearTooltipTimerRef.current = setTimeout(() => {
2936
2734
  if (!keepTooltipOpenRef.current) setHoveredNode(null);
2937
- }, 100);
2735
+ }, PEEK.hoverClose);
2938
2736
  },
2939
- children: /* @__PURE__ */ jsx11("div", { style: { maxWidth: "20rem", minWidth: "12rem", maxHeight: "15rem", overflowY: "auto" }, children: /* @__PURE__ */ jsx11(SketchyBoxKonva_default, { fill: "#ffffff", stroke: borderColor, backStroke: borderColor, padding: "1rem 1.25rem", children: /* @__PURE__ */ jsx11("p", { style: {
2940
- fontFamily: "'Playpen Sans', cursive",
2941
- fontSize: `${DESCRIPTION_TEXT_BASE_FONT_PX2}px`,
2942
- fontWeight: 300,
2943
- color: "#000000",
2944
- textAlign: "center",
2945
- lineHeight: 1.6,
2946
- margin: 0
2947
- }, children: renderNodeContent ? renderNodeContent({
2948
- summary: hoveredNode.summary,
2949
- nodeId: hoveredNode.nodeId,
2950
- nodeColor: nodeData?.color,
2951
- zoom: viewport.zoomLevel
2952
- }) : hoveredNode.summary }) }) })
2737
+ children: /* @__PURE__ */ jsx9(
2738
+ PeekCard,
2739
+ {
2740
+ node: peekNode,
2741
+ anchors,
2742
+ onStepInside: peekHasChild ? () => stepInside(peekNode.id) : void 0,
2743
+ renderContent: renderNodeContent ? (summary) => renderNodeContent({ summary, nodeId: peekNode.id, nodeColor: peekNode.color, zoom: viewport.zoomLevel }) : void 0
2744
+ }
2745
+ )
2953
2746
  }
2954
2747
  );
2955
2748
  })()
@@ -2959,91 +2752,29 @@ function VisualliCanvas(props) {
2959
2752
  }
2960
2753
 
2961
2754
  // src/VisualliRenderer.tsx
2962
- import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
2963
- function useSystemDark() {
2964
- const mq = typeof window !== "undefined" ? window.matchMedia("(prefers-color-scheme: dark)") : null;
2965
- const [dark, setDark] = useState4(mq?.matches ?? false);
2966
- useEffect7(() => {
2967
- if (!mq) return;
2968
- const handler = (e) => setDark(e.matches);
2969
- mq.addEventListener("change", handler);
2970
- return () => mq.removeEventListener("change", handler);
2971
- }, [mq]);
2972
- return dark;
2973
- }
2974
- function useFontInjection() {
2975
- useEffect7(() => {
2976
- if (typeof document === "undefined") return;
2977
- const links = [
2978
- "https://fonts.googleapis.com",
2979
- "https://fonts.gstatic.com",
2980
- "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",
2981
- "https://fonts.googleapis.com/css2?family=Playpen+Sans:wght@300;400;600;800&family=Story+Script:wght@400&display=swap"
2982
- ];
2983
- links.forEach((href, i) => {
2984
- const id = `visualli-font-${i}`;
2985
- if (!document.getElementById(id)) {
2986
- const link = document.createElement("link");
2987
- link.id = id;
2988
- link.rel = i < 2 ? "preconnect" : "stylesheet";
2989
- link.href = href;
2990
- if (i === 1) link.crossOrigin = "anonymous";
2991
- document.head.appendChild(link);
2992
- }
2993
- });
2994
- }, []);
2995
- }
2996
- function EmptyState({ isDark }) {
2997
- const bg = isDark ? "#131311" : "#F5F3EF";
2998
- const fg = isDark ? "rgba(240,237,230,0.35)" : "rgba(26,26,24,0.35)";
2999
- const bd = isDark ? "#2A2A28" : "#DDD9D0";
3000
- const code = { fontFamily: "monospace", fontSize: 12, padding: "1px 4px", borderRadius: 4, background: isDark ? "#222220" : "#E8E4DC", border: `1px solid ${bd}` };
3001
- return /* @__PURE__ */ jsxs8("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: [
3002
- /* @__PURE__ */ jsxs8("svg", { width: "48", height: "48", viewBox: "0 0 48 48", fill: "none", stroke: fg, strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: [
3003
- /* @__PURE__ */ jsx12("rect", { x: "10", y: "6", width: "28", height: "36", rx: "3" }),
3004
- /* @__PURE__ */ jsx12("line", { x1: "16", y1: "16", x2: "32", y2: "16" }),
3005
- /* @__PURE__ */ jsx12("line", { x1: "16", y1: "22", x2: "32", y2: "22" }),
3006
- /* @__PURE__ */ jsx12("line", { x1: "16", y1: "28", x2: "26", y2: "28" })
3007
- ] }),
3008
- /* @__PURE__ */ jsxs8("div", { style: { textAlign: "center", lineHeight: 1.7 }, children: [
3009
- /* @__PURE__ */ jsx12("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" }),
3010
- /* @__PURE__ */ jsxs8("p", { style: { margin: "4px 0 0", fontSize: 13 }, children: [
3011
- "Pass a ",
3012
- /* @__PURE__ */ jsx12("code", { style: code, children: "visualliFile" }),
3013
- " or ",
3014
- /* @__PURE__ */ jsx12("code", { style: code, children: "visualliString" }),
3015
- " prop."
3016
- ] })
3017
- ] })
2755
+ import { jsx as jsx10, jsxs as jsxs4 } from "react/jsx-runtime";
2756
+ 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)" };
2757
+ function StateNote({ topic, title, children, role }) {
2758
+ return /* @__PURE__ */ jsxs4("div", { className: "vi-map", style: stateBox, role, "aria-live": role === "status" ? "polite" : void 0, children: [
2759
+ /* @__PURE__ */ jsx10("svg", { width: "64", height: "52", viewBox: "-32 -26 64 52", "aria-hidden": "true", children: /* @__PURE__ */ jsx10("path", { d: blobPath3(shapeForLevel(0), 26, 20), fill: `var(--topic-${topic})`, stroke: `var(--topic-${topic}-ring)`, strokeWidth: "2" }) }),
2760
+ /* @__PURE__ */ jsx10("p", { className: "vi-text-label", style: { margin: 0, color: "var(--ink)" }, children: title }),
2761
+ children && /* @__PURE__ */ jsx10("div", { className: "vi-text-body-sm", style: { margin: 0, maxWidth: 480, overflowWrap: "anywhere" }, children })
3018
2762
  ] });
3019
2763
  }
3020
- function LoadingState({ isDark }) {
3021
- const bg = isDark ? "#131311" : "#F5F3EF";
3022
- const fg = isDark ? "rgba(240,237,230,0.5)" : "rgba(26,26,24,0.5)";
3023
- const dot = isDark ? "rgba(240,237,230,0.6)" : "rgba(26,26,24,0.45)";
3024
- return /* @__PURE__ */ jsxs8("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: [
3025
- /* @__PURE__ */ jsx12("div", { style: { display: "flex", gap: 8 }, children: [0, 1, 2].map((i) => /* @__PURE__ */ jsx12("div", { style: { width: 8, height: 8, borderRadius: "50%", background: dot, animation: `vr-pulse 1.2s ease-in-out ${i * 0.2}s infinite` } }, i)) }),
3026
- /* @__PURE__ */ jsx12("style", { children: `@keyframes vr-pulse{0%,80%,100%{opacity:.2;transform:scale(.9)}40%{opacity:1;transform:scale(1.1)}}` }),
3027
- /* @__PURE__ */ jsx12("p", { style: { margin: 0, fontSize: 14 }, children: "Loading .visualli file\u2026" })
2764
+ function EmptyState() {
2765
+ return /* @__PURE__ */ jsxs4(StateNote, { topic: "stone", title: "Nothing to show yet", children: [
2766
+ "Pass a ",
2767
+ /* @__PURE__ */ jsx10("code", { style: { fontFamily: "var(--font-mono)" }, children: "visualliFile" }),
2768
+ " or ",
2769
+ /* @__PURE__ */ jsx10("code", { style: { fontFamily: "var(--font-mono)" }, children: "visualliString" }),
2770
+ "."
3028
2771
  ] });
3029
2772
  }
3030
- function ErrorState({ message, isDark }) {
3031
- const bg = isDark ? "#131311" : "#F5F3EF";
3032
- const bd = isDark ? "#3a1f1f" : "#F0CECE";
3033
- const card = isDark ? "#1e1212" : "#FFF5F5";
3034
- const head = isDark ? "#F87171" : "#DC2626";
3035
- const body = isDark ? "rgba(248,113,113,0.8)" : "rgba(180,40,40,0.8)";
3036
- return /* @__PURE__ */ jsx12("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__ */ jsxs8("div", { style: { maxWidth: 480, width: "100%", padding: "20px 24px", borderRadius: 12, border: `1px solid ${bd}`, background: card }, children: [
3037
- /* @__PURE__ */ jsxs8("div", { style: { display: "flex", alignItems: "center", gap: 10, marginBottom: 12 }, children: [
3038
- /* @__PURE__ */ jsxs8("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: head, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
3039
- /* @__PURE__ */ jsx12("circle", { cx: "12", cy: "12", r: "10" }),
3040
- /* @__PURE__ */ jsx12("line", { x1: "12", y1: "8", x2: "12", y2: "12" }),
3041
- /* @__PURE__ */ jsx12("line", { x1: "12", y1: "16", x2: "12.01", y2: "16" })
3042
- ] }),
3043
- /* @__PURE__ */ jsx12("span", { style: { fontSize: 15, fontWeight: 700, color: head }, children: "Failed to parse .visualli file" })
3044
- ] }),
3045
- /* @__PURE__ */ jsx12("p", { style: { margin: 0, fontSize: 13, color: body, lineHeight: 1.6, wordBreak: "break-word", fontFamily: "monospace" }, children: message })
3046
- ] }) });
2773
+ function LoadingState() {
2774
+ return /* @__PURE__ */ jsx10(StateNote, { topic: "teal", title: "Opening the map\u2026", role: "status" });
2775
+ }
2776
+ function ErrorState({ message }) {
2777
+ return /* @__PURE__ */ jsx10(StateNote, { topic: "berry", title: "This map couldn't be opened", role: "alert", children: /* @__PURE__ */ jsx10("code", { style: { fontFamily: "var(--font-mono)" }, children: message }) });
3047
2778
  }
3048
2779
  var WORKER_SCRIPT = `
3049
2780
  self.onmessage = function(e) {
@@ -3055,6 +2786,7 @@ self.onmessage = function(e) {
3055
2786
  layers: new Map(),
3056
2787
  layersByLevel: new Map(),
3057
2788
  rootLayer: null,
2789
+ extensions: {},
3058
2790
  };
3059
2791
  for (var i = 0; i < lines.length; i++) {
3060
2792
  var line = lines[i].trim();
@@ -3070,6 +2802,8 @@ self.onmessage = function(e) {
3070
2802
  if (!doc.layersByLevel.has(obj.level)) doc.layersByLevel.set(obj.level, []);
3071
2803
  doc.layersByLevel.get(obj.level).push(obj);
3072
2804
  if (obj.level === 0) doc.rootLayer = obj;
2805
+ } else if (obj.type === 'extension' && typeof obj.id === 'string') {
2806
+ doc.extensions[obj.id] = Array.isArray(obj.data) ? obj.data : [];
3073
2807
  }
3074
2808
  }
3075
2809
  if (!doc.meta) throw new Error('Missing required meta section');
@@ -3088,9 +2822,9 @@ function getWorkerUrl() {
3088
2822
  return _workerUrl;
3089
2823
  }
3090
2824
  function useAsyncParse(content, useWorker) {
3091
- const [state, setState] = useState4({ status: "idle" });
3092
- const workerRef = useRef10(null);
3093
- useEffect7(() => {
2825
+ const [state, setState] = useState5({ status: "idle" });
2826
+ const workerRef = useRef11(null);
2827
+ useEffect8(() => {
3094
2828
  if (!content) {
3095
2829
  setState({ status: "idle" });
3096
2830
  return;
@@ -3132,16 +2866,16 @@ function useAsyncParse(content, useWorker) {
3132
2866
  cancelled = true;
3133
2867
  };
3134
2868
  }, [content, useWorker]);
3135
- useEffect7(() => () => {
2869
+ useEffect8(() => () => {
3136
2870
  workerRef.current?.terminate();
3137
2871
  }, []);
3138
2872
  return state;
3139
2873
  }
3140
2874
  function useFileText(file) {
3141
- const [text, setText] = useState4(void 0);
3142
- const [error, setError] = useState4(void 0);
3143
- const [loading, setLoading] = useState4(false);
3144
- useEffect7(() => {
2875
+ const [text, setText] = useState5(void 0);
2876
+ const [error, setError] = useState5(void 0);
2877
+ const [loading, setLoading] = useState5(false);
2878
+ useEffect8(() => {
3145
2879
  if (!file) {
3146
2880
  setText(void 0);
3147
2881
  setError(void 0);
@@ -3190,41 +2924,58 @@ function VisualliRenderer({
3190
2924
  visualliString,
3191
2925
  visualliFile,
3192
2926
  theme = "light",
2927
+ comfort,
2928
+ respectForcedColors,
2929
+ layout,
2930
+ controlsPosition,
2931
+ app,
3193
2932
  width = "100%",
3194
2933
  height = "100%",
3195
2934
  useWorker = true,
3196
2935
  chromaticImmersion = false,
2936
+ fontBaseUrl,
2937
+ loadWebFonts,
3197
2938
  className,
3198
2939
  style
3199
2940
  }) {
3200
- useFontInjection();
3201
- const systemDark = useSystemDark();
3202
- const isDark = theme === "dark" ? true : theme === "light" ? false : systemDark;
2941
+ const design = useDesign({ theme, comfort, respectForcedColors });
2942
+ useState5(() => {
2943
+ ensureDesignSystemStyles({ fontBaseUrl, loadWebFonts });
2944
+ return true;
2945
+ });
3203
2946
  const cssWidth = typeof width === "number" ? `${width}px` : width;
3204
2947
  const cssHeight = typeof height === "number" ? `${height}px` : height;
3205
2948
  const { text: fileText, error: fileReadError, loading: fileReading } = useFileText(visualliFile);
3206
2949
  const rawContent = fileText ?? visualliString;
3207
2950
  const parseState = useAsyncParse(rawContent, useWorker);
3208
- const resolvedDoc = useMemo8(() => {
2951
+ const resolvedDoc = useMemo10(() => {
3209
2952
  if (parseState.status === "ready") return parseState.doc;
3210
2953
  return null;
3211
2954
  }, [parseState]);
3212
2955
  const wrapperStyle = { width: cssWidth, height: cssHeight, overflow: "hidden", ...style };
2956
+ const attrs = design.attrs;
3213
2957
  if (!visualliString && !visualliFile) {
3214
- return /* @__PURE__ */ jsx12("div", { className, style: wrapperStyle, children: /* @__PURE__ */ jsx12(EmptyState, { isDark }) });
2958
+ return /* @__PURE__ */ jsx10("div", { className, ...attrs, style: wrapperStyle, children: /* @__PURE__ */ jsx10(EmptyState, {}) });
3215
2959
  }
3216
2960
  if (fileReading || parseState.status === "loading") {
3217
- return /* @__PURE__ */ jsx12("div", { className, style: wrapperStyle, children: /* @__PURE__ */ jsx12(LoadingState, { isDark }) });
2961
+ return /* @__PURE__ */ jsx10("div", { className, ...attrs, style: wrapperStyle, children: /* @__PURE__ */ jsx10(LoadingState, {}) });
3218
2962
  }
3219
2963
  if (fileReadError || parseState.status === "error") {
3220
2964
  const msg = fileReadError ?? (parseState.status === "error" ? parseState.message : "Unknown error");
3221
- return /* @__PURE__ */ jsx12("div", { className, style: wrapperStyle, children: /* @__PURE__ */ jsx12(ErrorState, { message: msg, isDark }) });
2965
+ return /* @__PURE__ */ jsx10("div", { className, ...attrs, style: wrapperStyle, children: /* @__PURE__ */ jsx10(ErrorState, { message: msg }) });
3222
2966
  }
3223
- return /* @__PURE__ */ jsx12("div", { className, style: wrapperStyle, children: /* @__PURE__ */ jsx12(
2967
+ return /* @__PURE__ */ jsx10("div", { className, style: wrapperStyle, children: /* @__PURE__ */ jsx10(
3224
2968
  VisualliCanvas,
3225
2969
  {
3226
2970
  preParsedVisualli: resolvedDoc ?? void 0,
3227
- isDark,
2971
+ theme,
2972
+ comfort,
2973
+ respectForcedColors,
2974
+ layout,
2975
+ controlsPosition,
2976
+ app,
2977
+ fontBaseUrl,
2978
+ loadWebFonts,
3228
2979
  chromaticImmersion,
3229
2980
  style: { width: "100%", height: "100%" }
3230
2981
  }
@@ -3233,60 +2984,219 @@ function VisualliRenderer({
3233
2984
 
3234
2985
  // src/context/VisualliContext.tsx
3235
2986
  import { createContext, useContext } from "react";
3236
- import { jsx as jsx13 } from "react/jsx-runtime";
2987
+ import { jsx as jsx11 } from "react/jsx-runtime";
3237
2988
  var VisualliContext = createContext(null);
3238
2989
  function VisualliProvider({
3239
2990
  children
3240
2991
  }) {
3241
2992
  const value = {};
3242
- return /* @__PURE__ */ jsx13(VisualliContext.Provider, { value, children });
2993
+ return /* @__PURE__ */ jsx11(VisualliContext.Provider, { value, children });
3243
2994
  }
3244
2995
  function useVisualli() {
3245
- const ctx = useContext(VisualliContext);
3246
- if (!ctx) {
2996
+ const ctx2 = useContext(VisualliContext);
2997
+ if (!ctx2) {
3247
2998
  return {};
3248
2999
  }
3249
- return ctx;
3000
+ return ctx2;
3001
+ }
3002
+
3003
+ // src/hooks/useViewportNodes.ts
3004
+ import { useMemo as useMemo11, useRef as useRef12 } from "react";
3005
+ import { RBushSpatialIndex as RBushSpatialIndex3, getViewportVisibleNodes } from "@visualli/core";
3006
+ var SPATIAL_INDEX_THRESHOLD2 = 200;
3007
+ function useViewportNodes(nodes, levelFilter, skipCulling = false, isDragging = false) {
3008
+ const { centerX, centerY, zoomLevel, canvasWidth, canvasHeight } = useViewportStore();
3009
+ const indexRef = useRef12(null);
3010
+ const nodesHashRef = useRef12(0);
3011
+ const nodesHash = nodes.length;
3012
+ if (nodesHash !== nodesHashRef.current) {
3013
+ nodesHashRef.current = nodesHash;
3014
+ indexRef.current = null;
3015
+ }
3016
+ return useMemo11(() => {
3017
+ if (nodes.length === 0) return [];
3018
+ if (!indexRef.current && nodes.length >= SPATIAL_INDEX_THRESHOLD2 && !skipCulling && !isDragging) {
3019
+ indexRef.current = new RBushSpatialIndex3();
3020
+ indexRef.current.bulkLoad(nodes.map((n) => ({
3021
+ nodeId: n.id,
3022
+ bounds: { minX: n.x, minY: n.y, maxX: n.x + n.width, maxY: n.y + n.height }
3023
+ })));
3024
+ }
3025
+ return getViewportVisibleNodes({
3026
+ nodes,
3027
+ viewport: { centerX, centerY, zoomLevel, rotation: 0, visibleBounds: { minX: 0, minY: 0, maxX: 0, maxY: 0 } },
3028
+ canvasWidth,
3029
+ canvasHeight,
3030
+ levelFilter,
3031
+ skipCulling,
3032
+ isDragging,
3033
+ spatialIndex: indexRef.current,
3034
+ spatialIndexThreshold: SPATIAL_INDEX_THRESHOLD2
3035
+ });
3036
+ }, [nodes, levelFilter, skipCulling, isDragging, centerX, centerY, zoomLevel, canvasWidth, canvasHeight]);
3250
3037
  }
3038
+
3039
+ // src/index.ts
3040
+ import {
3041
+ THEME_NAMES,
3042
+ TOPICS,
3043
+ DESIGN_SYSTEM_VERSION,
3044
+ TOKENS as TOKENS3,
3045
+ METRICS,
3046
+ resolveTheme as resolveTheme2,
3047
+ resolveComfort as resolveComfort2,
3048
+ ideaColor as ideaColor2,
3049
+ ideaStyle as ideaStyle2,
3050
+ topicStyle,
3051
+ getTokens,
3052
+ BLOB_SHAPES,
3053
+ RINGS as RINGS2,
3054
+ blobPath as blobPath4,
3055
+ blobRadius,
3056
+ edgePath,
3057
+ arrowPath,
3058
+ shapeForLevel as shapeForLevel2,
3059
+ IDEA as IDEA2,
3060
+ LABEL_GROWTH,
3061
+ ideaSize,
3062
+ labelGrowth as labelGrowth3
3063
+ } from "@visualli/core";
3064
+
3065
+ // src/components/KonvaNode.tsx
3066
+ import React12 from "react";
3067
+ import { jsx as jsx12 } from "react/jsx-runtime";
3068
+ var KonvaNode = React12.memo(function KonvaNode2({
3069
+ node,
3070
+ zoomLevel,
3071
+ isExternallyHovered = false,
3072
+ isExternallyPressed = false,
3073
+ isSelected = false,
3074
+ isDragging = false,
3075
+ design = DEFAULT_DESIGN
3076
+ }) {
3077
+ return /* @__PURE__ */ jsx12(
3078
+ KShape,
3079
+ {
3080
+ listening: false,
3081
+ perfectDrawEnabled: false,
3082
+ sceneFunc: (ctx2) => {
3083
+ drawIdea(ctx2._context, node, design, zoomLevel, { ...IDLE, hovered: isExternallyHovered, pressed: isExternallyPressed, selected: isSelected }, {
3084
+ shadows: !isDragging
3085
+ });
3086
+ }
3087
+ }
3088
+ );
3089
+ });
3090
+ var KonvaNode_default = KonvaNode;
3091
+
3092
+ // src/components/KonvaEdge.tsx
3093
+ import { memo as memo4, useMemo as useMemo12 } from "react";
3094
+ import { connectorGeometry as connectorGeometry3 } from "@visualli/core";
3095
+ import { jsx as jsx13 } from "react/jsx-runtime";
3096
+ var KonvaEdge = memo4(function KonvaEdge2({ sourceNode: from, targetNode: to, connection, zoomLevel = 1, design = DEFAULT_DESIGN }) {
3097
+ const g = useMemo12(
3098
+ () => prepareConnector(connectorGeometry3(from, to, design.metrics.edgeGap)),
3099
+ // eslint-disable-next-line react-hooks/exhaustive-deps
3100
+ [from.x, from.y, from.width, from.level, from.branchCount, to.x, to.y, to.width, to.level, to.branchCount, design.metrics.edgeGap]
3101
+ );
3102
+ return /* @__PURE__ */ jsx13(
3103
+ KShape,
3104
+ {
3105
+ listening: false,
3106
+ perfectDrawEnabled: false,
3107
+ sceneFunc: (ctx2) => {
3108
+ drawConnector(ctx2._context, g, design, zoomLevel, connection?.style === "dashed", connection?.label);
3109
+ }
3110
+ }
3111
+ );
3112
+ });
3113
+ var KonvaEdge_default = KonvaEdge;
3114
+
3115
+ // src/components/KonvaContainer.tsx
3116
+ import { memo as memo5, useMemo as useMemo13 } from "react";
3117
+ import { jsx as jsx14 } from "react/jsx-runtime";
3118
+ var KonvaContainer = memo5(function KonvaContainer2({ nodes, label = "", zoomLevel = 1, design = DEFAULT_DESIGN }) {
3119
+ const placed = useMemo13(() => {
3120
+ const map = new Map(nodes.map((n) => [n.id, n]));
3121
+ return layoutContainers([{ id: "c", label: label || void 0, nodeIds: nodes.map((n) => n.id) }], map, [], design)[0] ?? null;
3122
+ }, [nodes, label, design]);
3123
+ if (!placed) return null;
3124
+ return /* @__PURE__ */ jsx14(
3125
+ KShape,
3126
+ {
3127
+ listening: false,
3128
+ perfectDrawEnabled: false,
3129
+ sceneFunc: (ctx2) => {
3130
+ drawHull(ctx2._context, placed.hull, design);
3131
+ if (placed.pill) drawPill(ctx2._context, placed.pill, design, zoomLevel);
3132
+ }
3133
+ }
3134
+ );
3135
+ });
3136
+ var KonvaContainer_default = KonvaContainer;
3251
3137
  export {
3252
- ACTIVE_BLOB_TYPES,
3253
- ALL_BLOB_SHAPES2 as ALL_BLOB_SHAPES,
3254
- BLOB_LAYER_CONFIG,
3255
- BLOB_TEXT_OFFSETS,
3256
- DESCRIPTION_TEXT_BASE_FONT_PX,
3138
+ A11yLayer,
3139
+ BLOB_SHAPES,
3140
+ DEFAULT_DESIGN,
3141
+ DESIGN_SYSTEM_VERSION,
3142
+ DepthTrail,
3143
+ IDEA2 as IDEA,
3257
3144
  KonvaContainer_default as KonvaContainer,
3145
+ KonvaContainerLabelLayer,
3258
3146
  KonvaContainerLayer,
3259
3147
  KonvaEdge_default as KonvaEdge,
3260
3148
  KonvaEdgeLayer,
3261
- KonvaLayerTransitionAnimator,
3262
3149
  KonvaNode_default as KonvaNode,
3263
3150
  KonvaNodeLayer,
3264
3151
  KonvaStage_default as KonvaStage,
3265
- NODE_LAYER_CONFIG,
3266
- NODE_TEXT_BASE_FONT_PX,
3152
+ LABEL_GROWTH,
3153
+ METRICS,
3267
3154
  NavigationStack_default as NavigationStack,
3268
- SketchyBoxKonva_default as SketchyBoxKonva,
3155
+ PeekCard,
3156
+ RINGS2 as RINGS,
3157
+ THEME_NAMES,
3158
+ TOKENS3 as TOKENS,
3159
+ TOPICS,
3160
+ TermCard,
3269
3161
  VisualliCanvas,
3270
3162
  VisualliProvider,
3271
3163
  VisualliRenderer,
3272
- ZoomControls_default as ZoomControls,
3273
- buildBlobPathData,
3164
+ ZoomControls,
3165
+ arrowPath,
3166
+ blobPath4 as blobPath,
3167
+ blobRadius,
3274
3168
  calculateFitCenter,
3275
3169
  calculateFitView,
3276
3170
  calculateFitZoom,
3277
- computeEdgeLabelScale,
3278
- computeNodeTextScreenScale,
3279
- computeNodeTextWorldScale,
3280
- computeOverlayScale,
3281
- drawBlobPath,
3282
- getBlobTypeForLayer,
3171
+ drawHull,
3172
+ drawPill,
3173
+ edgePath,
3174
+ ensureDesignSystemStyles,
3283
3175
  getChildLayerForNode,
3284
3176
  getConnectionsForLayer,
3285
3177
  getLayerForNavigation,
3286
- konvaLayerTransitionAnimator,
3287
- useKonvaLayerTransition,
3178
+ getTokens,
3179
+ ideaColor2 as ideaColor,
3180
+ ideaMeasure,
3181
+ ideaSize,
3182
+ ideaStyle2 as ideaStyle,
3183
+ labelGrowth3 as labelGrowth,
3184
+ layoutContainers,
3185
+ loadCanvasFonts,
3186
+ makeDesign,
3187
+ measurePill,
3188
+ paintVars,
3189
+ resolveComfort2 as resolveComfort,
3190
+ resolveDesign,
3191
+ resolveTheme2 as resolveTheme,
3192
+ shapeForLevel2 as shapeForLevel,
3193
+ splitTerms,
3194
+ topicStyle,
3195
+ useDesign,
3288
3196
  useKonvaRenderer,
3197
+ useLayerChoreography,
3289
3198
  useNodeStore,
3199
+ useReaderEnv,
3290
3200
  useRenderConfigStore,
3291
3201
  useSelectionStore,
3292
3202
  useViewportNodes,