@visualli/react 0.1.7 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +13 -5
  2. package/dist/index.cjs +1900 -2033
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +354 -97
  5. package/dist/index.d.ts +354 -97
  6. package/dist/index.js +2005 -2095
  7. package/dist/index.js.map +1 -1
  8. package/fonts/Caveat-Variable.ttf +0 -0
  9. package/licenses/visualli-design-system/LICENSE +9 -0
  10. package/licenses/visualli-design-system/NOTICE.md +6 -0
  11. package/package.json +9 -4
  12. package/src/VisualliCanvas.tsx +452 -409
  13. package/src/VisualliRenderer.tsx +90 -112
  14. package/src/components/A11yLayer.tsx +73 -0
  15. package/src/components/KonvaContainer.tsx +28 -237
  16. package/src/components/KonvaContainerLabelLayer.tsx +41 -0
  17. package/src/components/KonvaContainerLayer.tsx +34 -33
  18. package/src/components/KonvaEdge.tsx +25 -240
  19. package/src/components/KonvaEdgeLayer.tsx +69 -49
  20. package/src/components/KonvaNode.tsx +21 -156
  21. package/src/components/KonvaNodeLayer.tsx +102 -34
  22. package/src/components/KonvaStage.tsx +17 -3
  23. package/src/components/NavigationStack.tsx +16 -159
  24. package/src/components/Overlays.tsx +327 -0
  25. package/src/components/ZoomControls.tsx +3 -107
  26. package/src/components/useFrames.ts +30 -0
  27. package/src/context/VisualliContext.tsx +3 -3
  28. package/src/design/choreography.ts +157 -0
  29. package/src/design/containers.ts +150 -0
  30. package/src/design/design.ts +115 -0
  31. package/src/design/drawing.ts +303 -0
  32. package/src/design/measure.ts +34 -0
  33. package/src/design/runtime.ts +154 -0
  34. package/src/design/useDesign.ts +38 -0
  35. package/src/generated/specCss.ts +15 -0
  36. package/src/hooks/index.ts +0 -2
  37. package/src/hooks/useKonvaRenderer.ts +13 -18
  38. package/src/hooks/useLayerChoreography.ts +159 -0
  39. package/src/hooks/useViewportNodes.ts +0 -1
  40. package/src/index.ts +22 -22
  41. package/src/stores/stores.ts +2 -2
  42. package/src/stores/useViewportStore.ts +12 -2
  43. package/src/animations/index.ts +0 -2
  44. package/src/animations/konvaLayerTransition.ts +0 -223
  45. package/src/components/SketchyBoxKonva.tsx +0 -121
  46. package/src/config/blobShapes.ts +0 -204
  47. package/src/config/index.ts +0 -21
  48. package/src/config/textScaling.ts +0 -69
  49. package/src/hooks/useKonvaLayerTransition.ts +0 -221
@@ -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
- }