@continuum-dev/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.
package/README.md CHANGED
@@ -311,6 +311,8 @@ Tracks non-data state (focus, expansion, scroll, zoom, offsets) inside the sessi
311
311
  const [viewport, setViewport] = useContinuumViewport('table');
312
312
  ```
313
313
 
314
+ If this hook is called from inside a collection-item scope, Continuum logs a development warning because viewport state is not currently scoped per collection item.
315
+
314
316
  #### `useContinuumSession()`
315
317
 
316
318
  Returns the active session for full session API access.
@@ -327,6 +329,8 @@ Subscribes to the full current `ContinuitySnapshot`.
327
329
  const snapshot = useContinuumSnapshot();
328
330
  ```
329
331
 
332
+ Snapshots are delivered as immutable top-level copies so consumer code cannot accidentally mutate session internals.
333
+
330
334
  #### `useContinuumHydrated()`
331
335
 
332
336
  Indicates whether provider initialization came from persisted storage.
@@ -351,6 +355,8 @@ Handles action dispatch with built-in loading and result state.
351
355
  const { dispatch, isDispatching, lastResult } = useContinuumAction('submit_form');
352
356
  ```
353
357
 
358
+ When multiple dispatches overlap, `isDispatching` and `lastResult` reflect the latest in-flight dispatch.
359
+
354
360
  Example action component:
355
361
 
356
362
  ```tsx
@@ -471,14 +477,15 @@ function UndoButton() {
471
477
  - scoped item state storage
472
478
  - default template values
473
479
  - canonical nested ids for collection children
480
+ - headless control wiring through your own collection components
474
481
 
475
- Rendered collection controls include attributes like:
482
+ Collection controls are now passed as props to your mapped components:
476
483
 
477
- - `data-continuum-collection-add`
478
- - `data-continuum-collection-remove`
479
- - `data-continuum-collection-item`
484
+ - collection root components receive `onAdd`, `canAdd`, `onRemove`, and `canRemove`
485
+ - template root components receive `itemIndex`, `onRemove`, and `canRemove`
486
+ - no renderer-owned wrapper elements or `data-continuum-*` control attributes are injected
480
487
 
481
- You get practical collection behavior out of the box while still controlling the visual wrapper component.
488
+ This keeps collection behavior built in while letting your design system fully own the markup and styles.
482
489
 
483
490
  ---
484
491
 
@@ -505,6 +512,7 @@ The fallback renders:
505
512
  Every rendered node is wrapped in `NodeErrorBoundary`.
506
513
 
507
514
  If one component crashes while rendering a dynamic node, sibling regions can keep working.
515
+ When a later rerender provides recoverable children, the boundary resets and the node can render again.
508
516
 
509
517
  ---
510
518
 
@@ -13,6 +13,7 @@ interface NodeErrorBoundaryState {
13
13
  export declare class NodeErrorBoundary extends Component<NodeErrorBoundaryProps, NodeErrorBoundaryState> {
14
14
  state: NodeErrorBoundaryState;
15
15
  static getDerivedStateFromError(error: unknown): NodeErrorBoundaryState;
16
+ componentDidUpdate(prevProps: NodeErrorBoundaryProps): void;
16
17
  render(): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | import("react/jsx-runtime").JSX.Element | null | undefined;
17
18
  }
18
19
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"error-boundary.d.ts","sourceRoot":"","sources":["../../../../packages/react/src/lib/error-boundary.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAElD,UAAU,sBAAsB;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED,UAAU,sBAAsB;IAC9B,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,qBAAa,iBAAkB,SAAQ,SAAS,CAC9C,sBAAsB,EACtB,sBAAsB,CACvB;IACU,KAAK,EAAE,sBAAsB,CAGpC;IAEF,MAAM,CAAC,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,sBAAsB;IAO9D,MAAM;CAUhB"}
1
+ {"version":3,"file":"error-boundary.d.ts","sourceRoot":"","sources":["../../../../packages/react/src/lib/error-boundary.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAElD,UAAU,sBAAsB;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED,UAAU,sBAAsB;IAC9B,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,qBAAa,iBAAkB,SAAQ,SAAS,CAC9C,sBAAsB,EACtB,sBAAsB,CACvB;IACU,KAAK,EAAE,sBAAsB,CAGpC;IAEF,MAAM,CAAC,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,sBAAsB;IAO9D,kBAAkB,CAAC,SAAS,EAAE,sBAAsB;IASpD,MAAM;CAUhB"}
@@ -14,6 +14,14 @@ export class NodeErrorBoundary extends Component {
14
14
  message: error instanceof Error ? error.message : String(error),
15
15
  };
16
16
  }
17
+ componentDidUpdate(prevProps) {
18
+ if (this.state.hasError && prevProps.children !== this.props.children) {
19
+ this.setState({
20
+ hasError: false,
21
+ message: '',
22
+ });
23
+ }
24
+ }
17
25
  render() {
18
26
  if (this.state.hasError) {
19
27
  return (_jsxs("div", { "data-continuum-render-error": this.props.nodeId, children: ["Node render failed: ", this.props.nodeId, " (", this.state.message, ")"] }));
@@ -1 +1 @@
1
- {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../../../packages/react/src/lib/hooks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,kBAAkB,EAAE,SAAS,EAAE,aAAa,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAwDrH,UAAU,cAAc;IACtB,aAAa,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,KAAK,MAAM,IAAI,CAAC;IACpE,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,SAAS,GAAG,SAAS,CAAC;IACxD,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;CAC1D;AAED;;;GAGG;AACH,eAAO,MAAM,qBAAqB,gDAA6C,CAAC;AAEhF;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,OAAO,CAQ7C;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,GACb,CAAC,SAAS,GAAG,SAAS,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC,CAiDrD;AAED;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,kBAAkB,GAAG,IAAI,CAiDhE;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,GACb,CAAC,aAAa,GAAG,SAAS,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC,CAkC7D;AAED;;GAEG;AACH,wBAAgB,uBAAuB;YAS3B,UAAU,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;WACjC,UAAU,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;iBACzB,UAAU,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC;iBACrC,UAAU,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC;EAmCrD;AAED;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,OAAO,CAQ9C;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG;IACpD,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,aAAa,GAAG,IAAI,CAAC;IAC/B,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB,CAsCA;AAED;;GAEG;AACH,wBAAgB,uBAAuB,IAAI;IACzC,cAAc,EAAE,OAAO,CAAC;IACxB,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,SAAS,EAAE,MAAM,IAAI,CAAC;CACvB,CAgEA;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG;IACpD,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;IACpD,aAAa,EAAE,OAAO,CAAC;IACvB,UAAU,EAAE,YAAY,GAAG,IAAI,CAAC;CACjC,CA0BA"}
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../../../packages/react/src/lib/hooks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,kBAAkB,EAAE,SAAS,EAAE,aAAa,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAwDrH,UAAU,cAAc;IACtB,aAAa,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,KAAK,MAAM,IAAI,CAAC;IACpE,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,SAAS,GAAG,SAAS,CAAC;IACxD,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;CAC1D;AAED;;;GAGG;AACH,eAAO,MAAM,qBAAqB,gDAA6C,CAAC;AAEhF;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,OAAO,CAQ7C;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,GACb,CAAC,SAAS,GAAG,SAAS,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC,CAiDrD;AAED;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,kBAAkB,GAAG,IAAI,CAiDhE;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,GACb,CAAC,aAAa,GAAG,SAAS,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC,CA4C7D;AAED;;GAEG;AACH,wBAAgB,uBAAuB;YAS3B,UAAU,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;WACjC,UAAU,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;iBACzB,UAAU,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC;iBACrC,UAAU,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC;EAmCrD;AAED;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,OAAO,CAQ9C;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG;IACpD,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,aAAa,GAAG,IAAI,CAAC;IAC/B,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB,CAsCA;AAED;;GAEG;AACH,wBAAgB,uBAAuB,IAAI;IACzC,cAAc,EAAE,OAAO,CAAC;IACxB,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,SAAS,EAAE,MAAM,IAAI,CAAC;CACvB,CAgEA;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG;IACpD,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;IACpD,aAAa,EAAE,OAAO,CAAC;IACvB,UAAU,EAAE,YAAY,GAAG,IAAI,CAAC;CACjC,CAgCA"}
package/lib/hooks.js CHANGED
@@ -82,7 +82,7 @@ export function useContinuumState(nodeId) {
82
82
  }
83
83
  valueCacheRef.current = nextValue;
84
84
  return nextValue;
85
- }, [store, nodeId]);
85
+ }, [scope, store, nodeId]);
86
86
  const value = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
87
87
  const setValue = useCallback((next) => {
88
88
  if (scope) {
@@ -136,9 +136,15 @@ export function useContinuumSnapshot() {
136
136
  */
137
137
  export function useContinuumViewport(nodeId) {
138
138
  const ctx = useContext(ContinuumContext);
139
+ const scope = useContext(NodeStateScopeContext);
139
140
  if (!ctx) {
140
141
  throw new Error('useContinuumViewport must be used within a <ContinuumProvider>');
141
142
  }
143
+ if (scope
144
+ && typeof process !== 'undefined'
145
+ && process.env.NODE_ENV !== 'production') {
146
+ console.warn(`useContinuumViewport("${nodeId}") called inside a collection scope. Viewport state is not supported for collection item nodes.`);
147
+ }
142
148
  const { session, store } = ctx;
143
149
  const viewportCacheRef = useRef(undefined);
144
150
  const subscribe = useCallback((onStoreChange) => store.subscribeNode(nodeId, onStoreChange), [store, nodeId]);
@@ -307,15 +313,21 @@ export function useContinuumAction(intentId) {
307
313
  const { session } = ctx;
308
314
  const [isDispatching, setIsDispatching] = useState(false);
309
315
  const [lastResult, setLastResult] = useState(null);
316
+ const dispatchIdRef = useRef(0);
310
317
  const dispatch = useCallback(async (nodeId) => {
318
+ const dispatchId = ++dispatchIdRef.current;
311
319
  setIsDispatching(true);
312
320
  try {
313
321
  const result = await session.dispatchAction(intentId, nodeId);
314
- setLastResult(result);
322
+ if (dispatchIdRef.current === dispatchId) {
323
+ setLastResult(result);
324
+ }
315
325
  return result;
316
326
  }
317
327
  finally {
318
- setIsDispatching(false);
328
+ if (dispatchIdRef.current === dispatchId) {
329
+ setIsDispatching(false);
330
+ }
319
331
  }
320
332
  }, [session, intentId]);
321
333
  return { dispatch, isDispatching, lastResult };
@@ -1 +1 @@
1
- {"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../../../../packages/react/src/lib/renderer.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAIV,cAAc,EAEf,MAAM,qBAAqB,CAAC;AA+W7B;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,cAAc,CAAA;CAAE,2CAQnE"}
1
+ {"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../../../../packages/react/src/lib/renderer.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAIV,cAAc,EAEf,MAAM,qBAAqB,CAAC;AAkZ7B;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,cAAc,CAAA;CAAE,2CAQnE"}
package/lib/renderer.js CHANGED
@@ -1,5 +1,5 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { memo, useContext, useMemo } from 'react';
1
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { memo, useCallback, useContext, useMemo } from 'react';
3
3
  import { getChildNodes } from '@continuum-dev/contract';
4
4
  import { ContinuumContext } from './context.js';
5
5
  import { NodeStateScopeContext, useContinuumState } from './hooks.js';
@@ -83,68 +83,90 @@ function collectTemplateDefaults(node, parentPath = '') {
83
83
  }
84
84
  return values;
85
85
  }
86
- const StatefulNodeRenderer = memo(function StatefulNodeRenderer({ definition, parentPath }) {
86
+ const StatefulNodeRenderer = memo(function StatefulNodeRenderer({ definition, parentPath, mappedProps, }) {
87
87
  const Component = useResolvedComponent(definition);
88
88
  const canonicalId = toCanonicalId(definition.id, parentPath);
89
89
  const [value, setValue] = useContinuumState(canonicalId);
90
90
  if (definition.hidden) {
91
91
  return null;
92
92
  }
93
- return (_jsx("div", { "data-continuum-id": definition.id, children: _jsx(NodeErrorBoundary, { nodeId: definition.id, children: _jsx(Component, { value: value, onChange: setValue, definition: definition, nodeId: canonicalId }) }) }));
93
+ return (_jsx(_Fragment, { children: _jsx(NodeErrorBoundary, { nodeId: definition.id, children: _jsx(Component, { value: value, onChange: setValue, definition: definition, nodeId: canonicalId, ...mappedProps }) }) }));
94
94
  });
95
- const ContainerNodeRenderer = memo(function ContainerNodeRenderer({ definition, parentPath }) {
95
+ const ContainerNodeRenderer = memo(function ContainerNodeRenderer({ definition, parentPath, mappedProps, }) {
96
96
  const Component = useResolvedComponent(definition);
97
97
  if (definition.hidden) {
98
98
  return null;
99
99
  }
100
100
  const canonicalId = toCanonicalId(definition.id, parentPath);
101
- const childNodes = getChildNodes(definition).map((child) => (_jsx(NodeRenderer, { definition: child, parentPath: canonicalId }, child.id)));
102
- return (_jsx("div", { "data-continuum-id": definition.id, children: _jsx(NodeErrorBoundary, { nodeId: definition.id, children: _jsx(Component, { value: undefined, onChange: noopOnChange, definition: definition, nodeId: canonicalId, children: childNodes }) }) }));
101
+ const childNodes = getChildNodes(definition).map((child) => (_jsx(NodeRenderer, { definition: child, parentPath: canonicalId, mappedProps: mappedProps }, child.id)));
102
+ return (_jsx(_Fragment, { children: _jsx(NodeErrorBoundary, { nodeId: definition.id, children: _jsx(Component, { value: undefined, onChange: noopOnChange, definition: definition, nodeId: canonicalId, ...mappedProps, children: childNodes }) }) }));
103
103
  });
104
104
  const CollectionItemRenderer = memo(function CollectionItemRenderer({ collectionCanonicalId, itemIndex, template, templateDefaults, canRemove, onRemove, }) {
105
105
  const ctx = useContext(ContinuumContext);
106
+ const parentScope = useContext(NodeStateScopeContext);
106
107
  if (!ctx) {
107
108
  throw new Error('ContinuumRenderer must be used within a <ContinuumProvider>');
108
109
  }
109
110
  const { session, store } = ctx;
110
- const scope = useMemo(() => ({
111
- subscribeNode: (_nodeId, listener) => store.subscribeNode(collectionCanonicalId, listener),
112
- getNodeValue: (nodeId) => {
113
- const relativeId = toRelativeNodeId(collectionCanonicalId, nodeId);
114
- if (!relativeId) {
115
- return undefined;
116
- }
117
- const collectionValue = normalizeCollectionNodeValue(store.getNodeValue(collectionCanonicalId));
118
- return (collectionValue.value.items[itemIndex]?.values?.[relativeId] ??
119
- templateDefaults[relativeId]);
120
- },
121
- setNodeValue: (nodeId, nextValue) => {
122
- const relativeId = toRelativeNodeId(collectionCanonicalId, nodeId);
123
- if (!relativeId) {
124
- return;
125
- }
126
- const collectionValue = normalizeCollectionNodeValue(store.getNodeValue(collectionCanonicalId));
127
- const items = collectionValue.value.items.map((item) => ({
128
- values: { ...item.values },
129
- }));
130
- while (items.length <= itemIndex) {
131
- items.push({ values: {} });
132
- }
133
- items[itemIndex] = {
134
- values: {
135
- ...items[itemIndex].values,
136
- [relativeId]: nextValue,
137
- },
138
- };
139
- session.updateState(collectionCanonicalId, {
140
- ...collectionValue,
141
- value: { items },
142
- });
143
- },
144
- }), [collectionCanonicalId, itemIndex, session, store, templateDefaults]);
145
- return (_jsx(NodeStateScopeContext.Provider, { value: scope, children: _jsxs("div", { "data-continuum-collection-item": `${collectionCanonicalId}:${itemIndex}`, className: "continuum-collection-item", children: [_jsx(NodeRenderer, { definition: template, parentPath: collectionCanonicalId }), canRemove ? (_jsx("div", { className: "continuum-collection-item-actions", children: _jsx("button", { type: "button", "data-continuum-collection-remove": `${collectionCanonicalId}:${itemIndex}`, onClick: () => onRemove(itemIndex), className: "continuum-collection-remove", children: "\u00D7" }) })) : null] }) }));
111
+ const scope = useMemo(() => {
112
+ const readCollectionValue = parentScope
113
+ ? () => normalizeCollectionNodeValue(parentScope.getNodeValue(collectionCanonicalId))
114
+ : () => normalizeCollectionNodeValue(store.getNodeValue(collectionCanonicalId));
115
+ const writeCollectionValue = parentScope
116
+ ? (next) => parentScope.setNodeValue(collectionCanonicalId, next)
117
+ : (next) => session.updateState(collectionCanonicalId, next);
118
+ const subscribeToCollection = parentScope
119
+ ? (listener) => parentScope.subscribeNode(collectionCanonicalId, listener)
120
+ : (listener) => store.subscribeNode(collectionCanonicalId, listener);
121
+ return {
122
+ subscribeNode: (_nodeId, listener) => subscribeToCollection(listener),
123
+ getNodeValue: (nodeId) => {
124
+ const relativeId = toRelativeNodeId(collectionCanonicalId, nodeId);
125
+ if (!relativeId) {
126
+ return undefined;
127
+ }
128
+ const collectionValue = readCollectionValue();
129
+ return (collectionValue.value.items[itemIndex]?.values?.[relativeId] ??
130
+ templateDefaults[relativeId]);
131
+ },
132
+ setNodeValue: (nodeId, nextValue) => {
133
+ const relativeId = toRelativeNodeId(collectionCanonicalId, nodeId);
134
+ if (!relativeId) {
135
+ return;
136
+ }
137
+ const collectionValue = readCollectionValue();
138
+ const items = collectionValue.value.items.map((item) => ({
139
+ values: { ...item.values },
140
+ }));
141
+ while (items.length <= itemIndex) {
142
+ items.push({ values: {} });
143
+ }
144
+ items[itemIndex] = {
145
+ values: {
146
+ ...items[itemIndex].values,
147
+ [relativeId]: nextValue,
148
+ },
149
+ };
150
+ writeCollectionValue({
151
+ ...collectionValue,
152
+ value: { items },
153
+ });
154
+ },
155
+ };
156
+ }, [collectionCanonicalId, itemIndex, session, store, templateDefaults, parentScope]);
157
+ return (_jsx(NodeStateScopeContext.Provider, { value: scope, children: _jsx(NodeRenderer, { definition: template, parentPath: collectionCanonicalId, mappedProps: {
158
+ itemIndex,
159
+ canRemove,
160
+ onRemove: () => onRemove(itemIndex),
161
+ } }) }));
146
162
  });
147
163
  const CollectionNodeRenderer = memo(function CollectionNodeRenderer({ definition, parentPath, }) {
164
+ const ctx = useContext(ContinuumContext);
165
+ const parentScope = useContext(NodeStateScopeContext);
166
+ if (!ctx) {
167
+ throw new Error('ContinuumRenderer must be used within a <ContinuumProvider>');
168
+ }
169
+ const { store } = ctx;
148
170
  const Component = useResolvedComponent(definition);
149
171
  const canonicalId = toCanonicalId(definition.id, parentPath);
150
172
  const [collectionValue, setCollectionValue] = useContinuumState(canonicalId);
@@ -158,52 +180,57 @@ const CollectionNodeRenderer = memo(function CollectionNodeRenderer({ definition
158
180
  if (definition.hidden) {
159
181
  return null;
160
182
  }
161
- const addItem = () => {
162
- if (!canAdd) {
183
+ const readCollectionValue = useCallback(() => normalizeCollectionNodeValue(parentScope
184
+ ? parentScope.getNodeValue(canonicalId)
185
+ : store.getNodeValue(canonicalId)), [canonicalId, parentScope, store]);
186
+ const addItem = useCallback(() => {
187
+ const current = readCollectionValue();
188
+ if (maxItems !== undefined && current.value.items.length >= maxItems) {
163
189
  return;
164
190
  }
165
191
  const items = [
166
- ...normalizedCollection.value.items.map((item) => ({
192
+ ...current.value.items.map((item) => ({
167
193
  values: { ...item.values },
168
194
  })),
169
195
  { values: { ...templateDefaults } },
170
196
  ];
171
197
  setCollectionValue({
172
- ...normalizedCollection,
198
+ ...current,
173
199
  value: { items },
174
200
  });
175
- };
176
- const removeItem = (index) => {
177
- if (!canRemove) {
201
+ }, [maxItems, readCollectionValue, setCollectionValue, templateDefaults]);
202
+ const removeItem = useCallback((index) => {
203
+ const current = readCollectionValue();
204
+ if (current.value.items.length <= minItems) {
178
205
  return;
179
206
  }
180
- const items = normalizedCollection.value.items
207
+ const items = current.value.items
181
208
  .map((item) => ({ values: { ...item.values } }))
182
209
  .filter((_, itemIndex) => itemIndex !== index);
183
210
  if (items.length < minItems) {
184
211
  return;
185
212
  }
186
213
  setCollectionValue({
187
- ...normalizedCollection,
214
+ ...current,
188
215
  value: { items },
189
216
  });
190
- };
217
+ }, [minItems, readCollectionValue, setCollectionValue]);
191
218
  const renderedItems = normalizedCollection.value.items.map((_, index) => (_jsx(CollectionItemRenderer, { collectionCanonicalId: canonicalId, itemIndex: index, template: definition.template, templateDefaults: templateDefaults, canRemove: canRemove, onRemove: removeItem }, index)));
192
- return (_jsx("div", { "data-continuum-id": definition.id, children: _jsx(NodeErrorBoundary, { nodeId: definition.id, children: _jsxs(Component, { value: collectionValue, onChange: setCollectionValue, definition: definition, nodeId: canonicalId, children: [renderedItems, _jsx("div", { className: "continuum-collection-add-container", children: _jsx("button", { type: "button", "data-continuum-collection-add": canonicalId, onClick: addItem, disabled: !canAdd, className: "continuum-collection-add", children: "+ Add item" }) })] }) }) }));
219
+ return (_jsx(_Fragment, { children: _jsx(NodeErrorBoundary, { nodeId: definition.id, children: _jsx(Component, { value: collectionValue, onChange: setCollectionValue, definition: definition, nodeId: canonicalId, canAdd: canAdd, canRemove: canRemove, onAdd: addItem, onRemove: removeItem, children: renderedItems }) }) }));
193
220
  });
194
- const NodeRenderer = memo(function NodeRenderer({ definition, parentPath }) {
221
+ const NodeRenderer = memo(function NodeRenderer({ definition, parentPath, mappedProps, }) {
195
222
  if (definition.type === 'collection') {
196
223
  return _jsx(CollectionNodeRenderer, { definition: definition, parentPath: parentPath });
197
224
  }
198
225
  const childNodes = getChildNodes(definition);
199
226
  if (childNodes.length > 0) {
200
- return _jsx(ContainerNodeRenderer, { definition: definition, parentPath: parentPath });
227
+ return _jsx(ContainerNodeRenderer, { definition: definition, parentPath: parentPath, mappedProps: mappedProps });
201
228
  }
202
- return _jsx(StatefulNodeRenderer, { definition: definition, parentPath: parentPath });
229
+ return _jsx(StatefulNodeRenderer, { definition: definition, parentPath: parentPath, mappedProps: mappedProps });
203
230
  });
204
231
  /**
205
232
  * Renders a `ViewDefinition` tree using components registered in `ContinuumProvider`.
206
233
  */
207
234
  export function ContinuumRenderer({ view }) {
208
- return (_jsx("div", { "data-continuum-view": view.viewId, children: (view.nodes ?? []).map((node) => (_jsx(NodeRenderer, { definition: node, parentPath: "" }, node.id))) }));
235
+ return (_jsx(_Fragment, { children: (view.nodes ?? []).map((node) => (_jsx(NodeRenderer, { definition: node, parentPath: "" }, node.id))) }));
209
236
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@continuum-dev/react",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -44,7 +44,7 @@
44
44
  "react": ">=18"
45
45
  },
46
46
  "dependencies": {
47
- "@continuum-dev/contract": "^0.1.2",
48
- "@continuum-dev/session": "^0.1.2"
47
+ "@continuum-dev/contract": "^0.1.3",
48
+ "@continuum-dev/session": "^0.1.3"
49
49
  }
50
50
  }