@3plate/graph-react 0.1.15 → 0.1.16
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 +172 -15
- package/dist/index.cjs +33 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -5
- package/dist/index.d.ts +19 -5
- package/dist/index.js +33 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @3plate/graph-react
|
|
2
2
|
|
|
3
|
-
React
|
|
3
|
+
React wrapper for [@3plate/graph](../../README.md) — a graph visualization library with stable layouts and incremental updates.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -8,27 +8,184 @@ React components for @3plate/graph visualization.
|
|
|
8
8
|
npm install @3plate/graph-react
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
React and React DOM are peer dependencies (React 18 or 19):
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install react react-dom
|
|
15
|
+
```
|
|
16
|
+
|
|
11
17
|
## Usage
|
|
12
18
|
|
|
13
19
|
```tsx
|
|
14
|
-
import {
|
|
20
|
+
import { Graph } from '@3plate/graph-react'
|
|
15
21
|
|
|
16
22
|
function App() {
|
|
17
|
-
const graph = createGraph({
|
|
18
|
-
nodes: [
|
|
19
|
-
{ id: '1', label: 'Node 1' },
|
|
20
|
-
{ id: '2', label: 'Node 2' },
|
|
21
|
-
],
|
|
22
|
-
edges: [{ id: 'e1', source: '1', target: '2' }],
|
|
23
|
-
})
|
|
24
|
-
|
|
25
23
|
return (
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
24
|
+
<Graph
|
|
25
|
+
nodes={[
|
|
26
|
+
{ id: 'a', title: 'Start' },
|
|
27
|
+
{ id: 'b', title: 'Process' },
|
|
28
|
+
{ id: 'c', title: 'End' },
|
|
29
|
+
]}
|
|
30
|
+
edges={[
|
|
31
|
+
{ source: 'a', target: 'b' },
|
|
32
|
+
{ source: 'b', target: 'c' },
|
|
33
|
+
]}
|
|
31
34
|
/>
|
|
32
35
|
)
|
|
33
36
|
}
|
|
34
37
|
```
|
|
38
|
+
|
|
39
|
+
The `<Graph>` component fills its container — give the parent a defined size:
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
<div style={{ width: '100%', height: 600 }}>
|
|
43
|
+
<Graph nodes={nodes} edges={edges} />
|
|
44
|
+
</div>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Custom Node Rendering
|
|
48
|
+
|
|
49
|
+
Use `options.canvas.renderNode` to control what appears inside each node. The function can return an **`HTMLElement` or a React node (JSX)**:
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
<Graph
|
|
53
|
+
nodes={nodes}
|
|
54
|
+
edges={edges}
|
|
55
|
+
options={{
|
|
56
|
+
canvas: {
|
|
57
|
+
renderNode: (node) => (
|
|
58
|
+
<div className="my-node">
|
|
59
|
+
<strong>{node.title}</strong>
|
|
60
|
+
<span>{node.status}</span>
|
|
61
|
+
</div>
|
|
62
|
+
),
|
|
63
|
+
},
|
|
64
|
+
}}
|
|
65
|
+
/>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
React content is mounted with `flushSync` so the graph can measure the node's dimensions before laying out the graph. Cleanup (unmounting the React root) happens automatically when a node is removed.
|
|
69
|
+
|
|
70
|
+
### Full React components
|
|
71
|
+
|
|
72
|
+
Any React component works, including those with hooks and state:
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
function NodeCard({ node }: { node: MyNode }) {
|
|
76
|
+
const [expanded, setExpanded] = useState(false)
|
|
77
|
+
return (
|
|
78
|
+
<div className="node-card" onClick={() => setExpanded(e => !e)}>
|
|
79
|
+
<h3>{node.title}</h3>
|
|
80
|
+
{expanded && <p>{node.description}</p>}
|
|
81
|
+
</div>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
<Graph
|
|
86
|
+
nodes={nodes}
|
|
87
|
+
edges={edges}
|
|
88
|
+
options={{
|
|
89
|
+
canvas: {
|
|
90
|
+
renderNode: (node) => <NodeCard node={node} />,
|
|
91
|
+
},
|
|
92
|
+
}}
|
|
93
|
+
/>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
> **Note:** Node dimensions are measured once when the node is first created. If your component can change size after the initial render (e.g. toggling expanded state), the graph layout won't automatically reflow. Prefer fixed-size or CSS-constrained node content.
|
|
97
|
+
|
|
98
|
+
## Props
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
type GraphProps<N, E> = {
|
|
102
|
+
nodes?: N[]
|
|
103
|
+
edges?: E[]
|
|
104
|
+
history?: Update<N, E>[] // Replay a sequence of graph states
|
|
105
|
+
ingestion?: IngestionConfig // WebSocket / file polling (alternative to nodes/edges)
|
|
106
|
+
options?: {
|
|
107
|
+
graph?: GraphOptions // Layout options (orientation, margins, etc.)
|
|
108
|
+
canvas?: {
|
|
109
|
+
renderNode?: (node: N, props?: NodeProps<N>) => ReactNode | HTMLElement
|
|
110
|
+
width?: string | number // default: '100%'
|
|
111
|
+
height?: string | number // default: '100%'
|
|
112
|
+
padding?: number // default: 20
|
|
113
|
+
editable?: boolean // default: false
|
|
114
|
+
panZoom?: boolean // default: true
|
|
115
|
+
colorMode?: 'light' | 'dark' | 'system' // default: 'system'
|
|
116
|
+
theme?: ThemeVars
|
|
117
|
+
nodeTypes?: Record<string, ThemeVars>
|
|
118
|
+
edgeTypes?: Record<string, ThemeVars>
|
|
119
|
+
}
|
|
120
|
+
props?: PropsOptions<N, E> // Extract id/title/ports from your data shape
|
|
121
|
+
}
|
|
122
|
+
events?: EventsOptions<N, E>
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Reactive Updates
|
|
127
|
+
|
|
128
|
+
Pass updated `nodes` and `edges` arrays and the graph updates automatically. Only changed nodes are re-measured and re-laid out:
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
const [nodes, setNodes] = useState(initialNodes)
|
|
132
|
+
const [edges, setEdges] = useState(initialEdges)
|
|
133
|
+
|
|
134
|
+
// The graph rerenders incrementally when nodes or edges change
|
|
135
|
+
return <Graph nodes={nodes} edges={edges} />
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Events
|
|
139
|
+
|
|
140
|
+
```tsx
|
|
141
|
+
<Graph
|
|
142
|
+
nodes={nodes}
|
|
143
|
+
edges={edges}
|
|
144
|
+
events={{
|
|
145
|
+
nodeClick: (node) => console.log('clicked', node),
|
|
146
|
+
edgeClick: (edge) => console.log('edge clicked', edge),
|
|
147
|
+
addNode: (props, done) => done({ id: crypto.randomUUID(), ...props }),
|
|
148
|
+
addEdge: (edge, done) => done(edge),
|
|
149
|
+
removeNode: (node, done) => done(confirm('Delete?')),
|
|
150
|
+
historyChange: (index, length) => setStep(`${index + 1}/${length}`),
|
|
151
|
+
}}
|
|
152
|
+
/>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Theming
|
|
156
|
+
|
|
157
|
+
```tsx
|
|
158
|
+
<Graph
|
|
159
|
+
nodes={[
|
|
160
|
+
{ id: 'a', type: 'success', title: 'Passed' },
|
|
161
|
+
{ id: 'b', type: 'error', title: 'Failed' },
|
|
162
|
+
]}
|
|
163
|
+
edges={edges}
|
|
164
|
+
options={{
|
|
165
|
+
canvas: {
|
|
166
|
+
colorMode: 'dark',
|
|
167
|
+
nodeTypes: {
|
|
168
|
+
success: { border: '#22c55e', text: '#dcfce7' },
|
|
169
|
+
error: { bg: '#fef2f2', border: '#ef4444', text: '#991b1b' },
|
|
170
|
+
},
|
|
171
|
+
edgeTypes: {
|
|
172
|
+
error: { color: '#ef4444' },
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
}}
|
|
176
|
+
/>
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Real-time Ingestion
|
|
180
|
+
|
|
181
|
+
```tsx
|
|
182
|
+
// WebSocket
|
|
183
|
+
<Graph ingestion={{ type: 'websocket', url: 'ws://localhost:8787' }} />
|
|
184
|
+
|
|
185
|
+
// Polling
|
|
186
|
+
<Graph ingestion={{ type: 'file', url: '/api/updates.ndjson', intervalMs: 1000 }} />
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## License
|
|
190
|
+
|
|
191
|
+
**GNU General Public License v3.0**. Commercial licenses available — see the [root README](../../README.md) for details.
|
package/dist/index.cjs
CHANGED
|
@@ -37,11 +37,39 @@ module.exports = __toCommonJS(index_exports);
|
|
|
37
37
|
|
|
38
38
|
// src/Graph.tsx
|
|
39
39
|
var import_react = __toESM(require("react"), 1);
|
|
40
|
+
var import_client = require("react-dom/client");
|
|
41
|
+
var import_react_dom = require("react-dom");
|
|
40
42
|
var import_graph_core = require("@3plate/graph-core");
|
|
43
|
+
function buildCoreOptions(options, pendingMounts) {
|
|
44
|
+
const userRenderNode = options?.canvas?.renderNode;
|
|
45
|
+
if (!userRenderNode) return options;
|
|
46
|
+
return {
|
|
47
|
+
...options,
|
|
48
|
+
canvas: {
|
|
49
|
+
...options?.canvas,
|
|
50
|
+
renderNode: (node, nodeProps) => {
|
|
51
|
+
const result = userRenderNode(node, nodeProps);
|
|
52
|
+
if (result instanceof HTMLElement) return result;
|
|
53
|
+
const el = document.createElement("div");
|
|
54
|
+
pendingMounts.set(el, result);
|
|
55
|
+
return el;
|
|
56
|
+
},
|
|
57
|
+
mountNode: (node, el) => {
|
|
58
|
+
const reactNode = pendingMounts.get(el);
|
|
59
|
+
if (reactNode === void 0) return;
|
|
60
|
+
pendingMounts.delete(el);
|
|
61
|
+
const root = (0, import_client.createRoot)(el);
|
|
62
|
+
(0, import_react_dom.flushSync)(() => root.render(reactNode));
|
|
63
|
+
return () => root.unmount();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
41
68
|
function Graph(props) {
|
|
42
69
|
const rootRef = (0, import_react.useRef)(null);
|
|
43
70
|
const apiRef = (0, import_react.useRef)(null);
|
|
44
71
|
const rootIdRef = (0, import_react.useRef)(`graph-${Math.random().toString(36).slice(2, 11)}`);
|
|
72
|
+
const pendingMounts = (0, import_react.useRef)(/* @__PURE__ */ new Map());
|
|
45
73
|
(0, import_react.useEffect)(() => {
|
|
46
74
|
if (!rootRef.current || apiRef.current) return;
|
|
47
75
|
rootRef.current.id = rootIdRef.current;
|
|
@@ -51,7 +79,7 @@ function Graph(props) {
|
|
|
51
79
|
edges: props.edges,
|
|
52
80
|
history: props.history,
|
|
53
81
|
ingestion: props.ingestion,
|
|
54
|
-
options: props.options,
|
|
82
|
+
options: buildCoreOptions(props.options, pendingMounts.current),
|
|
55
83
|
events: props.events
|
|
56
84
|
}).then((api) => {
|
|
57
85
|
apiRef.current = api;
|
|
@@ -71,7 +99,10 @@ function Graph(props) {
|
|
|
71
99
|
}, []);
|
|
72
100
|
(0, import_react.useEffect)(() => {
|
|
73
101
|
if (!apiRef.current) return;
|
|
74
|
-
apiRef.current.applyProps(
|
|
102
|
+
apiRef.current.applyProps({
|
|
103
|
+
...props,
|
|
104
|
+
options: buildCoreOptions(props.options, pendingMounts.current)
|
|
105
|
+
});
|
|
75
106
|
}, [props.nodes, props.edges, props.history, props.options]);
|
|
76
107
|
return /* @__PURE__ */ import_react.default.createElement("div", { ref: rootRef, style: { width: "100%", height: "100%" } });
|
|
77
108
|
}
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/Graph.tsx","../src/Playground.tsx"],"sourcesContent":["/**\n * @3plate/graph-react - React components for @3plate/graph graph visualization\n */\n\nexport { Graph } from './Graph'\nexport { Playground } from './Playground'\n\nexport type { GraphProps } from './Graph'\nexport type { PlaygroundProps } from './Playground'\n\n// Re-export types from core for convenience\nexport type {\n // API types\n API,\n APIArguments,\n APIOptions,\n Update,\n IngestionConfig,\n EventsOptions,\n // Callback parameter types\n NewNode,\n NewEdge,\n NodeProps,\n EdgeProps,\n PortProps,\n RenderNode,\n // Ingestion types\n IngestMessage,\n SnapshotMessage,\n UpdateMessage,\n HistoryMessage,\n // WebSocket types\n WebSocketStatus,\n WebSocketStatusListener,\n // Theming types\n ColorMode,\n ThemeVars,\n CanvasTheme,\n NodeTheme,\n PortTheme,\n EdgeTheme,\n // Common types\n Orientation,\n NodeAlign,\n PortStyle,\n} from '@3plate/graph-core'\n","import React, { useEffect, useRef } from 'react'\nimport { graph, type API } from '@3plate/graph-core'\nimport type { APIArguments, Update, IngestionConfig } from '@3plate/graph-core'\n\nexport type GraphProps<N, E> = {\n /** Initial nodes */\n nodes?: N[]\n /** Initial edges */\n edges?: E[]\n /** Initial history */\n history?: Update<N, E>[]\n /** Ingestion source configuration (alternative to nodes/edges/history) */\n ingestion?: IngestionConfig\n /** Options */\n options?: APIArguments<N, E>['options']\n /** Events */\n events?: APIArguments<N, E>['events']\n}\n\n/**\n * Graph component - renders a graph visualization\n * Intelligently handles prop changes by diffing nodes, edges, and history\n */\nexport function Graph<N, E>(props: GraphProps<N, E>) {\n const rootRef = useRef<HTMLDivElement>(null)\n const apiRef = useRef<API<N, E> | null>(null)\n const rootIdRef = useRef<string>(`graph-${Math.random().toString(36).slice(2, 11)}`)\n\n // Initialize API once\n useEffect(() => {\n if (!rootRef.current || apiRef.current) return\n\n rootRef.current.id = rootIdRef.current\n\n graph({\n root: rootIdRef.current,\n nodes: props.nodes,\n edges: props.edges,\n history: props.history,\n ingestion: props.ingestion,\n options: props.options,\n events: props.events,\n }).then(api => {\n apiRef.current = api\n })\n\n return () => {\n // Cleanup\n if (apiRef.current) {\n apiRef.current.destroy()\n apiRef.current = null\n }\n if (rootRef.current) {\n // Remove canvas from DOM\n const canvas = rootRef.current.querySelector('canvas, svg')\n if (canvas) {\n canvas.remove()\n }\n }\n }\n }, []) // Only run once on mount\n\n // Handle prop changes using the centralized applyProps method\n useEffect(() => {\n if (!apiRef.current) return\n apiRef.current.applyProps(props)\n }, [props.nodes, props.edges, props.history, props.options])\n\n return <div ref={rootRef} style={{ width: '100%', height: '100%' }} />\n}\n","import React, { useEffect, useRef } from 'react'\nimport { Playground as PlaygroundClass, type PlaygroundOptions, type Example } from '@3plate/graph-core'\n\nexport type PlaygroundProps = {\n /** Examples to display */\n examples: PlaygroundOptions['examples']\n /** Default example key */\n defaultExample?: string\n}\n\n/**\n * Playground component - renders the interactive playground with examples\n */\nexport function Playground(props: PlaygroundProps) {\n const rootRef = useRef<HTMLDivElement>(null)\n const playgroundRef = useRef<PlaygroundClass | null>(null)\n const rootIdRef = useRef<string>(`playground-${Math.random().toString(36).slice(2, 11)}`)\n const prevExamplesRef = useRef<Record<string, Example>>({})\n\n useEffect(() => {\n if (!rootRef.current || playgroundRef.current) return\n\n rootRef.current.id = rootIdRef.current\n\n const playground = new PlaygroundClass({\n root: rootIdRef.current,\n examples: props.examples,\n defaultExample: props.defaultExample,\n })\n playgroundRef.current = playground\n prevExamplesRef.current = { ...props.examples }\n playground.init()\n\n return () => {\n // Cleanup if needed\n playgroundRef.current = null\n }\n }, []) // Only initialize once\n\n // Handle examples changes\n useEffect(() => {\n if (!playgroundRef.current) return\n\n const playground = playgroundRef.current\n const prev = prevExamplesRef.current\n const current = props.examples\n\n // Get all keys from both previous and current\n const allKeys = new Set([...Object.keys(prev), ...Object.keys(current)])\n\n for (const key of allKeys) {\n const prevExample = prev[key]\n const currentExample = current[key]\n\n if (!prevExample && currentExample) {\n // Example was added\n playground.addExample(key, currentExample)\n } else if (prevExample && !currentExample) {\n // Example was removed\n playground.removeExample(key)\n } else if (prevExample && currentExample && !shallowEqualExample(prevExample, currentExample)) {\n // Example was modified\n playground.addExample(key, currentExample)\n }\n }\n\n prevExamplesRef.current = { ...current }\n }, [props.examples])\n\n return <div ref={rootRef} style={{ width: '100%', height: '100%' }} />\n}\n\n/**\n * Shallow comparison of two Example objects\n */\nfunction shallowEqualExample(a: Example, b: Example): boolean {\n if (a === b) return true\n if (a.name !== b.name) return false\n if (a.description !== b.description) return false\n if (!shallowEqualArray(a.nodes, b.nodes)) return false\n if (!shallowEqualArray(a.edges, b.edges)) return false\n if (!shallowEqualOptions(a.options, b.options)) return false\n if (!shallowEqualSource(a.source, b.source)) return false\n return true\n}\n\n/**\n * Shallow comparison of arrays\n */\nfunction shallowEqualArray<T>(a: T[] | undefined, b: T[] | undefined): boolean {\n if (a === b) return true\n if (!a || !b) return false\n if (a.length !== b.length) return false\n for (let i = 0; i < a.length; i++) {\n if (a[i] !== b[i]) return false\n }\n return true\n}\n\n/**\n * Shallow comparison of ExampleOptions\n */\nfunction shallowEqualOptions(\n a: Example['options'],\n b: Example['options']\n): boolean {\n if (a === b) return true\n if (!a || !b) return false\n // For now, do a simple reference check on options\n // Can be enhanced if needed\n return a === b\n}\n\n/**\n * Shallow comparison of ExampleSource\n */\nfunction shallowEqualSource(\n a: Example['source'],\n b: Example['source']\n): boolean {\n if (a === b) return true\n if (!a || !b) return false\n if (a.type !== b.type) return false\n if (a.type === 'file' && b.type === 'file') {\n return a.path === b.path\n }\n if (a.type === 'websocket' && b.type === 'websocket') {\n return a.url === b.url\n }\n return false\n}\n"],"mappings":"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","names":["React","import_react","import_graph_core","PlaygroundClass","React"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/Graph.tsx","../src/Playground.tsx"],"sourcesContent":["/**\n * @3plate/graph-react - React components for @3plate/graph graph visualization\n */\n\nexport { Graph } from './Graph'\nexport { Playground } from './Playground'\n\nexport type { GraphProps } from './Graph'\nexport type { PlaygroundProps } from './Playground'\n\n// Re-export types from core for convenience\nexport type {\n // API types\n API,\n APIArguments,\n APIOptions,\n Update,\n IngestionConfig,\n EventsOptions,\n // Callback parameter types\n NewNode,\n NewEdge,\n NodeProps,\n EdgeProps,\n PortProps,\n RenderNode,\n // Ingestion types\n IngestMessage,\n SnapshotMessage,\n UpdateMessage,\n HistoryMessage,\n // WebSocket types\n WebSocketStatus,\n WebSocketStatusListener,\n // Theming types\n ColorMode,\n ThemeVars,\n CanvasTheme,\n NodeTheme,\n PortTheme,\n EdgeTheme,\n // Common types\n Orientation,\n NodeAlign,\n PortStyle,\n} from '@3plate/graph-core'\n","import React, { useEffect, useRef, type ReactNode } from 'react'\nimport { createRoot } from 'react-dom/client'\nimport { flushSync } from 'react-dom'\nimport { graph, type API } from '@3plate/graph-core'\nimport type {\n APIArguments,\n APIOptions,\n CanvasOptions,\n NodeProps,\n Update,\n IngestionConfig,\n} from '@3plate/graph-core'\n\n/** Extends the core renderNode signature to also accept React nodes */\ntype ReactRenderNode<N> = (node: N, props?: NodeProps<N>) => ReactNode | HTMLElement\n\n/** CanvasOptions with an extended renderNode that accepts React nodes */\ntype ReactCanvasOptions<N> = Omit<CanvasOptions<N>, 'renderNode'> & {\n renderNode?: ReactRenderNode<N>\n}\n\n/** APIOptions with the React-extended canvas options */\ntype ReactAPIOptions<N, E> = Omit<APIOptions<N, E>, 'canvas'> & {\n canvas?: ReactCanvasOptions<N>\n}\n\nexport type GraphProps<N, E> = {\n /** Initial nodes */\n nodes?: N[]\n /** Initial edges */\n edges?: E[]\n /** Initial history */\n history?: Update<N, E>[]\n /** Ingestion source configuration (alternative to nodes/edges/history) */\n ingestion?: IngestionConfig\n /** Options */\n options?: ReactAPIOptions<N, E>\n /** Events */\n events?: APIArguments<N, E>['events']\n}\n\n/**\n * Converts ReactAPIOptions into core APIOptions.\n * When renderNode returns a ReactNode, a placeholder element is produced and\n * mountNode renders the React content into it synchronously via flushSync.\n */\nfunction buildCoreOptions<N, E>(\n options: ReactAPIOptions<N, E> | undefined,\n pendingMounts: Map<HTMLElement, ReactNode>,\n): APIOptions<N, E> | undefined {\n const userRenderNode = options?.canvas?.renderNode\n if (!userRenderNode) return options as APIOptions<N, E> | undefined\n\n return {\n ...options,\n canvas: {\n ...options?.canvas,\n renderNode: (node: N, nodeProps?: NodeProps<N>): HTMLElement => {\n const result = userRenderNode(node, nodeProps)\n if (result instanceof HTMLElement) return result\n const el = document.createElement('div')\n pendingMounts.set(el, result)\n return el\n },\n mountNode: (node: N, el: HTMLElement): (() => void) | void => {\n const reactNode = pendingMounts.get(el)\n if (reactNode === undefined) return\n pendingMounts.delete(el)\n const root = createRoot(el)\n flushSync(() => root.render(reactNode as ReactNode))\n return () => root.unmount()\n },\n },\n } as APIOptions<N, E>\n}\n\n/**\n * Graph component - renders a graph visualization.\n * Intelligently handles prop changes by diffing nodes, edges, and history.\n *\n * The `options.canvas.renderNode` function may return either an HTMLElement\n * or a React node (JSX). When JSX is returned the wrapper handles mounting\n * and cleanup automatically.\n */\nexport function Graph<N, E>(props: GraphProps<N, E>) {\n const rootRef = useRef<HTMLDivElement>(null)\n const apiRef = useRef<API<N, E> | null>(null)\n const rootIdRef = useRef<string>(`graph-${Math.random().toString(36).slice(2, 11)}`)\n const pendingMounts = useRef(new Map<HTMLElement, ReactNode>())\n\n // Initialize API once\n useEffect(() => {\n if (!rootRef.current || apiRef.current) return\n\n rootRef.current.id = rootIdRef.current\n\n graph({\n root: rootIdRef.current,\n nodes: props.nodes,\n edges: props.edges,\n history: props.history,\n ingestion: props.ingestion,\n options: buildCoreOptions(props.options, pendingMounts.current),\n events: props.events,\n }).then(api => {\n apiRef.current = api\n })\n\n return () => {\n // Cleanup\n if (apiRef.current) {\n apiRef.current.destroy()\n apiRef.current = null\n }\n if (rootRef.current) {\n // Remove canvas from DOM\n const canvas = rootRef.current.querySelector('canvas, svg')\n if (canvas) {\n canvas.remove()\n }\n }\n }\n }, []) // Only run once on mount\n\n // Handle prop changes using the centralized applyProps method\n useEffect(() => {\n if (!apiRef.current) return\n apiRef.current.applyProps({\n ...props,\n options: buildCoreOptions(props.options, pendingMounts.current),\n })\n }, [props.nodes, props.edges, props.history, props.options])\n\n return <div ref={rootRef} style={{ width: '100%', height: '100%' }} />\n}\n","import React, { useEffect, useRef } from 'react'\nimport { Playground as PlaygroundClass, type PlaygroundOptions, type Example } from '@3plate/graph-core'\n\nexport type PlaygroundProps = {\n /** Examples to display */\n examples: PlaygroundOptions['examples']\n /** Default example key */\n defaultExample?: string\n}\n\n/**\n * Playground component - renders the interactive playground with examples\n */\nexport function Playground(props: PlaygroundProps) {\n const rootRef = useRef<HTMLDivElement>(null)\n const playgroundRef = useRef<PlaygroundClass | null>(null)\n const rootIdRef = useRef<string>(`playground-${Math.random().toString(36).slice(2, 11)}`)\n const prevExamplesRef = useRef<Record<string, Example>>({})\n\n useEffect(() => {\n if (!rootRef.current || playgroundRef.current) return\n\n rootRef.current.id = rootIdRef.current\n\n const playground = new PlaygroundClass({\n root: rootIdRef.current,\n examples: props.examples,\n defaultExample: props.defaultExample,\n })\n playgroundRef.current = playground\n prevExamplesRef.current = { ...props.examples }\n playground.init()\n\n return () => {\n // Cleanup if needed\n playgroundRef.current = null\n }\n }, []) // Only initialize once\n\n // Handle examples changes\n useEffect(() => {\n if (!playgroundRef.current) return\n\n const playground = playgroundRef.current\n const prev = prevExamplesRef.current\n const current = props.examples\n\n // Get all keys from both previous and current\n const allKeys = new Set([...Object.keys(prev), ...Object.keys(current)])\n\n for (const key of allKeys) {\n const prevExample = prev[key]\n const currentExample = current[key]\n\n if (!prevExample && currentExample) {\n // Example was added\n playground.addExample(key, currentExample)\n } else if (prevExample && !currentExample) {\n // Example was removed\n playground.removeExample(key)\n } else if (prevExample && currentExample && !shallowEqualExample(prevExample, currentExample)) {\n // Example was modified\n playground.addExample(key, currentExample)\n }\n }\n\n prevExamplesRef.current = { ...current }\n }, [props.examples])\n\n return <div ref={rootRef} style={{ width: '100%', height: '100%' }} />\n}\n\n/**\n * Shallow comparison of two Example objects\n */\nfunction shallowEqualExample(a: Example, b: Example): boolean {\n if (a === b) return true\n if (a.name !== b.name) return false\n if (a.description !== b.description) return false\n if (!shallowEqualArray(a.nodes, b.nodes)) return false\n if (!shallowEqualArray(a.edges, b.edges)) return false\n if (!shallowEqualOptions(a.options, b.options)) return false\n if (!shallowEqualSource(a.source, b.source)) return false\n return true\n}\n\n/**\n * Shallow comparison of arrays\n */\nfunction shallowEqualArray<T>(a: T[] | undefined, b: T[] | undefined): boolean {\n if (a === b) return true\n if (!a || !b) return false\n if (a.length !== b.length) return false\n for (let i = 0; i < a.length; i++) {\n if (a[i] !== b[i]) return false\n }\n return true\n}\n\n/**\n * Shallow comparison of ExampleOptions\n */\nfunction shallowEqualOptions(\n a: Example['options'],\n b: Example['options']\n): boolean {\n if (a === b) return true\n if (!a || !b) return false\n // For now, do a simple reference check on options\n // Can be enhanced if needed\n return a === b\n}\n\n/**\n * Shallow comparison of ExampleSource\n */\nfunction shallowEqualSource(\n a: Example['source'],\n b: Example['source']\n): boolean {\n if (a === b) return true\n if (!a || !b) return false\n if (a.type !== b.type) return false\n if (a.type === 'file' && b.type === 'file') {\n return a.path === b.path\n }\n if (a.type === 'websocket' && b.type === 'websocket') {\n return a.url === b.url\n }\n return false\n}\n"],"mappings":"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","names":["React","import_react","import_graph_core","PlaygroundClass","React"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Update, IngestionConfig, APIArguments, PlaygroundOptions } from '@3plate/graph-core';
|
|
1
|
+
import React, { ReactNode } from 'react';
|
|
2
|
+
import { Update, IngestionConfig, APIOptions, CanvasOptions, NodeProps, APIArguments, PlaygroundOptions } from '@3plate/graph-core';
|
|
3
3
|
export { API, APIArguments, APIOptions, CanvasTheme, ColorMode, EdgeProps, EdgeTheme, EventsOptions, HistoryMessage, IngestMessage, IngestionConfig, NewEdge, NewNode, NodeAlign, NodeProps, NodeTheme, Orientation, PortProps, PortStyle, PortTheme, RenderNode, SnapshotMessage, ThemeVars, Update, UpdateMessage, WebSocketStatus, WebSocketStatusListener } from '@3plate/graph-core';
|
|
4
4
|
|
|
5
|
+
/** Extends the core renderNode signature to also accept React nodes */
|
|
6
|
+
type ReactRenderNode<N> = (node: N, props?: NodeProps<N>) => ReactNode | HTMLElement;
|
|
7
|
+
/** CanvasOptions with an extended renderNode that accepts React nodes */
|
|
8
|
+
type ReactCanvasOptions<N> = Omit<CanvasOptions<N>, 'renderNode'> & {
|
|
9
|
+
renderNode?: ReactRenderNode<N>;
|
|
10
|
+
};
|
|
11
|
+
/** APIOptions with the React-extended canvas options */
|
|
12
|
+
type ReactAPIOptions<N, E> = Omit<APIOptions<N, E>, 'canvas'> & {
|
|
13
|
+
canvas?: ReactCanvasOptions<N>;
|
|
14
|
+
};
|
|
5
15
|
type GraphProps<N, E> = {
|
|
6
16
|
/** Initial nodes */
|
|
7
17
|
nodes?: N[];
|
|
@@ -12,13 +22,17 @@ type GraphProps<N, E> = {
|
|
|
12
22
|
/** Ingestion source configuration (alternative to nodes/edges/history) */
|
|
13
23
|
ingestion?: IngestionConfig;
|
|
14
24
|
/** Options */
|
|
15
|
-
options?:
|
|
25
|
+
options?: ReactAPIOptions<N, E>;
|
|
16
26
|
/** Events */
|
|
17
27
|
events?: APIArguments<N, E>['events'];
|
|
18
28
|
};
|
|
19
29
|
/**
|
|
20
|
-
* Graph component - renders a graph visualization
|
|
21
|
-
* Intelligently handles prop changes by diffing nodes, edges, and history
|
|
30
|
+
* Graph component - renders a graph visualization.
|
|
31
|
+
* Intelligently handles prop changes by diffing nodes, edges, and history.
|
|
32
|
+
*
|
|
33
|
+
* The `options.canvas.renderNode` function may return either an HTMLElement
|
|
34
|
+
* or a React node (JSX). When JSX is returned the wrapper handles mounting
|
|
35
|
+
* and cleanup automatically.
|
|
22
36
|
*/
|
|
23
37
|
declare function Graph<N, E>(props: GraphProps<N, E>): React.JSX.Element;
|
|
24
38
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Update, IngestionConfig, APIArguments, PlaygroundOptions } from '@3plate/graph-core';
|
|
1
|
+
import React, { ReactNode } from 'react';
|
|
2
|
+
import { Update, IngestionConfig, APIOptions, CanvasOptions, NodeProps, APIArguments, PlaygroundOptions } from '@3plate/graph-core';
|
|
3
3
|
export { API, APIArguments, APIOptions, CanvasTheme, ColorMode, EdgeProps, EdgeTheme, EventsOptions, HistoryMessage, IngestMessage, IngestionConfig, NewEdge, NewNode, NodeAlign, NodeProps, NodeTheme, Orientation, PortProps, PortStyle, PortTheme, RenderNode, SnapshotMessage, ThemeVars, Update, UpdateMessage, WebSocketStatus, WebSocketStatusListener } from '@3plate/graph-core';
|
|
4
4
|
|
|
5
|
+
/** Extends the core renderNode signature to also accept React nodes */
|
|
6
|
+
type ReactRenderNode<N> = (node: N, props?: NodeProps<N>) => ReactNode | HTMLElement;
|
|
7
|
+
/** CanvasOptions with an extended renderNode that accepts React nodes */
|
|
8
|
+
type ReactCanvasOptions<N> = Omit<CanvasOptions<N>, 'renderNode'> & {
|
|
9
|
+
renderNode?: ReactRenderNode<N>;
|
|
10
|
+
};
|
|
11
|
+
/** APIOptions with the React-extended canvas options */
|
|
12
|
+
type ReactAPIOptions<N, E> = Omit<APIOptions<N, E>, 'canvas'> & {
|
|
13
|
+
canvas?: ReactCanvasOptions<N>;
|
|
14
|
+
};
|
|
5
15
|
type GraphProps<N, E> = {
|
|
6
16
|
/** Initial nodes */
|
|
7
17
|
nodes?: N[];
|
|
@@ -12,13 +22,17 @@ type GraphProps<N, E> = {
|
|
|
12
22
|
/** Ingestion source configuration (alternative to nodes/edges/history) */
|
|
13
23
|
ingestion?: IngestionConfig;
|
|
14
24
|
/** Options */
|
|
15
|
-
options?:
|
|
25
|
+
options?: ReactAPIOptions<N, E>;
|
|
16
26
|
/** Events */
|
|
17
27
|
events?: APIArguments<N, E>['events'];
|
|
18
28
|
};
|
|
19
29
|
/**
|
|
20
|
-
* Graph component - renders a graph visualization
|
|
21
|
-
* Intelligently handles prop changes by diffing nodes, edges, and history
|
|
30
|
+
* Graph component - renders a graph visualization.
|
|
31
|
+
* Intelligently handles prop changes by diffing nodes, edges, and history.
|
|
32
|
+
*
|
|
33
|
+
* The `options.canvas.renderNode` function may return either an HTMLElement
|
|
34
|
+
* or a React node (JSX). When JSX is returned the wrapper handles mounting
|
|
35
|
+
* and cleanup automatically.
|
|
22
36
|
*/
|
|
23
37
|
declare function Graph<N, E>(props: GraphProps<N, E>): React.JSX.Element;
|
|
24
38
|
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,38 @@
|
|
|
1
1
|
// src/Graph.tsx
|
|
2
2
|
import React, { useEffect, useRef } from "react";
|
|
3
|
+
import { createRoot } from "react-dom/client";
|
|
4
|
+
import { flushSync } from "react-dom";
|
|
3
5
|
import { graph } from "@3plate/graph-core";
|
|
6
|
+
function buildCoreOptions(options, pendingMounts) {
|
|
7
|
+
const userRenderNode = options?.canvas?.renderNode;
|
|
8
|
+
if (!userRenderNode) return options;
|
|
9
|
+
return {
|
|
10
|
+
...options,
|
|
11
|
+
canvas: {
|
|
12
|
+
...options?.canvas,
|
|
13
|
+
renderNode: (node, nodeProps) => {
|
|
14
|
+
const result = userRenderNode(node, nodeProps);
|
|
15
|
+
if (result instanceof HTMLElement) return result;
|
|
16
|
+
const el = document.createElement("div");
|
|
17
|
+
pendingMounts.set(el, result);
|
|
18
|
+
return el;
|
|
19
|
+
},
|
|
20
|
+
mountNode: (node, el) => {
|
|
21
|
+
const reactNode = pendingMounts.get(el);
|
|
22
|
+
if (reactNode === void 0) return;
|
|
23
|
+
pendingMounts.delete(el);
|
|
24
|
+
const root = createRoot(el);
|
|
25
|
+
flushSync(() => root.render(reactNode));
|
|
26
|
+
return () => root.unmount();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
4
31
|
function Graph(props) {
|
|
5
32
|
const rootRef = useRef(null);
|
|
6
33
|
const apiRef = useRef(null);
|
|
7
34
|
const rootIdRef = useRef(`graph-${Math.random().toString(36).slice(2, 11)}`);
|
|
35
|
+
const pendingMounts = useRef(/* @__PURE__ */ new Map());
|
|
8
36
|
useEffect(() => {
|
|
9
37
|
if (!rootRef.current || apiRef.current) return;
|
|
10
38
|
rootRef.current.id = rootIdRef.current;
|
|
@@ -14,7 +42,7 @@ function Graph(props) {
|
|
|
14
42
|
edges: props.edges,
|
|
15
43
|
history: props.history,
|
|
16
44
|
ingestion: props.ingestion,
|
|
17
|
-
options: props.options,
|
|
45
|
+
options: buildCoreOptions(props.options, pendingMounts.current),
|
|
18
46
|
events: props.events
|
|
19
47
|
}).then((api) => {
|
|
20
48
|
apiRef.current = api;
|
|
@@ -34,7 +62,10 @@ function Graph(props) {
|
|
|
34
62
|
}, []);
|
|
35
63
|
useEffect(() => {
|
|
36
64
|
if (!apiRef.current) return;
|
|
37
|
-
apiRef.current.applyProps(
|
|
65
|
+
apiRef.current.applyProps({
|
|
66
|
+
...props,
|
|
67
|
+
options: buildCoreOptions(props.options, pendingMounts.current)
|
|
68
|
+
});
|
|
38
69
|
}, [props.nodes, props.edges, props.history, props.options]);
|
|
39
70
|
return /* @__PURE__ */ React.createElement("div", { ref: rootRef, style: { width: "100%", height: "100%" } });
|
|
40
71
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/Graph.tsx","../src/Playground.tsx"],"sourcesContent":["import React, { useEffect, useRef } from 'react'\nimport { graph, type API } from '@3plate/graph-core'\nimport type { APIArguments, Update, IngestionConfig } from '@3plate/graph-core'\n\nexport type GraphProps<N, E> = {\n /** Initial nodes */\n nodes?: N[]\n /** Initial edges */\n edges?: E[]\n /** Initial history */\n history?: Update<N, E>[]\n /** Ingestion source configuration (alternative to nodes/edges/history) */\n ingestion?: IngestionConfig\n /** Options */\n options?: APIArguments<N, E>['options']\n /** Events */\n events?: APIArguments<N, E>['events']\n}\n\n/**\n * Graph component - renders a graph visualization\n * Intelligently handles prop changes by diffing nodes, edges, and history\n */\nexport function Graph<N, E>(props: GraphProps<N, E>) {\n const rootRef = useRef<HTMLDivElement>(null)\n const apiRef = useRef<API<N, E> | null>(null)\n const rootIdRef = useRef<string>(`graph-${Math.random().toString(36).slice(2, 11)}`)\n\n // Initialize API once\n useEffect(() => {\n if (!rootRef.current || apiRef.current) return\n\n rootRef.current.id = rootIdRef.current\n\n graph({\n root: rootIdRef.current,\n nodes: props.nodes,\n edges: props.edges,\n history: props.history,\n ingestion: props.ingestion,\n options: props.options,\n events: props.events,\n }).then(api => {\n apiRef.current = api\n })\n\n return () => {\n // Cleanup\n if (apiRef.current) {\n apiRef.current.destroy()\n apiRef.current = null\n }\n if (rootRef.current) {\n // Remove canvas from DOM\n const canvas = rootRef.current.querySelector('canvas, svg')\n if (canvas) {\n canvas.remove()\n }\n }\n }\n }, []) // Only run once on mount\n\n // Handle prop changes using the centralized applyProps method\n useEffect(() => {\n if (!apiRef.current) return\n apiRef.current.applyProps(props)\n }, [props.nodes, props.edges, props.history, props.options])\n\n return <div ref={rootRef} style={{ width: '100%', height: '100%' }} />\n}\n","import React, { useEffect, useRef } from 'react'\nimport { Playground as PlaygroundClass, type PlaygroundOptions, type Example } from '@3plate/graph-core'\n\nexport type PlaygroundProps = {\n /** Examples to display */\n examples: PlaygroundOptions['examples']\n /** Default example key */\n defaultExample?: string\n}\n\n/**\n * Playground component - renders the interactive playground with examples\n */\nexport function Playground(props: PlaygroundProps) {\n const rootRef = useRef<HTMLDivElement>(null)\n const playgroundRef = useRef<PlaygroundClass | null>(null)\n const rootIdRef = useRef<string>(`playground-${Math.random().toString(36).slice(2, 11)}`)\n const prevExamplesRef = useRef<Record<string, Example>>({})\n\n useEffect(() => {\n if (!rootRef.current || playgroundRef.current) return\n\n rootRef.current.id = rootIdRef.current\n\n const playground = new PlaygroundClass({\n root: rootIdRef.current,\n examples: props.examples,\n defaultExample: props.defaultExample,\n })\n playgroundRef.current = playground\n prevExamplesRef.current = { ...props.examples }\n playground.init()\n\n return () => {\n // Cleanup if needed\n playgroundRef.current = null\n }\n }, []) // Only initialize once\n\n // Handle examples changes\n useEffect(() => {\n if (!playgroundRef.current) return\n\n const playground = playgroundRef.current\n const prev = prevExamplesRef.current\n const current = props.examples\n\n // Get all keys from both previous and current\n const allKeys = new Set([...Object.keys(prev), ...Object.keys(current)])\n\n for (const key of allKeys) {\n const prevExample = prev[key]\n const currentExample = current[key]\n\n if (!prevExample && currentExample) {\n // Example was added\n playground.addExample(key, currentExample)\n } else if (prevExample && !currentExample) {\n // Example was removed\n playground.removeExample(key)\n } else if (prevExample && currentExample && !shallowEqualExample(prevExample, currentExample)) {\n // Example was modified\n playground.addExample(key, currentExample)\n }\n }\n\n prevExamplesRef.current = { ...current }\n }, [props.examples])\n\n return <div ref={rootRef} style={{ width: '100%', height: '100%' }} />\n}\n\n/**\n * Shallow comparison of two Example objects\n */\nfunction shallowEqualExample(a: Example, b: Example): boolean {\n if (a === b) return true\n if (a.name !== b.name) return false\n if (a.description !== b.description) return false\n if (!shallowEqualArray(a.nodes, b.nodes)) return false\n if (!shallowEqualArray(a.edges, b.edges)) return false\n if (!shallowEqualOptions(a.options, b.options)) return false\n if (!shallowEqualSource(a.source, b.source)) return false\n return true\n}\n\n/**\n * Shallow comparison of arrays\n */\nfunction shallowEqualArray<T>(a: T[] | undefined, b: T[] | undefined): boolean {\n if (a === b) return true\n if (!a || !b) return false\n if (a.length !== b.length) return false\n for (let i = 0; i < a.length; i++) {\n if (a[i] !== b[i]) return false\n }\n return true\n}\n\n/**\n * Shallow comparison of ExampleOptions\n */\nfunction shallowEqualOptions(\n a: Example['options'],\n b: Example['options']\n): boolean {\n if (a === b) return true\n if (!a || !b) return false\n // For now, do a simple reference check on options\n // Can be enhanced if needed\n return a === b\n}\n\n/**\n * Shallow comparison of ExampleSource\n */\nfunction shallowEqualSource(\n a: Example['source'],\n b: Example['source']\n): boolean {\n if (a === b) return true\n if (!a || !b) return false\n if (a.type !== b.type) return false\n if (a.type === 'file' && b.type === 'file') {\n return a.path === b.path\n }\n if (a.type === 'websocket' && b.type === 'websocket') {\n return a.url === b.url\n }\n return false\n}\n"],"mappings":"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","names":["React","useEffect","useRef"]}
|
|
1
|
+
{"version":3,"sources":["../src/Graph.tsx","../src/Playground.tsx"],"sourcesContent":["import React, { useEffect, useRef, type ReactNode } from 'react'\nimport { createRoot } from 'react-dom/client'\nimport { flushSync } from 'react-dom'\nimport { graph, type API } from '@3plate/graph-core'\nimport type {\n APIArguments,\n APIOptions,\n CanvasOptions,\n NodeProps,\n Update,\n IngestionConfig,\n} from '@3plate/graph-core'\n\n/** Extends the core renderNode signature to also accept React nodes */\ntype ReactRenderNode<N> = (node: N, props?: NodeProps<N>) => ReactNode | HTMLElement\n\n/** CanvasOptions with an extended renderNode that accepts React nodes */\ntype ReactCanvasOptions<N> = Omit<CanvasOptions<N>, 'renderNode'> & {\n renderNode?: ReactRenderNode<N>\n}\n\n/** APIOptions with the React-extended canvas options */\ntype ReactAPIOptions<N, E> = Omit<APIOptions<N, E>, 'canvas'> & {\n canvas?: ReactCanvasOptions<N>\n}\n\nexport type GraphProps<N, E> = {\n /** Initial nodes */\n nodes?: N[]\n /** Initial edges */\n edges?: E[]\n /** Initial history */\n history?: Update<N, E>[]\n /** Ingestion source configuration (alternative to nodes/edges/history) */\n ingestion?: IngestionConfig\n /** Options */\n options?: ReactAPIOptions<N, E>\n /** Events */\n events?: APIArguments<N, E>['events']\n}\n\n/**\n * Converts ReactAPIOptions into core APIOptions.\n * When renderNode returns a ReactNode, a placeholder element is produced and\n * mountNode renders the React content into it synchronously via flushSync.\n */\nfunction buildCoreOptions<N, E>(\n options: ReactAPIOptions<N, E> | undefined,\n pendingMounts: Map<HTMLElement, ReactNode>,\n): APIOptions<N, E> | undefined {\n const userRenderNode = options?.canvas?.renderNode\n if (!userRenderNode) return options as APIOptions<N, E> | undefined\n\n return {\n ...options,\n canvas: {\n ...options?.canvas,\n renderNode: (node: N, nodeProps?: NodeProps<N>): HTMLElement => {\n const result = userRenderNode(node, nodeProps)\n if (result instanceof HTMLElement) return result\n const el = document.createElement('div')\n pendingMounts.set(el, result)\n return el\n },\n mountNode: (node: N, el: HTMLElement): (() => void) | void => {\n const reactNode = pendingMounts.get(el)\n if (reactNode === undefined) return\n pendingMounts.delete(el)\n const root = createRoot(el)\n flushSync(() => root.render(reactNode as ReactNode))\n return () => root.unmount()\n },\n },\n } as APIOptions<N, E>\n}\n\n/**\n * Graph component - renders a graph visualization.\n * Intelligently handles prop changes by diffing nodes, edges, and history.\n *\n * The `options.canvas.renderNode` function may return either an HTMLElement\n * or a React node (JSX). When JSX is returned the wrapper handles mounting\n * and cleanup automatically.\n */\nexport function Graph<N, E>(props: GraphProps<N, E>) {\n const rootRef = useRef<HTMLDivElement>(null)\n const apiRef = useRef<API<N, E> | null>(null)\n const rootIdRef = useRef<string>(`graph-${Math.random().toString(36).slice(2, 11)}`)\n const pendingMounts = useRef(new Map<HTMLElement, ReactNode>())\n\n // Initialize API once\n useEffect(() => {\n if (!rootRef.current || apiRef.current) return\n\n rootRef.current.id = rootIdRef.current\n\n graph({\n root: rootIdRef.current,\n nodes: props.nodes,\n edges: props.edges,\n history: props.history,\n ingestion: props.ingestion,\n options: buildCoreOptions(props.options, pendingMounts.current),\n events: props.events,\n }).then(api => {\n apiRef.current = api\n })\n\n return () => {\n // Cleanup\n if (apiRef.current) {\n apiRef.current.destroy()\n apiRef.current = null\n }\n if (rootRef.current) {\n // Remove canvas from DOM\n const canvas = rootRef.current.querySelector('canvas, svg')\n if (canvas) {\n canvas.remove()\n }\n }\n }\n }, []) // Only run once on mount\n\n // Handle prop changes using the centralized applyProps method\n useEffect(() => {\n if (!apiRef.current) return\n apiRef.current.applyProps({\n ...props,\n options: buildCoreOptions(props.options, pendingMounts.current),\n })\n }, [props.nodes, props.edges, props.history, props.options])\n\n return <div ref={rootRef} style={{ width: '100%', height: '100%' }} />\n}\n","import React, { useEffect, useRef } from 'react'\nimport { Playground as PlaygroundClass, type PlaygroundOptions, type Example } from '@3plate/graph-core'\n\nexport type PlaygroundProps = {\n /** Examples to display */\n examples: PlaygroundOptions['examples']\n /** Default example key */\n defaultExample?: string\n}\n\n/**\n * Playground component - renders the interactive playground with examples\n */\nexport function Playground(props: PlaygroundProps) {\n const rootRef = useRef<HTMLDivElement>(null)\n const playgroundRef = useRef<PlaygroundClass | null>(null)\n const rootIdRef = useRef<string>(`playground-${Math.random().toString(36).slice(2, 11)}`)\n const prevExamplesRef = useRef<Record<string, Example>>({})\n\n useEffect(() => {\n if (!rootRef.current || playgroundRef.current) return\n\n rootRef.current.id = rootIdRef.current\n\n const playground = new PlaygroundClass({\n root: rootIdRef.current,\n examples: props.examples,\n defaultExample: props.defaultExample,\n })\n playgroundRef.current = playground\n prevExamplesRef.current = { ...props.examples }\n playground.init()\n\n return () => {\n // Cleanup if needed\n playgroundRef.current = null\n }\n }, []) // Only initialize once\n\n // Handle examples changes\n useEffect(() => {\n if (!playgroundRef.current) return\n\n const playground = playgroundRef.current\n const prev = prevExamplesRef.current\n const current = props.examples\n\n // Get all keys from both previous and current\n const allKeys = new Set([...Object.keys(prev), ...Object.keys(current)])\n\n for (const key of allKeys) {\n const prevExample = prev[key]\n const currentExample = current[key]\n\n if (!prevExample && currentExample) {\n // Example was added\n playground.addExample(key, currentExample)\n } else if (prevExample && !currentExample) {\n // Example was removed\n playground.removeExample(key)\n } else if (prevExample && currentExample && !shallowEqualExample(prevExample, currentExample)) {\n // Example was modified\n playground.addExample(key, currentExample)\n }\n }\n\n prevExamplesRef.current = { ...current }\n }, [props.examples])\n\n return <div ref={rootRef} style={{ width: '100%', height: '100%' }} />\n}\n\n/**\n * Shallow comparison of two Example objects\n */\nfunction shallowEqualExample(a: Example, b: Example): boolean {\n if (a === b) return true\n if (a.name !== b.name) return false\n if (a.description !== b.description) return false\n if (!shallowEqualArray(a.nodes, b.nodes)) return false\n if (!shallowEqualArray(a.edges, b.edges)) return false\n if (!shallowEqualOptions(a.options, b.options)) return false\n if (!shallowEqualSource(a.source, b.source)) return false\n return true\n}\n\n/**\n * Shallow comparison of arrays\n */\nfunction shallowEqualArray<T>(a: T[] | undefined, b: T[] | undefined): boolean {\n if (a === b) return true\n if (!a || !b) return false\n if (a.length !== b.length) return false\n for (let i = 0; i < a.length; i++) {\n if (a[i] !== b[i]) return false\n }\n return true\n}\n\n/**\n * Shallow comparison of ExampleOptions\n */\nfunction shallowEqualOptions(\n a: Example['options'],\n b: Example['options']\n): boolean {\n if (a === b) return true\n if (!a || !b) return false\n // For now, do a simple reference check on options\n // Can be enhanced if needed\n return a === b\n}\n\n/**\n * Shallow comparison of ExampleSource\n */\nfunction shallowEqualSource(\n a: Example['source'],\n b: Example['source']\n): boolean {\n if (a === b) return true\n if (!a || !b) return false\n if (a.type !== b.type) return false\n if (a.type === 'file' && b.type === 'file') {\n return a.path === b.path\n }\n if (a.type === 'websocket' && b.type === 'websocket') {\n return a.url === b.url\n }\n return false\n}\n"],"mappings":"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","names":["React","useEffect","useRef"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@3plate/graph-react",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "GPL-3.0",
|
|
6
6
|
"repository": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@3plate/graph-core": "0.1.
|
|
32
|
+
"@3plate/graph-core": "0.1.16"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@testing-library/jest-dom": "^6.6.3",
|