@continuum-dev/react 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +549 -0
- package/index.d.ts +7 -0
- package/index.d.ts.map +1 -0
- package/index.js +6 -0
- package/lib/context.d.ts +23 -0
- package/lib/context.d.ts.map +1 -0
- package/lib/context.js +185 -0
- package/lib/error-boundary.d.ts +16 -0
- package/lib/error-boundary.d.ts.map +1 -0
- package/lib/error-boundary.js +20 -0
- package/lib/fallback.d.ts +3 -0
- package/lib/fallback.d.ts.map +1 -0
- package/lib/fallback.js +13 -0
- package/lib/fallback.spec.d.ts +2 -0
- package/lib/fallback.spec.d.ts.map +1 -0
- package/lib/hooks.d.ts +32 -0
- package/lib/hooks.d.ts.map +1 -0
- package/lib/hooks.js +262 -0
- package/lib/integration.spec.d.ts +2 -0
- package/lib/integration.spec.d.ts.map +1 -0
- package/lib/persistence.d.ts +3 -0
- package/lib/persistence.d.ts.map +1 -0
- package/lib/persistence.spec.d.ts +2 -0
- package/lib/persistence.spec.d.ts.map +1 -0
- package/lib/renderer.d.ts +5 -0
- package/lib/renderer.d.ts.map +1 -0
- package/lib/renderer.js +206 -0
- package/lib/types.d.ts +31 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +1 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bryton Cooper
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
# ♾️ @continuum-dev/react
|
|
2
|
+
|
|
3
|
+
**Build interfaces that can change at runtime without hand-building the continuity layer yourself.**
|
|
4
|
+
|
|
5
|
+
Most React apps are written with one giant assumption: **the UI shape is basically fixed.**
|
|
6
|
+
|
|
7
|
+
That works great right up until your app starts doing things like:
|
|
8
|
+
|
|
9
|
+
- generating UI from AI output
|
|
10
|
+
- assembling screens from schemas
|
|
11
|
+
- rebuilding flows from workflow state
|
|
12
|
+
- streaming new layouts mid-session
|
|
13
|
+
- rendering dynamic forms that need persistence, undo, and conflict handling
|
|
14
|
+
|
|
15
|
+
At that point, the hard part is no longer just rendering data.
|
|
16
|
+
|
|
17
|
+
The hard part becomes:
|
|
18
|
+
|
|
19
|
+
- keeping user input attached to changing UI
|
|
20
|
+
- preserving state across structural changes
|
|
21
|
+
- preventing system updates from clobbering in-progress edits
|
|
22
|
+
- wiring nested collections without inventing a weird local-state framework
|
|
23
|
+
- recovering safely when generated nodes are incomplete, invalid, or unexpected
|
|
24
|
+
|
|
25
|
+
That is the hole Continuum fills.
|
|
26
|
+
|
|
27
|
+
`@continuum-dev/react` is the React layer for **Continuum**: a system for building interfaces that evolve at runtime while still feeling stable, stateful, and production-grade.
|
|
28
|
+
|
|
29
|
+
It is the point where the structural logic of `@continuum-dev/runtime` and the session model of `@continuum-dev/session` turn into a real user experience.
|
|
30
|
+
|
|
31
|
+
You bring your own components.
|
|
32
|
+
Continuum handles the continuity layer underneath.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## The "ohhhh" moment
|
|
37
|
+
|
|
38
|
+
### Without Continuum
|
|
39
|
+
|
|
40
|
+
Your app receives a new UI shape.
|
|
41
|
+
|
|
42
|
+
Now you need custom logic for:
|
|
43
|
+
|
|
44
|
+
- mapping changing nodes back to live state
|
|
45
|
+
- preserving user edits across view updates
|
|
46
|
+
- hydrating and persisting dynamic session state
|
|
47
|
+
- resolving user edits vs system updates
|
|
48
|
+
- handling nested collection item state
|
|
49
|
+
- preventing one broken dynamic node from blanking the whole screen
|
|
50
|
+
|
|
51
|
+
That usually starts as "just a few helpers" and quietly turns into a strange in-house framework.
|
|
52
|
+
|
|
53
|
+
### With Continuum
|
|
54
|
+
|
|
55
|
+
You:
|
|
56
|
+
|
|
57
|
+
- push a new `ViewDefinition`
|
|
58
|
+
- render it through your React component map
|
|
59
|
+
- read and update node state with hooks
|
|
60
|
+
- persist and hydrate the session
|
|
61
|
+
- resolve conflicts and suggestions when needed
|
|
62
|
+
- get collections, diagnostics, fallbacks, and per-node error isolation built in
|
|
63
|
+
|
|
64
|
+
Same React.
|
|
65
|
+
Same design system.
|
|
66
|
+
Way less glue code.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## What Continuum actually is
|
|
71
|
+
|
|
72
|
+
Continuum splits the problem into clear layers:
|
|
73
|
+
|
|
74
|
+
- `@continuum-dev/contract` defines shared types
|
|
75
|
+
- `@continuum-dev/runtime` reconciles evolving views and state
|
|
76
|
+
- `@continuum-dev/session` owns live state, history, persistence, and proposals
|
|
77
|
+
- `@continuum-dev/react` renders all of that into React
|
|
78
|
+
|
|
79
|
+
This package is the React binding.
|
|
80
|
+
|
|
81
|
+
It does **not** replace your design system.
|
|
82
|
+
It does **not** force a visual style.
|
|
83
|
+
It does **not** own your app shell.
|
|
84
|
+
|
|
85
|
+
It gives you a better runtime model for interfaces that are not fully static.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Why this feels different
|
|
90
|
+
|
|
91
|
+
### It renders change, not just data
|
|
92
|
+
|
|
93
|
+
Most libraries help you render a structure once.
|
|
94
|
+
|
|
95
|
+
Continuum is built for cases where the structure itself evolves during the session. The job is not just to render the latest tree. The job is to keep the interface usable while that tree changes.
|
|
96
|
+
|
|
97
|
+
### It is built to stay fast as views grow
|
|
98
|
+
|
|
99
|
+
`@continuum-dev/react` uses an external-store fan-out model powered by `useSyncExternalStore`.
|
|
100
|
+
|
|
101
|
+
In practice, that means updates stay focused: components subscribe to specific session state instead of forcing broad rerenders across the whole dynamic view.
|
|
102
|
+
|
|
103
|
+
### It fails safer
|
|
104
|
+
|
|
105
|
+
Every rendered node is wrapped in `NodeErrorBoundary`.
|
|
106
|
+
|
|
107
|
+
If one dynamic component blows up because a generated node is malformed or unexpected, the rest of the screen can keep working.
|
|
108
|
+
|
|
109
|
+
### It stays headless
|
|
110
|
+
|
|
111
|
+
Continuum provides structure, reconciliation, and session wiring.
|
|
112
|
+
You keep full control over components, branding, styling, and UX.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Install
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
npm install @continuum-dev/react @continuum-dev/session @continuum-dev/contract
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Peer dependency: `react >= 18`.
|
|
123
|
+
|
|
124
|
+
## 60-second example
|
|
125
|
+
|
|
126
|
+
This example maps your design system components to dynamic node types and renders a live Continuum view.
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
import { useEffect } from 'react';
|
|
130
|
+
import type { NodeValue, ViewDefinition } from '@continuum-dev/contract';
|
|
131
|
+
import {
|
|
132
|
+
ContinuumProvider,
|
|
133
|
+
ContinuumRenderer,
|
|
134
|
+
useContinuumSession,
|
|
135
|
+
useContinuumSnapshot,
|
|
136
|
+
type ContinuumNodeMap,
|
|
137
|
+
type ContinuumNodeProps,
|
|
138
|
+
} from '@continuum-dev/react';
|
|
139
|
+
|
|
140
|
+
const components: ContinuumNodeMap = {
|
|
141
|
+
field: ({ value, onChange, definition }: ContinuumNodeProps) => (
|
|
142
|
+
<label style={{ display: 'grid', gap: 6 }}>
|
|
143
|
+
<span>{definition.label ?? definition.key ?? definition.id}</span>
|
|
144
|
+
<input
|
|
145
|
+
value={typeof value?.value === 'string' ? value.value : ''}
|
|
146
|
+
onChange={(e) =>
|
|
147
|
+
onChange({
|
|
148
|
+
value: e.target.value,
|
|
149
|
+
isDirty: true,
|
|
150
|
+
} as NodeValue)
|
|
151
|
+
}
|
|
152
|
+
/>
|
|
153
|
+
</label>
|
|
154
|
+
),
|
|
155
|
+
group: ({ children }: ContinuumNodeProps) => (
|
|
156
|
+
<section style={{ display: 'grid', gap: 12 }}>{children}</section>
|
|
157
|
+
),
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const initialView: ViewDefinition = {
|
|
161
|
+
viewId: 'demo',
|
|
162
|
+
version: '1',
|
|
163
|
+
nodes: [
|
|
164
|
+
{
|
|
165
|
+
id: 'profile',
|
|
166
|
+
type: 'group',
|
|
167
|
+
children: [
|
|
168
|
+
{
|
|
169
|
+
id: 'email',
|
|
170
|
+
type: 'field',
|
|
171
|
+
dataType: 'string',
|
|
172
|
+
key: 'user.email',
|
|
173
|
+
label: 'Email',
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
function Screen() {
|
|
181
|
+
const session = useContinuumSession();
|
|
182
|
+
const snapshot = useContinuumSnapshot();
|
|
183
|
+
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
if (!session.getSnapshot()) {
|
|
186
|
+
session.pushView(initialView);
|
|
187
|
+
}
|
|
188
|
+
}, [session]);
|
|
189
|
+
|
|
190
|
+
if (!snapshot?.view) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return <ContinuumRenderer view={snapshot.view} />;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function App() {
|
|
198
|
+
return (
|
|
199
|
+
<ContinuumProvider components={components} persist="localStorage">
|
|
200
|
+
<main>
|
|
201
|
+
<h1>Profile</h1>
|
|
202
|
+
<Screen />
|
|
203
|
+
</main>
|
|
204
|
+
</ContinuumProvider>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Mental model
|
|
212
|
+
|
|
213
|
+
There are three big ideas behind this package.
|
|
214
|
+
|
|
215
|
+
1. You render views, not hardcoded screens.
|
|
216
|
+
The renderer walks a `ViewDefinition` tree and resolves each node through your component map.
|
|
217
|
+
|
|
218
|
+
2. State lives in the session.
|
|
219
|
+
Your components read current values from the active Continuum session and write updates back into it.
|
|
220
|
+
|
|
221
|
+
3. Dynamic interfaces need production behavior.
|
|
222
|
+
Persistence, diagnostics, conflict handling, collection state, fallbacks, and recovery are core parts of the model.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Core API
|
|
227
|
+
|
|
228
|
+
### `ContinuumProvider`
|
|
229
|
+
|
|
230
|
+
`ContinuumProvider` creates and owns a Continuum session for a React subtree. It can:
|
|
231
|
+
|
|
232
|
+
- create a fresh session
|
|
233
|
+
- hydrate from storage
|
|
234
|
+
- expose session and internal store through context
|
|
235
|
+
- persist updates back to storage
|
|
236
|
+
- stay stable through React Strict Mode replay behavior
|
|
237
|
+
|
|
238
|
+
```tsx
|
|
239
|
+
<ContinuumProvider components={components} persist="localStorage">
|
|
240
|
+
<App />
|
|
241
|
+
</ContinuumProvider>
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
#### Props
|
|
245
|
+
|
|
246
|
+
| Prop | Type | Description |
|
|
247
|
+
| --- | --- | --- |
|
|
248
|
+
| `components` | `ContinuumNodeMap` | Required map of node type to React component. |
|
|
249
|
+
| `persist` | `'localStorage' \| 'sessionStorage' \| false` | Optional browser storage strategy. |
|
|
250
|
+
| `storageKey` | `string` | Optional storage key. Default: `continuum_session`. |
|
|
251
|
+
| `maxPersistBytes` | `number` | Optional max serialized payload size before persistence is skipped. |
|
|
252
|
+
| `onPersistError` | `(error: ContinuumPersistError) => void` | Optional callback for `size_limit` and `storage_error`. |
|
|
253
|
+
| `sessionOptions` | `SessionOptions` | Optional session configuration passed to hydration/creation. |
|
|
254
|
+
| `children` | `React.ReactNode` | React subtree rendered inside the provider. |
|
|
255
|
+
|
|
256
|
+
### `ContinuumRenderer`
|
|
257
|
+
|
|
258
|
+
Renders a `ViewDefinition` tree through your component map.
|
|
259
|
+
|
|
260
|
+
```tsx
|
|
261
|
+
<ContinuumRenderer view={snapshot.view} />
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
#### What it does
|
|
265
|
+
|
|
266
|
+
- resolves node components by `definition.type`
|
|
267
|
+
- falls back to `components.default` if provided
|
|
268
|
+
- otherwise uses built-in `FallbackComponent`
|
|
269
|
+
- wraps each node in `NodeErrorBoundary`
|
|
270
|
+
- supports hidden nodes
|
|
271
|
+
- supports nested container nodes
|
|
272
|
+
- supports built-in collection behavior
|
|
273
|
+
- passes canonical scoped `nodeId` values to your components
|
|
274
|
+
|
|
275
|
+
### Hooks
|
|
276
|
+
|
|
277
|
+
#### `useContinuumState(nodeId)`
|
|
278
|
+
|
|
279
|
+
Primary hook for data-bearing components.
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
const [value, setValue] = useContinuumState('user_email');
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
#### `useContinuumConflict(nodeId)`
|
|
286
|
+
|
|
287
|
+
Use this when system proposals should not overwrite in-progress user edits automatically.
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
const { hasConflict, proposal, accept, reject } = useContinuumConflict('user_email');
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
#### `useContinuumDiagnostics()`
|
|
294
|
+
|
|
295
|
+
Returns timeline and reconciliation metadata:
|
|
296
|
+
|
|
297
|
+
- `issues`
|
|
298
|
+
- `diffs`
|
|
299
|
+
- `resolutions`
|
|
300
|
+
- `checkpoints`
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
const { issues, checkpoints } = useContinuumDiagnostics();
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
#### `useContinuumViewport(nodeId)`
|
|
307
|
+
|
|
308
|
+
Tracks non-data state (focus, expansion, scroll, zoom, offsets) inside the session model.
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
const [viewport, setViewport] = useContinuumViewport('table');
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
#### `useContinuumSession()`
|
|
315
|
+
|
|
316
|
+
Returns the active session for full session API access.
|
|
317
|
+
|
|
318
|
+
```ts
|
|
319
|
+
const session = useContinuumSession();
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
#### `useContinuumSnapshot()`
|
|
323
|
+
|
|
324
|
+
Subscribes to the full current `ContinuitySnapshot`.
|
|
325
|
+
|
|
326
|
+
```ts
|
|
327
|
+
const snapshot = useContinuumSnapshot();
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
#### `useContinuumHydrated()`
|
|
331
|
+
|
|
332
|
+
Indicates whether provider initialization came from persisted storage.
|
|
333
|
+
|
|
334
|
+
```ts
|
|
335
|
+
const hydrated = useContinuumHydrated();
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
#### `useContinuumSuggestions()`
|
|
339
|
+
|
|
340
|
+
Scans current snapshot values for suggestions and provides accept-all / reject-all actions.
|
|
341
|
+
|
|
342
|
+
```ts
|
|
343
|
+
const { hasSuggestions, acceptAll, rejectAll } = useContinuumSuggestions();
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
---
|
|
347
|
+
|
|
348
|
+
## The node contract
|
|
349
|
+
|
|
350
|
+
Each component in your `components` map receives this prop shape:
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
import type { NodeValue, ViewNode } from '@continuum-dev/contract';
|
|
354
|
+
|
|
355
|
+
interface ContinuumNodeProps<T = NodeValue> {
|
|
356
|
+
value: T | undefined;
|
|
357
|
+
onChange: (value: T) => void;
|
|
358
|
+
definition: ViewNode;
|
|
359
|
+
nodeId?: string;
|
|
360
|
+
children?: React.ReactNode;
|
|
361
|
+
[prop: string]: unknown;
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
`nodeId` is the canonical scoped id used by the renderer.
|
|
366
|
+
For nested nodes, it can look like `group/field`.
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## Example: conflict UI
|
|
371
|
+
|
|
372
|
+
```tsx
|
|
373
|
+
import { useContinuumConflict } from '@continuum-dev/react';
|
|
374
|
+
|
|
375
|
+
function EmailConflict({ nodeId }: { nodeId: string }) {
|
|
376
|
+
const { hasConflict, proposal, accept, reject } = useContinuumConflict(nodeId);
|
|
377
|
+
|
|
378
|
+
if (!hasConflict) {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return (
|
|
383
|
+
<div>
|
|
384
|
+
<div>Suggested value: {String(proposal?.value ?? '')}</div>
|
|
385
|
+
<button onClick={accept}>Accept</button>
|
|
386
|
+
<button onClick={reject}>Reject</button>
|
|
387
|
+
</div>
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
## Example: suggestion banner
|
|
393
|
+
|
|
394
|
+
```tsx
|
|
395
|
+
import { useContinuumSuggestions } from '@continuum-dev/react';
|
|
396
|
+
|
|
397
|
+
function SuggestionBanner() {
|
|
398
|
+
const { hasSuggestions, acceptAll, rejectAll } = useContinuumSuggestions();
|
|
399
|
+
|
|
400
|
+
if (!hasSuggestions) {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return (
|
|
405
|
+
<div>
|
|
406
|
+
<span>Suggested updates are available.</span>
|
|
407
|
+
<button onClick={acceptAll}>Accept all</button>
|
|
408
|
+
<button onClick={rejectAll}>Reject all</button>
|
|
409
|
+
</div>
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
## Example: undo with checkpoints
|
|
415
|
+
|
|
416
|
+
```tsx
|
|
417
|
+
import { useContinuumDiagnostics, useContinuumSession } from '@continuum-dev/react';
|
|
418
|
+
|
|
419
|
+
function UndoButton() {
|
|
420
|
+
const session = useContinuumSession();
|
|
421
|
+
const { checkpoints } = useContinuumDiagnostics();
|
|
422
|
+
|
|
423
|
+
const undo = () => {
|
|
424
|
+
const previous = checkpoints[checkpoints.length - 2];
|
|
425
|
+
if (previous) {
|
|
426
|
+
session.rewind(previous.checkpointId);
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
return <button onClick={undo}>Undo last change</button>;
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
---
|
|
435
|
+
|
|
436
|
+
## Collections
|
|
437
|
+
|
|
438
|
+
`@continuum-dev/react` includes built-in collection node support:
|
|
439
|
+
|
|
440
|
+
- initial item creation from `minItems`
|
|
441
|
+
- add behavior constrained by `maxItems`
|
|
442
|
+
- remove behavior constrained by `minItems`
|
|
443
|
+
- scoped item state storage
|
|
444
|
+
- default template values
|
|
445
|
+
- canonical nested ids for collection children
|
|
446
|
+
|
|
447
|
+
Rendered collection controls include attributes like:
|
|
448
|
+
|
|
449
|
+
- `data-continuum-collection-add`
|
|
450
|
+
- `data-continuum-collection-remove`
|
|
451
|
+
- `data-continuum-collection-item`
|
|
452
|
+
|
|
453
|
+
You get practical collection behavior out of the box while still controlling the visual wrapper component.
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
## Fallbacks and failure isolation
|
|
458
|
+
|
|
459
|
+
Dynamic interfaces are messy in real production environments. This package is designed to fail more safely.
|
|
460
|
+
|
|
461
|
+
### Unknown node types
|
|
462
|
+
|
|
463
|
+
Node resolution order:
|
|
464
|
+
|
|
465
|
+
1. `components[definition.type]`
|
|
466
|
+
2. `components.default`
|
|
467
|
+
3. built-in `FallbackComponent`
|
|
468
|
+
|
|
469
|
+
The fallback renders:
|
|
470
|
+
|
|
471
|
+
- unknown node type information
|
|
472
|
+
- editable text input when possible
|
|
473
|
+
- raw node definition for diagnostics
|
|
474
|
+
|
|
475
|
+
### Per-node error boundaries
|
|
476
|
+
|
|
477
|
+
Every rendered node is wrapped in `NodeErrorBoundary`.
|
|
478
|
+
|
|
479
|
+
If one component crashes while rendering a dynamic node, sibling regions can keep working.
|
|
480
|
+
|
|
481
|
+
---
|
|
482
|
+
|
|
483
|
+
## Persistence behavior
|
|
484
|
+
|
|
485
|
+
When `persist` is enabled, provider-level session persistence supports:
|
|
486
|
+
|
|
487
|
+
- hydration on provider creation
|
|
488
|
+
- persistence writes through the session layer
|
|
489
|
+
- optional payload size limits with `maxPersistBytes`
|
|
490
|
+
- optional `onPersistError` callback for:
|
|
491
|
+
- `size_limit`
|
|
492
|
+
- `storage_error`
|
|
493
|
+
|
|
494
|
+
Supported storage targets:
|
|
495
|
+
|
|
496
|
+
- `localStorage`
|
|
497
|
+
- `sessionStorage`
|
|
498
|
+
|
|
499
|
+
Example:
|
|
500
|
+
|
|
501
|
+
```tsx
|
|
502
|
+
<ContinuumProvider
|
|
503
|
+
components={components}
|
|
504
|
+
persist="localStorage"
|
|
505
|
+
maxPersistBytes={100_000}
|
|
506
|
+
onPersistError={(error) => {
|
|
507
|
+
console.error(error);
|
|
508
|
+
}}
|
|
509
|
+
>
|
|
510
|
+
<App />
|
|
511
|
+
</ContinuumProvider>
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
---
|
|
515
|
+
|
|
516
|
+
## When to use this package
|
|
517
|
+
|
|
518
|
+
`@continuum-dev/react` is a strong fit when UI can change during a session and is driven by:
|
|
519
|
+
|
|
520
|
+
- AI output
|
|
521
|
+
- schemas
|
|
522
|
+
- workflows
|
|
523
|
+
- server-driven definitions
|
|
524
|
+
- dynamic internal tools
|
|
525
|
+
- resumable multi-step experiences
|
|
526
|
+
- long-lived interfaces where persistence and history matter
|
|
527
|
+
|
|
528
|
+
If your UI is fully static and your state model is simple, you may not need Continuum.
|
|
529
|
+
|
|
530
|
+
---
|
|
531
|
+
|
|
532
|
+
## Ecosystem
|
|
533
|
+
|
|
534
|
+
Continuum packages:
|
|
535
|
+
|
|
536
|
+
- `@continuum-dev/contract`: shared types and view/data contracts
|
|
537
|
+
- `@continuum-dev/runtime`: stateless reconciliation engine
|
|
538
|
+
- `@continuum-dev/session`: live session, persistence, proposals, checkpoints, history
|
|
539
|
+
- `@continuum-dev/react`: React bindings and renderer
|
|
540
|
+
|
|
541
|
+
---
|
|
542
|
+
|
|
543
|
+
## In one sentence
|
|
544
|
+
|
|
545
|
+
If React is your UI engine, `@continuum-dev/react` is the layer that lets dynamic, evolving interfaces stop feeling fragile.
|
|
546
|
+
|
|
547
|
+
## License
|
|
548
|
+
|
|
549
|
+
MIT
|
package/index.d.ts
ADDED
package/index.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../packages/react/src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,yBAAyB,CAAC;AACxC,cAAc,mBAAmB,CAAC"}
|
package/index.js
ADDED
package/lib/context.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Session } from '@continuum-dev/session';
|
|
2
|
+
import type { ContinuitySnapshot, NodeValue, ViewportState } from '@continuum-dev/contract';
|
|
3
|
+
import type { ContinuumNodeMap, ContinuumProviderProps } from './types.js';
|
|
4
|
+
type Listener = () => void;
|
|
5
|
+
export interface ContinuumStore {
|
|
6
|
+
getSnapshot(): ContinuitySnapshot | null;
|
|
7
|
+
subscribeSnapshot(listener: Listener): () => void;
|
|
8
|
+
subscribeDiagnostics(listener: Listener): () => void;
|
|
9
|
+
getNodeValue(nodeId: string): NodeValue | undefined;
|
|
10
|
+
getNodeViewport(nodeId: string): ViewportState | undefined;
|
|
11
|
+
subscribeNode(nodeId: string, listener: Listener): () => void;
|
|
12
|
+
destroy(): void;
|
|
13
|
+
}
|
|
14
|
+
export interface ContinuumContextValue {
|
|
15
|
+
session: Session;
|
|
16
|
+
store: ContinuumStore;
|
|
17
|
+
componentMap: ContinuumNodeMap;
|
|
18
|
+
wasHydrated: boolean;
|
|
19
|
+
}
|
|
20
|
+
export declare const ContinuumContext: import("react").Context<ContinuumContextValue | null>;
|
|
21
|
+
export declare function ContinuumProvider({ components, persist, storageKey, maxPersistBytes, onPersistError, sessionOptions, children, }: ContinuumProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
22
|
+
export {};
|
|
23
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../../../packages/react/src/lib/context.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,kBAAkB,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACxF,OAAO,KAAK,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAE3E,KAAK,QAAQ,GAAG,MAAM,IAAI,CAAC;AA+C3B,MAAM,WAAW,cAAc;IAC7B,WAAW,IAAI,kBAAkB,GAAG,IAAI,CAAC;IACzC,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,IAAI,CAAC;IAClD,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,IAAI,CAAC;IACrD,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAAC;IACpD,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAAC;IAC3D,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,MAAM,IAAI,CAAC;IAC9D,OAAO,IAAI,IAAI,CAAC;CACjB;AAoFD,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,cAAc,CAAC;IACtB,YAAY,EAAE,gBAAgB,CAAC;IAC/B,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,eAAO,MAAM,gBAAgB,uDAAoD,CAAC;AAuClF,wBAAgB,iBAAiB,CAAC,EAChC,UAAU,EACV,OAAe,EACf,UAAgC,EAChC,eAAe,EACf,cAAc,EACd,cAAc,EACd,QAAQ,GACT,EAAE,sBAAsB,2CAsDxB"}
|