@visualli/react 0.1.7 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -5
- package/dist/index.cjs +1900 -2033
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +354 -97
- package/dist/index.d.ts +354 -97
- package/dist/index.js +2005 -2095
- 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 +452 -409
- 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.1). Do not edit here: changes come in through PRs from the private design system. MIT license, see ../LICENSE. */\n:root, [data-theme="light"] {\n --font-display: "Atkinson Hyperlegible Next", system-ui, sans-serif;\n --font-ui: "Atkinson Hyperlegible Next", system-ui, -apple-system, "Segoe UI", sans-serif;\n --font-hand: Kalam, "Atkinson Hyperlegible Next", cursive;\n --font-note: Caveat, Kalam, cursive;\n --font-mono: "Atkinson Hyperlegible Mono", ui-monospace, "SF Mono", Menlo, monospace;\n --canvas: #f6f3ee;\n --surface: #ffffff;\n --surface-sunken: #f0ece5;\n --surface-overlay: #ffffff;\n --line: #e4dfd6;\n --line-strong: #7a7f94;\n --ink: #182140;\n --ink-muted: #4f5671;\n --ink-subtle: #5f647c;\n --action: #182140;\n --on-action: #ffffff;\n --focus: #325e8c;\n --link: #2f5a87;\n --selection: rgba(18, 199, 211, 0.28);\n --node-ink: #182140;\n --edge: #2a3048;\n --edge-label: #4f5671;\n --topic-teal: #87f2f8;\n --topic-teal-ring: #00cdd5;\n --topic-harbor: #bcdbff;\n --topic-harbor-ring: #76b2f5;\n --topic-iris: #e0c8fe;\n --topic-iris-ring: #bd96ea;\n --topic-berry: #ffbdc4;\n --topic-berry-ring: #f47e8f;\n --topic-coral: #ffc7b2;\n --topic-coral-ring: #f38e65;\n --topic-amber: #ffcf9d;\n --topic-amber-ring: #eb9c38;\n --topic-sun: #ffea94;\n --topic-sun-ring: #dfbd04;\n --topic-stone: #e0dacf;\n --topic-stone-ring: #bab09f;\n --space-1: 4px;\n --space-2: 8px;\n --space-3: 12px;\n --space-4: 16px;\n --space-7: 48px;\n --radius-sm: 10px;\n --radius-md: 16px;\n --radius-lg: 24px;\n --radius-pill: 999px;\n --shadow-float: 0 1px 2px rgba(24, 33, 64, 0.06), 0 8px 24px rgba(24, 33, 64, 0.08);\n --shadow-pop: 0 2px 4px rgba(24, 33, 64, 0.08), 0 16px 40px rgba(24, 33, 64, 0.14);\n --node-width: 200px;\n --node-height: 148px;\n --node-stroke: 3px;\n --edge-width: 2.5px;\n --edge-gap: 15px;\n --edge-arc-max: 45px;\n --immersion-alpha: 0.15;\n --zoom-min: 0.3;\n --zoom-max: 5;\n --duration-instant: 100ms;\n --duration-quick: 160ms;\n --duration-base: 240ms;\n --duration-zoom: 500ms;\n --duration-reveal: 420ms;\n --stagger-reveal: 70ms;\n --ease-zoom: cubic-bezier(0.2, 0.8, 0.2, 1);\n --ease-standard: cubic-bezier(0.4, 0, 0.2, 1);\n --ease-bloom: cubic-bezier(0.34, 1.4, 0.64, 1);\n --z-chrome: 10;\n --z-card: 20;\n --z-menu: 40;\n }\n[data-theme="dark"] {\n --canvas: #0f1220;\n --surface: #171b2b;\n --surface-sunken: #0f1220;\n --surface-overlay: #212639;\n --line: #2b3148;\n --line-strong: #6c7392;\n --ink: #eef0f7;\n --ink-muted: #b3b8cc;\n --ink-subtle: #8f95ae;\n --action: #f6f3ee;\n --on-action: #182140;\n --focus: #12c7d3;\n --link: #5fd8e1;\n --selection: rgba(18, 199, 211, 0.32);\n --edge: #c9cddc;\n --edge-label: #b3b8cc;\n --topic-teal-ring: #0fc5cd;\n --topic-harbor-ring: #73aceb;\n --topic-iris-ring: #b691e1;\n --topic-berry-ring: #eb798a;\n --topic-coral-ring: #ea8962;\n --topic-amber-ring: #e29737;\n --topic-sun-ring: #d8b70a;\n --topic-stone-ring: #b4a999;\n --shadow-float: 0 1px 2px rgba(0, 0, 0, 0.40), 0 8px 24px rgba(0, 0, 0, 0.35);\n --shadow-pop: 0 2px 4px rgba(0, 0, 0, 0.50), 0 16px 40px rgba(0, 0, 0, 0.50);\n }\n[data-theme="focus-light"] {\n --canvas: #f4f2ee;\n --edge: #5a6079;\n --edge-label: #5a6079;\n --topic-teal: #bce8ea;\n --topic-teal-ring: #67c6cb;\n --topic-harbor: #c9d9ec;\n --topic-harbor-ring: #8bb2de;\n --topic-iris: #d9cfe8;\n --topic-iris-ring: #b79ed4;\n --topic-berry: #edc6c9;\n --topic-berry-ring: #da9099;\n --topic-coral: #eecec1;\n --topic-coral-ring: #db9b82;\n --topic-amber: #eed4ba;\n --topic-amber-ring: #d6a570;\n --topic-sun: #f4eac1;\n --topic-sun-ring: #d2bf73;\n --topic-stone: #dddad5;\n }\n[data-theme="focus-dark"] {\n --canvas: #12141c;\n --surface: #171b2b;\n --surface-sunken: #0f1220;\n --surface-overlay: #212639;\n --line: #2b3148;\n --line-strong: #6c7392;\n --ink: #eef0f7;\n --ink-muted: #b3b8cc;\n --ink-subtle: #8f95ae;\n --action: #f6f3ee;\n --on-action: #182140;\n --focus: #12c7d3;\n --link: #5fd8e1;\n --selection: rgba(18, 199, 211, 0.32);\n --edge: #a9aec0;\n --edge-label: #b3b8cc;\n --topic-teal: #bce8ea;\n --topic-teal-ring: #71bdc1;\n --topic-harbor: #c9d9ec;\n --topic-harbor-ring: #8cabce;\n --topic-iris: #d9cfe8;\n --topic-iris-ring: #ae9bc6;\n --topic-berry: #edc6c9;\n --topic-berry-ring: #cc8e95;\n --topic-coral: #eecec1;\n --topic-coral-ring: #cd9884;\n --topic-amber: #eed4ba;\n --topic-amber-ring: #c9a176;\n --topic-sun: #f4eac1;\n --topic-sun-ring: #c9b97a;\n --topic-stone: #dddad5;\n --topic-stone-ring: #b4a999;\n }\n[data-theme="colorsafe-light"] {\n --topic-teal: #b0ecbc;\n --topic-teal-ring: #6cc982;\n --topic-harbor: #abdbfe;\n --topic-harbor-ring: #60b4ec;\n --topic-iris: #f6bde2;\n --topic-iris-ring: #dc87c0;\n --topic-berry: #feb79e;\n --topic-berry-ring: #ec7f58;\n --topic-coral: #fece96;\n --topic-coral-ring: #e49d41;\n --topic-amber: #b2e8fa;\n --topic-amber-ring: #5cc4e2;\n --topic-sun: #f6f097;\n --topic-sun-ring: #d0c637;\n --topic-stone: #dbdbdb;\n --topic-stone-ring: #b7afb1;\n }\n[data-theme="colorsafe-dark"] {\n --canvas: #0f1220;\n --surface: #171b2b;\n --surface-sunken: #0f1220;\n --surface-overlay: #212639;\n --line: #2b3148;\n --line-strong: #6c7392;\n --ink: #eef0f7;\n --ink-muted: #b3b8cc;\n --ink-subtle: #8f95ae;\n --action: #f6f3ee;\n --on-action: #182140;\n --focus: #12c7d3;\n --link: #5fd8e1;\n --selection: rgba(18, 199, 211, 0.32);\n --edge: #c9cddc;\n --edge-label: #b3b8cc;\n --topic-teal: #b0ecbc;\n --topic-teal-ring: #69c27e;\n --topic-harbor: #abdbfe;\n --topic-harbor-ring: #5eade2;\n --topic-iris: #f6bde2;\n --topic-iris-ring: #d383b9;\n --topic-berry: #feb79e;\n --topic-berry-ring: #e37a55;\n --topic-coral: #fece96;\n --topic-coral-ring: #db9840;\n --topic-amber: #b2e8fa;\n --topic-amber-ring: #5bbdda;\n --topic-sun: #f6f097;\n --topic-sun-ring: #c9bf37;\n --topic-stone: #dbdbdb;\n --topic-stone-ring: #b1a8aa;\n }\n[data-theme="contrast-light"] {\n --canvas: #ffffff;\n --surface-sunken: #f2f2f2;\n --line: #5c6178;\n --line-strong: #000000;\n --ink: #000000;\n --ink-muted: #1f2433;\n --ink-subtle: #3a3f52;\n --action: #000000;\n --focus: #0b4a8f;\n --link: #0b4a8f;\n --node-ink: #000000;\n --edge: #000000;\n --edge-label: #1f2433;\n --topic-teal-ring: #006b70;\n --topic-harbor-ring: #0e599d;\n --topic-iris-ring: #683b92;\n --topic-berry-ring: #96173a;\n --topic-coral-ring: #923400;\n --topic-amber-ring: #7e4d00;\n --topic-sun-ring: #7a6600;\n --topic-stone-ring: #63594b;\n --shadow-float: none;\n --shadow-pop: none;\n }\n[data-theme="contrast-dark"] {\n --canvas: #000000;\n --surface: #000000;\n --surface-sunken: #0d0d0d;\n --surface-overlay: #0d0d0d;\n --line: #9aa0b8;\n --line-strong: #ffffff;\n --ink: #ffffff;\n --ink-muted: #e6e8f0;\n --ink-subtle: #c8ccda;\n --action: #ffffff;\n --on-action: #000000;\n --focus: #5fe8f0;\n --link: #7ff0f5;\n --selection: rgba(18, 199, 211, 0.32);\n --node-ink: #000000;\n --edge: #ffffff;\n --edge-label: #e6e8f0;\n --topic-teal-ring: #64eff7;\n --topic-harbor-ring: #b0d5fe;\n --topic-iris-ring: #dcbfff;\n --topic-berry-ring: #feb3bb;\n --topic-coral-ring: #ffbea5;\n --topic-amber-ring: #ffc78a;\n --topic-sun-ring: #ffe26d;\n --topic-stone-ring: #ddd3c2;\n --shadow-float: none;\n --shadow-pop: none;\n }\n[data-motion="reduced"] {\n --duration-instant: 120ms;\n --duration-quick: 0ms;\n --duration-base: 0ms;\n --duration-zoom: 0ms;\n --duration-reveal: 0ms;\n --stagger-reveal: 0ms;\n }\n@media (prefers-reduced-motion: reduce) {\n :root:not([data-motion="full"]) {\n --duration-instant: 120ms;\n --duration-quick: 0ms;\n --duration-base: 0ms;\n --duration-zoom: 0ms;\n --duration-reveal: 0ms;\n --stagger-reveal: 0ms;\n }\n}\n\n/* Type styles */\n.vi-text-heading { font-family: var(--font-ui); font-size: 17px; line-height: 24px; font-weight: 700; }\n.vi-text-body { font-family: var(--font-ui); font-size: 15px; line-height: 22px; font-weight: 400; }\n.vi-text-body-sm { font-family: var(--font-ui); font-size: 13px; line-height: 18px; font-weight: 400; }\n.vi-text-label { font-family: var(--font-ui); font-size: 13px; line-height: 16px; font-weight: 600; letter-spacing: 0.01em; }\n.vi-text-caption { font-family: var(--font-ui); font-size: 12px; line-height: 16px; font-weight: 400; }\n.vi-text-node-root { font-family: var(--font-hand); font-size: 30px; line-height: 34px; font-weight: 700; }\n.vi-text-node-label { font-family: var(--font-hand); font-size: 22px; line-height: 26px; font-weight: 700; }\n.vi-text-edge-label { font-family: var(--font-note); font-size: 18px; line-height: 20px; font-weight: 500; }\n.vi-text-annotation { font-family: var(--font-note); font-size: 24px; line-height: 26px; font-weight: 600; }\n';
|
|
623
|
+
var SPEC_COMPONENT_CSS = '/* Styles for the spec components (map, nodes, connectors, peek, terms, depth trail, controls) \u2014 generated from the Visualli design system (v0.2.1). Do not edit here: changes come in through PRs from the private design system. MIT license, see ../LICENSE. */\n:root { --vi-fs-body: 15px; --vi-fs-sm: 13px; --vi-fs-cap: 12px; --vi-lh-body: 22px; --vi-label-scale: 1; }\n[data-theme$="dark"] :where([class^="vi-"], [class*=" vi-"]) { color-scheme: dark; }\n[data-scale="lg"] { --vi-fs-body: 17px; --vi-fs-sm: 15px; --vi-fs-cap: 14px; --vi-lh-body: 26px; --vi-label-scale: 1.15; }\n[data-type="readable"] { --font-hand: "Atkinson Hyperlegible Next", system-ui, sans-serif; --font-note: "Atkinson Hyperlegible Next", system-ui, sans-serif; --font-display: "Atkinson Hyperlegible Next", system-ui, sans-serif; }\n[data-type="readable"] .vi-node__label, [data-type="readable"] .vi-edge__label, [data-type="readable"] .vi-map__group-label { font-family: var(--font-ui) !important; font-weight: 700; letter-spacing: 0.02em; font-style: normal; }\n.vi-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }\n.vi-icon { flex: none; display: block; }\n:where([class^="vi-"], [class*=" vi-"]):where(button, a, input, textarea, [tabindex]):focus-visible, :where([class^="vi-"], [class*=" vi-"]) :where(button, a, input, textarea, [tabindex]):focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }\n.vi-iconbtn { display: inline-grid; place-items: center; border-radius: var(--radius-sm); border: 1.5px solid transparent; background: transparent; color: var(--ink); cursor: pointer; transition: background var(--duration-instant); flex: none; }\n.vi-iconbtn--sm { width: 32px; height: 32px; }\n.vi-iconbtn--md { width: 40px; height: 40px; }\n.vi-iconbtn--lg { width: 48px; height: 48px; border-radius: var(--radius-md); }\n.vi-iconbtn:hover { background: var(--surface-sunken); }\n.vi-iconbtn--surface { background: var(--surface); border-color: var(--line); }\n.vi-iconbtn--surface:hover { border-color: var(--line-strong); }\n.vi-iconbtn--primary { background: var(--action); color: var(--on-action); border-radius: var(--radius-pill); }\n.vi-iconbtn.is-active { background: var(--action); color: var(--on-action); }\n[data-theme^="contrast"] .vi-ctrls { border: 2px solid var(--line-strong); }\n.vi-node { position: relative; display: inline-grid; place-items: center; border: 0; padding: 0; background: none; color: var(--node-ink); cursor: default; transition: transform var(--duration-base) var(--ease-bloom), opacity var(--duration-base); -webkit-tap-highlight-color: transparent; }\nbutton.vi-node { cursor: pointer; }\n.vi-node__svg { position: absolute; inset: 0; overflow: visible; }\n.vi-node__body { filter: drop-shadow(var(--vi-node-shadow, 0 6px 14px rgba(24, 33, 64, .10))); }\n[data-theme$="dark"] .vi-node__body { --vi-node-shadow: 0 0 22px rgba(255, 255, 255, .10); }\n[data-theme^="contrast"] .vi-node__body { filter: none; }\n.vi-node__rings { transform-origin: center; transition: transform var(--duration-base) var(--ease-standard); }\n@media (hover: hover) {\n button.vi-node:hover .vi-node__rings { transform: rotate(var(--vi-ring-turn, -2deg)) scale(var(--vi-ring-grow, 1.015)); }\n button.vi-node:hover { transform: translateY(calc(var(--vi-lift, 2px) * -1)); }\n}\n.vi-node { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; touch-action: manipulation; }\n.vi-node.is-selected { transform: translateY(-2px); }\n.vi-node.is-selected .vi-node__rings { transform: rotate(var(--vi-ring-turn, -2deg)) scale(var(--vi-ring-grow, 1.015)); }\n.vi-node.is-selected .vi-node__body { stroke-width: 4; }\n.vi-node:focus-visible { outline: none; }\n.vi-node:focus-visible .vi-node__body { stroke: var(--focus); stroke-width: 4; }\n.vi-node.is-selected .vi-node__body { stroke: var(--ink); }\n.vi-node__label { position: relative; text-align: center; color: var(--node-ink); display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; font-size: calc(var(--vi-nfs, 22px) * var(--vi-label-scale) * var(--vi-ninv, 1)); line-height: 1.15; padding-top: 2px; }\n.vi-node__label.node-root { --vi-nfs: 30px; }\n.vi-node--mini .vi-node__label { --vi-nfs: 15px; }\n.vi-node.is-dimmed { opacity: 1; }\n[data-theme^="focus"] .vi-node.is-dimmed { opacity: .4; }\n.vi-map.is-touch .vi-node.is-dimmed { opacity: .6; }\n[data-theme^="focus"] .vi-map.is-touch .vi-node.is-dimmed { opacity: .4; }\n.vi-edge__line { fill: none; stroke: var(--edge); stroke-width: 2.5; stroke-linecap: round; }\n.vi-edge.is-dashed .vi-edge__line { stroke-dasharray: 7 7; }\n.vi-edge__arrow { fill: none; stroke: var(--edge); stroke-width: 2.5; stroke-linecap: round; stroke-linejoin: round; }\n.vi-edge__label { font: 500 calc(18px * var(--vi-inv, 1)) / 1 var(--font-note); fill: var(--edge-label); paint-order: stroke; stroke: var(--canvas); stroke-width: 5px; stroke-linejoin: round; letter-spacing: .01em; }\n[data-type="readable"] .vi-edge__label { font-size: calc(14px * var(--vi-inv, 1)); font-weight: 600; }\n.vi-fact { position: relative; width: 300px; box-sizing: border-box; padding: var(--space-4) var(--space-4) var(--space-3); background: var(--surface-overlay); color: var(--ink); border: 2px solid var(--vi-fact-ring); border-radius: 14px 18px 12px 16px / 16px 12px 18px 14px; font: 400 var(--vi-fs-sm)/20px var(--font-ui); z-index: var(--z-card); animation: vi-pop var(--duration-quick) var(--ease-standard); }\n.vi-fact { box-shadow: 5px 6px 0 0 var(--vi-fact-fill), 5px 6px 0 2px var(--vi-fact-ring); }\n[data-theme^="contrast"] .vi-fact { border-color: var(--ink); }\n[data-theme^="contrast"] .vi-fact { box-shadow: none; }\n.vi-fact__title { font: 700 22px/26px var(--font-hand); margin-bottom: var(--space-1); }\n.vi-fact__body { color: var(--ink); }\n.vi-fact__foot { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); margin-top: var(--space-3); padding-top: var(--space-2); border-top: 1px dashed var(--line); }\n.vi-fact__acts { display: inline-flex; gap: var(--space-3); align-items: center; flex: none; }\n.vi-fact__link { color: var(--link); font: 600 var(--vi-fs-cap)/16px var(--font-ui); }\n.vi-fact__step { display: inline-flex; align-items: center; gap: 2px; border: 0; background: var(--action); color: var(--on-action); font: 600 var(--vi-fs-cap)/16px var(--font-ui); padding: 5px 8px 5px 10px; border-radius: var(--radius-pill); cursor: pointer; }\n.vi-trail ol { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; position: relative; }\n.vi-trail li { position: relative; }\n.vi-trail li + li::before { content: ""; position: absolute; left: 13px; top: -8px; height: 10px; border-left: 1.5px dashed var(--line-strong); }\n.vi-trail button { display: inline-flex; align-items: center; gap: 6px; border: 0; background: none; padding: 4px 8px 4px 5px; border-radius: var(--radius-sm); color: var(--ink-muted); font: 400 var(--vi-fs-cap)/16px var(--font-ui); cursor: pointer; }\n.vi-trail button span { display: inline-flex; align-items: center; gap: 4px; }\n.vi-trail button:hover:not(:disabled) { background: var(--surface-sunken); color: var(--ink); }\n.vi-trail li.is-current button { color: var(--ink); font-weight: 700; font-size: var(--vi-fs-sm); cursor: default; background: var(--surface); box-shadow: var(--shadow-float); }\n.vi-trail li.is-current button:disabled { opacity: 1; }\n.vi-ctrls { display: inline-flex; align-items: center; gap: 2px; padding: 4px; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-md); box-shadow: var(--shadow-float); }\n.vi-ctrls__pct { min-width: 44px; text-align: center; font: 600 var(--vi-fs-cap)/16px var(--font-ui); color: var(--ink-muted); font-variant-numeric: tabular-nums; }\n.vi-ctrls__div { width: 1px; height: 20px; background: var(--line); margin: 0 4px; }\n.vi-map { position: relative; overflow: hidden; background: var(--canvas); border-radius: inherit; isolation: isolate; }\n.vi-map__immersion { position: absolute; inset: 0; opacity: var(--immersion-alpha); transition: background var(--duration-base); z-index: -1; }\n.vi-map__world { position: absolute; left: 0; top: 0; transform-origin: 0 0; }\n.vi-map__edges { position: absolute; overflow: visible; pointer-events: none; }\n.vi-map__group rect { fill: none; stroke: var(--line-strong); stroke-width: 1.5; stroke-dasharray: 10 6; }\n.vi-map__group-label { position: absolute; transform: translate(-50%, -50%); z-index: 2; max-width: 420px; box-sizing: border-box; padding: 6px 22px 8px; background: var(--topic-stone); border: 1.5px solid var(--topic-stone-ring); border-radius: var(--radius-pill); font: 700 22px/1.1 var(--font-note); color: var(--node-ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; pointer-events: none; font-size: calc(26px * var(--vi-label-scale) * var(--vi-ninv, 1)); }\n[data-theme^="contrast"] .vi-map__group-label { border: 2px solid var(--line-strong); }\n.vi-map__slot { position: absolute; transform: translate(-50%, -50%); }\n.vi-map.is-reveal .vi-map__slot { animation: vi-bloom var(--duration-reveal) var(--ease-bloom) backwards; }\n.vi-map.is-reveal .vi-edge { animation: vi-fade var(--duration-base) var(--ease-standard) backwards; }\n.vi-map.is-reveal .vi-edge__line { stroke-dasharray: 1; stroke-dashoffset: 1; animation: vi-line var(--vi-line-draw, .5s) var(--ease-standard) forwards; animation-delay: inherit; }\n.vi-map.is-reveal .vi-edge.is-dashed .vi-edge__line { stroke-dasharray: 7 7; stroke-dashoffset: 0; animation: vi-fade var(--vi-line-draw, .5s) var(--ease-standard) backwards; animation-delay: inherit; }\n.vi-map:not(.is-reveal) .vi-edge__line { stroke-dasharray: none; }\n.vi-map:not(.is-reveal) .vi-edge.is-dashed .vi-edge__line { stroke-dasharray: 7 7; }\n.vi-map__lens { position: absolute; inset: 0; transform-origin: var(--vi-ox) var(--vi-oy); }\n.vi-map.is-diving .vi-map__lens { transform: scale(var(--vi-dive-scale, 3.2)); opacity: 0; transition: transform var(--duration-zoom) var(--ease-zoom), opacity var(--vi-dive-fade, 160ms) var(--vi-dive-fade-delay, 150ms); }\n.vi-map.is-surfacing .vi-map__lens { transform: scale(var(--vi-surface-scale, .6)); opacity: 0; transform-origin: 50% 50%; transition: transform var(--vi-surface-dur, 220ms) var(--ease-zoom), opacity var(--vi-surface-fade, 200ms); }\n.vi-map.is-reveal .vi-map__lens { animation: vi-settle var(--vi-settle-dur, 260ms) var(--ease-standard); }\n[data-motion="reduced"] .vi-map__lens { animation: none !important; transform: none !important; }\n.vi-map__fact { position: absolute; }\n.vi-map.is-idle .vi-map__world { transition: transform var(--duration-base) var(--ease-standard); }\n.vi-map__hint { position: absolute; left: var(--space-4); right: var(--space-4); margin: 0; text-align: center; color: var(--ink-muted); pointer-events: none; z-index: var(--z-chrome); animation: vi-fade var(--duration-base) var(--ease-standard); }\n.vi-fact__head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-2); }\n.vi-fact__back { display: inline-flex; align-items: center; gap: 2px; border: 0; background: none; padding: 2px 0; margin-bottom: var(--space-1); color: var(--link); font: 600 var(--vi-fs-cap)/16px var(--font-ui); cursor: pointer; }\n.vi-term { border: 0; background: none; padding: 0; font: inherit; color: var(--link); text-decoration: underline; text-decoration-style: dotted; text-decoration-thickness: 1.5px; text-underline-offset: 3px; cursor: pointer; }\n.vi-fact--sheet { position: absolute; left: 0; right: 0; bottom: 0; width: auto; max-width: 560px; margin: 0 auto; max-height: var(--vi-sheet-h, 300px); overflow-y: auto; overscroll-behavior: contain; padding: 0 var(--space-4) calc(var(--space-4) + env(safe-area-inset-bottom, 0px)); border: 0; border-top: 2px solid var(--vi-fact-ring); border-radius: var(--radius-lg) var(--radius-lg) 0 0; box-shadow: 0 -6px 0 0 var(--vi-fact-fill), var(--shadow-pop); font-size: var(--vi-fs-body); line-height: var(--vi-lh-body); animation: vi-sheet-in var(--duration-base) var(--ease-standard); transition: transform var(--duration-quick) var(--ease-standard), max-height var(--duration-base) var(--ease-standard); }\n.vi-fact--sheet.is-expanded { max-height: calc(100% - var(--space-7)); }\n.vi-fact__grip { position: sticky; top: 0; display: flex; justify-content: center; margin: 0 calc(var(--space-4) * -1); background: var(--surface-overlay); touch-action: none; cursor: grab; z-index: 1; }\n.vi-fact__handle { position: relative; width: 64px; height: 24px; border: 0; padding: 0; background: none; cursor: pointer; }\n.vi-fact__handle::after { content: ""; position: absolute; left: 12px; right: 12px; top: 10px; height: 5px; border-radius: var(--radius-pill); background: var(--line-strong); }\n.vi-fact--sheet .vi-fact__title { font-size: calc(24px * var(--vi-label-scale)); line-height: 1.15; padding-top: var(--space-1); }\n.vi-fact--sheet .vi-fact__close { margin: 0 calc(var(--space-2) * -1) 0 0; }\n.vi-fact--sheet .vi-fact__step { min-height: 44px; padding: 0 var(--space-3) 0 var(--space-4); font-size: var(--vi-fs-sm); }\n.vi-fact--sheet .vi-fact__link { font-size: var(--vi-fs-sm); min-height: 44px; display: inline-flex; align-items: center; }\n[data-motion="reduced"] .vi-fact--sheet { animation: vi-fade 120ms; }\n@media (prefers-reduced-motion: reduce) {\n .vi-fact--sheet { animation: vi-fade 120ms; }\n .vi-map.is-idle .vi-map__world { transition: none; }\n}\n.vi-map__trail { position: absolute; top: var(--space-4); left: var(--space-4); z-index: var(--z-chrome); }\n.vi-map__ctrls { position: absolute; top: var(--space-4); right: var(--space-4); z-index: var(--z-chrome); }\n[data-reveal="instant"] .vi-map.is-reveal .vi-map__slot, [data-motion="reduced"] .vi-map.is-reveal .vi-map__slot { animation: vi-fade var(--vi-reduced-fade, 120ms) backwards; animation-delay: 0ms !important; }\n[data-reveal="instant"] .vi-map.is-reveal .vi-edge, [data-reveal="instant"] .vi-map.is-reveal .vi-edge__line, [data-motion="reduced"] .vi-map.is-reveal .vi-edge, [data-motion="reduced"] .vi-map.is-reveal .vi-edge__line { animation: none; stroke-dashoffset: 0; }\n@media (prefers-reduced-motion: reduce) {\n .vi-map.is-reveal .vi-map__slot { animation: vi-fade var(--vi-reduced-fade, 120ms) backwards; animation-delay: 0ms !important; }\n .vi-map.is-reveal .vi-edge, .vi-map.is-reveal .vi-edge__line { animation: none; stroke-dashoffset: 0; }\n}\n[data-motion="reduced"] :where([class^="vi-"], [class*=" vi-"]), [data-motion="reduced"] :where([class^="vi-"], [class*=" vi-"])::before, [data-motion="reduced"] :where([class^="vi-"], [class*=" vi-"])::after { transition-duration: 1ms !important; }\n.vi-fact__acts { flex-wrap: wrap; justify-content: flex-end; }\n.vi-fact__link { display: inline-flex; align-items: center; gap: 2px; }\n.vi-anchor { position: relative; display: inline; }\n.vi-anchor.is-open .vi-term { background: var(--selection); border-radius: 3px; }\n.vi-anchor__pop { position: absolute; left: -12px; top: calc(100% + 6px); z-index: var(--z-menu); display: block; width: max-content; }\n.vi-anchor-card { position: relative; display: block; width: min(300px, calc(100vw - 32px)); box-sizing: border-box; padding: var(--space-3) var(--space-4) var(--space-3); background: var(--surface-overlay); color: var(--ink); text-align: left; white-space: normal; font: 400 var(--vi-fs-sm)/20px var(--font-ui); border: 2px solid var(--vi-fact-ring); border-radius: 12px 16px 10px 14px / 14px 10px 16px 12px; box-shadow: 4px 5px 0 0 var(--vi-fact-fill), 4px 5px 0 2px var(--vi-fact-ring), var(--shadow-pop); animation: vi-pop var(--duration-quick) var(--ease-standard); }\n.vi-anchor-card.is-in-sheet { width: auto; border: 0; box-shadow: none; padding: 0; animation: none; background: transparent; }\n[data-theme^="contrast"] .vi-anchor-card { border-color: var(--ink); box-shadow: none; }\n.vi-anchor-card__eyebrow { font: 400 11px/14px var(--font-display); letter-spacing: .14em; text-transform: uppercase; color: var(--ink-muted); }\n.vi-anchor-card__word { font: 700 20px/24px var(--font-hand); color: var(--ink); margin: 2px 0 var(--space-1); }\n.vi-fact--sheet .vi-anchor-card__word { font-size: calc(24px * var(--vi-label-scale)); line-height: 1.15; }\n.vi-anchor-card__desc { margin: 0; color: var(--ink); }\n.vi-anchor-card__acts { display: flex; align-items: center; justify-content: flex-end; flex-wrap: wrap; gap: var(--space-3); margin-top: var(--space-3); padding-top: var(--space-2); border-top: 1px dashed var(--line); }\n.vi-fact--sheet .vi-anchor-card__acts .vi-fact__link { min-height: 44px; }\n[data-type="readable"] .vi-anchor-card__word { font-family: var(--font-ui); }\n[data-motion="reduced"] .vi-anchor-card { animation: vi-fade 120ms; }\n@media (prefers-reduced-motion: reduce) {\n .vi-anchor-card { animation: vi-fade 120ms; }\n}\n.vi-peek .vi-fact__foot { flex-wrap: wrap; row-gap: var(--space-2); }\n.vi-peek .vi-fact__acts { margin-left: auto; }\n.vi-map.has-highlight .vi-node.is-dimmed { opacity: .35; transition: opacity var(--duration-quick) var(--ease-standard); }\n.vi-map.has-highlight .vi-node.is-selected .vi-node__body { stroke: var(--ink); }\n.vi-map { container-type: inline-size; }\n.vi-edge__hit { fill: none; stroke: transparent; stroke-width: 18; pointer-events: stroke; cursor: pointer; }\n.vi-edge__hit:hover + .vi-edge__line { stroke-width: 3.5; }\n.vi-edge.is-selected .vi-edge__line, .vi-edge.is-selected .vi-edge__arrow { stroke: var(--focus); stroke-width: 3.5; }\n.vi-map__group-hit { fill: none; stroke: transparent; stroke-width: 16; pointer-events: stroke; cursor: pointer; }\n.vi-map__group.is-selected rect:not(.vi-map__group-hit) { stroke: var(--focus); stroke-width: 2.5; stroke-dasharray: none; }\n.vi-map__group-label.is-selected { border-color: var(--focus); color: var(--focus); box-shadow: 0 0 0 1px var(--focus); }\n.vi-map { touch-action: pan-x pan-y; }\n.vi-map.is-app { touch-action: none; }\n.vi-map.is-pinching .vi-map__world { transition: none; }\n.vi-map.is-instant.is-reveal .vi-map__slot { animation: vi-fade var(--vi-reduced-fade, 120ms) backwards; animation-delay: 0ms !important; }\n.vi-map.is-instant.is-reveal .vi-edge, .vi-map.is-instant.is-reveal .vi-edge__line { animation: none; stroke-dashoffset: 0; }\n.vi-map.is-instant .vi-node, .vi-map.is-instant .vi-node__rings { transition: none; }\n.vi-map.is-detail-body .vi-node:not(.is-selected):not(:focus-visible):not(:hover) :is(.vi-node__rings, .vi-node__label), .vi-map.is-detail-speck .vi-node:not(.is-selected):not(:focus-visible):not(:hover) :is(.vi-node__rings, .vi-node__label) { display: none; }\n.vi-map.is-detail-body .vi-node:not(.is-selected):not(:focus-visible):not(:hover) .vi-node__body, .vi-map.is-detail-speck .vi-node:not(.is-selected):not(:focus-visible):not(:hover) .vi-node__body, .vi-map.no-shadows .vi-node__body { filter: none; }\n@keyframes vi-pop { from { opacity: 0; transform: translateY(-4px) scale(.98); } }\n@keyframes vi-fade { from { opacity: 0; } }\n@keyframes vi-bloom { from { opacity: 0; transform: translate(calc(-50% + var(--vi-dx)), calc(-50% + var(--vi-dy))) scale(var(--vi-bloom-from, .55)); } }\n@keyframes vi-line { to { stroke-dashoffset: 0; } }\n@keyframes vi-settle { from { transform: scale(var(--vi-settle-from, .94)); } }\n@keyframes vi-sheet-in { from { transform: translateY(100%); } }\n';
|
|
624
|
+
var SPEC_FONTS_URL = "https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible+Next:ital,wght@0,200..800;1,200..800&family=Atkinson+Hyperlegible+Mono:wght@400;600&family=Kalam:wght@400;700&display=swap";
|
|
625
|
+
var SPEC_DESIGN_SYSTEM_VERSION = "0.2.1";
|
|
626
|
+
var SPEC_BUNDLED_FONTS = [
|
|
627
|
+
{
|
|
628
|
+
"family": "Caveat",
|
|
629
|
+
"file": "Caveat-Variable.ttf",
|
|
630
|
+
"weight": "400 700"
|
|
631
|
+
}
|
|
632
|
+
];
|
|
633
|
+
|
|
634
|
+
// src/design/runtime.ts
|
|
635
|
+
var STYLE_ID = "visualli-design-system";
|
|
636
|
+
var LAYOUT_CSS = `
|
|
637
|
+
.vi-map__ctrls { transition: bottom var(--duration-quick) var(--ease-standard); } /* controlsPosition 'bottom-right': rising above the sheet */
|
|
638
|
+
.vi-map.is-compact .vi-trail li:not(:first-child):not(.is-current):not(:nth-last-child(2)) { display: none; }
|
|
639
|
+
.vi-map.is-compact .vi-trail button span { max-width: 40vw; max-width: 40cqw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
640
|
+
.vi-map.is-compact .vi-fact:not(.vi-fact--sheet) { width: min(300px, calc(100cqw - 2 * var(--space-4))); }
|
|
641
|
+
.vi-map.is-touch .vi-trail button { min-height: 32px; }
|
|
642
|
+
`;
|
|
643
|
+
var FONTS_ID = "visualli-design-system-fonts";
|
|
644
|
+
var CAVEAT_ID = "visualli-design-system-caveat";
|
|
645
|
+
var SDK_VERSION = true ? "0.2.1" : "latest";
|
|
646
|
+
var webFontsSheet = Promise.resolve();
|
|
647
|
+
function ensureDesignSystemStyles(assets = {}) {
|
|
648
|
+
if (typeof document === "undefined") return Promise.resolve();
|
|
649
|
+
if (!document.getElementById(STYLE_ID)) {
|
|
650
|
+
const style = document.createElement("style");
|
|
651
|
+
style.id = STYLE_ID;
|
|
652
|
+
style.setAttribute("data-design-system", SPEC_DESIGN_SYSTEM_VERSION);
|
|
653
|
+
style.textContent = `${SPEC_TOKENS_CSS}
|
|
654
|
+
${SPEC_COMPONENT_CSS}
|
|
655
|
+
${LAYOUT_CSS}`;
|
|
656
|
+
document.head.appendChild(style);
|
|
657
|
+
}
|
|
658
|
+
if (assets.loadWebFonts !== false && SPEC_FONTS_URL && !document.getElementById(FONTS_ID)) {
|
|
659
|
+
const link = document.createElement("link");
|
|
660
|
+
link.id = FONTS_ID;
|
|
661
|
+
link.rel = "stylesheet";
|
|
662
|
+
link.href = SPEC_FONTS_URL;
|
|
663
|
+
webFontsSheet = new Promise((resolve) => {
|
|
664
|
+
link.addEventListener("load", () => resolve(), { once: true });
|
|
665
|
+
link.addEventListener("error", () => resolve(), { once: true });
|
|
666
|
+
});
|
|
667
|
+
document.head.appendChild(link);
|
|
668
|
+
}
|
|
669
|
+
if (!document.getElementById(CAVEAT_ID)) {
|
|
670
|
+
const base = (assets.fontBaseUrl ?? `https://cdn.jsdelivr.net/npm/@visualli/react@${SDK_VERSION}/fonts`).replace(/\/$/, "");
|
|
671
|
+
const style = document.createElement("style");
|
|
672
|
+
style.id = CAVEAT_ID;
|
|
673
|
+
style.textContent = SPEC_BUNDLED_FONTS.map((f) => `@font-face { font-family: "${f.family}"; src: url("${base}/${f.file}"); font-weight: ${f.weight}; font-display: swap; }`).join("\n");
|
|
674
|
+
document.head.appendChild(style);
|
|
675
|
+
}
|
|
676
|
+
return webFontsSheet;
|
|
677
|
+
}
|
|
678
|
+
var firstFamily = (stack) => stack.split(",")[0].trim();
|
|
679
|
+
var CANVAS_FONT_FACES = ["node-label", "edge-label", "body"].map((style) => {
|
|
680
|
+
const t = TYPE_STYLES[style];
|
|
681
|
+
return `${t.weight} ${t.size}px ${firstFamily(TOKENS.light[t.family])}`;
|
|
682
|
+
});
|
|
683
|
+
var fontsEpochValue = 0;
|
|
684
|
+
var fontListeners = /* @__PURE__ */ new Set();
|
|
685
|
+
var fontsEpoch = () => fontsEpochValue;
|
|
686
|
+
function useFontsEpoch() {
|
|
687
|
+
return useSyncExternalStore(onFontsChange, fontsEpoch, () => 0);
|
|
688
|
+
}
|
|
689
|
+
var onFontsChange = (cb) => {
|
|
690
|
+
fontListeners.add(cb);
|
|
691
|
+
return () => fontListeners.delete(cb);
|
|
692
|
+
};
|
|
693
|
+
function loadCanvasFonts(sheet = Promise.resolve(), timeoutMs = 3e3) {
|
|
694
|
+
if (typeof document === "undefined" || !("fonts" in document)) return Promise.resolve();
|
|
695
|
+
const fonts = document.fonts;
|
|
696
|
+
const loads = sheet.then(() => Promise.all(CANVAS_FONT_FACES.map((f) => fonts.load(f).catch(() => [])))).then(() => fonts.ready).then(() => void 0);
|
|
697
|
+
const timeout = new Promise((r) => setTimeout(r, timeoutMs));
|
|
698
|
+
return Promise.race([loads, timeout]).then(() => {
|
|
699
|
+
fontsEpochValue++;
|
|
700
|
+
fontListeners.forEach((cb) => cb());
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
if (typeof document !== "undefined" && "fonts" in document) {
|
|
704
|
+
document.fonts.addEventListener?.("loadingdone", () => {
|
|
705
|
+
fontsEpochValue++;
|
|
706
|
+
fontListeners.forEach((cb) => cb());
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
var currentPixelRatio = () => typeof window === "undefined" ? 1 : Math.min(Math.max(window.devicePixelRatio || 1, 1), 3);
|
|
710
|
+
function watchPixelRatio(cb) {
|
|
711
|
+
if (typeof window === "undefined" || !window.matchMedia) return () => {
|
|
712
|
+
};
|
|
713
|
+
let mql;
|
|
714
|
+
let stopped = false;
|
|
715
|
+
const arm = () => {
|
|
716
|
+
mql = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
|
|
717
|
+
mql.addEventListener("change", onChange, { once: true });
|
|
718
|
+
};
|
|
719
|
+
const onChange = () => {
|
|
720
|
+
if (stopped) return;
|
|
721
|
+
cb(currentPixelRatio());
|
|
722
|
+
arm();
|
|
723
|
+
};
|
|
724
|
+
arm();
|
|
725
|
+
return () => {
|
|
726
|
+
stopped = true;
|
|
727
|
+
mql.removeEventListener("change", onChange);
|
|
728
|
+
};
|
|
729
|
+
}
|
|
655
730
|
|
|
656
731
|
// src/components/KonvaStage.tsx
|
|
657
732
|
import { jsx } from "react/jsx-runtime";
|
|
@@ -677,10 +752,22 @@ var KonvaStage = forwardRef(function KonvaStage2({
|
|
|
677
752
|
}
|
|
678
753
|
}, []);
|
|
679
754
|
useEffect3(() => {
|
|
755
|
+
const stage = internalRef.current;
|
|
680
756
|
return () => {
|
|
681
|
-
|
|
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,17 +2340,14 @@ function VisualliCanvas(props) {
|
|
|
2656
2340
|
if (hovered !== pointerNodeIdRef.current) {
|
|
2657
2341
|
pointerNodeIdRef.current = hovered;
|
|
2658
2342
|
setPointerNodeId(hovered);
|
|
2659
|
-
|
|
2660
|
-
}
|
|
2661
|
-
if (hovered) {
|
|
2662
|
-
stage.container().style.cursor = "pointer";
|
|
2663
|
-
} else {
|
|
2664
|
-
stage.container().style.cursor = "grab";
|
|
2343
|
+
onNodeHoverRef.current?.(hovered);
|
|
2665
2344
|
}
|
|
2345
|
+
stage.container().style.cursor = hovered ? "pointer" : "";
|
|
2666
2346
|
}
|
|
2667
2347
|
rendererHandleMouseMove(e);
|
|
2668
2348
|
}, [toWorldCoords, hitTestNode, handleDragStart, handleDragMove, rendererHandleMouseMove]);
|
|
2669
2349
|
const handleStageMouseUp = useCallback4((e) => {
|
|
2350
|
+
if (isEmulatedMouse(e.evt)) return;
|
|
2670
2351
|
setPressedNodeId(null);
|
|
2671
2352
|
if (stageActiveNodeIdRef.current) {
|
|
2672
2353
|
const nodeId = stageActiveNodeIdRef.current;
|
|
@@ -2684,7 +2365,12 @@ function VisualliCanvas(props) {
|
|
|
2684
2365
|
const node = nodes.get(nodeId);
|
|
2685
2366
|
if (node) {
|
|
2686
2367
|
const childLayer = doc ? getChildLayerForNode(doc, nodeId, currentLayerId ?? "") : null;
|
|
2687
|
-
if (childLayer) {
|
|
2368
|
+
if (touchModeRef.current && childLayer && pinnedIdRef.current === nodeId) {
|
|
2369
|
+
setPinnedId(null);
|
|
2370
|
+
handleNavigate(nodeId);
|
|
2371
|
+
} else if (touchModeRef.current && (node.description || childLayer)) {
|
|
2372
|
+
openSheet(node);
|
|
2373
|
+
} else if (childLayer) {
|
|
2688
2374
|
handleNavigate(nodeId);
|
|
2689
2375
|
}
|
|
2690
2376
|
onNodeClick?.(node);
|
|
@@ -2697,6 +2383,12 @@ function VisualliCanvas(props) {
|
|
|
2697
2383
|
stageDragCommittedRef.current = false;
|
|
2698
2384
|
return;
|
|
2699
2385
|
}
|
|
2386
|
+
const tap = emptyTapRef.current;
|
|
2387
|
+
emptyTapRef.current = null;
|
|
2388
|
+
if (tap && pinnedIdRef.current) {
|
|
2389
|
+
const end = clientOf(e.evt);
|
|
2390
|
+
if (!Number.isFinite(end.x) || Math.hypot(end.x - tap.x, end.y - tap.y) < 6) setPinnedId(null);
|
|
2391
|
+
}
|
|
2700
2392
|
isCanvasPanningRef.current = false;
|
|
2701
2393
|
rendererHandleMouseUp(e);
|
|
2702
2394
|
}, [toWorldCoords, handleDragEnd, nodes, handleNavigate, onNodeClick, rendererHandleMouseUp]);
|
|
@@ -2711,31 +2403,75 @@ function VisualliCanvas(props) {
|
|
|
2711
2403
|
const localY = rect ? e.evt.clientY - rect.top : e.evt.clientY;
|
|
2712
2404
|
setCanvasContextMenu({ x: localX, y: localY });
|
|
2713
2405
|
}, [navStack.length]);
|
|
2714
|
-
const
|
|
2715
|
-
const
|
|
2716
|
-
const
|
|
2717
|
-
const
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2406
|
+
const touchPointsRef = useRef10(/* @__PURE__ */ new Map());
|
|
2407
|
+
const pinchRef = useRef10(null);
|
|
2408
|
+
const pinchEndedAtRef = useRef10(-Infinity);
|
|
2409
|
+
const [pinching, setPinching] = useState4(false);
|
|
2410
|
+
const localOf = (e) => {
|
|
2411
|
+
const r = containerRef.current.getBoundingClientRect();
|
|
2412
|
+
return { x: e.clientX - r.left, y: e.clientY - r.top };
|
|
2413
|
+
};
|
|
2414
|
+
const onPinchDown = (e) => {
|
|
2415
|
+
if (!GESTURE.pinchZoom || !GESTURE.pointers.includes(e.pointerType)) return;
|
|
2416
|
+
touchPointsRef.current.set(e.pointerId, localOf(e));
|
|
2417
|
+
if (touchPointsRef.current.size !== 2 || isTransitioningRef.current || isAnimating()) return;
|
|
2418
|
+
const [a, b] = [...touchPointsRef.current.values()];
|
|
2419
|
+
const vp = useViewportStore.getState();
|
|
2420
|
+
pinchRef.current = { d0: Math.hypot(a.x - b.x, a.y - b.y), m0: { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 }, z0: vp.zoomLevel, c0: { x: vp.centerX, y: vp.centerY } };
|
|
2421
|
+
rendererCancelPan();
|
|
2422
|
+
isCanvasPanningRef.current = false;
|
|
2423
|
+
emptyTapRef.current = null;
|
|
2424
|
+
stageActiveNodeIdRef.current = null;
|
|
2425
|
+
stageDragCommittedRef.current = false;
|
|
2426
|
+
isDraggingRef.current = false;
|
|
2427
|
+
setPressedNodeId(null);
|
|
2428
|
+
setPinching(true);
|
|
2429
|
+
};
|
|
2430
|
+
const onPinchMove = (e) => {
|
|
2431
|
+
if (!touchPointsRef.current.has(e.pointerId)) return;
|
|
2432
|
+
touchPointsRef.current.set(e.pointerId, localOf(e));
|
|
2433
|
+
const p = pinchRef.current;
|
|
2434
|
+
if (!p || touchPointsRef.current.size < 2) return;
|
|
2435
|
+
const [a, b] = [...touchPointsRef.current.values()];
|
|
2436
|
+
const fit = useViewportStore.getState().fitScale;
|
|
2437
|
+
const z = pinchZoom(p.z0 / fit, p.d0, Math.hypot(a.x - b.x, a.y - b.y)) * fit;
|
|
2438
|
+
const m = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
|
|
2439
|
+
const cw = canvasSizeRef.current.width || containerRef.current?.clientWidth || 800;
|
|
2440
|
+
const ch = canvasSizeRef.current.height || containerRef.current?.clientHeight || 600;
|
|
2441
|
+
const t = zoomAround({ x: cw / 2 - p.c0.x * p.z0, y: ch / 2 - p.c0.y * p.z0 }, p.z0, z, p.m0, GESTURE.twoFingerPan ? m : p.m0);
|
|
2442
|
+
setZoom(z);
|
|
2443
|
+
setCenter((cw / 2 - t.x) / z, (ch / 2 - t.y) / z);
|
|
2444
|
+
};
|
|
2445
|
+
const onPinchUp = (e) => {
|
|
2446
|
+
if (!touchPointsRef.current.delete(e.pointerId)) return;
|
|
2447
|
+
if (pinchRef.current && touchPointsRef.current.size < 2) {
|
|
2448
|
+
pinchRef.current = null;
|
|
2449
|
+
pinchEndedAtRef.current = performance.now();
|
|
2450
|
+
setPinching(false);
|
|
2722
2451
|
}
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2452
|
+
};
|
|
2453
|
+
const inPinch = () => pinchRef.current !== null || touchPointsRef.current.size > 1 || performance.now() - pinchEndedAtRef.current < 300;
|
|
2454
|
+
const handleStageTouchStart = useCallback4((e) => {
|
|
2455
|
+
if (inPinch() || e.evt.touches.length > 1) return;
|
|
2456
|
+
handleStageMouseDown(e);
|
|
2457
|
+
}, [handleStageMouseDown]);
|
|
2458
|
+
const handleStageTouchMove = useCallback4((e) => {
|
|
2459
|
+
if (inPinch() || e.evt.touches.length > 1) return;
|
|
2460
|
+
handleStageMouseMove(e);
|
|
2461
|
+
}, [handleStageMouseMove]);
|
|
2462
|
+
const handleStageTouchEnd = useCallback4((e) => {
|
|
2463
|
+
if (inPinch()) {
|
|
2464
|
+
isCanvasPanningRef.current = false;
|
|
2733
2465
|
return;
|
|
2734
2466
|
}
|
|
2467
|
+
handleStageMouseUp(e);
|
|
2468
|
+
}, [handleStageMouseUp]);
|
|
2469
|
+
const handleWheel = useCallback4((e) => {
|
|
2735
2470
|
rendererHandleWheel(e);
|
|
2736
|
-
}, [rendererHandleWheel
|
|
2737
|
-
const lastHoverCheckRef =
|
|
2471
|
+
}, [rendererHandleWheel]);
|
|
2472
|
+
const lastHoverCheckRef = useRef10(0);
|
|
2738
2473
|
const handleCanvasMouseMove = useCallback4((e) => {
|
|
2474
|
+
if (touchModeRef.current || isEmulatedMouse(e.nativeEvent)) return;
|
|
2739
2475
|
if (!containerRef.current || isDraggingRef.current || isCanvasPanningRef.current) {
|
|
2740
2476
|
setHoveredNode(null);
|
|
2741
2477
|
return;
|
|
@@ -2771,7 +2507,7 @@ function VisualliCanvas(props) {
|
|
|
2771
2507
|
if (clearTooltipTimerRef.current) clearTimeout(clearTooltipTimerRef.current);
|
|
2772
2508
|
clearTooltipTimerRef.current = setTimeout(() => {
|
|
2773
2509
|
if (!tooltipHoverRef.current && !keepTooltipOpenRef.current) setHoveredNode(null);
|
|
2774
|
-
},
|
|
2510
|
+
}, PEEK.hoverClose);
|
|
2775
2511
|
} else {
|
|
2776
2512
|
if (clearTooltipTimerRef.current) {
|
|
2777
2513
|
clearTimeout(clearTooltipTimerRef.current);
|
|
@@ -2779,7 +2515,7 @@ function VisualliCanvas(props) {
|
|
|
2779
2515
|
}
|
|
2780
2516
|
}
|
|
2781
2517
|
}, [nodes, viewport, spatialIndexRef]);
|
|
2782
|
-
|
|
2518
|
+
useEffect7(() => {
|
|
2783
2519
|
const handleKeepTooltipOpen = (event) => {
|
|
2784
2520
|
keepTooltipOpenRef.current = event.detail.keep;
|
|
2785
2521
|
};
|
|
@@ -2788,35 +2524,84 @@ function VisualliCanvas(props) {
|
|
|
2788
2524
|
window.removeEventListener("keepNodeTooltipOpen", handleKeepTooltipOpen);
|
|
2789
2525
|
};
|
|
2790
2526
|
}, []);
|
|
2791
|
-
|
|
2527
|
+
useEffect7(() => {
|
|
2792
2528
|
return () => {
|
|
2793
2529
|
if (clearTooltipTimerRef.current) clearTimeout(clearTooltipTimerRef.current);
|
|
2794
2530
|
spatialIndexRef.current?.clear();
|
|
2795
2531
|
if (dragMoveRafRef.current !== null) cancelAnimationFrame(dragMoveRafRef.current);
|
|
2796
2532
|
};
|
|
2797
2533
|
}, []);
|
|
2798
|
-
const
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2534
|
+
const activateNode = useCallback4((node) => {
|
|
2535
|
+
const childLayer = doc ? getChildLayerForNode(doc, node.id, currentLayerId ?? "") : null;
|
|
2536
|
+
if (touchModeRef.current && childLayer && pinnedIdRef.current === node.id) {
|
|
2537
|
+
setPinnedId(null);
|
|
2538
|
+
handleNavigate(node.id);
|
|
2539
|
+
} else if (touchModeRef.current && (node.description || childLayer)) openSheet(node);
|
|
2540
|
+
else if (childLayer) handleNavigate(node.id);
|
|
2541
|
+
onNodeClick?.(node);
|
|
2542
|
+
}, [doc, currentLayerId, handleNavigate, onNodeClick]);
|
|
2543
|
+
const stepInside = useCallback4((nodeId) => {
|
|
2544
|
+
setHoveredNode(null);
|
|
2545
|
+
handleNavigate(nodeId);
|
|
2546
|
+
}, [handleNavigate]);
|
|
2547
|
+
useEffect7(() => {
|
|
2548
|
+
if (!hoveredNode) tooltipHoverRef.current = false;
|
|
2549
|
+
}, [hoveredNode]);
|
|
2550
|
+
const handleKeyDown = useCallback4((e) => {
|
|
2551
|
+
if (e.target.closest('input, textarea, [contenteditable="true"]')) return;
|
|
2552
|
+
const cmd = e.metaKey || e.ctrlKey;
|
|
2553
|
+
if (cmd && !e.altKey) {
|
|
2554
|
+
if (e.key === "+" || e.key === "=") {
|
|
2555
|
+
e.preventDefault();
|
|
2556
|
+
setZoom(viewport.zoomLevel * VIEW2.zoomStep);
|
|
2557
|
+
} else if (e.key === "-" || e.key === "_") {
|
|
2558
|
+
e.preventDefault();
|
|
2559
|
+
setZoom(viewport.zoomLevel / VIEW2.zoomStep);
|
|
2560
|
+
} else if (e.key === "0") {
|
|
2561
|
+
e.preventDefault();
|
|
2562
|
+
fitToScreen();
|
|
2563
|
+
}
|
|
2564
|
+
return;
|
|
2565
|
+
}
|
|
2566
|
+
if (e.altKey || e.shiftKey) return;
|
|
2567
|
+
if (e.key === "Escape" && pinnedIdRef.current) {
|
|
2568
|
+
e.preventDefault();
|
|
2569
|
+
setPinnedId(null);
|
|
2570
|
+
} else if ((e.key === "Escape" || e.key === "Backspace") && navStack.length > 1) {
|
|
2571
|
+
e.preventDefault();
|
|
2572
|
+
handleNavigateBack(navStack.length - 2);
|
|
2573
|
+
}
|
|
2574
|
+
}, [navStack.length, handleNavigateBack, setZoom, viewport.zoomLevel, fitToScreen]);
|
|
2802
2575
|
if (!doc) {
|
|
2803
|
-
return /* @__PURE__ */
|
|
2576
|
+
return /* @__PURE__ */ jsx9("div", { className: `vi-map ${className}`, ...design.attrs, style: { display: "flex", alignItems: "center", justifyContent: "center", ...style }, children: /* @__PURE__ */ jsx9("span", { className: "vi-text-body-sm", style: { color: "var(--ink-muted)" }, children: "No document" }) });
|
|
2804
2577
|
}
|
|
2805
2578
|
const hasBackOption = navStack.length > 1;
|
|
2806
|
-
|
|
2579
|
+
const currentLayerTitle = navStack[navStack.length - 1]?.label ?? "";
|
|
2580
|
+
const peekNode = hoveredNode ? nodes.get(hoveredNode.nodeId) : void 0;
|
|
2581
|
+
const pinnedNode = pinnedId ? nodes.get(pinnedId) : void 0;
|
|
2582
|
+
const pinnedHasChild = !!(pinnedNode && getChildLayerForNode(doc, pinnedNode.id, currentLayerId ?? ""));
|
|
2583
|
+
const compact = (viewport.canvasWidth || canvasSizeRef.current.width || 1024) < 560;
|
|
2584
|
+
const medium = !compact && (viewport.canvasWidth || canvasSizeRef.current.width || 1024) < 900;
|
|
2585
|
+
const peekHasChild = !!(peekNode && doc && getChildLayerForNode(doc, peekNode.id, currentLayerId ?? ""));
|
|
2586
|
+
return /* @__PURE__ */ jsxs3(
|
|
2807
2587
|
"div",
|
|
2808
2588
|
{
|
|
2809
2589
|
ref: containerRef,
|
|
2810
|
-
className
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2590
|
+
className: `vi-map${touchMode ? " is-touch" : ""}${compact ? " is-compact" : ""}${medium ? " is-medium" : ""}${pinching ? " is-pinching" : ""}${props.app ? " is-app" : ""} ${className}`,
|
|
2591
|
+
...design.attrs,
|
|
2592
|
+
"data-reveal": instantReveal ? "instant" : void 0,
|
|
2593
|
+
role: "group",
|
|
2594
|
+
"aria-label": `${doc.meta?.title ?? "Map"}${currentLayerTitle ? `, ${currentLayerTitle}` : ""}`,
|
|
2595
|
+
onKeyDown: handleKeyDown,
|
|
2596
|
+
tabIndex: -1,
|
|
2597
|
+
onPointerDownCapture: (e) => {
|
|
2598
|
+
onPinchDown(e);
|
|
2599
|
+
if (!e.target.closest("button, a, input")) containerRef.current?.focus({ preventScroll: true });
|
|
2819
2600
|
},
|
|
2601
|
+
onPointerMoveCapture: onPinchMove,
|
|
2602
|
+
onPointerUpCapture: onPinchUp,
|
|
2603
|
+
onPointerCancelCapture: onPinchUp,
|
|
2604
|
+
style: { position: "relative", overflow: "hidden", userSelect: "none", width: "100%", height: "100%", outline: "none", ...motionVars(), ...style },
|
|
2820
2605
|
onMouseMove: handleCanvasMouseMove,
|
|
2821
2606
|
onMouseLeave: () => {
|
|
2822
2607
|
if (!tooltipHoverRef.current && !keepTooltipOpenRef.current) {
|
|
@@ -2824,7 +2609,8 @@ function VisualliCanvas(props) {
|
|
|
2824
2609
|
}
|
|
2825
2610
|
},
|
|
2826
2611
|
children: [
|
|
2827
|
-
/* @__PURE__ */
|
|
2612
|
+
/* @__PURE__ */ jsx9("div", { className: "vi-map__immersion", style: { background: immersionFill ?? "transparent" }, "aria-hidden": "true" }),
|
|
2613
|
+
/* @__PURE__ */ jsx9("div", { ref: canvasWrapperRef, style: { position: "relative", zIndex: 1, pointerEvents: "auto" }, children: fontsReady && /* @__PURE__ */ jsxs3(
|
|
2828
2614
|
KonvaStage_default,
|
|
2829
2615
|
{
|
|
2830
2616
|
ref: stageRef,
|
|
@@ -2837,56 +2623,59 @@ function VisualliCanvas(props) {
|
|
|
2837
2623
|
onTouchMove: handleStageTouchMove,
|
|
2838
2624
|
onTouchEnd: handleStageTouchEnd,
|
|
2839
2625
|
children: [
|
|
2840
|
-
/* @__PURE__ */
|
|
2841
|
-
/* @__PURE__ */
|
|
2842
|
-
|
|
2843
|
-
{
|
|
2844
|
-
nodes: flatNodes,
|
|
2845
|
-
connections,
|
|
2846
|
-
isDark,
|
|
2847
|
-
isDragging: isDraggingState
|
|
2848
|
-
}
|
|
2849
|
-
),
|
|
2850
|
-
/* @__PURE__ */ jsx11(
|
|
2626
|
+
/* @__PURE__ */ jsx9(KonvaContainerLayer, { nodes: flatNodes, containers, connections, design }),
|
|
2627
|
+
/* @__PURE__ */ jsx9(KonvaEdgeLayer, { nodes: flatNodes, connections, design, isDragging: isDraggingState, clock }),
|
|
2628
|
+
/* @__PURE__ */ jsx9(
|
|
2851
2629
|
KonvaNodeLayer,
|
|
2852
2630
|
{
|
|
2631
|
+
design,
|
|
2632
|
+
clock,
|
|
2853
2633
|
isTransitioning,
|
|
2854
2634
|
isDragging: isDraggingState,
|
|
2855
2635
|
hoveredNodeId: pointerNodeId,
|
|
2856
|
-
pressedNodeId
|
|
2636
|
+
pressedNodeId,
|
|
2637
|
+
selectedNodeId,
|
|
2638
|
+
focusedNodeId,
|
|
2639
|
+
touch: touchMode,
|
|
2640
|
+
peekedNodeId: touchMode ? pinnedId : null
|
|
2857
2641
|
}
|
|
2858
|
-
)
|
|
2642
|
+
),
|
|
2643
|
+
/* @__PURE__ */ jsx9(KonvaContainerLabelLayer, { containers, connections, design })
|
|
2859
2644
|
]
|
|
2860
2645
|
}
|
|
2861
2646
|
) }),
|
|
2862
|
-
|
|
2647
|
+
/* @__PURE__ */ jsx9(
|
|
2648
|
+
A11yLayer,
|
|
2649
|
+
{
|
|
2650
|
+
nodes: flatNodes,
|
|
2651
|
+
label: `Ideas in ${currentLayerTitle || "this map"}`,
|
|
2652
|
+
hasChildLayer: (id) => !!getChildLayerForNode(doc, id, currentLayerId ?? ""),
|
|
2653
|
+
onActivate: activateNode,
|
|
2654
|
+
onFocusNode: setFocusedNodeId,
|
|
2655
|
+
announcement: hasBackOption ? `Now inside ${currentLayerTitle}` : void 0
|
|
2656
|
+
}
|
|
2657
|
+
),
|
|
2658
|
+
canvasContextMenu && hasBackOption && /* @__PURE__ */ jsx9(
|
|
2863
2659
|
"div",
|
|
2864
2660
|
{
|
|
2865
2661
|
role: "menu",
|
|
2866
2662
|
"aria-label": "Canvas options",
|
|
2867
2663
|
style: {
|
|
2868
2664
|
position: "absolute",
|
|
2869
|
-
zIndex:
|
|
2665
|
+
zIndex: "var(--z-menu)",
|
|
2870
2666
|
pointerEvents: "auto",
|
|
2871
2667
|
left: `${Math.min(canvasContextMenu.x, Math.max(8, (canvasSizeRef.current.width || 800) - 152))}px`,
|
|
2872
2668
|
top: `${Math.min(canvasContextMenu.y, Math.max(8, (canvasSizeRef.current.height || 600) - 72))}px`
|
|
2873
2669
|
},
|
|
2874
2670
|
onContextMenu: (e) => e.preventDefault(),
|
|
2875
2671
|
onPointerDown: (e) => e.stopPropagation(),
|
|
2876
|
-
children: /* @__PURE__ */
|
|
2672
|
+
children: /* @__PURE__ */ jsx9(
|
|
2877
2673
|
"button",
|
|
2878
2674
|
{
|
|
2879
2675
|
type: "button",
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
border: `1px solid ${isDark ? "#333330" : "#DDD9D0"}`,
|
|
2884
|
-
background: isDark ? "#1C1C1A" : "#FAF8F4",
|
|
2885
|
-
color: isDark ? "#F0EDE6" : "#1A1A18",
|
|
2886
|
-
cursor: "pointer",
|
|
2887
|
-
fontSize: 13,
|
|
2888
|
-
boxShadow: "0 4px 12px rgba(0,0,0,0.15)"
|
|
2889
|
-
},
|
|
2676
|
+
role: "menuitem",
|
|
2677
|
+
className: "vi-iconbtn vi-iconbtn--surface",
|
|
2678
|
+
style: { width: "auto", padding: "6px var(--space-4)", boxShadow: "var(--shadow-pop)" },
|
|
2890
2679
|
onClick: () => {
|
|
2891
2680
|
setCanvasContextMenu(null);
|
|
2892
2681
|
handleNavigateBack(navStack.length - 2);
|
|
@@ -2896,32 +2685,41 @@ function VisualliCanvas(props) {
|
|
|
2896
2685
|
)
|
|
2897
2686
|
}
|
|
2898
2687
|
),
|
|
2899
|
-
/* @__PURE__ */
|
|
2900
|
-
/* @__PURE__ */
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2688
|
+
/* @__PURE__ */ jsx9(NavigationStack_default, { stack: navStack, onNavigateBack: handleNavigateBack, top: navigationStackTop, left: navigationStackLeft }),
|
|
2689
|
+
/* @__PURE__ */ jsx9(
|
|
2690
|
+
"div",
|
|
2691
|
+
{
|
|
2692
|
+
className: "vi-map__ctrls",
|
|
2693
|
+
style: props.controlsPosition === "bottom-right" ? { top: "auto", bottom: pinnedNode && touchMode ? `calc(var(--space-4) + ${sheetHeight}px)` : "var(--space-4)" } : void 0,
|
|
2694
|
+
children: /* @__PURE__ */ jsx9(ZoomControls, { onFit: fitToScreen, touch: touchMode })
|
|
2695
|
+
}
|
|
2696
|
+
),
|
|
2697
|
+
renderOverlay?.({ isDark, theme: design.theme, containerWidth: canvasSizeRef.current.width || 0, containerHeight: canvasSizeRef.current.height || 0 }),
|
|
2698
|
+
touchMode && pinnedNode && /* @__PURE__ */ jsx9(
|
|
2699
|
+
PeekSheet,
|
|
2700
|
+
{
|
|
2701
|
+
node: pinnedNode,
|
|
2702
|
+
anchors,
|
|
2703
|
+
height: sheetHeight,
|
|
2704
|
+
expanded: sheetExpanded,
|
|
2705
|
+
onToggleExpanded: () => setSheetExpanded((v) => !v),
|
|
2706
|
+
onClose: () => setPinnedId(null),
|
|
2707
|
+
onStepInside: pinnedHasChild ? () => {
|
|
2708
|
+
setPinnedId(null);
|
|
2709
|
+
handleNavigate(pinnedNode.id);
|
|
2710
|
+
} : void 0,
|
|
2711
|
+
renderContent: renderNodeContent ? (summary) => renderNodeContent({ summary, nodeId: pinnedNode.id, nodeColor: pinnedNode.color, zoom: viewport.zoomLevel }) : void 0
|
|
2712
|
+
}
|
|
2713
|
+
),
|
|
2714
|
+
!touchMode && hoveredNode && hoveredNodePosition && peekNode && (() => {
|
|
2715
|
+
const rings = ringsFor3(peekNode.branchCount);
|
|
2716
|
+
const half = nodeRadii3(peekNode).rx * (rings.length ? rings[rings.length - 1].scale : 1) * hoveredNodePosition.zoom;
|
|
2717
|
+
const at = peekPosition({ x: hoveredNodePosition.screenX, y: hoveredNodePosition.screenY, half }, { w: canvasSizeRef.current.width || 800, h: canvasSizeRef.current.height || 600 });
|
|
2718
|
+
return /* @__PURE__ */ jsx9(
|
|
2913
2719
|
"div",
|
|
2914
2720
|
{
|
|
2915
|
-
|
|
2916
|
-
style: {
|
|
2917
|
-
position: "absolute",
|
|
2918
|
-
zIndex: 50,
|
|
2919
|
-
left: safeLeft,
|
|
2920
|
-
top: safeTop,
|
|
2921
|
-
transform: "translate(-50%, -100%)",
|
|
2922
|
-
transformOrigin: "bottom center",
|
|
2923
|
-
pointerEvents: "auto"
|
|
2924
|
-
},
|
|
2721
|
+
className: "vi-map__fact",
|
|
2722
|
+
style: { left: at.left, top: at.top, pointerEvents: "auto", zIndex: "var(--z-card)" },
|
|
2925
2723
|
onMouseEnter: () => {
|
|
2926
2724
|
tooltipHoverRef.current = true;
|
|
2927
2725
|
if (clearTooltipTimerRef.current) {
|
|
@@ -2934,22 +2732,17 @@ function VisualliCanvas(props) {
|
|
|
2934
2732
|
if (clearTooltipTimerRef.current) clearTimeout(clearTooltipTimerRef.current);
|
|
2935
2733
|
clearTooltipTimerRef.current = setTimeout(() => {
|
|
2936
2734
|
if (!keepTooltipOpenRef.current) setHoveredNode(null);
|
|
2937
|
-
},
|
|
2735
|
+
}, PEEK.hoverClose);
|
|
2938
2736
|
},
|
|
2939
|
-
children: /* @__PURE__ */
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
summary: hoveredNode.summary,
|
|
2949
|
-
nodeId: hoveredNode.nodeId,
|
|
2950
|
-
nodeColor: nodeData?.color,
|
|
2951
|
-
zoom: viewport.zoomLevel
|
|
2952
|
-
}) : hoveredNode.summary }) }) })
|
|
2737
|
+
children: /* @__PURE__ */ jsx9(
|
|
2738
|
+
PeekCard,
|
|
2739
|
+
{
|
|
2740
|
+
node: peekNode,
|
|
2741
|
+
anchors,
|
|
2742
|
+
onStepInside: peekHasChild ? () => stepInside(peekNode.id) : void 0,
|
|
2743
|
+
renderContent: renderNodeContent ? (summary) => renderNodeContent({ summary, nodeId: peekNode.id, nodeColor: peekNode.color, zoom: viewport.zoomLevel }) : void 0
|
|
2744
|
+
}
|
|
2745
|
+
)
|
|
2953
2746
|
}
|
|
2954
2747
|
);
|
|
2955
2748
|
})()
|
|
@@ -2959,91 +2752,29 @@ function VisualliCanvas(props) {
|
|
|
2959
2752
|
}
|
|
2960
2753
|
|
|
2961
2754
|
// src/VisualliRenderer.tsx
|
|
2962
|
-
import { jsx as
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
mq.addEventListener("change", handler);
|
|
2970
|
-
return () => mq.removeEventListener("change", handler);
|
|
2971
|
-
}, [mq]);
|
|
2972
|
-
return dark;
|
|
2973
|
-
}
|
|
2974
|
-
function useFontInjection() {
|
|
2975
|
-
useEffect7(() => {
|
|
2976
|
-
if (typeof document === "undefined") return;
|
|
2977
|
-
const links = [
|
|
2978
|
-
"https://fonts.googleapis.com",
|
|
2979
|
-
"https://fonts.gstatic.com",
|
|
2980
|
-
"https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800&family=Nunito+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300&display=swap",
|
|
2981
|
-
"https://fonts.googleapis.com/css2?family=Playpen+Sans:wght@300;400;600;800&family=Story+Script:wght@400&display=swap"
|
|
2982
|
-
];
|
|
2983
|
-
links.forEach((href, i) => {
|
|
2984
|
-
const id = `visualli-font-${i}`;
|
|
2985
|
-
if (!document.getElementById(id)) {
|
|
2986
|
-
const link = document.createElement("link");
|
|
2987
|
-
link.id = id;
|
|
2988
|
-
link.rel = i < 2 ? "preconnect" : "stylesheet";
|
|
2989
|
-
link.href = href;
|
|
2990
|
-
if (i === 1) link.crossOrigin = "anonymous";
|
|
2991
|
-
document.head.appendChild(link);
|
|
2992
|
-
}
|
|
2993
|
-
});
|
|
2994
|
-
}, []);
|
|
2995
|
-
}
|
|
2996
|
-
function EmptyState({ isDark }) {
|
|
2997
|
-
const bg = isDark ? "#131311" : "#F5F3EF";
|
|
2998
|
-
const fg = isDark ? "rgba(240,237,230,0.35)" : "rgba(26,26,24,0.35)";
|
|
2999
|
-
const bd = isDark ? "#2A2A28" : "#DDD9D0";
|
|
3000
|
-
const code = { fontFamily: "monospace", fontSize: 12, padding: "1px 4px", borderRadius: 4, background: isDark ? "#222220" : "#E8E4DC", border: `1px solid ${bd}` };
|
|
3001
|
-
return /* @__PURE__ */ jsxs8("div", { style: { display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", width: "100%", height: "100%", gap: 16, background: bg, color: fg, fontFamily: "'Nunito', system-ui, sans-serif" }, children: [
|
|
3002
|
-
/* @__PURE__ */ jsxs8("svg", { width: "48", height: "48", viewBox: "0 0 48 48", fill: "none", stroke: fg, strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
3003
|
-
/* @__PURE__ */ jsx12("rect", { x: "10", y: "6", width: "28", height: "36", rx: "3" }),
|
|
3004
|
-
/* @__PURE__ */ jsx12("line", { x1: "16", y1: "16", x2: "32", y2: "16" }),
|
|
3005
|
-
/* @__PURE__ */ jsx12("line", { x1: "16", y1: "22", x2: "32", y2: "22" }),
|
|
3006
|
-
/* @__PURE__ */ jsx12("line", { x1: "16", y1: "28", x2: "26", y2: "28" })
|
|
3007
|
-
] }),
|
|
3008
|
-
/* @__PURE__ */ jsxs8("div", { style: { textAlign: "center", lineHeight: 1.7 }, children: [
|
|
3009
|
-
/* @__PURE__ */ jsx12("p", { style: { margin: 0, fontSize: 15, fontWeight: 600, color: isDark ? "rgba(240,237,230,0.7)" : "rgba(26,26,24,0.7)" }, children: "No .visualli file provided" }),
|
|
3010
|
-
/* @__PURE__ */ jsxs8("p", { style: { margin: "4px 0 0", fontSize: 13 }, children: [
|
|
3011
|
-
"Pass a ",
|
|
3012
|
-
/* @__PURE__ */ jsx12("code", { style: code, children: "visualliFile" }),
|
|
3013
|
-
" or ",
|
|
3014
|
-
/* @__PURE__ */ jsx12("code", { style: code, children: "visualliString" }),
|
|
3015
|
-
" prop."
|
|
3016
|
-
] })
|
|
3017
|
-
] })
|
|
2755
|
+
import { jsx as jsx10, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
2756
|
+
var stateBox = { display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", width: "100%", height: "100%", gap: "var(--space-3)", padding: "var(--space-7)", boxSizing: "border-box", textAlign: "center", color: "var(--ink-muted)", background: "var(--canvas)" };
|
|
2757
|
+
function StateNote({ topic, title, children, role }) {
|
|
2758
|
+
return /* @__PURE__ */ jsxs4("div", { className: "vi-map", style: stateBox, role, "aria-live": role === "status" ? "polite" : void 0, children: [
|
|
2759
|
+
/* @__PURE__ */ jsx10("svg", { width: "64", height: "52", viewBox: "-32 -26 64 52", "aria-hidden": "true", children: /* @__PURE__ */ jsx10("path", { d: blobPath3(shapeForLevel(0), 26, 20), fill: `var(--topic-${topic})`, stroke: `var(--topic-${topic}-ring)`, strokeWidth: "2" }) }),
|
|
2760
|
+
/* @__PURE__ */ jsx10("p", { className: "vi-text-label", style: { margin: 0, color: "var(--ink)" }, children: title }),
|
|
2761
|
+
children && /* @__PURE__ */ jsx10("div", { className: "vi-text-body-sm", style: { margin: 0, maxWidth: 480, overflowWrap: "anywhere" }, children })
|
|
3018
2762
|
] });
|
|
3019
2763
|
}
|
|
3020
|
-
function
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
/* @__PURE__ */
|
|
3026
|
-
|
|
3027
|
-
/* @__PURE__ */ jsx12("p", { style: { margin: 0, fontSize: 14 }, children: "Loading .visualli file\u2026" })
|
|
2764
|
+
function EmptyState() {
|
|
2765
|
+
return /* @__PURE__ */ jsxs4(StateNote, { topic: "stone", title: "Nothing to show yet", children: [
|
|
2766
|
+
"Pass a ",
|
|
2767
|
+
/* @__PURE__ */ jsx10("code", { style: { fontFamily: "var(--font-mono)" }, children: "visualliFile" }),
|
|
2768
|
+
" or ",
|
|
2769
|
+
/* @__PURE__ */ jsx10("code", { style: { fontFamily: "var(--font-mono)" }, children: "visualliString" }),
|
|
2770
|
+
"."
|
|
3028
2771
|
] });
|
|
3029
2772
|
}
|
|
3030
|
-
function
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
const body = isDark ? "rgba(248,113,113,0.8)" : "rgba(180,40,40,0.8)";
|
|
3036
|
-
return /* @__PURE__ */ jsx12("div", { style: { display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", width: "100%", height: "100%", padding: "24px", background: bg, fontFamily: "'Nunito', system-ui, sans-serif" }, children: /* @__PURE__ */ jsxs8("div", { style: { maxWidth: 480, width: "100%", padding: "20px 24px", borderRadius: 12, border: `1px solid ${bd}`, background: card }, children: [
|
|
3037
|
-
/* @__PURE__ */ jsxs8("div", { style: { display: "flex", alignItems: "center", gap: 10, marginBottom: 12 }, children: [
|
|
3038
|
-
/* @__PURE__ */ jsxs8("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: head, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
3039
|
-
/* @__PURE__ */ jsx12("circle", { cx: "12", cy: "12", r: "10" }),
|
|
3040
|
-
/* @__PURE__ */ jsx12("line", { x1: "12", y1: "8", x2: "12", y2: "12" }),
|
|
3041
|
-
/* @__PURE__ */ jsx12("line", { x1: "12", y1: "16", x2: "12.01", y2: "16" })
|
|
3042
|
-
] }),
|
|
3043
|
-
/* @__PURE__ */ jsx12("span", { style: { fontSize: 15, fontWeight: 700, color: head }, children: "Failed to parse .visualli file" })
|
|
3044
|
-
] }),
|
|
3045
|
-
/* @__PURE__ */ jsx12("p", { style: { margin: 0, fontSize: 13, color: body, lineHeight: 1.6, wordBreak: "break-word", fontFamily: "monospace" }, children: message })
|
|
3046
|
-
] }) });
|
|
2773
|
+
function LoadingState() {
|
|
2774
|
+
return /* @__PURE__ */ jsx10(StateNote, { topic: "teal", title: "Opening the map\u2026", role: "status" });
|
|
2775
|
+
}
|
|
2776
|
+
function ErrorState({ message }) {
|
|
2777
|
+
return /* @__PURE__ */ jsx10(StateNote, { topic: "berry", title: "This map couldn't be opened", role: "alert", children: /* @__PURE__ */ jsx10("code", { style: { fontFamily: "var(--font-mono)" }, children: message }) });
|
|
3047
2778
|
}
|
|
3048
2779
|
var WORKER_SCRIPT = `
|
|
3049
2780
|
self.onmessage = function(e) {
|
|
@@ -3055,6 +2786,7 @@ self.onmessage = function(e) {
|
|
|
3055
2786
|
layers: new Map(),
|
|
3056
2787
|
layersByLevel: new Map(),
|
|
3057
2788
|
rootLayer: null,
|
|
2789
|
+
extensions: {},
|
|
3058
2790
|
};
|
|
3059
2791
|
for (var i = 0; i < lines.length; i++) {
|
|
3060
2792
|
var line = lines[i].trim();
|
|
@@ -3070,6 +2802,8 @@ self.onmessage = function(e) {
|
|
|
3070
2802
|
if (!doc.layersByLevel.has(obj.level)) doc.layersByLevel.set(obj.level, []);
|
|
3071
2803
|
doc.layersByLevel.get(obj.level).push(obj);
|
|
3072
2804
|
if (obj.level === 0) doc.rootLayer = obj;
|
|
2805
|
+
} else if (obj.type === 'extension' && typeof obj.id === 'string') {
|
|
2806
|
+
doc.extensions[obj.id] = Array.isArray(obj.data) ? obj.data : [];
|
|
3073
2807
|
}
|
|
3074
2808
|
}
|
|
3075
2809
|
if (!doc.meta) throw new Error('Missing required meta section');
|
|
@@ -3088,9 +2822,9 @@ function getWorkerUrl() {
|
|
|
3088
2822
|
return _workerUrl;
|
|
3089
2823
|
}
|
|
3090
2824
|
function useAsyncParse(content, useWorker) {
|
|
3091
|
-
const [state, setState] =
|
|
3092
|
-
const workerRef =
|
|
3093
|
-
|
|
2825
|
+
const [state, setState] = useState5({ status: "idle" });
|
|
2826
|
+
const workerRef = useRef11(null);
|
|
2827
|
+
useEffect8(() => {
|
|
3094
2828
|
if (!content) {
|
|
3095
2829
|
setState({ status: "idle" });
|
|
3096
2830
|
return;
|
|
@@ -3132,16 +2866,16 @@ function useAsyncParse(content, useWorker) {
|
|
|
3132
2866
|
cancelled = true;
|
|
3133
2867
|
};
|
|
3134
2868
|
}, [content, useWorker]);
|
|
3135
|
-
|
|
2869
|
+
useEffect8(() => () => {
|
|
3136
2870
|
workerRef.current?.terminate();
|
|
3137
2871
|
}, []);
|
|
3138
2872
|
return state;
|
|
3139
2873
|
}
|
|
3140
2874
|
function useFileText(file) {
|
|
3141
|
-
const [text, setText] =
|
|
3142
|
-
const [error, setError] =
|
|
3143
|
-
const [loading, setLoading] =
|
|
3144
|
-
|
|
2875
|
+
const [text, setText] = useState5(void 0);
|
|
2876
|
+
const [error, setError] = useState5(void 0);
|
|
2877
|
+
const [loading, setLoading] = useState5(false);
|
|
2878
|
+
useEffect8(() => {
|
|
3145
2879
|
if (!file) {
|
|
3146
2880
|
setText(void 0);
|
|
3147
2881
|
setError(void 0);
|
|
@@ -3190,41 +2924,58 @@ function VisualliRenderer({
|
|
|
3190
2924
|
visualliString,
|
|
3191
2925
|
visualliFile,
|
|
3192
2926
|
theme = "light",
|
|
2927
|
+
comfort,
|
|
2928
|
+
respectForcedColors,
|
|
2929
|
+
layout,
|
|
2930
|
+
controlsPosition,
|
|
2931
|
+
app,
|
|
3193
2932
|
width = "100%",
|
|
3194
2933
|
height = "100%",
|
|
3195
2934
|
useWorker = true,
|
|
3196
2935
|
chromaticImmersion = false,
|
|
2936
|
+
fontBaseUrl,
|
|
2937
|
+
loadWebFonts,
|
|
3197
2938
|
className,
|
|
3198
2939
|
style
|
|
3199
2940
|
}) {
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
2941
|
+
const design = useDesign({ theme, comfort, respectForcedColors });
|
|
2942
|
+
useState5(() => {
|
|
2943
|
+
ensureDesignSystemStyles({ fontBaseUrl, loadWebFonts });
|
|
2944
|
+
return true;
|
|
2945
|
+
});
|
|
3203
2946
|
const cssWidth = typeof width === "number" ? `${width}px` : width;
|
|
3204
2947
|
const cssHeight = typeof height === "number" ? `${height}px` : height;
|
|
3205
2948
|
const { text: fileText, error: fileReadError, loading: fileReading } = useFileText(visualliFile);
|
|
3206
2949
|
const rawContent = fileText ?? visualliString;
|
|
3207
2950
|
const parseState = useAsyncParse(rawContent, useWorker);
|
|
3208
|
-
const resolvedDoc =
|
|
2951
|
+
const resolvedDoc = useMemo10(() => {
|
|
3209
2952
|
if (parseState.status === "ready") return parseState.doc;
|
|
3210
2953
|
return null;
|
|
3211
2954
|
}, [parseState]);
|
|
3212
2955
|
const wrapperStyle = { width: cssWidth, height: cssHeight, overflow: "hidden", ...style };
|
|
2956
|
+
const attrs = design.attrs;
|
|
3213
2957
|
if (!visualliString && !visualliFile) {
|
|
3214
|
-
return /* @__PURE__ */
|
|
2958
|
+
return /* @__PURE__ */ jsx10("div", { className, ...attrs, style: wrapperStyle, children: /* @__PURE__ */ jsx10(EmptyState, {}) });
|
|
3215
2959
|
}
|
|
3216
2960
|
if (fileReading || parseState.status === "loading") {
|
|
3217
|
-
return /* @__PURE__ */
|
|
2961
|
+
return /* @__PURE__ */ jsx10("div", { className, ...attrs, style: wrapperStyle, children: /* @__PURE__ */ jsx10(LoadingState, {}) });
|
|
3218
2962
|
}
|
|
3219
2963
|
if (fileReadError || parseState.status === "error") {
|
|
3220
2964
|
const msg = fileReadError ?? (parseState.status === "error" ? parseState.message : "Unknown error");
|
|
3221
|
-
return /* @__PURE__ */
|
|
2965
|
+
return /* @__PURE__ */ jsx10("div", { className, ...attrs, style: wrapperStyle, children: /* @__PURE__ */ jsx10(ErrorState, { message: msg }) });
|
|
3222
2966
|
}
|
|
3223
|
-
return /* @__PURE__ */
|
|
2967
|
+
return /* @__PURE__ */ jsx10("div", { className, style: wrapperStyle, children: /* @__PURE__ */ jsx10(
|
|
3224
2968
|
VisualliCanvas,
|
|
3225
2969
|
{
|
|
3226
2970
|
preParsedVisualli: resolvedDoc ?? void 0,
|
|
3227
|
-
|
|
2971
|
+
theme,
|
|
2972
|
+
comfort,
|
|
2973
|
+
respectForcedColors,
|
|
2974
|
+
layout,
|
|
2975
|
+
controlsPosition,
|
|
2976
|
+
app,
|
|
2977
|
+
fontBaseUrl,
|
|
2978
|
+
loadWebFonts,
|
|
3228
2979
|
chromaticImmersion,
|
|
3229
2980
|
style: { width: "100%", height: "100%" }
|
|
3230
2981
|
}
|
|
@@ -3233,60 +2984,219 @@ function VisualliRenderer({
|
|
|
3233
2984
|
|
|
3234
2985
|
// src/context/VisualliContext.tsx
|
|
3235
2986
|
import { createContext, useContext } from "react";
|
|
3236
|
-
import { jsx as
|
|
2987
|
+
import { jsx as jsx11 } from "react/jsx-runtime";
|
|
3237
2988
|
var VisualliContext = createContext(null);
|
|
3238
2989
|
function VisualliProvider({
|
|
3239
2990
|
children
|
|
3240
2991
|
}) {
|
|
3241
2992
|
const value = {};
|
|
3242
|
-
return /* @__PURE__ */
|
|
2993
|
+
return /* @__PURE__ */ jsx11(VisualliContext.Provider, { value, children });
|
|
3243
2994
|
}
|
|
3244
2995
|
function useVisualli() {
|
|
3245
|
-
const
|
|
3246
|
-
if (!
|
|
2996
|
+
const ctx2 = useContext(VisualliContext);
|
|
2997
|
+
if (!ctx2) {
|
|
3247
2998
|
return {};
|
|
3248
2999
|
}
|
|
3249
|
-
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]);
|
|
3250
3037
|
}
|
|
3038
|
+
|
|
3039
|
+
// src/index.ts
|
|
3040
|
+
import {
|
|
3041
|
+
THEME_NAMES,
|
|
3042
|
+
TOPICS,
|
|
3043
|
+
DESIGN_SYSTEM_VERSION,
|
|
3044
|
+
TOKENS as TOKENS3,
|
|
3045
|
+
METRICS,
|
|
3046
|
+
resolveTheme as resolveTheme2,
|
|
3047
|
+
resolveComfort as resolveComfort2,
|
|
3048
|
+
ideaColor as ideaColor2,
|
|
3049
|
+
ideaStyle as ideaStyle2,
|
|
3050
|
+
topicStyle,
|
|
3051
|
+
getTokens,
|
|
3052
|
+
BLOB_SHAPES,
|
|
3053
|
+
RINGS as RINGS2,
|
|
3054
|
+
blobPath as blobPath4,
|
|
3055
|
+
blobRadius,
|
|
3056
|
+
edgePath,
|
|
3057
|
+
arrowPath,
|
|
3058
|
+
shapeForLevel as shapeForLevel2,
|
|
3059
|
+
IDEA as IDEA2,
|
|
3060
|
+
LABEL_GROWTH,
|
|
3061
|
+
ideaSize,
|
|
3062
|
+
labelGrowth as labelGrowth3
|
|
3063
|
+
} from "@visualli/core";
|
|
3064
|
+
|
|
3065
|
+
// src/components/KonvaNode.tsx
|
|
3066
|
+
import React12 from "react";
|
|
3067
|
+
import { jsx as jsx12 } from "react/jsx-runtime";
|
|
3068
|
+
var KonvaNode = React12.memo(function KonvaNode2({
|
|
3069
|
+
node,
|
|
3070
|
+
zoomLevel,
|
|
3071
|
+
isExternallyHovered = false,
|
|
3072
|
+
isExternallyPressed = false,
|
|
3073
|
+
isSelected = false,
|
|
3074
|
+
isDragging = false,
|
|
3075
|
+
design = DEFAULT_DESIGN
|
|
3076
|
+
}) {
|
|
3077
|
+
return /* @__PURE__ */ jsx12(
|
|
3078
|
+
KShape,
|
|
3079
|
+
{
|
|
3080
|
+
listening: false,
|
|
3081
|
+
perfectDrawEnabled: false,
|
|
3082
|
+
sceneFunc: (ctx2) => {
|
|
3083
|
+
drawIdea(ctx2._context, node, design, zoomLevel, { ...IDLE, hovered: isExternallyHovered, pressed: isExternallyPressed, selected: isSelected }, {
|
|
3084
|
+
shadows: !isDragging
|
|
3085
|
+
});
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
);
|
|
3089
|
+
});
|
|
3090
|
+
var KonvaNode_default = KonvaNode;
|
|
3091
|
+
|
|
3092
|
+
// src/components/KonvaEdge.tsx
|
|
3093
|
+
import { memo as memo4, useMemo as useMemo12 } from "react";
|
|
3094
|
+
import { connectorGeometry as connectorGeometry3 } from "@visualli/core";
|
|
3095
|
+
import { jsx as jsx13 } from "react/jsx-runtime";
|
|
3096
|
+
var KonvaEdge = memo4(function KonvaEdge2({ sourceNode: from, targetNode: to, connection, zoomLevel = 1, design = DEFAULT_DESIGN }) {
|
|
3097
|
+
const g = useMemo12(
|
|
3098
|
+
() => prepareConnector(connectorGeometry3(from, to, design.metrics.edgeGap)),
|
|
3099
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
3100
|
+
[from.x, from.y, from.width, from.level, from.branchCount, to.x, to.y, to.width, to.level, to.branchCount, design.metrics.edgeGap]
|
|
3101
|
+
);
|
|
3102
|
+
return /* @__PURE__ */ jsx13(
|
|
3103
|
+
KShape,
|
|
3104
|
+
{
|
|
3105
|
+
listening: false,
|
|
3106
|
+
perfectDrawEnabled: false,
|
|
3107
|
+
sceneFunc: (ctx2) => {
|
|
3108
|
+
drawConnector(ctx2._context, g, design, zoomLevel, connection?.style === "dashed", connection?.label);
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
);
|
|
3112
|
+
});
|
|
3113
|
+
var KonvaEdge_default = KonvaEdge;
|
|
3114
|
+
|
|
3115
|
+
// src/components/KonvaContainer.tsx
|
|
3116
|
+
import { memo as memo5, useMemo as useMemo13 } from "react";
|
|
3117
|
+
import { jsx as jsx14 } from "react/jsx-runtime";
|
|
3118
|
+
var KonvaContainer = memo5(function KonvaContainer2({ nodes, label = "", zoomLevel = 1, design = DEFAULT_DESIGN }) {
|
|
3119
|
+
const placed = useMemo13(() => {
|
|
3120
|
+
const map = new Map(nodes.map((n) => [n.id, n]));
|
|
3121
|
+
return layoutContainers([{ id: "c", label: label || void 0, nodeIds: nodes.map((n) => n.id) }], map, [], design)[0] ?? null;
|
|
3122
|
+
}, [nodes, label, design]);
|
|
3123
|
+
if (!placed) return null;
|
|
3124
|
+
return /* @__PURE__ */ jsx14(
|
|
3125
|
+
KShape,
|
|
3126
|
+
{
|
|
3127
|
+
listening: false,
|
|
3128
|
+
perfectDrawEnabled: false,
|
|
3129
|
+
sceneFunc: (ctx2) => {
|
|
3130
|
+
drawHull(ctx2._context, placed.hull, design);
|
|
3131
|
+
if (placed.pill) drawPill(ctx2._context, placed.pill, design, zoomLevel);
|
|
3132
|
+
}
|
|
3133
|
+
}
|
|
3134
|
+
);
|
|
3135
|
+
});
|
|
3136
|
+
var KonvaContainer_default = KonvaContainer;
|
|
3251
3137
|
export {
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3138
|
+
A11yLayer,
|
|
3139
|
+
BLOB_SHAPES,
|
|
3140
|
+
DEFAULT_DESIGN,
|
|
3141
|
+
DESIGN_SYSTEM_VERSION,
|
|
3142
|
+
DepthTrail,
|
|
3143
|
+
IDEA2 as IDEA,
|
|
3257
3144
|
KonvaContainer_default as KonvaContainer,
|
|
3145
|
+
KonvaContainerLabelLayer,
|
|
3258
3146
|
KonvaContainerLayer,
|
|
3259
3147
|
KonvaEdge_default as KonvaEdge,
|
|
3260
3148
|
KonvaEdgeLayer,
|
|
3261
|
-
KonvaLayerTransitionAnimator,
|
|
3262
3149
|
KonvaNode_default as KonvaNode,
|
|
3263
3150
|
KonvaNodeLayer,
|
|
3264
3151
|
KonvaStage_default as KonvaStage,
|
|
3265
|
-
|
|
3266
|
-
|
|
3152
|
+
LABEL_GROWTH,
|
|
3153
|
+
METRICS,
|
|
3267
3154
|
NavigationStack_default as NavigationStack,
|
|
3268
|
-
|
|
3155
|
+
PeekCard,
|
|
3156
|
+
RINGS2 as RINGS,
|
|
3157
|
+
THEME_NAMES,
|
|
3158
|
+
TOKENS3 as TOKENS,
|
|
3159
|
+
TOPICS,
|
|
3160
|
+
TermCard,
|
|
3269
3161
|
VisualliCanvas,
|
|
3270
3162
|
VisualliProvider,
|
|
3271
3163
|
VisualliRenderer,
|
|
3272
|
-
|
|
3273
|
-
|
|
3164
|
+
ZoomControls,
|
|
3165
|
+
arrowPath,
|
|
3166
|
+
blobPath4 as blobPath,
|
|
3167
|
+
blobRadius,
|
|
3274
3168
|
calculateFitCenter,
|
|
3275
3169
|
calculateFitView,
|
|
3276
3170
|
calculateFitZoom,
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
drawBlobPath,
|
|
3282
|
-
getBlobTypeForLayer,
|
|
3171
|
+
drawHull,
|
|
3172
|
+
drawPill,
|
|
3173
|
+
edgePath,
|
|
3174
|
+
ensureDesignSystemStyles,
|
|
3283
3175
|
getChildLayerForNode,
|
|
3284
3176
|
getConnectionsForLayer,
|
|
3285
3177
|
getLayerForNavigation,
|
|
3286
|
-
|
|
3287
|
-
|
|
3178
|
+
getTokens,
|
|
3179
|
+
ideaColor2 as ideaColor,
|
|
3180
|
+
ideaMeasure,
|
|
3181
|
+
ideaSize,
|
|
3182
|
+
ideaStyle2 as ideaStyle,
|
|
3183
|
+
labelGrowth3 as labelGrowth,
|
|
3184
|
+
layoutContainers,
|
|
3185
|
+
loadCanvasFonts,
|
|
3186
|
+
makeDesign,
|
|
3187
|
+
measurePill,
|
|
3188
|
+
paintVars,
|
|
3189
|
+
resolveComfort2 as resolveComfort,
|
|
3190
|
+
resolveDesign,
|
|
3191
|
+
resolveTheme2 as resolveTheme,
|
|
3192
|
+
shapeForLevel2 as shapeForLevel,
|
|
3193
|
+
splitTerms,
|
|
3194
|
+
topicStyle,
|
|
3195
|
+
useDesign,
|
|
3288
3196
|
useKonvaRenderer,
|
|
3197
|
+
useLayerChoreography,
|
|
3289
3198
|
useNodeStore,
|
|
3199
|
+
useReaderEnv,
|
|
3290
3200
|
useRenderConfigStore,
|
|
3291
3201
|
useSelectionStore,
|
|
3292
3202
|
useViewportNodes,
|