@visualli/react 0.1.6 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -5
- package/dist/index.cjs +1900 -2032
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +355 -97
- package/dist/index.d.ts +355 -97
- package/dist/index.js +2005 -2094
- package/dist/index.js.map +1 -1
- package/fonts/Caveat-Variable.ttf +0 -0
- package/licenses/visualli-design-system/LICENSE +9 -0
- package/licenses/visualli-design-system/NOTICE.md +6 -0
- package/package.json +9 -4
- package/src/VisualliCanvas.tsx +453 -408
- package/src/VisualliRenderer.tsx +90 -112
- package/src/components/A11yLayer.tsx +73 -0
- package/src/components/KonvaContainer.tsx +28 -237
- package/src/components/KonvaContainerLabelLayer.tsx +41 -0
- package/src/components/KonvaContainerLayer.tsx +34 -33
- package/src/components/KonvaEdge.tsx +25 -240
- package/src/components/KonvaEdgeLayer.tsx +69 -49
- package/src/components/KonvaNode.tsx +21 -156
- package/src/components/KonvaNodeLayer.tsx +102 -34
- package/src/components/KonvaStage.tsx +17 -3
- package/src/components/NavigationStack.tsx +16 -159
- package/src/components/Overlays.tsx +327 -0
- package/src/components/ZoomControls.tsx +3 -107
- package/src/components/useFrames.ts +30 -0
- package/src/context/VisualliContext.tsx +3 -3
- package/src/design/choreography.ts +157 -0
- package/src/design/containers.ts +150 -0
- package/src/design/design.ts +115 -0
- package/src/design/drawing.ts +303 -0
- package/src/design/measure.ts +34 -0
- package/src/design/runtime.ts +154 -0
- package/src/design/useDesign.ts +38 -0
- package/src/generated/specCss.ts +15 -0
- package/src/hooks/index.ts +0 -2
- package/src/hooks/useKonvaRenderer.ts +13 -18
- package/src/hooks/useLayerChoreography.ts +159 -0
- package/src/hooks/useViewportNodes.ts +0 -1
- package/src/index.ts +22 -22
- package/src/stores/stores.ts +2 -2
- package/src/stores/useViewportStore.ts +12 -2
- package/src/animations/index.ts +0 -2
- package/src/animations/konvaLayerTransition.ts +0 -223
- package/src/components/SketchyBoxKonva.tsx +0 -121
- package/src/config/blobShapes.ts +0 -204
- package/src/config/index.ts +0 -21
- package/src/config/textScaling.ts +0 -69
- 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
|
|
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
|
|
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
|
-
|
|
13
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
178
|
+
var useSelectionStore = create3((set) => {
|
|
169
179
|
selectionStore.subscribe((state) => set(state));
|
|
170
180
|
return selectionStore.getState();
|
|
171
181
|
});
|
|
172
|
-
var useRenderConfigStore = create3((set
|
|
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
|
|
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((
|
|
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
|
-
|
|
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/
|
|
305
|
-
import { useCallback as useCallback2,
|
|
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/
|
|
485
|
-
function
|
|
486
|
-
const {
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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 (
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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
|
-
|
|
623
|
-
|
|
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
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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.0). Do not edit here: changes come in through PRs from the private design system. MIT license, see ../LICENSE. */\n:root, [data-theme="light"] {\n --font-display: "Atkinson Hyperlegible Next", system-ui, sans-serif;\n --font-ui: "Atkinson Hyperlegible Next", system-ui, -apple-system, "Segoe UI", sans-serif;\n --font-hand: Kalam, "Atkinson Hyperlegible Next", cursive;\n --font-note: Caveat, Kalam, cursive;\n --font-mono: "Atkinson Hyperlegible Mono", ui-monospace, "SF Mono", Menlo, monospace;\n --canvas: #f6f3ee;\n --surface: #ffffff;\n --surface-sunken: #f0ece5;\n --surface-overlay: #ffffff;\n --line: #e4dfd6;\n --line-strong: #7a7f94;\n --ink: #182140;\n --ink-muted: #4f5671;\n --ink-subtle: #5f647c;\n --action: #182140;\n --on-action: #ffffff;\n --focus: #325e8c;\n --link: #2f5a87;\n --selection: rgba(18, 199, 211, 0.28);\n --node-ink: #182140;\n --edge: #2a3048;\n --edge-label: #4f5671;\n --topic-teal: #87f2f8;\n --topic-teal-ring: #00cdd5;\n --topic-harbor: #bcdbff;\n --topic-harbor-ring: #76b2f5;\n --topic-iris: #e0c8fe;\n --topic-iris-ring: #bd96ea;\n --topic-berry: #ffbdc4;\n --topic-berry-ring: #f47e8f;\n --topic-coral: #ffc7b2;\n --topic-coral-ring: #f38e65;\n --topic-amber: #ffcf9d;\n --topic-amber-ring: #eb9c38;\n --topic-sun: #ffea94;\n --topic-sun-ring: #dfbd04;\n --topic-stone: #e0dacf;\n --topic-stone-ring: #bab09f;\n --space-1: 4px;\n --space-2: 8px;\n --space-3: 12px;\n --space-4: 16px;\n --space-7: 48px;\n --radius-sm: 10px;\n --radius-md: 16px;\n --radius-lg: 24px;\n --radius-pill: 999px;\n --shadow-float: 0 1px 2px rgba(24, 33, 64, 0.06), 0 8px 24px rgba(24, 33, 64, 0.08);\n --shadow-pop: 0 2px 4px rgba(24, 33, 64, 0.08), 0 16px 40px rgba(24, 33, 64, 0.14);\n --node-width: 200px;\n --node-height: 148px;\n --node-stroke: 3px;\n --edge-width: 2.5px;\n --edge-gap: 15px;\n --edge-arc-max: 45px;\n --immersion-alpha: 0.15;\n --zoom-min: 0.3;\n --zoom-max: 5;\n --duration-instant: 100ms;\n --duration-quick: 160ms;\n --duration-base: 240ms;\n --duration-zoom: 500ms;\n --duration-reveal: 420ms;\n --stagger-reveal: 70ms;\n --ease-zoom: cubic-bezier(0.2, 0.8, 0.2, 1);\n --ease-standard: cubic-bezier(0.4, 0, 0.2, 1);\n --ease-bloom: cubic-bezier(0.34, 1.4, 0.64, 1);\n --z-chrome: 10;\n --z-card: 20;\n --z-menu: 40;\n }\n[data-theme="dark"] {\n --canvas: #0f1220;\n --surface: #171b2b;\n --surface-sunken: #0f1220;\n --surface-overlay: #212639;\n --line: #2b3148;\n --line-strong: #6c7392;\n --ink: #eef0f7;\n --ink-muted: #b3b8cc;\n --ink-subtle: #8f95ae;\n --action: #f6f3ee;\n --on-action: #182140;\n --focus: #12c7d3;\n --link: #5fd8e1;\n --selection: rgba(18, 199, 211, 0.32);\n --edge: #c9cddc;\n --edge-label: #b3b8cc;\n --topic-teal-ring: #0fc5cd;\n --topic-harbor-ring: #73aceb;\n --topic-iris-ring: #b691e1;\n --topic-berry-ring: #eb798a;\n --topic-coral-ring: #ea8962;\n --topic-amber-ring: #e29737;\n --topic-sun-ring: #d8b70a;\n --topic-stone-ring: #b4a999;\n --shadow-float: 0 1px 2px rgba(0, 0, 0, 0.40), 0 8px 24px rgba(0, 0, 0, 0.35);\n --shadow-pop: 0 2px 4px rgba(0, 0, 0, 0.50), 0 16px 40px rgba(0, 0, 0, 0.50);\n }\n[data-theme="focus-light"] {\n --canvas: #f4f2ee;\n --edge: #5a6079;\n --edge-label: #5a6079;\n --topic-teal: #bce8ea;\n --topic-teal-ring: #67c6cb;\n --topic-harbor: #c9d9ec;\n --topic-harbor-ring: #8bb2de;\n --topic-iris: #d9cfe8;\n --topic-iris-ring: #b79ed4;\n --topic-berry: #edc6c9;\n --topic-berry-ring: #da9099;\n --topic-coral: #eecec1;\n --topic-coral-ring: #db9b82;\n --topic-amber: #eed4ba;\n --topic-amber-ring: #d6a570;\n --topic-sun: #f4eac1;\n --topic-sun-ring: #d2bf73;\n --topic-stone: #dddad5;\n }\n[data-theme="focus-dark"] {\n --canvas: #12141c;\n --surface: #171b2b;\n --surface-sunken: #0f1220;\n --surface-overlay: #212639;\n --line: #2b3148;\n --line-strong: #6c7392;\n --ink: #eef0f7;\n --ink-muted: #b3b8cc;\n --ink-subtle: #8f95ae;\n --action: #f6f3ee;\n --on-action: #182140;\n --focus: #12c7d3;\n --link: #5fd8e1;\n --selection: rgba(18, 199, 211, 0.32);\n --edge: #a9aec0;\n --edge-label: #b3b8cc;\n --topic-teal: #bce8ea;\n --topic-teal-ring: #71bdc1;\n --topic-harbor: #c9d9ec;\n --topic-harbor-ring: #8cabce;\n --topic-iris: #d9cfe8;\n --topic-iris-ring: #ae9bc6;\n --topic-berry: #edc6c9;\n --topic-berry-ring: #cc8e95;\n --topic-coral: #eecec1;\n --topic-coral-ring: #cd9884;\n --topic-amber: #eed4ba;\n --topic-amber-ring: #c9a176;\n --topic-sun: #f4eac1;\n --topic-sun-ring: #c9b97a;\n --topic-stone: #dddad5;\n --topic-stone-ring: #b4a999;\n }\n[data-theme="colorsafe-light"] {\n --topic-teal: #b0ecbc;\n --topic-teal-ring: #6cc982;\n --topic-harbor: #abdbfe;\n --topic-harbor-ring: #60b4ec;\n --topic-iris: #f6bde2;\n --topic-iris-ring: #dc87c0;\n --topic-berry: #feb79e;\n --topic-berry-ring: #ec7f58;\n --topic-coral: #fece96;\n --topic-coral-ring: #e49d41;\n --topic-amber: #b2e8fa;\n --topic-amber-ring: #5cc4e2;\n --topic-sun: #f6f097;\n --topic-sun-ring: #d0c637;\n --topic-stone: #dbdbdb;\n --topic-stone-ring: #b7afb1;\n }\n[data-theme="colorsafe-dark"] {\n --canvas: #0f1220;\n --surface: #171b2b;\n --surface-sunken: #0f1220;\n --surface-overlay: #212639;\n --line: #2b3148;\n --line-strong: #6c7392;\n --ink: #eef0f7;\n --ink-muted: #b3b8cc;\n --ink-subtle: #8f95ae;\n --action: #f6f3ee;\n --on-action: #182140;\n --focus: #12c7d3;\n --link: #5fd8e1;\n --selection: rgba(18, 199, 211, 0.32);\n --edge: #c9cddc;\n --edge-label: #b3b8cc;\n --topic-teal: #b0ecbc;\n --topic-teal-ring: #69c27e;\n --topic-harbor: #abdbfe;\n --topic-harbor-ring: #5eade2;\n --topic-iris: #f6bde2;\n --topic-iris-ring: #d383b9;\n --topic-berry: #feb79e;\n --topic-berry-ring: #e37a55;\n --topic-coral: #fece96;\n --topic-coral-ring: #db9840;\n --topic-amber: #b2e8fa;\n --topic-amber-ring: #5bbdda;\n --topic-sun: #f6f097;\n --topic-sun-ring: #c9bf37;\n --topic-stone: #dbdbdb;\n --topic-stone-ring: #b1a8aa;\n }\n[data-theme="contrast-light"] {\n --canvas: #ffffff;\n --surface-sunken: #f2f2f2;\n --line: #5c6178;\n --line-strong: #000000;\n --ink: #000000;\n --ink-muted: #1f2433;\n --ink-subtle: #3a3f52;\n --action: #000000;\n --focus: #0b4a8f;\n --link: #0b4a8f;\n --node-ink: #000000;\n --edge: #000000;\n --edge-label: #1f2433;\n --topic-teal-ring: #006b70;\n --topic-harbor-ring: #0e599d;\n --topic-iris-ring: #683b92;\n --topic-berry-ring: #96173a;\n --topic-coral-ring: #923400;\n --topic-amber-ring: #7e4d00;\n --topic-sun-ring: #7a6600;\n --topic-stone-ring: #63594b;\n --shadow-float: none;\n --shadow-pop: none;\n }\n[data-theme="contrast-dark"] {\n --canvas: #000000;\n --surface: #000000;\n --surface-sunken: #0d0d0d;\n --surface-overlay: #0d0d0d;\n --line: #9aa0b8;\n --line-strong: #ffffff;\n --ink: #ffffff;\n --ink-muted: #e6e8f0;\n --ink-subtle: #c8ccda;\n --action: #ffffff;\n --on-action: #000000;\n --focus: #5fe8f0;\n --link: #7ff0f5;\n --selection: rgba(18, 199, 211, 0.32);\n --node-ink: #000000;\n --edge: #ffffff;\n --edge-label: #e6e8f0;\n --topic-teal-ring: #64eff7;\n --topic-harbor-ring: #b0d5fe;\n --topic-iris-ring: #dcbfff;\n --topic-berry-ring: #feb3bb;\n --topic-coral-ring: #ffbea5;\n --topic-amber-ring: #ffc78a;\n --topic-sun-ring: #ffe26d;\n --topic-stone-ring: #ddd3c2;\n --shadow-float: none;\n --shadow-pop: none;\n }\n[data-motion="reduced"] {\n --duration-instant: 120ms;\n --duration-quick: 0ms;\n --duration-base: 0ms;\n --duration-zoom: 0ms;\n --duration-reveal: 0ms;\n --stagger-reveal: 0ms;\n }\n@media (prefers-reduced-motion: reduce) {\n :root:not([data-motion="full"]) {\n --duration-instant: 120ms;\n --duration-quick: 0ms;\n --duration-base: 0ms;\n --duration-zoom: 0ms;\n --duration-reveal: 0ms;\n --stagger-reveal: 0ms;\n }\n}\n\n/* Type styles */\n.heading { font-family: var(--font-ui); font-size: 17px; line-height: 24px; font-weight: 700; }\n.body { font-family: var(--font-ui); font-size: 15px; line-height: 22px; font-weight: 400; }\n.body-sm { font-family: var(--font-ui); font-size: 13px; line-height: 18px; font-weight: 400; }\n.label { font-family: var(--font-ui); font-size: 13px; line-height: 16px; font-weight: 600; letter-spacing: 0.01em; }\n.caption { font-family: var(--font-ui); font-size: 12px; line-height: 16px; font-weight: 400; }\n.node-root { font-family: var(--font-hand); font-size: 30px; line-height: 34px; font-weight: 700; }\n.node-label { font-family: var(--font-hand); font-size: 22px; line-height: 26px; font-weight: 700; }\n.edge-label { font-family: var(--font-note); font-size: 18px; line-height: 20px; font-weight: 500; }\n.annotation { font-family: var(--font-note); font-size: 24px; line-height: 26px; font-weight: 600; }\n';
|
|
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.0). Do not edit here: changes come in through PRs from the private design system. MIT license, see ../LICENSE. */\n:root { --vi-fs-body: 15px; --vi-fs-sm: 13px; --vi-fs-cap: 12px; --vi-lh-body: 22px; --vi-label-scale: 1; color-scheme: light; }\n[data-theme$="dark"] { color-scheme: dark; }\n[data-scale="lg"] { --vi-fs-body: 17px; --vi-fs-sm: 15px; --vi-fs-cap: 14px; --vi-lh-body: 26px; --vi-label-scale: 1.15; }\n[data-type="readable"] { --font-hand: "Atkinson Hyperlegible Next", system-ui, sans-serif; --font-note: "Atkinson Hyperlegible Next", system-ui, sans-serif; --font-display: "Atkinson Hyperlegible Next", system-ui, sans-serif; letter-spacing: 0.01em; }\n[data-type="readable"] .vi-node__label, [data-type="readable"] .vi-edge__label, [data-type="readable"] .vi-map__group-label { font-family: var(--font-ui) !important; font-weight: 700; letter-spacing: 0.02em; font-style: normal; }\n\n.vi-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }\n.vi-icon { flex: none; display: block; }\n.vi-map :where(button, a, input, textarea, [tabindex]):focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }\n.vi-iconbtn { display: inline-grid; place-items: center; border-radius: var(--radius-sm); border: 1.5px solid transparent; background: transparent; color: var(--ink); cursor: pointer; transition: background var(--duration-instant); flex: none; }\n.vi-iconbtn--sm { width: 32px; height: 32px; }\n.vi-iconbtn--md { width: 40px; height: 40px; }\n.vi-iconbtn--lg { width: 48px; height: 48px; border-radius: var(--radius-md); }\n.vi-iconbtn:hover { background: var(--surface-sunken); }\n.vi-iconbtn--surface { background: var(--surface); border-color: var(--line); }\n.vi-iconbtn--surface:hover { border-color: var(--line-strong); }\n.vi-iconbtn--primary { background: var(--action); color: var(--on-action); border-radius: var(--radius-pill); }\n.vi-iconbtn.is-active { background: var(--action); color: var(--on-action); }\n[data-theme^="contrast"] .vi-ctrls { border: 2px solid var(--line-strong); }\n.vi-node { position: relative; display: inline-grid; place-items: center; border: 0; padding: 0; background: none; color: var(--node-ink); cursor: default; transition: transform var(--duration-base) var(--ease-bloom), opacity var(--duration-base); -webkit-tap-highlight-color: transparent; }\nbutton.vi-node { cursor: pointer; }\n.vi-node__svg { position: absolute; inset: 0; overflow: visible; }\n.vi-node__body { filter: drop-shadow(var(--vi-node-shadow, 0 6px 14px rgba(24, 33, 64, .10))); }\n[data-theme$="dark"] .vi-node__body { --vi-node-shadow: 0 0 22px rgba(255, 255, 255, .10); }\n[data-theme^="contrast"] .vi-node__body { filter: none; }\n.vi-node__rings { transform-origin: center; transition: transform var(--duration-base) var(--ease-standard); }\n@media (hover: hover) {\n button.vi-node:hover .vi-node__rings { transform: rotate(var(--vi-ring-turn, -2deg)) scale(var(--vi-ring-grow, 1.015)); }\n button.vi-node:hover { transform: translateY(calc(var(--vi-lift, 2px) * -1)); }\n}\n.vi-node { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; touch-action: manipulation; }\n.vi-node.is-selected { transform: translateY(-2px); }\n.vi-node.is-selected .vi-node__rings { transform: rotate(var(--vi-ring-turn, -2deg)) scale(var(--vi-ring-grow, 1.015)); }\n.vi-node.is-selected .vi-node__body { stroke-width: 4; }\n.vi-node:focus-visible { outline: none; }\n.vi-node:focus-visible .vi-node__body { stroke: var(--focus); stroke-width: 4; }\n.vi-node.is-selected .vi-node__body { stroke: var(--ink); }\n.vi-node__label { position: relative; text-align: center; color: var(--node-ink); display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; font-size: calc(var(--vi-nfs, 22px) * var(--vi-label-scale) * var(--vi-ninv, 1)); line-height: 1.15; padding-top: 2px; }\n.vi-node__label.node-root { --vi-nfs: 30px; }\n.vi-node--mini .vi-node__label { --vi-nfs: 15px; }\n.vi-node.is-dimmed { opacity: 1; }\n[data-theme^="focus"] .vi-node.is-dimmed { opacity: .4; }\n.vi-map.is-touch .vi-node.is-dimmed { opacity: .6; }\n[data-theme^="focus"] .vi-map.is-touch .vi-node.is-dimmed { opacity: .4; }\n.vi-edge__line { fill: none; stroke: var(--edge); stroke-width: 2.5; stroke-linecap: round; }\n.vi-edge.is-dashed .vi-edge__line { stroke-dasharray: 7 7; }\n.vi-edge__arrow { fill: none; stroke: var(--edge); stroke-width: 2.5; stroke-linecap: round; stroke-linejoin: round; }\n.vi-edge__label { font: 500 calc(18px * var(--vi-inv, 1)) / 1 var(--font-note); fill: var(--edge-label); paint-order: stroke; stroke: var(--canvas); stroke-width: 5px; stroke-linejoin: round; letter-spacing: .01em; }\n[data-type="readable"] .vi-edge__label { font-size: calc(14px * var(--vi-inv, 1)); font-weight: 600; }\n.vi-fact { position: relative; width: 300px; box-sizing: border-box; padding: var(--space-4) var(--space-4) var(--space-3); background: var(--surface-overlay); color: var(--ink); border: 2px solid var(--vi-fact-ring); border-radius: 14px 18px 12px 16px / 16px 12px 18px 14px; font: 400 var(--vi-fs-sm)/20px var(--font-ui); z-index: var(--z-card); animation: vi-pop var(--duration-quick) var(--ease-standard); }\n.vi-fact { box-shadow: 5px 6px 0 0 var(--vi-fact-fill), 5px 6px 0 2px var(--vi-fact-ring); }\n[data-theme^="contrast"] .vi-fact { border-color: var(--ink); }\n[data-theme^="contrast"] .vi-fact { box-shadow: none; }\n.vi-fact__title { font: 700 22px/26px var(--font-hand); margin-bottom: var(--space-1); }\n.vi-fact__body { color: var(--ink); }\n.vi-fact__foot { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); margin-top: var(--space-3); padding-top: var(--space-2); border-top: 1px dashed var(--line); }\n.vi-fact__acts { display: inline-flex; gap: var(--space-3); align-items: center; flex: none; }\n.vi-fact__link { color: var(--link); font: 600 var(--vi-fs-cap)/16px var(--font-ui); }\n.vi-fact__step { display: inline-flex; align-items: center; gap: 2px; border: 0; background: var(--action); color: var(--on-action); font: 600 var(--vi-fs-cap)/16px var(--font-ui); padding: 5px 8px 5px 10px; border-radius: var(--radius-pill); cursor: pointer; }\n.vi-trail ol { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; position: relative; }\n.vi-trail li { position: relative; }\n.vi-trail li + li::before { content: ""; position: absolute; left: 13px; top: -8px; height: 10px; border-left: 1.5px dashed var(--line-strong); }\n.vi-trail button { display: inline-flex; align-items: center; gap: 6px; border: 0; background: none; padding: 4px 8px 4px 5px; border-radius: var(--radius-sm); color: var(--ink-muted); font: 400 var(--vi-fs-cap)/16px var(--font-ui); cursor: pointer; }\n.vi-trail button span { display: inline-flex; align-items: center; gap: 4px; }\n.vi-trail button:hover:not(:disabled) { background: var(--surface-sunken); color: var(--ink); }\n.vi-trail li.is-current button { color: var(--ink); font-weight: 700; font-size: var(--vi-fs-sm); cursor: default; background: var(--surface); box-shadow: var(--shadow-float); }\n.vi-trail li.is-current button:disabled { opacity: 1; }\n.vi-ctrls { display: inline-flex; align-items: center; gap: 2px; padding: 4px; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-md); box-shadow: var(--shadow-float); }\n.vi-ctrls__pct { min-width: 44px; text-align: center; font: 600 var(--vi-fs-cap)/16px var(--font-ui); color: var(--ink-muted); font-variant-numeric: tabular-nums; }\n.vi-ctrls__div { width: 1px; height: 20px; background: var(--line); margin: 0 4px; }\n.vi-map { position: relative; overflow: hidden; background: var(--canvas); border-radius: inherit; isolation: isolate; }\n.vi-map__immersion { position: absolute; inset: 0; opacity: var(--immersion-alpha); transition: background var(--duration-base); z-index: -1; }\n.vi-map__world { position: absolute; left: 0; top: 0; transform-origin: 0 0; }\n.vi-map__edges { position: absolute; overflow: visible; pointer-events: none; }\n.vi-map__group rect { fill: none; stroke: var(--line-strong); stroke-width: 1.5; stroke-dasharray: 10 6; }\n.vi-map__group-label { position: absolute; transform: translate(-50%, -50%); z-index: 2; max-width: 420px; box-sizing: border-box; padding: 6px 22px 8px; background: var(--topic-stone); border: 1.5px solid var(--topic-stone-ring); border-radius: var(--radius-pill); font: 700 22px/1.1 var(--font-note); color: var(--node-ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; pointer-events: none; font-size: calc(26px * var(--vi-label-scale) * var(--vi-ninv, 1)); }\n[data-theme^="contrast"] .vi-map__group-label { border: 2px solid var(--line-strong); }\n.vi-map__slot { position: absolute; transform: translate(-50%, -50%); }\n.vi-map.is-reveal .vi-map__slot { animation: vi-bloom var(--duration-reveal) var(--ease-bloom) backwards; }\n.vi-map.is-reveal .vi-edge { animation: vi-fade var(--duration-base) var(--ease-standard) backwards; }\n.vi-map.is-reveal .vi-edge__line { stroke-dasharray: 1; stroke-dashoffset: 1; animation: vi-line var(--vi-line-draw, .5s) var(--ease-standard) forwards; animation-delay: inherit; }\n.vi-map.is-reveal .vi-edge.is-dashed .vi-edge__line { stroke-dasharray: 7 7; stroke-dashoffset: 0; animation: vi-fade var(--vi-line-draw, .5s) var(--ease-standard) backwards; animation-delay: inherit; }\n.vi-map:not(.is-reveal) .vi-edge__line { stroke-dasharray: none; }\n.vi-map:not(.is-reveal) .vi-edge.is-dashed .vi-edge__line { stroke-dasharray: 7 7; }\n.vi-map__lens { position: absolute; inset: 0; transform-origin: var(--vi-ox) var(--vi-oy); }\n.vi-map.is-diving .vi-map__lens { transform: scale(var(--vi-dive-scale, 3.2)); opacity: 0; transition: transform var(--duration-zoom) var(--ease-zoom), opacity var(--vi-dive-fade, 160ms) var(--vi-dive-fade-delay, 150ms); }\n.vi-map.is-surfacing .vi-map__lens { transform: scale(var(--vi-surface-scale, .6)); opacity: 0; transform-origin: 50% 50%; transition: transform var(--vi-surface-dur, 220ms) var(--ease-zoom), opacity var(--vi-surface-fade, 200ms); }\n.vi-map.is-reveal .vi-map__lens { animation: vi-settle var(--vi-settle-dur, 260ms) var(--ease-standard); }\n[data-motion="reduced"] .vi-map__lens { animation: none !important; transform: none !important; }\n.vi-map__fact { position: absolute; }\n.vi-map.is-idle .vi-map__world { transition: transform var(--duration-base) var(--ease-standard); }\n.vi-map__hint { position: absolute; left: var(--space-4); right: var(--space-4); margin: 0; text-align: center; color: var(--ink-muted); pointer-events: none; z-index: var(--z-chrome); animation: vi-fade var(--duration-base) var(--ease-standard); }\n.vi-fact__head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-2); }\n.vi-fact__back { display: inline-flex; align-items: center; gap: 2px; border: 0; background: none; padding: 2px 0; margin-bottom: var(--space-1); color: var(--link); font: 600 var(--vi-fs-cap)/16px var(--font-ui); cursor: pointer; }\n.vi-term { border: 0; background: none; padding: 0; font: inherit; color: var(--link); text-decoration: underline; text-decoration-style: dotted; text-decoration-thickness: 1.5px; text-underline-offset: 3px; cursor: pointer; }\n.vi-fact--sheet { position: absolute; left: 0; right: 0; bottom: 0; width: auto; max-width: 560px; margin: 0 auto; max-height: var(--vi-sheet-h, 300px); overflow-y: auto; overscroll-behavior: contain; padding: 0 var(--space-4) calc(var(--space-4) + env(safe-area-inset-bottom, 0px)); border: 0; border-top: 2px solid var(--vi-fact-ring); border-radius: var(--radius-lg) var(--radius-lg) 0 0; box-shadow: 0 -6px 0 0 var(--vi-fact-fill), var(--shadow-pop); font-size: var(--vi-fs-body); line-height: var(--vi-lh-body); animation: vi-sheet-in var(--duration-base) var(--ease-standard); transition: transform var(--duration-quick) var(--ease-standard), max-height var(--duration-base) var(--ease-standard); }\n.vi-fact--sheet.is-expanded { max-height: calc(100% - var(--space-7)); }\n.vi-fact__grip { position: sticky; top: 0; display: flex; justify-content: center; margin: 0 calc(var(--space-4) * -1); background: var(--surface-overlay); touch-action: none; cursor: grab; z-index: 1; }\n.vi-fact__handle { position: relative; width: 64px; height: 24px; border: 0; padding: 0; background: none; cursor: pointer; }\n.vi-fact__handle::after { content: ""; position: absolute; left: 12px; right: 12px; top: 10px; height: 5px; border-radius: var(--radius-pill); background: var(--line-strong); }\n.vi-fact--sheet .vi-fact__title { font-size: calc(24px * var(--vi-label-scale)); line-height: 1.15; padding-top: var(--space-1); }\n.vi-fact--sheet .vi-fact__close { margin: 0 calc(var(--space-2) * -1) 0 0; }\n.vi-fact--sheet .vi-fact__step { min-height: 44px; padding: 0 var(--space-3) 0 var(--space-4); font-size: var(--vi-fs-sm); }\n.vi-fact--sheet .vi-fact__link { font-size: var(--vi-fs-sm); min-height: 44px; display: inline-flex; align-items: center; }\n[data-motion="reduced"] .vi-fact--sheet { animation: vi-fade 120ms; }\n@media (prefers-reduced-motion: reduce) {\n .vi-fact--sheet { animation: vi-fade 120ms; }\n .vi-map.is-idle .vi-map__world { transition: none; }\n}\n.vi-map__trail { position: absolute; top: var(--space-4); left: var(--space-4); z-index: var(--z-chrome); }\n.vi-map__ctrls { position: absolute; top: var(--space-4); right: var(--space-4); z-index: var(--z-chrome); }\n[data-reveal="instant"] .vi-map.is-reveal .vi-map__slot, [data-motion="reduced"] .vi-map.is-reveal .vi-map__slot { animation: vi-fade var(--vi-reduced-fade, 120ms) backwards; animation-delay: 0ms !important; }\n[data-reveal="instant"] .vi-map.is-reveal .vi-edge, [data-reveal="instant"] .vi-map.is-reveal .vi-edge__line, [data-motion="reduced"] .vi-map.is-reveal .vi-edge, [data-motion="reduced"] .vi-map.is-reveal .vi-edge__line { animation: none; stroke-dashoffset: 0; }\n@media (prefers-reduced-motion: reduce) {\n .vi-map.is-reveal .vi-map__slot { animation: vi-fade var(--vi-reduced-fade, 120ms) backwards; animation-delay: 0ms !important; }\n .vi-map.is-reveal .vi-edge, .vi-map.is-reveal .vi-edge__line { animation: none; stroke-dashoffset: 0; }\n}\n[data-motion="reduced"] *, [data-motion="reduced"] *::before, [data-motion="reduced"] *::after { transition-duration: 1ms !important; }\n.vi-fact__acts { flex-wrap: wrap; justify-content: flex-end; }\n.vi-fact__link { display: inline-flex; align-items: center; gap: 2px; }\n.vi-anchor { position: relative; display: inline; }\n.vi-anchor.is-open .vi-term { background: var(--selection); border-radius: 3px; }\n.vi-anchor__pop { position: absolute; left: -12px; top: calc(100% + 6px); z-index: var(--z-menu); display: block; width: max-content; }\n.vi-anchor-card { position: relative; display: block; width: min(300px, calc(100vw - 32px)); box-sizing: border-box; padding: var(--space-3) var(--space-4) var(--space-3); background: var(--surface-overlay); color: var(--ink); text-align: left; white-space: normal; font: 400 var(--vi-fs-sm)/20px var(--font-ui); border: 2px solid var(--vi-fact-ring); border-radius: 12px 16px 10px 14px / 14px 10px 16px 12px; box-shadow: 4px 5px 0 0 var(--vi-fact-fill), 4px 5px 0 2px var(--vi-fact-ring), var(--shadow-pop); animation: vi-pop var(--duration-quick) var(--ease-standard); }\n.vi-anchor-card.is-in-sheet { width: auto; border: 0; box-shadow: none; padding: 0; animation: none; background: transparent; }\n[data-theme^="contrast"] .vi-anchor-card { border-color: var(--ink); box-shadow: none; }\n.vi-anchor-card__eyebrow { font: 400 11px/14px var(--font-display); letter-spacing: .14em; text-transform: uppercase; color: var(--ink-muted); }\n.vi-anchor-card__word { font: 700 20px/24px var(--font-hand); color: var(--ink); margin: 2px 0 var(--space-1); }\n.vi-fact--sheet .vi-anchor-card__word { font-size: calc(24px * var(--vi-label-scale)); line-height: 1.15; }\n.vi-anchor-card__desc { margin: 0; color: var(--ink); }\n.vi-anchor-card__acts { display: flex; align-items: center; justify-content: flex-end; flex-wrap: wrap; gap: var(--space-3); margin-top: var(--space-3); padding-top: var(--space-2); border-top: 1px dashed var(--line); }\n.vi-fact--sheet .vi-anchor-card__acts .vi-fact__link { min-height: 44px; }\n[data-type="readable"] .vi-anchor-card__word { font-family: var(--font-ui); }\n[data-motion="reduced"] .vi-anchor-card { animation: vi-fade 120ms; }\n@media (prefers-reduced-motion: reduce) {\n .vi-anchor-card { animation: vi-fade 120ms; }\n}\n.vi-peek .vi-fact__foot { flex-wrap: wrap; row-gap: var(--space-2); }\n.vi-peek .vi-fact__acts { margin-left: auto; }\n.vi-map.has-highlight .vi-node.is-dimmed { opacity: .35; transition: opacity var(--duration-quick) var(--ease-standard); }\n.vi-map.has-highlight .vi-node.is-selected .vi-node__body { stroke: var(--ink); }\n.vi-map { container-type: inline-size; }\n.vi-edge__hit { fill: none; stroke: transparent; stroke-width: 18; pointer-events: stroke; cursor: pointer; }\n.vi-edge__hit:hover + .vi-edge__line { stroke-width: 3.5; }\n.vi-edge.is-selected .vi-edge__line, .vi-edge.is-selected .vi-edge__arrow { stroke: var(--focus); stroke-width: 3.5; }\n.vi-map__group-hit { fill: none; stroke: transparent; stroke-width: 16; pointer-events: stroke; cursor: pointer; }\n.vi-map__group.is-selected rect:not(.vi-map__group-hit) { stroke: var(--focus); stroke-width: 2.5; stroke-dasharray: none; }\n.vi-map__group-label.is-selected { border-color: var(--focus); color: var(--focus); box-shadow: 0 0 0 1px var(--focus); }\n.vi-map { touch-action: pan-x pan-y; }\n.vi-map.is-app { touch-action: none; }\n.vi-map.is-pinching .vi-map__world { transition: none; }\n.vi-map.is-instant.is-reveal .vi-map__slot { animation: vi-fade var(--vi-reduced-fade, 120ms) backwards; animation-delay: 0ms !important; }\n.vi-map.is-instant.is-reveal .vi-edge, .vi-map.is-instant.is-reveal .vi-edge__line { animation: none; stroke-dashoffset: 0; }\n.vi-map.is-instant .vi-node, .vi-map.is-instant .vi-node__rings { transition: none; }\n.vi-map.is-detail-body .vi-node:not(.is-selected):not(:focus-visible):not(:hover) :is(.vi-node__rings, .vi-node__label), .vi-map.is-detail-speck .vi-node:not(.is-selected):not(:focus-visible):not(:hover) :is(.vi-node__rings, .vi-node__label) { display: none; }\n.vi-map.is-detail-body .vi-node:not(.is-selected):not(:focus-visible):not(:hover) .vi-node__body, .vi-map.is-detail-speck .vi-node:not(.is-selected):not(:focus-visible):not(:hover) .vi-node__body, .vi-map.no-shadows .vi-node__body { filter: none; }\n@keyframes vi-pop { from { opacity: 0; transform: translateY(-4px) scale(.98); } }\n@keyframes vi-fade { from { opacity: 0; } }\n@keyframes vi-bloom { from { opacity: 0; transform: translate(calc(-50% + var(--vi-dx)), calc(-50% + var(--vi-dy))) scale(var(--vi-bloom-from, .55)); } }\n@keyframes vi-line { to { stroke-dashoffset: 0; } }\n@keyframes vi-settle { from { transform: scale(var(--vi-settle-from, .94)); } }\n@keyframes vi-sheet-in { from { transform: translateY(100%); } }\n';
|
|
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.0";
|
|
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.0" : "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
|
-
|
|
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,
|
|
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/
|
|
718
|
-
import {
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
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
|
|
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/
|
|
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
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
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
|
-
|
|
778
|
-
var
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
const
|
|
1065
|
-
const
|
|
1066
|
-
const
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
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
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
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
|
|
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
|
|
1133
|
+
const fonts = useFontsEpoch();
|
|
1190
1134
|
const storeNodes = useNodeStore((s) => s.nodes);
|
|
1191
|
-
const
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
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
|
-
}, [
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
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 {
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
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
|
|
1209
|
+
const fonts = useFontsEpoch();
|
|
1437
1210
|
const storeNodes = useNodeStore((s) => s.nodes);
|
|
1438
|
-
const
|
|
1439
|
-
const
|
|
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
|
-
}, [
|
|
1443
|
-
|
|
1444
|
-
const
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
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/
|
|
1466
|
-
import {
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
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
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
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
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
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
|
|
1515
|
-
|
|
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
|
|
1518
|
-
if (
|
|
1519
|
-
|
|
1520
|
-
for (
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
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
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
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
|
|
1339
|
+
return out;
|
|
1537
1340
|
}
|
|
1538
|
-
function
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
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
|
|
1574
|
-
|
|
1575
|
-
const
|
|
1576
|
-
const
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
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
|
|
1682
|
-
function
|
|
1683
|
-
const
|
|
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
|
-
}, [
|
|
1389
|
+
}, [placed, design]);
|
|
1688
1390
|
if (containers.length === 0) return null;
|
|
1689
|
-
return /* @__PURE__ */
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
{
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
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/
|
|
1705
|
-
import {
|
|
1706
|
-
import { jsx as
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
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
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
}
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
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
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
})
|
|
1754
|
-
|
|
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
|
|
1757
|
-
const
|
|
1758
|
-
|
|
1759
|
-
|
|
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
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
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
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
]
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
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
|
-
|
|
1872
|
-
const
|
|
1873
|
-
const
|
|
1874
|
-
const
|
|
1875
|
-
const
|
|
1876
|
-
const
|
|
1877
|
-
const
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
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
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
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
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
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
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
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
|
-
|
|
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
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
var
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
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
|
-
|
|
2043
|
-
|
|
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__ */
|
|
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/
|
|
2052
|
-
import { jsx as
|
|
2053
|
-
function
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
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 {
|
|
2101
|
-
const
|
|
2102
|
-
|
|
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 =
|
|
2129
|
-
const [navStack, setNavStack] =
|
|
2130
|
-
const [currentLayerId, setCurrentLayerId] =
|
|
2131
|
-
const parentViewports =
|
|
2132
|
-
const
|
|
2133
|
-
const
|
|
2134
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
2175
|
-
const
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
if (
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
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
|
|
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
|
|
2042
|
+
const { rx: rx2, ry: ry2 } = nodeRadii3(node);
|
|
2271
2043
|
spatialIndexRef.current.remove(nodeId, {
|
|
2272
|
-
minX: oldPos.x -
|
|
2273
|
-
minY: oldPos.y -
|
|
2274
|
-
maxX: oldPos.x +
|
|
2275
|
-
maxY: oldPos.y +
|
|
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
|
|
2050
|
+
const { rx, ry } = nodeRadii3(node);
|
|
2279
2051
|
spatialIndexRef.current.insert(nodeId, {
|
|
2280
|
-
minX: x -
|
|
2281
|
-
minY: y -
|
|
2282
|
-
maxX: x +
|
|
2283
|
-
maxY: y +
|
|
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 =
|
|
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
|
-
|
|
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,
|
|
2072
|
+
}, [flatNodes, containers, setCenter, setZoom]);
|
|
2302
2073
|
fitToScreenRef.current = fitToScreen;
|
|
2303
|
-
|
|
2074
|
+
useEffect7(() => {
|
|
2304
2075
|
fitToScreen();
|
|
2305
2076
|
}, [currentLayerId]);
|
|
2306
|
-
const didFitAfterMeasureRef =
|
|
2307
|
-
|
|
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 =
|
|
2313
|
-
const {
|
|
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
|
-
|
|
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
|
-
|
|
2463
|
-
|
|
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
|
-
|
|
2467
|
-
|
|
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
|
-
|
|
2484
|
-
}, [navStack.length,
|
|
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 =
|
|
2544
|
-
const stageDragNodeOffsetRef =
|
|
2545
|
-
const stageMouseDownPosRef =
|
|
2546
|
-
const stageDragCommittedRef =
|
|
2547
|
-
const stageLastPointerCheckRef =
|
|
2548
|
-
const pointerNodeIdRef =
|
|
2549
|
-
const isCanvasPanningRef =
|
|
2550
|
-
const isDraggingRef =
|
|
2551
|
-
const [pointerNodeId, setPointerNodeId] =
|
|
2552
|
-
const [pressedNodeId, setPressedNodeId] =
|
|
2553
|
-
const [isDraggingState, setIsDraggingState] =
|
|
2554
|
-
const [hoveredNode, setHoveredNode] =
|
|
2555
|
-
const tooltipHoverRef =
|
|
2556
|
-
const keepTooltipOpenRef =
|
|
2557
|
-
const clearTooltipTimerRef =
|
|
2558
|
-
const hoveredNodePosition =
|
|
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] =
|
|
2571
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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,16 +2340,14 @@ function VisualliCanvas(props) {
|
|
|
2656
2340
|
if (hovered !== pointerNodeIdRef.current) {
|
|
2657
2341
|
pointerNodeIdRef.current = hovered;
|
|
2658
2342
|
setPointerNodeId(hovered);
|
|
2343
|
+
onNodeHoverRef.current?.(hovered);
|
|
2659
2344
|
}
|
|
2660
|
-
|
|
2661
|
-
stage.container().style.cursor = "pointer";
|
|
2662
|
-
} else {
|
|
2663
|
-
stage.container().style.cursor = "grab";
|
|
2664
|
-
}
|
|
2345
|
+
stage.container().style.cursor = hovered ? "pointer" : "";
|
|
2665
2346
|
}
|
|
2666
2347
|
rendererHandleMouseMove(e);
|
|
2667
2348
|
}, [toWorldCoords, hitTestNode, handleDragStart, handleDragMove, rendererHandleMouseMove]);
|
|
2668
2349
|
const handleStageMouseUp = useCallback4((e) => {
|
|
2350
|
+
if (isEmulatedMouse(e.evt)) return;
|
|
2669
2351
|
setPressedNodeId(null);
|
|
2670
2352
|
if (stageActiveNodeIdRef.current) {
|
|
2671
2353
|
const nodeId = stageActiveNodeIdRef.current;
|
|
@@ -2683,7 +2365,12 @@ function VisualliCanvas(props) {
|
|
|
2683
2365
|
const node = nodes.get(nodeId);
|
|
2684
2366
|
if (node) {
|
|
2685
2367
|
const childLayer = doc ? getChildLayerForNode(doc, nodeId, currentLayerId ?? "") : null;
|
|
2686
|
-
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) {
|
|
2687
2374
|
handleNavigate(nodeId);
|
|
2688
2375
|
}
|
|
2689
2376
|
onNodeClick?.(node);
|
|
@@ -2696,6 +2383,12 @@ function VisualliCanvas(props) {
|
|
|
2696
2383
|
stageDragCommittedRef.current = false;
|
|
2697
2384
|
return;
|
|
2698
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
|
+
}
|
|
2699
2392
|
isCanvasPanningRef.current = false;
|
|
2700
2393
|
rendererHandleMouseUp(e);
|
|
2701
2394
|
}, [toWorldCoords, handleDragEnd, nodes, handleNavigate, onNodeClick, rendererHandleMouseUp]);
|
|
@@ -2710,31 +2403,75 @@ function VisualliCanvas(props) {
|
|
|
2710
2403
|
const localY = rect ? e.evt.clientY - rect.top : e.evt.clientY;
|
|
2711
2404
|
setCanvasContextMenu({ x: localX, y: localY });
|
|
2712
2405
|
}, [navStack.length]);
|
|
2713
|
-
const
|
|
2714
|
-
const
|
|
2715
|
-
const
|
|
2716
|
-
const
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
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);
|
|
2721
2451
|
}
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
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;
|
|
2732
2465
|
return;
|
|
2733
2466
|
}
|
|
2467
|
+
handleStageMouseUp(e);
|
|
2468
|
+
}, [handleStageMouseUp]);
|
|
2469
|
+
const handleWheel = useCallback4((e) => {
|
|
2734
2470
|
rendererHandleWheel(e);
|
|
2735
|
-
}, [rendererHandleWheel
|
|
2736
|
-
const lastHoverCheckRef =
|
|
2471
|
+
}, [rendererHandleWheel]);
|
|
2472
|
+
const lastHoverCheckRef = useRef10(0);
|
|
2737
2473
|
const handleCanvasMouseMove = useCallback4((e) => {
|
|
2474
|
+
if (touchModeRef.current || isEmulatedMouse(e.nativeEvent)) return;
|
|
2738
2475
|
if (!containerRef.current || isDraggingRef.current || isCanvasPanningRef.current) {
|
|
2739
2476
|
setHoveredNode(null);
|
|
2740
2477
|
return;
|
|
@@ -2770,7 +2507,7 @@ function VisualliCanvas(props) {
|
|
|
2770
2507
|
if (clearTooltipTimerRef.current) clearTimeout(clearTooltipTimerRef.current);
|
|
2771
2508
|
clearTooltipTimerRef.current = setTimeout(() => {
|
|
2772
2509
|
if (!tooltipHoverRef.current && !keepTooltipOpenRef.current) setHoveredNode(null);
|
|
2773
|
-
},
|
|
2510
|
+
}, PEEK.hoverClose);
|
|
2774
2511
|
} else {
|
|
2775
2512
|
if (clearTooltipTimerRef.current) {
|
|
2776
2513
|
clearTimeout(clearTooltipTimerRef.current);
|
|
@@ -2778,7 +2515,7 @@ function VisualliCanvas(props) {
|
|
|
2778
2515
|
}
|
|
2779
2516
|
}
|
|
2780
2517
|
}, [nodes, viewport, spatialIndexRef]);
|
|
2781
|
-
|
|
2518
|
+
useEffect7(() => {
|
|
2782
2519
|
const handleKeepTooltipOpen = (event) => {
|
|
2783
2520
|
keepTooltipOpenRef.current = event.detail.keep;
|
|
2784
2521
|
};
|
|
@@ -2787,35 +2524,84 @@ function VisualliCanvas(props) {
|
|
|
2787
2524
|
window.removeEventListener("keepNodeTooltipOpen", handleKeepTooltipOpen);
|
|
2788
2525
|
};
|
|
2789
2526
|
}, []);
|
|
2790
|
-
|
|
2527
|
+
useEffect7(() => {
|
|
2791
2528
|
return () => {
|
|
2792
2529
|
if (clearTooltipTimerRef.current) clearTimeout(clearTooltipTimerRef.current);
|
|
2793
2530
|
spatialIndexRef.current?.clear();
|
|
2794
2531
|
if (dragMoveRafRef.current !== null) cancelAnimationFrame(dragMoveRafRef.current);
|
|
2795
2532
|
};
|
|
2796
2533
|
}, []);
|
|
2797
|
-
const
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
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]);
|
|
2801
2575
|
if (!doc) {
|
|
2802
|
-
return /* @__PURE__ */
|
|
2576
|
+
return /* @__PURE__ */ jsx9("div", { className: `vi-map ${className}`, ...design.attrs, style: { display: "flex", alignItems: "center", justifyContent: "center", ...style }, children: /* @__PURE__ */ jsx9("span", { className: "body-sm", style: { color: "var(--ink-muted)" }, children: "No document" }) });
|
|
2803
2577
|
}
|
|
2804
2578
|
const hasBackOption = navStack.length > 1;
|
|
2805
|
-
|
|
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(
|
|
2806
2587
|
"div",
|
|
2807
2588
|
{
|
|
2808
2589
|
ref: containerRef,
|
|
2809
|
-
className
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
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 });
|
|
2818
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 },
|
|
2819
2605
|
onMouseMove: handleCanvasMouseMove,
|
|
2820
2606
|
onMouseLeave: () => {
|
|
2821
2607
|
if (!tooltipHoverRef.current && !keepTooltipOpenRef.current) {
|
|
@@ -2823,7 +2609,8 @@ function VisualliCanvas(props) {
|
|
|
2823
2609
|
}
|
|
2824
2610
|
},
|
|
2825
2611
|
children: [
|
|
2826
|
-
/* @__PURE__ */
|
|
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(
|
|
2827
2614
|
KonvaStage_default,
|
|
2828
2615
|
{
|
|
2829
2616
|
ref: stageRef,
|
|
@@ -2836,56 +2623,59 @@ function VisualliCanvas(props) {
|
|
|
2836
2623
|
onTouchMove: handleStageTouchMove,
|
|
2837
2624
|
onTouchEnd: handleStageTouchEnd,
|
|
2838
2625
|
children: [
|
|
2839
|
-
/* @__PURE__ */
|
|
2840
|
-
/* @__PURE__ */
|
|
2841
|
-
|
|
2842
|
-
{
|
|
2843
|
-
nodes: flatNodes,
|
|
2844
|
-
connections,
|
|
2845
|
-
isDark,
|
|
2846
|
-
isDragging: isDraggingState
|
|
2847
|
-
}
|
|
2848
|
-
),
|
|
2849
|
-
/* @__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(
|
|
2850
2629
|
KonvaNodeLayer,
|
|
2851
2630
|
{
|
|
2631
|
+
design,
|
|
2632
|
+
clock,
|
|
2852
2633
|
isTransitioning,
|
|
2853
2634
|
isDragging: isDraggingState,
|
|
2854
2635
|
hoveredNodeId: pointerNodeId,
|
|
2855
|
-
pressedNodeId
|
|
2636
|
+
pressedNodeId,
|
|
2637
|
+
selectedNodeId,
|
|
2638
|
+
focusedNodeId,
|
|
2639
|
+
touch: touchMode,
|
|
2640
|
+
peekedNodeId: touchMode ? pinnedId : null
|
|
2856
2641
|
}
|
|
2857
|
-
)
|
|
2642
|
+
),
|
|
2643
|
+
/* @__PURE__ */ jsx9(KonvaContainerLabelLayer, { containers, connections, design })
|
|
2858
2644
|
]
|
|
2859
2645
|
}
|
|
2860
2646
|
) }),
|
|
2861
|
-
|
|
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(
|
|
2862
2659
|
"div",
|
|
2863
2660
|
{
|
|
2864
2661
|
role: "menu",
|
|
2865
2662
|
"aria-label": "Canvas options",
|
|
2866
2663
|
style: {
|
|
2867
2664
|
position: "absolute",
|
|
2868
|
-
zIndex:
|
|
2665
|
+
zIndex: "var(--z-menu)",
|
|
2869
2666
|
pointerEvents: "auto",
|
|
2870
2667
|
left: `${Math.min(canvasContextMenu.x, Math.max(8, (canvasSizeRef.current.width || 800) - 152))}px`,
|
|
2871
2668
|
top: `${Math.min(canvasContextMenu.y, Math.max(8, (canvasSizeRef.current.height || 600) - 72))}px`
|
|
2872
2669
|
},
|
|
2873
2670
|
onContextMenu: (e) => e.preventDefault(),
|
|
2874
2671
|
onPointerDown: (e) => e.stopPropagation(),
|
|
2875
|
-
children: /* @__PURE__ */
|
|
2672
|
+
children: /* @__PURE__ */ jsx9(
|
|
2876
2673
|
"button",
|
|
2877
2674
|
{
|
|
2878
2675
|
type: "button",
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
border: `1px solid ${isDark ? "#333330" : "#DDD9D0"}`,
|
|
2883
|
-
background: isDark ? "#1C1C1A" : "#FAF8F4",
|
|
2884
|
-
color: isDark ? "#F0EDE6" : "#1A1A18",
|
|
2885
|
-
cursor: "pointer",
|
|
2886
|
-
fontSize: 13,
|
|
2887
|
-
boxShadow: "0 4px 12px rgba(0,0,0,0.15)"
|
|
2888
|
-
},
|
|
2676
|
+
role: "menuitem",
|
|
2677
|
+
className: "vi-iconbtn vi-iconbtn--surface",
|
|
2678
|
+
style: { width: "auto", padding: "6px var(--space-4)", boxShadow: "var(--shadow-pop)" },
|
|
2889
2679
|
onClick: () => {
|
|
2890
2680
|
setCanvasContextMenu(null);
|
|
2891
2681
|
handleNavigateBack(navStack.length - 2);
|
|
@@ -2895,32 +2685,41 @@ function VisualliCanvas(props) {
|
|
|
2895
2685
|
)
|
|
2896
2686
|
}
|
|
2897
2687
|
),
|
|
2898
|
-
/* @__PURE__ */
|
|
2899
|
-
/* @__PURE__ */
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
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(
|
|
2912
2719
|
"div",
|
|
2913
2720
|
{
|
|
2914
|
-
|
|
2915
|
-
style: {
|
|
2916
|
-
position: "absolute",
|
|
2917
|
-
zIndex: 50,
|
|
2918
|
-
left: safeLeft,
|
|
2919
|
-
top: safeTop,
|
|
2920
|
-
transform: "translate(-50%, -100%)",
|
|
2921
|
-
transformOrigin: "bottom center",
|
|
2922
|
-
pointerEvents: "auto"
|
|
2923
|
-
},
|
|
2721
|
+
className: "vi-map__fact",
|
|
2722
|
+
style: { left: at.left, top: at.top, pointerEvents: "auto", zIndex: "var(--z-card)" },
|
|
2924
2723
|
onMouseEnter: () => {
|
|
2925
2724
|
tooltipHoverRef.current = true;
|
|
2926
2725
|
if (clearTooltipTimerRef.current) {
|
|
@@ -2933,22 +2732,17 @@ function VisualliCanvas(props) {
|
|
|
2933
2732
|
if (clearTooltipTimerRef.current) clearTimeout(clearTooltipTimerRef.current);
|
|
2934
2733
|
clearTooltipTimerRef.current = setTimeout(() => {
|
|
2935
2734
|
if (!keepTooltipOpenRef.current) setHoveredNode(null);
|
|
2936
|
-
},
|
|
2735
|
+
}, PEEK.hoverClose);
|
|
2937
2736
|
},
|
|
2938
|
-
children: /* @__PURE__ */
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
summary: hoveredNode.summary,
|
|
2948
|
-
nodeId: hoveredNode.nodeId,
|
|
2949
|
-
nodeColor: nodeData?.color,
|
|
2950
|
-
zoom: viewport.zoomLevel
|
|
2951
|
-
}) : 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
|
+
)
|
|
2952
2746
|
}
|
|
2953
2747
|
);
|
|
2954
2748
|
})()
|
|
@@ -2958,91 +2752,29 @@ function VisualliCanvas(props) {
|
|
|
2958
2752
|
}
|
|
2959
2753
|
|
|
2960
2754
|
// src/VisualliRenderer.tsx
|
|
2961
|
-
import { jsx as
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
mq.addEventListener("change", handler);
|
|
2969
|
-
return () => mq.removeEventListener("change", handler);
|
|
2970
|
-
}, [mq]);
|
|
2971
|
-
return dark;
|
|
2972
|
-
}
|
|
2973
|
-
function useFontInjection() {
|
|
2974
|
-
useEffect7(() => {
|
|
2975
|
-
if (typeof document === "undefined") return;
|
|
2976
|
-
const links = [
|
|
2977
|
-
"https://fonts.googleapis.com",
|
|
2978
|
-
"https://fonts.gstatic.com",
|
|
2979
|
-
"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",
|
|
2980
|
-
"https://fonts.googleapis.com/css2?family=Playpen+Sans:wght@300;400;600;800&family=Story+Script:wght@400&display=swap"
|
|
2981
|
-
];
|
|
2982
|
-
links.forEach((href, i) => {
|
|
2983
|
-
const id = `visualli-font-${i}`;
|
|
2984
|
-
if (!document.getElementById(id)) {
|
|
2985
|
-
const link = document.createElement("link");
|
|
2986
|
-
link.id = id;
|
|
2987
|
-
link.rel = i < 2 ? "preconnect" : "stylesheet";
|
|
2988
|
-
link.href = href;
|
|
2989
|
-
if (i === 1) link.crossOrigin = "anonymous";
|
|
2990
|
-
document.head.appendChild(link);
|
|
2991
|
-
}
|
|
2992
|
-
});
|
|
2993
|
-
}, []);
|
|
2994
|
-
}
|
|
2995
|
-
function EmptyState({ isDark }) {
|
|
2996
|
-
const bg = isDark ? "#131311" : "#F5F3EF";
|
|
2997
|
-
const fg = isDark ? "rgba(240,237,230,0.35)" : "rgba(26,26,24,0.35)";
|
|
2998
|
-
const bd = isDark ? "#2A2A28" : "#DDD9D0";
|
|
2999
|
-
const code = { fontFamily: "monospace", fontSize: 12, padding: "1px 4px", borderRadius: 4, background: isDark ? "#222220" : "#E8E4DC", border: `1px solid ${bd}` };
|
|
3000
|
-
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: [
|
|
3001
|
-
/* @__PURE__ */ jsxs8("svg", { width: "48", height: "48", viewBox: "0 0 48 48", fill: "none", stroke: fg, strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
3002
|
-
/* @__PURE__ */ jsx12("rect", { x: "10", y: "6", width: "28", height: "36", rx: "3" }),
|
|
3003
|
-
/* @__PURE__ */ jsx12("line", { x1: "16", y1: "16", x2: "32", y2: "16" }),
|
|
3004
|
-
/* @__PURE__ */ jsx12("line", { x1: "16", y1: "22", x2: "32", y2: "22" }),
|
|
3005
|
-
/* @__PURE__ */ jsx12("line", { x1: "16", y1: "28", x2: "26", y2: "28" })
|
|
3006
|
-
] }),
|
|
3007
|
-
/* @__PURE__ */ jsxs8("div", { style: { textAlign: "center", lineHeight: 1.7 }, children: [
|
|
3008
|
-
/* @__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" }),
|
|
3009
|
-
/* @__PURE__ */ jsxs8("p", { style: { margin: "4px 0 0", fontSize: 13 }, children: [
|
|
3010
|
-
"Pass a ",
|
|
3011
|
-
/* @__PURE__ */ jsx12("code", { style: code, children: "visualliFile" }),
|
|
3012
|
-
" or ",
|
|
3013
|
-
/* @__PURE__ */ jsx12("code", { style: code, children: "visualliString" }),
|
|
3014
|
-
" prop."
|
|
3015
|
-
] })
|
|
3016
|
-
] })
|
|
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: "label", style: { margin: 0, color: "var(--ink)" }, children: title }),
|
|
2761
|
+
children && /* @__PURE__ */ jsx10("div", { className: "body-sm", style: { margin: 0, maxWidth: 480, overflowWrap: "anywhere" }, children })
|
|
3017
2762
|
] });
|
|
3018
2763
|
}
|
|
3019
|
-
function
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
/* @__PURE__ */
|
|
3025
|
-
|
|
3026
|
-
/* @__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
|
+
"."
|
|
3027
2771
|
] });
|
|
3028
2772
|
}
|
|
3029
|
-
function
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
const body = isDark ? "rgba(248,113,113,0.8)" : "rgba(180,40,40,0.8)";
|
|
3035
|
-
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: [
|
|
3036
|
-
/* @__PURE__ */ jsxs8("div", { style: { display: "flex", alignItems: "center", gap: 10, marginBottom: 12 }, children: [
|
|
3037
|
-
/* @__PURE__ */ jsxs8("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: head, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
3038
|
-
/* @__PURE__ */ jsx12("circle", { cx: "12", cy: "12", r: "10" }),
|
|
3039
|
-
/* @__PURE__ */ jsx12("line", { x1: "12", y1: "8", x2: "12", y2: "12" }),
|
|
3040
|
-
/* @__PURE__ */ jsx12("line", { x1: "12", y1: "16", x2: "12.01", y2: "16" })
|
|
3041
|
-
] }),
|
|
3042
|
-
/* @__PURE__ */ jsx12("span", { style: { fontSize: 15, fontWeight: 700, color: head }, children: "Failed to parse .visualli file" })
|
|
3043
|
-
] }),
|
|
3044
|
-
/* @__PURE__ */ jsx12("p", { style: { margin: 0, fontSize: 13, color: body, lineHeight: 1.6, wordBreak: "break-word", fontFamily: "monospace" }, children: message })
|
|
3045
|
-
] }) });
|
|
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 }) });
|
|
3046
2778
|
}
|
|
3047
2779
|
var WORKER_SCRIPT = `
|
|
3048
2780
|
self.onmessage = function(e) {
|
|
@@ -3054,6 +2786,7 @@ self.onmessage = function(e) {
|
|
|
3054
2786
|
layers: new Map(),
|
|
3055
2787
|
layersByLevel: new Map(),
|
|
3056
2788
|
rootLayer: null,
|
|
2789
|
+
extensions: {},
|
|
3057
2790
|
};
|
|
3058
2791
|
for (var i = 0; i < lines.length; i++) {
|
|
3059
2792
|
var line = lines[i].trim();
|
|
@@ -3069,6 +2802,8 @@ self.onmessage = function(e) {
|
|
|
3069
2802
|
if (!doc.layersByLevel.has(obj.level)) doc.layersByLevel.set(obj.level, []);
|
|
3070
2803
|
doc.layersByLevel.get(obj.level).push(obj);
|
|
3071
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 : [];
|
|
3072
2807
|
}
|
|
3073
2808
|
}
|
|
3074
2809
|
if (!doc.meta) throw new Error('Missing required meta section');
|
|
@@ -3087,9 +2822,9 @@ function getWorkerUrl() {
|
|
|
3087
2822
|
return _workerUrl;
|
|
3088
2823
|
}
|
|
3089
2824
|
function useAsyncParse(content, useWorker) {
|
|
3090
|
-
const [state, setState] =
|
|
3091
|
-
const workerRef =
|
|
3092
|
-
|
|
2825
|
+
const [state, setState] = useState5({ status: "idle" });
|
|
2826
|
+
const workerRef = useRef11(null);
|
|
2827
|
+
useEffect8(() => {
|
|
3093
2828
|
if (!content) {
|
|
3094
2829
|
setState({ status: "idle" });
|
|
3095
2830
|
return;
|
|
@@ -3131,16 +2866,16 @@ function useAsyncParse(content, useWorker) {
|
|
|
3131
2866
|
cancelled = true;
|
|
3132
2867
|
};
|
|
3133
2868
|
}, [content, useWorker]);
|
|
3134
|
-
|
|
2869
|
+
useEffect8(() => () => {
|
|
3135
2870
|
workerRef.current?.terminate();
|
|
3136
2871
|
}, []);
|
|
3137
2872
|
return state;
|
|
3138
2873
|
}
|
|
3139
2874
|
function useFileText(file) {
|
|
3140
|
-
const [text, setText] =
|
|
3141
|
-
const [error, setError] =
|
|
3142
|
-
const [loading, setLoading] =
|
|
3143
|
-
|
|
2875
|
+
const [text, setText] = useState5(void 0);
|
|
2876
|
+
const [error, setError] = useState5(void 0);
|
|
2877
|
+
const [loading, setLoading] = useState5(false);
|
|
2878
|
+
useEffect8(() => {
|
|
3144
2879
|
if (!file) {
|
|
3145
2880
|
setText(void 0);
|
|
3146
2881
|
setError(void 0);
|
|
@@ -3189,41 +2924,58 @@ function VisualliRenderer({
|
|
|
3189
2924
|
visualliString,
|
|
3190
2925
|
visualliFile,
|
|
3191
2926
|
theme = "light",
|
|
2927
|
+
comfort,
|
|
2928
|
+
respectForcedColors,
|
|
2929
|
+
layout,
|
|
2930
|
+
controlsPosition,
|
|
2931
|
+
app,
|
|
3192
2932
|
width = "100%",
|
|
3193
2933
|
height = "100%",
|
|
3194
2934
|
useWorker = true,
|
|
3195
2935
|
chromaticImmersion = false,
|
|
2936
|
+
fontBaseUrl,
|
|
2937
|
+
loadWebFonts,
|
|
3196
2938
|
className,
|
|
3197
2939
|
style
|
|
3198
2940
|
}) {
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
2941
|
+
const design = useDesign({ theme, comfort, respectForcedColors });
|
|
2942
|
+
useState5(() => {
|
|
2943
|
+
ensureDesignSystemStyles({ fontBaseUrl, loadWebFonts });
|
|
2944
|
+
return true;
|
|
2945
|
+
});
|
|
3202
2946
|
const cssWidth = typeof width === "number" ? `${width}px` : width;
|
|
3203
2947
|
const cssHeight = typeof height === "number" ? `${height}px` : height;
|
|
3204
2948
|
const { text: fileText, error: fileReadError, loading: fileReading } = useFileText(visualliFile);
|
|
3205
2949
|
const rawContent = fileText ?? visualliString;
|
|
3206
2950
|
const parseState = useAsyncParse(rawContent, useWorker);
|
|
3207
|
-
const resolvedDoc =
|
|
2951
|
+
const resolvedDoc = useMemo10(() => {
|
|
3208
2952
|
if (parseState.status === "ready") return parseState.doc;
|
|
3209
2953
|
return null;
|
|
3210
2954
|
}, [parseState]);
|
|
3211
2955
|
const wrapperStyle = { width: cssWidth, height: cssHeight, overflow: "hidden", ...style };
|
|
2956
|
+
const attrs = design.attrs;
|
|
3212
2957
|
if (!visualliString && !visualliFile) {
|
|
3213
|
-
return /* @__PURE__ */
|
|
2958
|
+
return /* @__PURE__ */ jsx10("div", { className, ...attrs, style: wrapperStyle, children: /* @__PURE__ */ jsx10(EmptyState, {}) });
|
|
3214
2959
|
}
|
|
3215
2960
|
if (fileReading || parseState.status === "loading") {
|
|
3216
|
-
return /* @__PURE__ */
|
|
2961
|
+
return /* @__PURE__ */ jsx10("div", { className, ...attrs, style: wrapperStyle, children: /* @__PURE__ */ jsx10(LoadingState, {}) });
|
|
3217
2962
|
}
|
|
3218
2963
|
if (fileReadError || parseState.status === "error") {
|
|
3219
2964
|
const msg = fileReadError ?? (parseState.status === "error" ? parseState.message : "Unknown error");
|
|
3220
|
-
return /* @__PURE__ */
|
|
2965
|
+
return /* @__PURE__ */ jsx10("div", { className, ...attrs, style: wrapperStyle, children: /* @__PURE__ */ jsx10(ErrorState, { message: msg }) });
|
|
3221
2966
|
}
|
|
3222
|
-
return /* @__PURE__ */
|
|
2967
|
+
return /* @__PURE__ */ jsx10("div", { className, style: wrapperStyle, children: /* @__PURE__ */ jsx10(
|
|
3223
2968
|
VisualliCanvas,
|
|
3224
2969
|
{
|
|
3225
2970
|
preParsedVisualli: resolvedDoc ?? void 0,
|
|
3226
|
-
|
|
2971
|
+
theme,
|
|
2972
|
+
comfort,
|
|
2973
|
+
respectForcedColors,
|
|
2974
|
+
layout,
|
|
2975
|
+
controlsPosition,
|
|
2976
|
+
app,
|
|
2977
|
+
fontBaseUrl,
|
|
2978
|
+
loadWebFonts,
|
|
3227
2979
|
chromaticImmersion,
|
|
3228
2980
|
style: { width: "100%", height: "100%" }
|
|
3229
2981
|
}
|
|
@@ -3232,60 +2984,219 @@ function VisualliRenderer({
|
|
|
3232
2984
|
|
|
3233
2985
|
// src/context/VisualliContext.tsx
|
|
3234
2986
|
import { createContext, useContext } from "react";
|
|
3235
|
-
import { jsx as
|
|
2987
|
+
import { jsx as jsx11 } from "react/jsx-runtime";
|
|
3236
2988
|
var VisualliContext = createContext(null);
|
|
3237
2989
|
function VisualliProvider({
|
|
3238
2990
|
children
|
|
3239
2991
|
}) {
|
|
3240
2992
|
const value = {};
|
|
3241
|
-
return /* @__PURE__ */
|
|
2993
|
+
return /* @__PURE__ */ jsx11(VisualliContext.Provider, { value, children });
|
|
3242
2994
|
}
|
|
3243
2995
|
function useVisualli() {
|
|
3244
|
-
const
|
|
3245
|
-
if (!
|
|
2996
|
+
const ctx2 = useContext(VisualliContext);
|
|
2997
|
+
if (!ctx2) {
|
|
3246
2998
|
return {};
|
|
3247
2999
|
}
|
|
3248
|
-
return
|
|
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]);
|
|
3249
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;
|
|
3250
3137
|
export {
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3138
|
+
A11yLayer,
|
|
3139
|
+
BLOB_SHAPES,
|
|
3140
|
+
DEFAULT_DESIGN,
|
|
3141
|
+
DESIGN_SYSTEM_VERSION,
|
|
3142
|
+
DepthTrail,
|
|
3143
|
+
IDEA2 as IDEA,
|
|
3256
3144
|
KonvaContainer_default as KonvaContainer,
|
|
3145
|
+
KonvaContainerLabelLayer,
|
|
3257
3146
|
KonvaContainerLayer,
|
|
3258
3147
|
KonvaEdge_default as KonvaEdge,
|
|
3259
3148
|
KonvaEdgeLayer,
|
|
3260
|
-
KonvaLayerTransitionAnimator,
|
|
3261
3149
|
KonvaNode_default as KonvaNode,
|
|
3262
3150
|
KonvaNodeLayer,
|
|
3263
3151
|
KonvaStage_default as KonvaStage,
|
|
3264
|
-
|
|
3265
|
-
|
|
3152
|
+
LABEL_GROWTH,
|
|
3153
|
+
METRICS,
|
|
3266
3154
|
NavigationStack_default as NavigationStack,
|
|
3267
|
-
|
|
3155
|
+
PeekCard,
|
|
3156
|
+
RINGS2 as RINGS,
|
|
3157
|
+
THEME_NAMES,
|
|
3158
|
+
TOKENS3 as TOKENS,
|
|
3159
|
+
TOPICS,
|
|
3160
|
+
TermCard,
|
|
3268
3161
|
VisualliCanvas,
|
|
3269
3162
|
VisualliProvider,
|
|
3270
3163
|
VisualliRenderer,
|
|
3271
|
-
|
|
3272
|
-
|
|
3164
|
+
ZoomControls,
|
|
3165
|
+
arrowPath,
|
|
3166
|
+
blobPath4 as blobPath,
|
|
3167
|
+
blobRadius,
|
|
3273
3168
|
calculateFitCenter,
|
|
3274
3169
|
calculateFitView,
|
|
3275
3170
|
calculateFitZoom,
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
drawBlobPath,
|
|
3281
|
-
getBlobTypeForLayer,
|
|
3171
|
+
drawHull,
|
|
3172
|
+
drawPill,
|
|
3173
|
+
edgePath,
|
|
3174
|
+
ensureDesignSystemStyles,
|
|
3282
3175
|
getChildLayerForNode,
|
|
3283
3176
|
getConnectionsForLayer,
|
|
3284
3177
|
getLayerForNavigation,
|
|
3285
|
-
|
|
3286
|
-
|
|
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,
|
|
3287
3196
|
useKonvaRenderer,
|
|
3197
|
+
useLayerChoreography,
|
|
3288
3198
|
useNodeStore,
|
|
3199
|
+
useReaderEnv,
|
|
3289
3200
|
useRenderConfigStore,
|
|
3290
3201
|
useSelectionStore,
|
|
3291
3202
|
useViewportNodes,
|