@3plate/graph-react 0.1.15 → 0.1.17

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @3plate/graph-react
2
2
 
3
- React components for @3plate/graph visualization.
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 { GraphView, createGraph } from '@3plate/graph-react'
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
- <GraphView
27
- graph={graph}
28
- width={800}
29
- height={600}
30
- onNodeClick={nodeId => console.log('Node clicked:', nodeId)}
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(props);
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
  }
@@ -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, APIOptions } 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 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 as 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 */\nexport type APIOptions<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?: APIOptions<N, E>\n /** Events */\n events?: APIArguments<N, E>['events']\n}\n\n/**\n * Converts APIOptions 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: APIOptions<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 unknown 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 unknown 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';
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';
1
+ import React, { ReactNode } from 'react';
2
+ import { Update, IngestionConfig, APIOptions as APIOptions$1, CanvasOptions, NodeProps, APIArguments, PlaygroundOptions } from '@3plate/graph-core';
3
+ export { API, APIArguments, 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 APIOptions<N, E> = Omit<APIOptions$1<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?: APIArguments<N, E>['options'];
25
+ options?: APIOptions<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
 
@@ -33,4 +47,4 @@ type PlaygroundProps = {
33
47
  */
34
48
  declare function Playground(props: PlaygroundProps): React.JSX.Element;
35
49
 
36
- export { Graph, type GraphProps, Playground, type PlaygroundProps };
50
+ export { type APIOptions, Graph, type GraphProps, Playground, type PlaygroundProps };
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';
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';
1
+ import React, { ReactNode } from 'react';
2
+ import { Update, IngestionConfig, APIOptions as APIOptions$1, CanvasOptions, NodeProps, APIArguments, PlaygroundOptions } from '@3plate/graph-core';
3
+ export { API, APIArguments, 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 APIOptions<N, E> = Omit<APIOptions$1<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?: APIArguments<N, E>['options'];
25
+ options?: APIOptions<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
 
@@ -33,4 +47,4 @@ type PlaygroundProps = {
33
47
  */
34
48
  declare function Playground(props: PlaygroundProps): React.JSX.Element;
35
49
 
36
- export { Graph, type GraphProps, Playground, type PlaygroundProps };
50
+ export { type APIOptions, Graph, type GraphProps, Playground, type PlaygroundProps };
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(props);
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 as 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 */\nexport type APIOptions<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?: APIOptions<N, E>\n /** Events */\n events?: APIArguments<N, E>['events']\n}\n\n/**\n * Converts APIOptions 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: APIOptions<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 unknown 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 unknown 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.15",
3
+ "version": "0.1.17",
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.15"
32
+ "@3plate/graph-core": "0.1.16"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@testing-library/jest-dom": "^6.6.3",