@simplex-viz/react 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +98 -64
  2. package/package.json +3 -3
package/README.md CHANGED
@@ -1,3 +1,7 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/ghackenberg/simplex-viz/main/docs/assets/logo-react.png" width="640" alt="SimplexViz / react — @simplex-viz/react" />
3
+ </p>
4
+
1
5
  # @simplex-viz/react
2
6
 
3
7
  End-to-end type-safe React bindings, declarative JSX node renderers, and imperative controller hooks for `@simplex-viz/core` and `@simplex-viz/svg`.
@@ -6,7 +10,15 @@ End-to-end type-safe React bindings, declarative JSX node renderers, and imperat
6
10
  - **Source & Monorepo:** <https://github.com/ghackenberg/simplex-viz/tree/main/packages/react>
7
11
  - **Companion Packages:** [`@simplex-viz/core`](https://www.npmjs.com/package/@simplex-viz/core) · [`@simplex-viz/svg`](https://www.npmjs.com/package/@simplex-viz/svg)
8
12
 
9
- ---
13
+ ## Declarative JSX Slot Architecture
14
+
15
+ In React, `<SimplexTopicGraph />` lets you drop custom JSX directly into cluster and content slots instead of hand-writing imperative SVG DOM code. The wireframe preview below illustrates how those two component slots populate a single visualization:
16
+
17
+ <p align="center">
18
+ <img src="https://raw.githubusercontent.com/ghackenberg/simplex-viz/main/docs/assets/preview-react.png" width="100%" alt="@simplex-viz/react preview showing minimalist renderClusterNode and renderContentNode JSX slot placeholders with automatic getBBox() collision sizing" />
19
+ </p>
20
+
21
+ *Figure: Architectural slot layout rendered with `@simplex-viz/react`. Dashed topic cards illustrate the `renderClusterNode` JSX slot positioned on a subtle 60° simplex mesh, while minimalist skeleton content cards represent `renderContentNode` slots complete with color accents, type/time metadata, and interactive selection highlights (amber for pinned, cyan for hovered). Every JSX element is measured automatically via offscreen `getBBox()` so custom React SVG cards remain strictly overlap-free.*
10
22
 
11
23
  ## Features
12
24
 
@@ -17,28 +29,21 @@ End-to-end type-safe React bindings, declarative JSX node renderers, and imperat
17
29
  - **Full generic type inference**: Infers `<TItem, TContentVisual, TClusterVisual, TTagMeta, TClusterMeta>` end-to-end across `items`, `getId`, `getTags`, `deriveClusterMeta`, `visualResolver`, `renderContentNode`, `renderClusterNode`, hover events, and click targets.
18
30
  - **`useSimplexGraphController`**: Imperative hook for live item mutations (`addItems`, `removeItems`, `setItems`), camera controls (`zoomBy`, `resetCamera`), and telemetry getters.
19
31
 
20
- ---
21
-
22
32
  ## Installation
23
33
 
34
+ Install the React wrapper together with its core engine and SVG scene-graph peer packages (`react >= 18.0.0` and `react-dom >= 18.0.0`):
35
+
24
36
  ```bash
25
37
  npm install @simplex-viz/core @simplex-viz/svg @simplex-viz/react
26
38
  ```
27
39
 
28
- Peer dependencies: `react >= 18.0.0` and `react-dom >= 18.0.0`.
40
+ ## Quickstart: Declarative JSX Node Renderers
29
41
 
30
- ---
42
+ ### Step 1 — Define Domain, Visual, and Cluster Metadata Types
31
43
 
32
- ## Quickstart: Declarative JSX Node Renderers
44
+ Describe your domain item (`Article`), visual cache descriptors (`ArticleVisual`, `ClusterVisual`), and aggregate cluster stats (`ClusterStats`). TypeScript preserves these types across every callback prop on `<SimplexTopicGraph />`:
33
45
 
34
46
  ```tsx
35
- import { useMemo, useState } from 'react';
36
- import {
37
- SimplexTopicGraph,
38
- useSimplexGraphController,
39
- } from '@simplex-viz/react';
40
- import type { VisualResolver } from '@simplex-viz/core';
41
-
42
47
  interface Article {
43
48
  readonly slug: string;
44
49
  readonly title: string;
@@ -64,6 +69,77 @@ interface ClusterStats {
64
69
  readonly count: number;
65
70
  readonly avgMinutes: number;
66
71
  }
72
+ ```
73
+
74
+ ### Step 2 — Define Declarative JSX Node Renderers
75
+
76
+ Write standard SVG JSX centered around `(0, 0)` for individual items and topic clusters. `@simplex-viz/react` measures the resulting `<g>` subtree automatically via offscreen `getBBox()` whenever `getVisualCacheKey` changes, guaranteeing collision-free card placement:
77
+
78
+ ```tsx
79
+ import type {
80
+ ClusterDescriptor,
81
+ ContentLayoutContext,
82
+ NodeRenderState,
83
+ } from '@simplex-viz/core';
84
+
85
+ const renderArticleNode = (
86
+ article: Article,
87
+ ctx: ContentLayoutContext<Article, undefined, ClusterStats>,
88
+ state: NodeRenderState<ArticleVisual>,
89
+ ) => (
90
+ <g>
91
+ <rect
92
+ x={-54}
93
+ y={-15}
94
+ width={108}
95
+ height={30}
96
+ rx={6}
97
+ fill="#0f172a"
98
+ stroke={state.interaction.isPinned ? '#f59e0b' : ctx.mixedColorHex}
99
+ strokeWidth={state.interaction.isHovered ? 2 : 1.2}
100
+ />
101
+ <text textAnchor="middle" y={4} fill="#f8fafc" fontSize={9}>
102
+ {article.title.slice(0, 16)}
103
+ </text>
104
+ </g>
105
+ );
106
+
107
+ const renderTopicCluster = (
108
+ cluster: ClusterDescriptor<Article, undefined, ClusterStats>,
109
+ state: NodeRenderState<ClusterVisual>,
110
+ ) => (
111
+ <g>
112
+ <rect
113
+ x={-68}
114
+ y={-20}
115
+ width={136}
116
+ height={40}
117
+ rx={8}
118
+ fill="#090d16"
119
+ stroke={state.interaction.isHovered ? '#f8fafc' : cluster.colorHex}
120
+ strokeWidth={1.6}
121
+ />
122
+ <text textAnchor="middle" y={-2} fill="#f8fafc" fontSize={10} fontWeight={600}>
123
+ {`${cluster.code} · ${cluster.defaultLabel}`}
124
+ </text>
125
+ <text textAnchor="middle" y={12} fill="#94a3b8" fontSize={8.5}>
126
+ {`${cluster.meta.count} items · avg ${cluster.meta.avgMinutes.toFixed(0)}m`}
127
+ </text>
128
+ </g>
129
+ );
130
+ ```
131
+
132
+ ### Step 3 — Initialize State, Controller Hook & Memoized `VisualResolver`
133
+
134
+ Inside your React component, set up selection state, create a typed controller handle with `useSimplexGraphController`, and memoize `visualResolver` with `useMemo` so re-renders never trigger unintended crossfade transitions:
135
+
136
+ ```tsx
137
+ import { useMemo, useState } from 'react';
138
+ import {
139
+ SimplexTopicGraph,
140
+ useSimplexGraphController,
141
+ } from '@simplex-viz/react';
142
+ import type { VisualResolver } from '@simplex-viz/core';
67
143
 
68
144
  export function KnowledgeAtlas({ articles }: { articles: readonly Article[] }) {
69
145
  const [focusedClusterUid, setFocusedClusterUid] = useState<string | null>(null);
@@ -97,7 +173,13 @@ export function KnowledgeAtlas({ articles }: { articles: readonly Article[] }) {
97
173
  }),
98
174
  [],
99
175
  );
176
+ ```
177
+
178
+ ### Step 4 — Render `<SimplexTopicGraph />` with Camera Toolbar & Click Handling
179
+
180
+ Finally, wire the camera toolbar (`handle.zoomBy`, `handle.resetCamera`), pass the JSX renderers from Step 2, and handle left-clicks to toggle pinned items or isolate clicked topic clusters:
100
181
 
182
+ ```tsx
101
183
  return (
102
184
  <div style={{ width: '100%', height: '640px', position: 'relative' }}>
103
185
  <div style={{ position: 'absolute', top: 12, left: 12, zIndex: 10 }}>
@@ -120,48 +202,8 @@ export function KnowledgeAtlas({ articles }: { articles: readonly Article[] }) {
120
202
  visualResolver={visualResolver}
121
203
  pinnedIds={pinnedIds}
122
204
  focusedClusterUid={focusedClusterUid}
123
- renderContentNode={(article, ctx, state) => (
124
- <g>
125
- <rect
126
- x={-54}
127
- y={-15}
128
- width={108}
129
- height={30}
130
- rx={6}
131
- fill="#0f172a"
132
- stroke={state.interaction.isPinned ? '#f59e0b' : ctx.mixedColorHex}
133
- strokeWidth={state.interaction.isHovered ? 2 : 1.2}
134
- />
135
- <text
136
- textAnchor="middle"
137
- y={4}
138
- fill="#f8fafc"
139
- fontSize={9}
140
- >
141
- {article.title.slice(0, 16)}
142
- </text>
143
- </g>
144
- )}
145
- renderClusterNode={(cluster, state) => (
146
- <g>
147
- <rect
148
- x={-68}
149
- y={-20}
150
- width={136}
151
- height={40}
152
- rx={8}
153
- fill="#090d16"
154
- stroke={state.interaction.isHovered ? '#f8fafc' : cluster.colorHex}
155
- strokeWidth={1.6}
156
- />
157
- <text textAnchor="middle" y={-2} fill="#f8fafc" fontSize={10} fontWeight={600}>
158
- {`${cluster.code} · ${cluster.defaultLabel}`}
159
- </text>
160
- <text textAnchor="middle" y={12} fill="#94a3b8" fontSize={8.5}>
161
- {`${cluster.meta.count} items · avg ${cluster.meta.avgMinutes.toFixed(0)}m`}
162
- </text>
163
- </g>
164
- )}
205
+ renderContentNode={renderArticleNode}
206
+ renderClusterNode={renderTopicCluster}
165
207
  onLeftClick={(target) => {
166
208
  if (target.kind === 'content') {
167
209
  setPinnedIds((prev) => {
@@ -184,11 +226,9 @@ export function KnowledgeAtlas({ articles }: { articles: readonly Article[] }) {
184
226
  }
185
227
  ```
186
228
 
187
- ---
188
-
189
229
  ## Using Portable `SvgNodeView` Plugins (60 fps Without JSX Re-Renders)
190
230
 
191
- For larger graphs or built-in shape morphing, pass `contentView` and `clusterView` from `@simplex-viz/svg` instead of `renderContentNode` / `renderClusterNode`:
231
+ For larger graphs or built-in shape morphing without per-frame JSX overhead, instantiate `GeometricShapeSvgView` and `ClusterAnchorSvgView` once outside the component and pass them via `contentView` and `clusterView` instead of `renderContentNode` / `renderClusterNode`:
192
232
 
193
233
  ```tsx
194
234
  import { SimplexTopicGraph } from '@simplex-viz/react';
@@ -216,8 +256,6 @@ export function FastGraph({ items, visualResolver }) {
216
256
 
217
257
  > **Tip:** Memoize `visualResolver` (e.g., via `useMemo`) and keep `contentView` / `clusterView` instances stable outside render to avoid unnecessary crossfades. Changing `visualResolver` or switching view props intentionally triggers a smooth crossfade/morph transition.
218
258
 
219
- ---
220
-
221
259
  ## `<SimplexTopicGraph />` Props Reference
222
260
 
223
261
  ### Data, Typing & Metadata
@@ -264,8 +302,6 @@ export function FastGraph({ items, visualResolver }) {
264
302
  | `onFpsUpdate` | `(fps: number) => void` |
265
303
  | `controllerRef` | Ref populated with the underlying `SimplexSvgController` instance. |
266
304
 
267
- ---
268
-
269
305
  ## `useSimplexGraphController()` Hook
270
306
 
271
307
  Returns `{ controllerRef, handle }`:
@@ -279,8 +315,6 @@ Returns `{ controllerRef, handle }`:
279
315
  - `handle.getClusters()` — Returns current `ClusterDescriptor[]`.
280
316
  - `handle.getRawController()` — Returns the underlying `SimplexSvgController | null`.
281
317
 
282
- ---
283
-
284
318
  ## License
285
319
 
286
320
  MIT — see [LICENSE](https://github.com/ghackenberg/simplex-viz/blob/main/LICENSE).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simplex-viz/react",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "End-to-end type-safe React bindings, declarative JSX node renderers, and hooks for @simplex-viz/core and @simplex-viz/svg.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -20,8 +20,8 @@
20
20
  "LICENSE"
21
21
  ],
22
22
  "dependencies": {
23
- "@simplex-viz/core": "^0.1.2",
24
- "@simplex-viz/svg": "^0.1.2"
23
+ "@simplex-viz/core": "^0.1.3",
24
+ "@simplex-viz/svg": "^0.1.3"
25
25
  },
26
26
  "peerDependencies": {
27
27
  "react": ">=18.0.0",