@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 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
@@ -0,0 +1,7 @@
1
+ export * from './lib/types.js';
2
+ export * from './lib/context.js';
3
+ export * from './lib/hooks.js';
4
+ export * from './lib/renderer.js';
5
+ export * from './lib/error-boundary.js';
6
+ export * from './lib/fallback.js';
7
+ //# sourceMappingURL=index.d.ts.map
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
@@ -0,0 +1,6 @@
1
+ export * from './lib/types.js';
2
+ export * from './lib/context.js';
3
+ export * from './lib/hooks.js';
4
+ export * from './lib/renderer.js';
5
+ export * from './lib/error-boundary.js';
6
+ export * from './lib/fallback.js';
@@ -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"}