@visualli/react 0.1.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 +335 -0
- package/dist/index.cjs +3262 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +375 -0
- package/dist/index.d.ts +375 -0
- package/dist/index.js +3289 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -0
- package/src/VisualliCanvas.tsx +1087 -0
- package/src/VisualliRenderer.tsx +427 -0
- package/src/animations/index.ts +2 -0
- package/src/animations/konvaLayerTransition.ts +223 -0
- package/src/components/KonvaContainer.tsx +255 -0
- package/src/components/KonvaContainerLayer.tsx +63 -0
- package/src/components/KonvaEdge.tsx +254 -0
- package/src/components/KonvaEdgeLayer.tsx +72 -0
- package/src/components/KonvaNode.tsx +177 -0
- package/src/components/KonvaNodeLayer.tsx +61 -0
- package/src/components/KonvaStage.tsx +80 -0
- package/src/components/NavigationStack.tsx +184 -0
- package/src/components/SketchyBoxKonva.tsx +121 -0
- package/src/components/ZoomControls.tsx +110 -0
- package/src/config/blobShapes.ts +204 -0
- package/src/config/index.ts +21 -0
- package/src/config/textScaling.ts +69 -0
- package/src/context/VisualliContext.tsx +50 -0
- package/src/hooks/index.ts +7 -0
- package/src/hooks/useKonvaLayerTransition.ts +221 -0
- package/src/hooks/useKonvaRenderer.ts +185 -0
- package/src/hooks/useViewportNodes.ts +59 -0
- package/src/index.ts +81 -0
- package/src/konvaCompat.ts +29 -0
- package/src/stores/index.ts +3 -0
- package/src/stores/stores.ts +17 -0
- package/src/stores/useNodeStore.ts +101 -0
- package/src/stores/useViewportStore.ts +95 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/layerNavigation.ts +15 -0
package/README.md
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
# @visualli/react
|
|
2
|
+
|
|
3
|
+
React canvas rendering for Visualli — powered by Konva. Drop-in component that displays a `VisualliDocument` as an interactive, zoomable, navigable mind-map.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **GPU-accelerated canvas** via `react-konva` — handles thousands of nodes at 60 fps
|
|
8
|
+
- **Organic blob nodes** — 6 quadratic-bezier blob shapes that cycle by level
|
|
9
|
+
- **Layer navigation** — double-click any node to drill into its child layer, breadcrumb back
|
|
10
|
+
- **Animated transitions** — rAF-driven zoom-into-layer / zoom-out-to-parent with color crossfade
|
|
11
|
+
- **Zoom controls** — +/− buttons, %, fit-to-screen
|
|
12
|
+
- **Viewport culling** — RBush spatial index keeps only visible nodes on the canvas
|
|
13
|
+
- **Zustand stores** — fine-grained subscriptions for nodes, viewport, selection, render config
|
|
14
|
+
- **Light / dark theme** — single `isDark` prop
|
|
15
|
+
- **🆕 Extension system** — inject custom parser middlewares and UI components at runtime
|
|
16
|
+
- **🆕 Stream fetching** — `useVisualliStream` hook for backend JSONL streams
|
|
17
|
+
- **🆕 Context provider** — `VisualliProvider` for dependency injection
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @visualli/react @visualli/core konva react-konva zustand
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Peer dependencies: `react@^18`, `react-dom@^18`
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
import { VisualliCanvas } from '@visualli/react';
|
|
31
|
+
|
|
32
|
+
// Option A — pass a pre-parsed VisualliDocument
|
|
33
|
+
import { parseVisualliFile } from '@visualli/core';
|
|
34
|
+
|
|
35
|
+
const doc = parseVisualliFile(rawJsonlString);
|
|
36
|
+
|
|
37
|
+
export default function App() {
|
|
38
|
+
return (
|
|
39
|
+
<div style={{ width: '100vw', height: '100vh' }}>
|
|
40
|
+
<VisualliCanvas document={doc} isDark={false} />
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Option B — pass the raw JSONL string directly
|
|
46
|
+
export default function App() {
|
|
47
|
+
return (
|
|
48
|
+
<div style={{ width: '100vw', height: '100vh' }}>
|
|
49
|
+
<VisualliCanvas visualliString={rawJsonlString} isDark={true} />
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## `VisualliCanvas` Props
|
|
56
|
+
|
|
57
|
+
| Prop | Type | Default | Description |
|
|
58
|
+
|------|------|---------|-------------|
|
|
59
|
+
| `document` | `VisualliDocument` | — | Pre-parsed document |
|
|
60
|
+
| `visualliString` | `string` | — | Raw JSONL — parsed internally |
|
|
61
|
+
| `isDark` | `boolean` | `false` | Light / dark canvas theme |
|
|
62
|
+
| `onNodeClick` | `(node: FlatNode) => void` | — | Single-click callback |
|
|
63
|
+
| `onLayerChange` | `(id: string, layer: VisualliLayer) => void` | — | Fired after navigation |
|
|
64
|
+
| `className` | `string` | `''` | Extra CSS classes on the wrapper div |
|
|
65
|
+
| `style` | `React.CSSProperties` | — | Inline styles on the wrapper div |
|
|
66
|
+
|
|
67
|
+
> The component fills its parent container — set an explicit `width` / `height` on the wrapper.
|
|
68
|
+
|
|
69
|
+
## Architecture
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
VisualliCanvas
|
|
73
|
+
├── KonvaStage react-konva <Stage>, position/scale from viewport store
|
|
74
|
+
│ ├── KonvaContainerLayer <Layer> — convex-hull outlines (non-interactive)
|
|
75
|
+
│ ├── KonvaEdgeLayer <Layer> — bezier edges between visible nodes
|
|
76
|
+
│ └── KonvaNodeLayer <Layer> — blob nodes, handles click/dblclick
|
|
77
|
+
├── NavigationStack DOM overlay — breadcrumb trail, click to go back
|
|
78
|
+
└── ZoomControls DOM overlay — +/−/% buttons, fit-to-screen
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Stores (Zustand)
|
|
82
|
+
|
|
83
|
+
Access any store directly for advanced use cases:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { useViewportStore, useNodeStore, useSelectionStore } from '@visualli/react';
|
|
87
|
+
|
|
88
|
+
// Read viewport
|
|
89
|
+
const { centerX, centerY, zoomLevel } = useViewportStore();
|
|
90
|
+
|
|
91
|
+
// Programmatic zoom
|
|
92
|
+
useViewportStore.getState().setZoom(1.5);
|
|
93
|
+
useViewportStore.getState().setCenter(0, 0);
|
|
94
|
+
|
|
95
|
+
// Read selected node
|
|
96
|
+
const selectedId = useSelectionStore(s => s.selectedId);
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Hooks
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
import { useViewportNodes } from '@visualli/react';
|
|
103
|
+
|
|
104
|
+
// Get nodes currently visible in the viewport (culled)
|
|
105
|
+
const visible = useViewportNodes(allNodes, /* optional level filter */ 0);
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Navigation Stack
|
|
109
|
+
|
|
110
|
+
Layer navigation is fully internal but observable via the `onLayerChange` callback. The breadcrumb UI renders automatically — no props required.
|
|
111
|
+
|
|
112
|
+
Drill-in: **double-click** a node that has a child layer.
|
|
113
|
+
Back: click any crumb in the breadcrumb bar, or use `onNavigateBack` exposed by `NavigationStack` directly.
|
|
114
|
+
|
|
115
|
+
## Extension System 🆕
|
|
116
|
+
|
|
117
|
+
The extension system allows you to inject custom parser middlewares and UI components at runtime without modifying the SDK.
|
|
118
|
+
|
|
119
|
+
### Basic Setup with Provider
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
import { VisualliProvider, VisualliCanvas } from '@visualli/react';
|
|
123
|
+
|
|
124
|
+
function App() {
|
|
125
|
+
return (
|
|
126
|
+
<VisualliProvider>
|
|
127
|
+
<VisualliCanvas document={document} />
|
|
128
|
+
</VisualliProvider>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
### With Custom Middleware
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
import { VisualliProvider } from '@visualli/react';
|
|
136
|
+
import type { ParserMiddleware } from '@visualli/core';
|
|
137
|
+
|
|
138
|
+
const myMiddleware: ParserMiddleware = (data) => {
|
|
139
|
+
if (data.type === 'extension') {
|
|
140
|
+
return { ...data, enhanced: true };
|
|
141
|
+
}
|
|
142
|
+
return data;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
<VisualliProvider middlewares={[myMiddleware]}>
|
|
146
|
+
<App />
|
|
147
|
+
</VisualliProvider>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### With Extension Components
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
import type { ExtensionComponentProps } from '@visualli/react';
|
|
154
|
+
|
|
155
|
+
function MyExtension({ extension, document }: ExtensionComponentProps) {
|
|
156
|
+
return (
|
|
157
|
+
<div style={{ position: 'absolute', top: 20, right: 20 }}>
|
|
158
|
+
<p>{extension.data?.message}</p>
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const extensions = {
|
|
164
|
+
'my-ext-id': MyExtension,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
<VisualliProvider extensions={extensions}>
|
|
168
|
+
<VisualliCanvas document={document} />
|
|
169
|
+
</VisualliProvider>
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Stream Fetching
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
import { useVisualliStream, VisualliCanvas } from '@visualli/react';
|
|
176
|
+
|
|
177
|
+
function MindMapViewer({ apiUrl }: { apiUrl: string }) {
|
|
178
|
+
const { document, isLoading, error, progress } = useVisualliStream(apiUrl);
|
|
179
|
+
|
|
180
|
+
if (isLoading) return <div>Loading... {progress}%</div>;
|
|
181
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
182
|
+
if (!document) return null;
|
|
183
|
+
|
|
184
|
+
return <VisualliCanvas document={document} />;
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Complete Example
|
|
189
|
+
|
|
190
|
+
```tsx
|
|
191
|
+
import {
|
|
192
|
+
VisualliProvider,
|
|
193
|
+
useVisualliStream,
|
|
194
|
+
VisualliCanvas
|
|
195
|
+
} from '@visualli/react';
|
|
196
|
+
import type {
|
|
197
|
+
ParserMiddleware,
|
|
198
|
+
ExtensionComponentProps
|
|
199
|
+
} from '@visualli/react';
|
|
200
|
+
|
|
201
|
+
// Middleware
|
|
202
|
+
const middleware: ParserMiddleware = (data) => {
|
|
203
|
+
if (data.type === 'extension') {
|
|
204
|
+
return { ...data, processed: true };
|
|
205
|
+
}
|
|
206
|
+
return data;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// Extension Component
|
|
210
|
+
function TooltipExtension({ extension }: ExtensionComponentProps) {
|
|
211
|
+
return (
|
|
212
|
+
<div style={{
|
|
213
|
+
position: 'absolute',
|
|
214
|
+
top: 20,
|
|
215
|
+
right: 20,
|
|
216
|
+
background: 'white',
|
|
217
|
+
padding: '12px',
|
|
218
|
+
borderRadius: '8px',
|
|
219
|
+
pointerEvents: 'auto'
|
|
220
|
+
}}>
|
|
221
|
+
{extension.data?.message}
|
|
222
|
+
</div>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// App
|
|
227
|
+
function App() {
|
|
228
|
+
const { document, isLoading } = useVisualliStream('/api/mindmap');
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<VisualliProvider
|
|
232
|
+
middlewares={[middleware]}
|
|
233
|
+
extensions={{ 'tooltip': TooltipExtension }}
|
|
234
|
+
>
|
|
235
|
+
{isLoading ? <Loading /> : <VisualliCanvas document={document} />}
|
|
236
|
+
</VisualliProvider>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
📚 **See `EXTENSION_GUIDE.md` for comprehensive documentation and examples.**
|
|
242
|
+
|
|
243
|
+
## Exports
|
|
244
|
+
|
|
245
|
+
### Component
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
import { VisualliCanvas } from '@visualli/react';
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Context & Provider 🆕
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
import { VisualliProvider, useVisualli } from '@visualli/react';
|
|
255
|
+
import type {
|
|
256
|
+
VisualliProviderProps,
|
|
257
|
+
VisualliContextValue,
|
|
258
|
+
ExtensionComponentProps,
|
|
259
|
+
ExtensionRegistry
|
|
260
|
+
} from '@visualli/react';
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Stores
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
import { useNodeStore, useViewportStore, useSelectionStore, useRenderConfigStore } from '@visualli/react';
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Hooks
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
import { useKonvaRenderer, useKonvaLayerTransition, useViewportNodes } from '@visualli/react';
|
|
273
|
+
|
|
274
|
+
// 🆕 Stream fetching hook
|
|
275
|
+
import { useVisualliStream } from '@visualli/react';
|
|
276
|
+
import type { UseVisualliStreamReturn } from '@visualli/react';
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Sub-components (composition)
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
import {
|
|
283
|
+
KonvaStage, KonvaNode, KonvaEdge,
|
|
284
|
+
KonvaNodeLayer, KonvaEdgeLayer,
|
|
285
|
+
KonvaContainerLayer,
|
|
286
|
+
NavigationStack, ZoomControls,
|
|
287
|
+
} from '@visualli/react';
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Utilities
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
import {
|
|
294
|
+
getChildLayerForNode, getConnectionsForLayer,
|
|
295
|
+
calculateFitView, calculateFitZoom, calculateFitCenter,
|
|
296
|
+
} from '@visualli/react';
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Config helpers
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
import {
|
|
303
|
+
getBlobTypeForLayer, buildBlobPathData, ALL_BLOB_SHAPES,
|
|
304
|
+
computeNodeTextWorldScale, computeNodeTextScreenScale,
|
|
305
|
+
} from '@visualli/react';
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## TypeScript
|
|
309
|
+
|
|
310
|
+
```jsonc
|
|
311
|
+
// tsconfig.json
|
|
312
|
+
{
|
|
313
|
+
"compilerOptions": {
|
|
314
|
+
"moduleResolution": "bundler",
|
|
315
|
+
"jsx": "react-jsx",
|
|
316
|
+
"target": "ES2020"
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
## Requirements
|
|
322
|
+
|
|
323
|
+
- Node.js ≥ 22
|
|
324
|
+
- React 18
|
|
325
|
+
- `@visualli/core` must be built (`npm run build` in `sdk-core/`) before typechecking
|
|
326
|
+
|
|
327
|
+
## Typecheck
|
|
328
|
+
|
|
329
|
+
```bash
|
|
330
|
+
# Build core first
|
|
331
|
+
cd ../sdk-core && npm run build
|
|
332
|
+
|
|
333
|
+
# Typecheck react package
|
|
334
|
+
cd ../sdk-react && npx tsc --noEmit
|
|
335
|
+
```
|