@visualli/react 0.1.7 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
// ─── Text Scaling Helpers ─────────────────────────────────────────────────────
|
|
2
|
-
//
|
|
3
|
-
// Determines the scale applied to node title text, keeping text legible and
|
|
4
|
-
// fitted inside the blob shape across the full zoom range.
|
|
5
|
-
//
|
|
6
|
-
// Matches the visualli.ai textScaling logic exactly.
|
|
7
|
-
|
|
8
|
-
import { NODE_HEIGHT } from '@visualli/core';
|
|
9
|
-
|
|
10
|
-
export const NODE_TEXT_BASE_FONT_PX = 30;
|
|
11
|
-
export const DESCRIPTION_TEXT_BASE_FONT_PX = 12;
|
|
12
|
-
export const CONTAINER_LABEL_BASE_FONT_PX = 24;
|
|
13
|
-
export const EDGE_LABEL_BASE_FONT_PX = 18;
|
|
14
|
-
export const OVERLAY_FONT_UPPER_BOOST_PX = 8;
|
|
15
|
-
|
|
16
|
-
// Keep text away from blob edges
|
|
17
|
-
const NODE_TEXT_SAFE_BLOB_FILL_RATIO = 0.88;
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Computes world-space text scale for node title:
|
|
21
|
-
* - inverse zoom for screen-space consistency
|
|
22
|
-
* - clamped so text stays inside the node blob with safe padding
|
|
23
|
-
* - uses base width of 300px for consistent text sizing
|
|
24
|
-
* - allows taller text box for multi-line wrapping
|
|
25
|
-
*
|
|
26
|
-
* Improved from visualli.ai to handle long labels better with multi-line support.
|
|
27
|
-
*/
|
|
28
|
-
export function computeNodeTextWorldScale(nodeWidth: number, zoomLevel: number): number {
|
|
29
|
-
const safeZoom = Math.max(zoomLevel, 0.0001);
|
|
30
|
-
|
|
31
|
-
// Use a consistent base width (300px) for text scaling
|
|
32
|
-
// This keeps font size more consistent across different node widths
|
|
33
|
-
const baseWidth = 300;
|
|
34
|
-
|
|
35
|
-
const baseRadiusX = baseWidth / 2;
|
|
36
|
-
const baseRadiusY = Math.max((NODE_HEIGHT * 1.6) / 2, baseRadiusX * 0.74);
|
|
37
|
-
const textNaturalWidth = baseWidth - 40;
|
|
38
|
-
// Increase text height to allow 2-3 lines of wrapped text
|
|
39
|
-
const textNaturalHeight = NODE_HEIGHT * 1.2;
|
|
40
|
-
|
|
41
|
-
const maxScaleX = ((baseRadiusX * 2) * NODE_TEXT_SAFE_BLOB_FILL_RATIO) / Math.max(textNaturalWidth, 1);
|
|
42
|
-
const maxScaleY = ((baseRadiusY * 2) * NODE_TEXT_SAFE_BLOB_FILL_RATIO) / Math.max(textNaturalHeight, 1);
|
|
43
|
-
const fitScale = Math.max(0.01, Math.min(maxScaleX, maxScaleY));
|
|
44
|
-
|
|
45
|
-
return Math.min(1 / safeZoom, fitScale);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Screen-space scale (zoom-adjusted) for text overlays. */
|
|
49
|
-
export function computeNodeTextScreenScale(nodeWidth: number, zoomLevel: number): number {
|
|
50
|
-
return computeNodeTextWorldScale(nodeWidth, zoomLevel) * zoomLevel;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Screen-space overlay scale for description/semantic tooltips.
|
|
55
|
-
* - lower bound: 1x
|
|
56
|
-
* - upper bound: node-title-root + OVERLAY_FONT_UPPER_BOOST_PX
|
|
57
|
-
*/
|
|
58
|
-
export function computeOverlayScale(zoomLevel: number): number {
|
|
59
|
-
const safeZoom = Math.max(zoomLevel, 0.0001);
|
|
60
|
-
const inverseScale = 1 / safeZoom;
|
|
61
|
-
const maxScale = (NODE_TEXT_BASE_FONT_PX + OVERLAY_FONT_UPPER_BOOST_PX) / NODE_TEXT_BASE_FONT_PX;
|
|
62
|
-
return Math.min(Math.max(inverseScale, 1), maxScale);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Scale for edge path labels in screen pixels. */
|
|
66
|
-
export function computeEdgeLabelScale(zoomLevel: number): number {
|
|
67
|
-
const safeZoom = Math.max(zoomLevel, 0.0001);
|
|
68
|
-
return Math.min(Math.max(EDGE_LABEL_BASE_FONT_PX / safeZoom, 10), 32);
|
|
69
|
-
}
|
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
// ─── useKonvaLayerTransition ──────────────────────────────────────────────────
|
|
2
|
-
//
|
|
3
|
-
// Manages layer navigation animations (zoom into child layer, zoom back to
|
|
4
|
-
// parent). Uses the same callback-driven pattern as the visualli.ai reference:
|
|
5
|
-
// - onViewportChange: imperatively updates the Konva stage each rAF frame
|
|
6
|
-
// - onOpacityChange: writes directly to canvasWrapperRef DOM (no React state)
|
|
7
|
-
// - onBackgroundColorChange: writes directly to bgRef DOM
|
|
8
|
-
// - onPhaseChange('swap'|'zoom-back'): triggers React layer content swap
|
|
9
|
-
// - onComplete: syncs final viewport to Zustand and ends transition
|
|
10
|
-
|
|
11
|
-
import { useCallback, useRef, useEffect } from 'react';
|
|
12
|
-
import type Konva from 'konva';
|
|
13
|
-
import type { VisualliDocument, FlatNode } from '@visualli/core';
|
|
14
|
-
import { useViewportStore } from '../stores/useViewportStore';
|
|
15
|
-
import { konvaLayerTransitionAnimator } from '../animations/konvaLayerTransition';
|
|
16
|
-
import type { AnimatorViewport } from '../animations/konvaLayerTransition';
|
|
17
|
-
import { calculateFitView } from '../utils/layerNavigation';
|
|
18
|
-
|
|
19
|
-
export interface LayerTransitionOptions {
|
|
20
|
-
/** Ref to the Konva Stage for imperative position/scale updates each frame. */
|
|
21
|
-
stageRef: React.RefObject<Konva.Stage | null>;
|
|
22
|
-
/** Ref to the wrapping div whose CSS `opacity` is driven during transitions. */
|
|
23
|
-
canvasWrapperRef: React.RefObject<HTMLDivElement | null>;
|
|
24
|
-
/** Ref to the background color div. */
|
|
25
|
-
bgRef?: React.RefObject<HTMLDivElement | null>;
|
|
26
|
-
/** Called at the content-swap point — use to switch the displayed layer. */
|
|
27
|
-
onSwapLayer: (newLayerId: string) => void;
|
|
28
|
-
/** Called when a zoom-out swap happens — use to update the nav stack. */
|
|
29
|
-
onSwapBack: () => void;
|
|
30
|
-
/** Called after the transition fully completes. */
|
|
31
|
-
onComplete: () => void;
|
|
32
|
-
/**
|
|
33
|
-
* GPU CSS-transform mode: called each rAF frame instead of applyViewport.
|
|
34
|
-
* When provided, the caller is responsible for moving the canvas content.
|
|
35
|
-
*/
|
|
36
|
-
onViewportFrame?: (vp: AnimatorViewport) => void;
|
|
37
|
-
/**
|
|
38
|
-
* Called once when the animation reaches its final viewport.
|
|
39
|
-
* When provided, replaces the default applyViewport call at completion.
|
|
40
|
-
*/
|
|
41
|
-
onViewportFinal?: (vp: AnimatorViewport) => void;
|
|
42
|
-
/**
|
|
43
|
-
* Called at phase-change points ('swap', 'zoom-back') in addition to the
|
|
44
|
-
* internal content-swap logic. Use to reset CSS transforms before Konva redraws.
|
|
45
|
-
*/
|
|
46
|
-
onPhaseChange?: (phase: string) => void;
|
|
47
|
-
/**
|
|
48
|
-
* Called with 'start' before the animator fires its first frame and with
|
|
49
|
-
* 'end' after onComplete. Use to set up / tear down GPU hints (willChange, etc.).
|
|
50
|
-
*/
|
|
51
|
-
onTransitionLifecycle?: (phase: 'start' | 'end') => void;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export interface LayerTransitionHandlers {
|
|
55
|
-
zoomIntoLayer: (
|
|
56
|
-
doc: VisualliDocument,
|
|
57
|
-
targetNode: FlatNode,
|
|
58
|
-
childLayerId: string,
|
|
59
|
-
childNodes: FlatNode[],
|
|
60
|
-
fromColor: string,
|
|
61
|
-
toColor: string,
|
|
62
|
-
) => void;
|
|
63
|
-
zoomOutToParent: (
|
|
64
|
-
parentViewport: AnimatorViewport,
|
|
65
|
-
fromColor: string,
|
|
66
|
-
toColor: string,
|
|
67
|
-
) => void;
|
|
68
|
-
isTransitioning: () => boolean;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function useKonvaLayerTransition(opts: LayerTransitionOptions): LayerTransitionHandlers {
|
|
72
|
-
const {
|
|
73
|
-
stageRef, canvasWrapperRef, bgRef,
|
|
74
|
-
onSwapLayer, onSwapBack, onComplete,
|
|
75
|
-
onViewportFrame, onViewportFinal, onPhaseChange: onPhaseChangeExt,
|
|
76
|
-
onTransitionLifecycle,
|
|
77
|
-
} = opts;
|
|
78
|
-
const setZoom = useViewportStore(s => s.setZoom);
|
|
79
|
-
const setCenter = useViewportStore(s => s.setCenter);
|
|
80
|
-
const getState = useViewportStore.getState;
|
|
81
|
-
|
|
82
|
-
const swappedRef = useRef(false);
|
|
83
|
-
const canvasSizeRef = useRef({ width: 800, height: 600 });
|
|
84
|
-
|
|
85
|
-
// Keep canvas size ref fresh from viewport store (updated by ResizeObserver in useKonvaRenderer)
|
|
86
|
-
useEffect(() => {
|
|
87
|
-
const update = () => {
|
|
88
|
-
const vp = useViewportStore.getState();
|
|
89
|
-
canvasSizeRef.current = { width: vp.canvasWidth || 800, height: vp.canvasHeight || 600 };
|
|
90
|
-
};
|
|
91
|
-
update();
|
|
92
|
-
// Subscribe to viewport store changes instead of window resize
|
|
93
|
-
const unsubscribe = useViewportStore.subscribe(update);
|
|
94
|
-
return unsubscribe;
|
|
95
|
-
}, []);
|
|
96
|
-
|
|
97
|
-
// Apply viewport to Konva stage imperatively (no React re-render per frame)
|
|
98
|
-
const applyViewport = useCallback((vp: AnimatorViewport) => {
|
|
99
|
-
const stage = stageRef.current;
|
|
100
|
-
if (!stage) return;
|
|
101
|
-
const { width, height } = canvasSizeRef.current;
|
|
102
|
-
stage.x(width / 2 - vp.centerX * vp.zoomLevel);
|
|
103
|
-
stage.y(height / 2 - vp.centerY * vp.zoomLevel);
|
|
104
|
-
stage.scaleX(vp.zoomLevel);
|
|
105
|
-
stage.scaleY(vp.zoomLevel);
|
|
106
|
-
stage.getLayers().forEach(l => l.drawScene());
|
|
107
|
-
// Sync to Zustand so viewport store reflects reality
|
|
108
|
-
setCenter(vp.centerX, vp.centerY);
|
|
109
|
-
setZoom(vp.zoomLevel);
|
|
110
|
-
}, [stageRef, setCenter, setZoom]);
|
|
111
|
-
|
|
112
|
-
const applyOpacity = useCallback((opacity: number) => {
|
|
113
|
-
if (canvasWrapperRef.current) {
|
|
114
|
-
canvasWrapperRef.current.style.opacity = String(opacity);
|
|
115
|
-
}
|
|
116
|
-
}, [canvasWrapperRef]);
|
|
117
|
-
|
|
118
|
-
const applyBgColor = useCallback((color: string) => {
|
|
119
|
-
if (bgRef?.current) {
|
|
120
|
-
bgRef.current.style.backgroundColor = color;
|
|
121
|
-
}
|
|
122
|
-
}, [bgRef]);
|
|
123
|
-
|
|
124
|
-
const zoomIntoLayer = useCallback((
|
|
125
|
-
_doc: VisualliDocument,
|
|
126
|
-
targetNode: FlatNode,
|
|
127
|
-
childLayerId: string,
|
|
128
|
-
childNodes: FlatNode[],
|
|
129
|
-
fromColor: string,
|
|
130
|
-
toColor: string,
|
|
131
|
-
): void => {
|
|
132
|
-
if (konvaLayerTransitionAnimator.active) return;
|
|
133
|
-
|
|
134
|
-
const vp = getState();
|
|
135
|
-
const stage = stageRef.current;
|
|
136
|
-
const w = stage?.width() ?? 800;
|
|
137
|
-
const h = stage?.height() ?? 600;
|
|
138
|
-
const fit = calculateFitView(childNodes, w, h);
|
|
139
|
-
|
|
140
|
-
swappedRef.current = false;
|
|
141
|
-
|
|
142
|
-
onTransitionLifecycle?.('start');
|
|
143
|
-
konvaLayerTransitionAnimator.animateZoomIntoLayer(
|
|
144
|
-
{ centerX: vp.centerX, centerY: vp.centerY, zoomLevel: vp.zoomLevel },
|
|
145
|
-
{ x: targetNode.x, y: targetNode.y, width: targetNode.width, height: targetNode.height },
|
|
146
|
-
{ x: fit.centerX, y: fit.centerY },
|
|
147
|
-
fromColor,
|
|
148
|
-
toColor,
|
|
149
|
-
{
|
|
150
|
-
onUpdate: ({ viewport, opacity, backgroundColor }) => {
|
|
151
|
-
if (onViewportFrame) { onViewportFrame(viewport); } else { applyViewport(viewport); }
|
|
152
|
-
applyOpacity(opacity);
|
|
153
|
-
applyBgColor(backgroundColor);
|
|
154
|
-
},
|
|
155
|
-
onComplete: () => {
|
|
156
|
-
const finalVp = { centerX: fit.centerX, centerY: fit.centerY, zoomLevel: fit.zoomLevel };
|
|
157
|
-
if (onViewportFinal) { onViewportFinal(finalVp); } else { applyViewport(finalVp); }
|
|
158
|
-
applyOpacity(1);
|
|
159
|
-
onComplete();
|
|
160
|
-
onTransitionLifecycle?.('end');
|
|
161
|
-
},
|
|
162
|
-
onPhaseChange: (phase) => {
|
|
163
|
-
onPhaseChangeExt?.(phase);
|
|
164
|
-
if (phase === 'swap' && !swappedRef.current) {
|
|
165
|
-
swappedRef.current = true;
|
|
166
|
-
onSwapLayer(childLayerId);
|
|
167
|
-
}
|
|
168
|
-
},
|
|
169
|
-
},
|
|
170
|
-
fit.zoomLevel,
|
|
171
|
-
w,
|
|
172
|
-
h,
|
|
173
|
-
);
|
|
174
|
-
}, [stageRef, getState, applyViewport, applyOpacity, applyBgColor, onSwapLayer, onComplete, onViewportFrame, onViewportFinal, onPhaseChangeExt, onTransitionLifecycle]);
|
|
175
|
-
|
|
176
|
-
const zoomOutToParent = useCallback((
|
|
177
|
-
parentViewport: AnimatorViewport,
|
|
178
|
-
fromColor: string,
|
|
179
|
-
toColor: string,
|
|
180
|
-
): void => {
|
|
181
|
-
if (konvaLayerTransitionAnimator.active) return;
|
|
182
|
-
|
|
183
|
-
const vp = getState();
|
|
184
|
-
swappedRef.current = false;
|
|
185
|
-
|
|
186
|
-
onTransitionLifecycle?.('start');
|
|
187
|
-
konvaLayerTransitionAnimator.animateZoomOutToParent(
|
|
188
|
-
{ centerX: vp.centerX, centerY: vp.centerY, zoomLevel: vp.zoomLevel },
|
|
189
|
-
parentViewport,
|
|
190
|
-
fromColor,
|
|
191
|
-
toColor,
|
|
192
|
-
{
|
|
193
|
-
onUpdate: ({ viewport, opacity, backgroundColor }) => {
|
|
194
|
-
if (onViewportFrame) { onViewportFrame(viewport); } else { applyViewport(viewport); }
|
|
195
|
-
applyOpacity(opacity);
|
|
196
|
-
applyBgColor(backgroundColor);
|
|
197
|
-
},
|
|
198
|
-
onComplete: () => {
|
|
199
|
-
if (onViewportFinal) { onViewportFinal(parentViewport); } else { applyViewport(parentViewport); }
|
|
200
|
-
applyOpacity(1);
|
|
201
|
-
onComplete();
|
|
202
|
-
onTransitionLifecycle?.('end');
|
|
203
|
-
},
|
|
204
|
-
onPhaseChange: (phase) => {
|
|
205
|
-
onPhaseChangeExt?.(phase);
|
|
206
|
-
if (phase === 'zoom-back' && !swappedRef.current) {
|
|
207
|
-
swappedRef.current = true;
|
|
208
|
-
onSwapBack();
|
|
209
|
-
}
|
|
210
|
-
},
|
|
211
|
-
},
|
|
212
|
-
);
|
|
213
|
-
}, [getState, applyViewport, applyOpacity, applyBgColor, onSwapBack, onComplete, onViewportFrame, onViewportFinal, onPhaseChangeExt, onTransitionLifecycle]);
|
|
214
|
-
|
|
215
|
-
const isTransitioning = useCallback(() => konvaLayerTransitionAnimator.active, []);
|
|
216
|
-
|
|
217
|
-
// Cancel on unmount
|
|
218
|
-
useEffect(() => () => { konvaLayerTransitionAnimator.cancel(); }, []);
|
|
219
|
-
|
|
220
|
-
return { zoomIntoLayer, zoomOutToParent, isTransitioning };
|
|
221
|
-
}
|