@elytracms/studio 0.0.58 → 0.0.59
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/dist/components/builder/blocks-model.js +12 -7
- package/dist/components/builder/blocks-model.js.map +1 -1
- package/dist/components/builder/canvas-dnd.d.ts +59 -0
- package/dist/components/builder/canvas-dnd.js +207 -0
- package/dist/components/builder/canvas-dnd.js.map +1 -0
- package/dist/components/builder/canvas-dom.d.ts +11 -0
- package/dist/components/builder/canvas-dom.js +17 -0
- package/dist/components/builder/canvas-dom.js.map +1 -0
- package/dist/components/builder/canvas-inline-editing.d.ts +91 -0
- package/dist/components/builder/canvas-inline-editing.js +242 -0
- package/dist/components/builder/canvas-inline-editing.js.map +1 -0
- package/dist/components/builder/canvas-render-context.d.ts +83 -0
- package/dist/components/builder/canvas-render-context.js +196 -0
- package/dist/components/builder/canvas-render-context.js.map +1 -0
- package/dist/components/builder/canvas.d.ts +4 -1
- package/dist/components/builder/canvas.js +32 -567
- package/dist/components/builder/canvas.js.map +1 -1
- package/dist/components/builder/composition-field-editor.js +14 -4
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/insert-context.d.ts +5 -2
- package/dist/components/builder/insert-context.js +1 -3
- package/dist/components/builder/insert-context.js.map +1 -1
- package/dist/components/builder/node-factory.d.ts +12 -0
- package/dist/components/builder/node-factory.js +18 -1
- package/dist/components/builder/node-factory.js.map +1 -1
- package/dist/components/cms/asset-field.d.ts +22 -0
- package/dist/components/cms/asset-field.js +300 -0
- package/dist/components/cms/asset-field.js.map +1 -0
- package/dist/components/cms/field-control-lib.d.ts +43 -0
- package/dist/components/cms/field-control-lib.js +98 -0
- package/dist/components/cms/field-control-lib.js.map +1 -0
- package/dist/components/cms/field-control.d.ts +7 -35
- package/dist/components/cms/field-control.js +13 -556
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/object-array-field.d.ts +29 -0
- package/dist/components/cms/object-array-field.js +205 -0
- package/dist/components/cms/object-array-field.js.map +1 -0
- package/dist/components/project-session.d.ts +19 -0
- package/dist/components/project-session.js +29 -2
- package/dist/components/project-session.js.map +1 -1
- package/dist/components/use-hydration-phase.d.ts +6 -4
- package/dist/components/use-hydration-phase.js.map +1 -1
- package/dist/lib/data/collection-content.d.ts +12 -7
- package/dist/lib/data/collection-content.js +6 -4
- package/dist/lib/data/collection-content.js.map +1 -1
- package/dist/lib/operations/session-hydration.d.ts +4 -1
- package/dist/lib/operations/session-hydration.js +4 -1
- package/dist/lib/operations/session-hydration.js.map +1 -1
- package/dist/lib/persistence/index.js +13 -1
- package/dist/lib/persistence/index.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +1 -1
- package/dist/lib/workspace/index.js +1 -1
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/lib/workspace/workspace-source.d.ts +17 -22
- package/dist/lib/workspace/workspace-source.js +1 -49
- package/dist/lib/workspace/workspace-source.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ChevronDown, ChevronUp, GripVertical, MoreHorizontal, Plus, Trash2 } from 'lucide-react';
|
|
3
|
+
import { createContext, useContext, useRef, useState } from 'react';
|
|
4
|
+
import { isFieldHidden } from '@elytracms/core/cms-core';
|
|
5
|
+
import { Button, Card, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Field, Row, Stack, Text, } from '@elytracms/studio/ui';
|
|
6
|
+
import { cn } from '@elytracms/studio/lib/utils';
|
|
7
|
+
import { localizedLabel, translate } from '@elytracms/studio/lib/i18n';
|
|
8
|
+
import { fieldLabel } from './cms-labels.js';
|
|
9
|
+
import { FieldControl } from './field-control.js';
|
|
10
|
+
import { depthRail, depthTint, isTabularObjectType, objectItemSummary, reorder } from './field-control-lib.js';
|
|
11
|
+
/**
|
|
12
|
+
* The object / repeater field controls (EC-253, legibility-rebuilt EC-305) —
|
|
13
|
+
* split from `field-control.tsx` (EC-330). {@link ObjectField} dispatches by
|
|
14
|
+
* shape (single card / editable table / card list); {@link ObjectItemForm} is
|
|
15
|
+
* the recursive subfield form the asset-field family reuses for per-use
|
|
16
|
+
* sub-fields and asset metadata. Cells and subfields render through
|
|
17
|
+
* `FieldControl`, so any subfield type — including a nested repeater — works
|
|
18
|
+
* for free.
|
|
19
|
+
*/
|
|
20
|
+
function asRecord(value) {
|
|
21
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
22
|
+
? value
|
|
23
|
+
: {};
|
|
24
|
+
}
|
|
25
|
+
// EC-305: nested-array depth, propagated via context so each array level can encode
|
|
26
|
+
// its depth (rail colour + tint) without prop-drilling through FieldControl's many
|
|
27
|
+
// branches. The detail editor renders top-level fields with the default depth 0.
|
|
28
|
+
const ArrayDepthContext = createContext(0);
|
|
29
|
+
const useArrayDepth = () => useContext(ArrayDepthContext);
|
|
30
|
+
/**
|
|
31
|
+
* Shared array state for an object repeater (EC-253/EC-294/EC-305): the value-only
|
|
32
|
+
* items + a parallel stable-key list kept in lockstep through every structural op
|
|
33
|
+
* (the EC-253 C2 key↔value lock), add/remove/move, and native drag-reorder. Used by
|
|
34
|
+
* both the card list (B) and the table (C) so they behave identically.
|
|
35
|
+
*/
|
|
36
|
+
function useObjectArray(field, value, onChange) {
|
|
37
|
+
const items = Array.isArray(value) ? value : [];
|
|
38
|
+
const seq = useRef(0);
|
|
39
|
+
const keysRef = useRef([]);
|
|
40
|
+
if (keysRef.current.length !== items.length) {
|
|
41
|
+
keysRef.current = items.map((_, i) => keysRef.current[i] ?? seq.current++);
|
|
42
|
+
}
|
|
43
|
+
const keys = keysRef.current;
|
|
44
|
+
const commit = (nextItems, nextKeys) => {
|
|
45
|
+
keysRef.current = nextKeys;
|
|
46
|
+
onChange(nextItems);
|
|
47
|
+
};
|
|
48
|
+
const move = (from, to) => commit(reorder(items, from, to), reorder(keys, from, to));
|
|
49
|
+
const removeAt = (i) => commit(items.filter((_, j) => j !== i), keys.filter((_, j) => j !== i));
|
|
50
|
+
const addItem = () => {
|
|
51
|
+
const seed = {};
|
|
52
|
+
for (const sub of field.fields)
|
|
53
|
+
if (sub.default !== undefined)
|
|
54
|
+
seed[sub.name] = sub.default;
|
|
55
|
+
const key = seq.current++;
|
|
56
|
+
commit([...items, seed], [...keys, key]);
|
|
57
|
+
return key;
|
|
58
|
+
};
|
|
59
|
+
const setItem = (i, updated) => {
|
|
60
|
+
const nextItems = items.slice();
|
|
61
|
+
nextItems[i] = updated;
|
|
62
|
+
commit(nextItems, keys);
|
|
63
|
+
};
|
|
64
|
+
const [dragIndex, setDragIndex] = useState(null);
|
|
65
|
+
const [overIndex, setOverIndex] = useState(null);
|
|
66
|
+
const endDrag = () => {
|
|
67
|
+
setDragIndex(null);
|
|
68
|
+
setOverIndex(null);
|
|
69
|
+
};
|
|
70
|
+
const drop = (to) => {
|
|
71
|
+
if (dragIndex !== null && dragIndex !== to)
|
|
72
|
+
move(dragIndex, to);
|
|
73
|
+
endDrag();
|
|
74
|
+
};
|
|
75
|
+
return { items, keys, move, removeAt, addItem, setItem, dragIndex, overIndex, setDragIndex, setOverIndex, endDrag, drop };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* (E) Chrome reduction: the per-row move/remove controls collapse into one hover/
|
|
79
|
+
* focus overflow menu, so a row reads as content, not a six-button toolbar. Drag is
|
|
80
|
+
* the primary reorder; the arrows stay here for keyboard/a11y.
|
|
81
|
+
*/
|
|
82
|
+
function ItemOverflowMenu({ index, count, onMove, onRemove, }) {
|
|
83
|
+
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "sm", "aria-label": translate('cms.field.itemActions'), children: _jsx(MoreHorizontal, { className: "size-3.5" }) }) }), _jsxs(DropdownMenuContent, { align: "end", children: [_jsxs(DropdownMenuItem, { disabled: index === 0, onClick: () => onMove(index, index - 1), children: [_jsx(ChevronUp, {}), " ", translate('cms.field.moveUp')] }), _jsxs(DropdownMenuItem, { disabled: index === count - 1, onClick: () => onMove(index, index + 1), children: [_jsx(ChevronDown, {}), " ", translate('cms.field.moveDown')] }), _jsxs(DropdownMenuItem, { onClick: onRemove, children: [_jsx(Trash2, {}), " ", translate('common.remove')] })] })] }));
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The nested subfield form for one object item (EC-253). Reuses {@link FieldControl}
|
|
87
|
+
* recursively, so any subfield type — including a nested object/repeater — works for
|
|
88
|
+
* free. Writing a subfield merges it back into the item and bubbles the new item up.
|
|
89
|
+
*/
|
|
90
|
+
export function ObjectItemForm({ fields, value, onChange, readOnly, idPrefix, }) {
|
|
91
|
+
return (_jsx(Stack, { gap: "3", className: "mt-2", children: fields.map((sub) => {
|
|
92
|
+
// EC-273: a conditionally-hidden subfield (its predicate keys off the item's
|
|
93
|
+
// own sibling values) is omitted — e.g. a discriminated link's `href` shows
|
|
94
|
+
// only when `linkType === 'href'`. Re-evaluated as the item is edited.
|
|
95
|
+
if (isFieldHidden(sub, { values: value }))
|
|
96
|
+
return null;
|
|
97
|
+
const subId = `${idPrefix}-${sub.name}`;
|
|
98
|
+
return (_jsx(Field, { htmlFor: subId, hint: localizedLabel(sub.form?.description) || undefined, label: _jsxs(Row, { gap: "2", children: [_jsx("span", { children: fieldLabel(sub) }), sub.validation?.required ? _jsx("span", { className: "text-destructive", children: "*" }) : null] }), children: _jsx(FieldControl, { id: subId, field: sub, value: value[sub.name], readOnly: readOnly, onChange: (next) => {
|
|
99
|
+
const updated = { ...value };
|
|
100
|
+
if (next === undefined)
|
|
101
|
+
delete updated[sub.name];
|
|
102
|
+
else
|
|
103
|
+
updated[sub.name] = next;
|
|
104
|
+
onChange(updated);
|
|
105
|
+
} }) }, sub.name));
|
|
106
|
+
}) }));
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Object / repeater field control (EC-253), legibility-rebuilt (EC-305). A single
|
|
110
|
+
* object (`multiple: false`) renders a depth-encoded card; an array (`'many'`)
|
|
111
|
+
* dispatches by shape: a **flat** object type (all scalar subfields) → a compact
|
|
112
|
+
* editable TABLE (C); a richer/nested type → a card list with depth encoding (B),
|
|
113
|
+
* summary-first collapse (D), and reduced per-row chrome (E). Editing stays in the
|
|
114
|
+
* shared field form — no canvas escape (unlike `blocks`) — so props and document
|
|
115
|
+
* fields share this control. Depth flows via {@link ArrayDepthContext}.
|
|
116
|
+
*/
|
|
117
|
+
export function ObjectField({ field, value, onChange, readOnly, id, }) {
|
|
118
|
+
const depth = useArrayDepth();
|
|
119
|
+
if (field.multiple === false) {
|
|
120
|
+
return (_jsx(Card, { className: cn('border-l-2 p-3', depthRail(depth), depthTint(depth)), children: _jsx(ArrayDepthContext.Provider, { value: depth + 1, children: _jsx(ObjectItemForm, { fields: field.fields, value: asRecord(value), readOnly: readOnly, idPrefix: id, onChange: onChange }) }) }));
|
|
121
|
+
}
|
|
122
|
+
const props = { field, value, onChange, readOnly, id, depth };
|
|
123
|
+
return isTabularObjectType(field) ? _jsx(ObjectTableField, { ...props }) : _jsx(ObjectCardList, { ...props });
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* (B + D + E) The card list for a nested/rich object array: depth-encoded cards
|
|
127
|
+
* (coloured rail + tint), items collapsed by default to a scannable summary outline
|
|
128
|
+
* (D), accordion expansion (open one → collapse siblings), and the per-row controls
|
|
129
|
+
* collapsed into one overflow menu (E). Drag-to-reorder; arrows live in the menu for
|
|
130
|
+
* a11y. Nested arrays render one depth deeper via the context provider.
|
|
131
|
+
*/
|
|
132
|
+
function ObjectCardList({ field, value, onChange, readOnly, id, depth }) {
|
|
133
|
+
const arr = useObjectArray(field, value, onChange);
|
|
134
|
+
const { items, keys } = arr;
|
|
135
|
+
// (D) Default COLLAPSED: track which keys are EXPANDED (empty = all collapsed).
|
|
136
|
+
// (B) Accordion: expanding one collapses the siblings.
|
|
137
|
+
const [expandedKeys, setExpandedKeys] = useState(() => new Set());
|
|
138
|
+
const toggle = (key) => setExpandedKeys((prev) => {
|
|
139
|
+
if (prev.has(key)) {
|
|
140
|
+
const next = new Set(prev);
|
|
141
|
+
next.delete(key);
|
|
142
|
+
return next;
|
|
143
|
+
}
|
|
144
|
+
return new Set([key]);
|
|
145
|
+
});
|
|
146
|
+
const allExpanded = items.length > 0 && keys.every((k) => expandedKeys.has(k));
|
|
147
|
+
const onAdd = () => setExpandedKeys(new Set([arr.addItem()]));
|
|
148
|
+
return (_jsx(ArrayDepthContext.Provider, { value: depth + 1, children: _jsxs(Stack, { gap: "2", children: [items.length > 1 && !readOnly ? (_jsx(Row, { justify: "end", children: _jsx(Button, { variant: "ghost", size: "sm", onClick: () => setExpandedKeys(allExpanded ? new Set() : new Set(keys)), children: translate(allExpanded ? 'cms.field.collapseAll' : 'cms.field.expandAll') }) })) : null, items.length === 0 ? (_jsx(Text, { size: "sm", tone: "subtle", children: translate('cms.field.noItems') })) : (items.map((item, i) => {
|
|
149
|
+
// keysRef is reconciled to items.length, so keys[i] is always present;
|
|
150
|
+
// `?? i` only satisfies noUncheckedIndexedAccess (never taken at runtime).
|
|
151
|
+
const key = keys[i] ?? i;
|
|
152
|
+
const record = asRecord(item);
|
|
153
|
+
const isExpanded = expandedKeys.has(key);
|
|
154
|
+
return (_jsxs(Card, { onDragOver: (e) => {
|
|
155
|
+
if (arr.dragIndex === null)
|
|
156
|
+
return;
|
|
157
|
+
e.preventDefault();
|
|
158
|
+
if (arr.overIndex !== i)
|
|
159
|
+
arr.setOverIndex(i);
|
|
160
|
+
}, onDrop: () => arr.drop(i), className: cn('border-l-2 p-0', depthRail(depth), depthTint(depth), arr.dragIndex === i ? 'opacity-50' : '', arr.overIndex === i && arr.dragIndex !== null && arr.dragIndex !== i
|
|
161
|
+
? 'border-l-primary ring-1 ring-primary/40'
|
|
162
|
+
: ''), children: [_jsxs(Row, { className: "select-none border-b border-border/60 bg-muted/20 px-2 py-1.5", children: [!readOnly ? (_jsx("span", { draggable: true, onDragStart: (e) => {
|
|
163
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
164
|
+
e.dataTransfer.setData('text/plain', String(i));
|
|
165
|
+
arr.setDragIndex(i);
|
|
166
|
+
}, onDragEnd: arr.endDrag, "aria-label": translate('cms.field.dragToReorder'), className: "cursor-grab text-muted-foreground/50 hover:text-muted-foreground active:cursor-grabbing", children: _jsx(GripVertical, { className: "size-3.5" }) })) : null, _jsxs("button", { type: "button", onClick: () => toggle(key), "aria-expanded": isExpanded, className: "flex min-w-0 flex-1 items-center gap-1.5 text-left", children: [_jsx(ChevronDown, { className: cn('size-3.5 shrink-0 text-muted-foreground transition-transform', isExpanded ? '' : '-rotate-90'), "aria-hidden": true }), _jsx(Text, { size: "sm", className: "min-w-0 truncate font-medium", children: objectItemSummary(field.fields, record, i) })] }), !readOnly ? (_jsx(ItemOverflowMenu, { index: i, count: items.length, onMove: arr.move, onRemove: () => arr.removeAt(i) })) : null] }), isExpanded ? (_jsx("div", { className: "px-3 pb-3", children: _jsx(ObjectItemForm, { fields: field.fields, value: record, readOnly: readOnly, idPrefix: `${id}-${i}`, onChange: (updated) => arr.setItem(i, updated) }) })) : null] }, key));
|
|
167
|
+
})), !readOnly ? (_jsxs(Button, { variant: "outline", size: "sm", onClick: onAdd, children: [_jsx(Plus, { className: "size-3" }), " ", translate('cms.field.addItem')] })) : null] }) }));
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* (C) A compact editable table for a FLAT object array — columns are the scalar
|
|
171
|
+
* subfields, one row per item. Kills the card-stack sprawl for the commonest case
|
|
172
|
+
* (option lists, opening hours, social links). Drag-reorder + an overflow menu per
|
|
173
|
+
* row; cells reuse {@link FieldControl} so each subfield keeps its native control.
|
|
174
|
+
*/
|
|
175
|
+
function ObjectTableField({ field, value, onChange, readOnly, id, depth }) {
|
|
176
|
+
const arr = useObjectArray(field, value, onChange);
|
|
177
|
+
const { items, keys } = arr;
|
|
178
|
+
const cols = field.fields;
|
|
179
|
+
const span = cols.length + (readOnly ? 0 : 2);
|
|
180
|
+
return (_jsxs("div", { className: cn('overflow-hidden rounded-md border border-border border-l-2', depthRail(depth), depthTint(depth)), children: [_jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b border-border/60 bg-muted/30 text-left", children: [!readOnly ? _jsx("th", { className: "w-7" }) : null, cols.map((c) => (_jsxs("th", { className: "px-2 py-1.5 text-xs font-medium text-muted-foreground", children: [fieldLabel(c), c.validation?.required ? _jsx("span", { className: "text-destructive", children: " *" }) : null] }, c.name))), !readOnly ? _jsx("th", { className: "w-9" }) : null] }) }), _jsx("tbody", { children: items.length === 0 ? (_jsx("tr", { children: _jsx("td", { colSpan: span, className: "px-2 py-2 text-muted-foreground", children: _jsx(Text, { size: "sm", tone: "subtle", children: translate('cms.field.noItems') }) }) })) : (items.map((item, i) => {
|
|
181
|
+
const key = keys[i] ?? i;
|
|
182
|
+
const record = asRecord(item);
|
|
183
|
+
return (_jsxs("tr", { onDragOver: (e) => {
|
|
184
|
+
if (arr.dragIndex === null)
|
|
185
|
+
return;
|
|
186
|
+
e.preventDefault();
|
|
187
|
+
if (arr.overIndex !== i)
|
|
188
|
+
arr.setOverIndex(i);
|
|
189
|
+
}, onDrop: () => arr.drop(i), className: cn('border-b border-border/40 align-top last:border-0', arr.dragIndex === i ? 'opacity-50' : '', arr.overIndex === i && arr.dragIndex !== null && arr.dragIndex !== i
|
|
190
|
+
? 'bg-primary/5'
|
|
191
|
+
: ''), children: [!readOnly ? (_jsx("td", { className: "px-1 py-1", children: _jsx("span", { draggable: true, onDragStart: (e) => {
|
|
192
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
193
|
+
e.dataTransfer.setData('text/plain', String(i));
|
|
194
|
+
arr.setDragIndex(i);
|
|
195
|
+
}, onDragEnd: arr.endDrag, "aria-label": translate('cms.field.dragToReorder'), className: "inline-flex cursor-grab text-muted-foreground/50 hover:text-muted-foreground active:cursor-grabbing", children: _jsx(GripVertical, { className: "size-3.5" }) }) })) : null, cols.map((c) => (_jsx("td", { className: "px-2 py-1", children: _jsx(FieldControl, { id: `${id}-${i}-${c.name}`, field: c, value: record[c.name], readOnly: readOnly, onChange: (next) => {
|
|
196
|
+
const updated = { ...record };
|
|
197
|
+
if (next === undefined)
|
|
198
|
+
delete updated[c.name];
|
|
199
|
+
else
|
|
200
|
+
updated[c.name] = next;
|
|
201
|
+
arr.setItem(i, updated);
|
|
202
|
+
} }) }, c.name))), !readOnly ? (_jsx("td", { className: "px-1 py-1", children: _jsx(ItemOverflowMenu, { index: i, count: items.length, onMove: arr.move, onRemove: () => arr.removeAt(i) }) })) : null] }, key));
|
|
203
|
+
})) })] }), !readOnly ? (_jsx("div", { className: "border-t border-border/60 p-1.5", children: _jsxs(Button, { variant: "ghost", size: "sm", onClick: () => arr.addItem(), children: [_jsx(Plus, { className: "size-3" }), " ", translate('common.add')] }) })) : null] }));
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=object-array-field.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"object-array-field.js","sourceRoot":"","sources":["../../../src/components/cms/object-array-field.tsx"],"names":[],"mappings":"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","sourcesContent":["import { ChevronDown, ChevronUp, GripVertical, MoreHorizontal, Plus, Trash2 } from 'lucide-react'\nimport { createContext, useContext, useRef, useState } from 'react'\nimport type { FieldDef, ObjectFieldDef } from '@elytracms/core/cms-core'\nimport { isFieldHidden } from '@elytracms/core/cms-core'\nimport {\n Button,\n Card,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n Field,\n Row,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { fieldLabel } from './cms-labels'\nimport { FieldControl } from './field-control'\nimport { depthRail, depthTint, isTabularObjectType, objectItemSummary, reorder } from './field-control-lib'\n\n/**\n * The object / repeater field controls (EC-253, legibility-rebuilt EC-305) —\n * split from `field-control.tsx` (EC-330). {@link ObjectField} dispatches by\n * shape (single card / editable table / card list); {@link ObjectItemForm} is\n * the recursive subfield form the asset-field family reuses for per-use\n * sub-fields and asset metadata. Cells and subfields render through\n * `FieldControl`, so any subfield type — including a nested repeater — works\n * for free.\n */\n\nfunction asRecord(value: unknown): Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : {}\n}\n\n// EC-305: nested-array depth, propagated via context so each array level can encode\n// its depth (rail colour + tint) without prop-drilling through FieldControl's many\n// branches. The detail editor renders top-level fields with the default depth 0.\nconst ArrayDepthContext = createContext(0)\nconst useArrayDepth = () => useContext(ArrayDepthContext)\n\n/**\n * Shared array state for an object repeater (EC-253/EC-294/EC-305): the value-only\n * items + a parallel stable-key list kept in lockstep through every structural op\n * (the EC-253 C2 key↔value lock), add/remove/move, and native drag-reorder. Used by\n * both the card list (B) and the table (C) so they behave identically.\n */\nfunction useObjectArray(field: ObjectFieldDef, value: unknown, onChange: (value: unknown) => void) {\n const items = Array.isArray(value) ? (value as Record<string, unknown>[]) : []\n const seq = useRef(0)\n const keysRef = useRef<number[]>([])\n if (keysRef.current.length !== items.length) {\n keysRef.current = items.map((_, i) => keysRef.current[i] ?? seq.current++)\n }\n const keys = keysRef.current\n const commit = (nextItems: Record<string, unknown>[], nextKeys: number[]) => {\n keysRef.current = nextKeys\n onChange(nextItems)\n }\n const move = (from: number, to: number) => commit(reorder(items, from, to), reorder(keys, from, to))\n const removeAt = (i: number) =>\n commit(items.filter((_, j) => j !== i), keys.filter((_, j) => j !== i))\n const addItem = (): number => {\n const seed: Record<string, unknown> = {}\n for (const sub of field.fields) if (sub.default !== undefined) seed[sub.name] = sub.default\n const key = seq.current++\n commit([...items, seed], [...keys, key])\n return key\n }\n const setItem = (i: number, updated: Record<string, unknown>) => {\n const nextItems = items.slice()\n nextItems[i] = updated\n commit(nextItems, keys)\n }\n const [dragIndex, setDragIndex] = useState<number | null>(null)\n const [overIndex, setOverIndex] = useState<number | null>(null)\n const endDrag = () => {\n setDragIndex(null)\n setOverIndex(null)\n }\n const drop = (to: number) => {\n if (dragIndex !== null && dragIndex !== to) move(dragIndex, to)\n endDrag()\n }\n return { items, keys, move, removeAt, addItem, setItem, dragIndex, overIndex, setDragIndex, setOverIndex, endDrag, drop }\n}\n\n/**\n * (E) Chrome reduction: the per-row move/remove controls collapse into one hover/\n * focus overflow menu, so a row reads as content, not a six-button toolbar. Drag is\n * the primary reorder; the arrows stay here for keyboard/a11y.\n */\nfunction ItemOverflowMenu({\n index,\n count,\n onMove,\n onRemove,\n}: {\n index: number\n count: number\n onMove: (from: number, to: number) => void\n onRemove: () => void\n}) {\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"ghost\" size=\"sm\" aria-label={translate('cms.field.itemActions')}>\n <MoreHorizontal className=\"size-3.5\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuItem disabled={index === 0} onClick={() => onMove(index, index - 1)}>\n <ChevronUp /> {translate('cms.field.moveUp')}\n </DropdownMenuItem>\n <DropdownMenuItem disabled={index === count - 1} onClick={() => onMove(index, index + 1)}>\n <ChevronDown /> {translate('cms.field.moveDown')}\n </DropdownMenuItem>\n <DropdownMenuItem onClick={onRemove}>\n <Trash2 /> {translate('common.remove')}\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n\n/**\n * The nested subfield form for one object item (EC-253). Reuses {@link FieldControl}\n * recursively, so any subfield type — including a nested object/repeater — works for\n * free. Writing a subfield merges it back into the item and bubbles the new item up.\n */\nexport function ObjectItemForm({\n fields,\n value,\n onChange,\n readOnly,\n idPrefix,\n}: {\n fields: readonly FieldDef[]\n value: Record<string, unknown>\n onChange: (value: Record<string, unknown>) => void\n readOnly: boolean\n idPrefix: string\n}) {\n return (\n <Stack gap=\"3\" className=\"mt-2\">\n {fields.map((sub) => {\n // EC-273: a conditionally-hidden subfield (its predicate keys off the item's\n // own sibling values) is omitted — e.g. a discriminated link's `href` shows\n // only when `linkType === 'href'`. Re-evaluated as the item is edited.\n if (isFieldHidden(sub, { values: value })) return null\n const subId = `${idPrefix}-${sub.name}`\n return (\n <Field\n key={sub.name}\n htmlFor={subId}\n hint={localizedLabel(sub.form?.description) || undefined}\n label={\n <Row gap=\"2\">\n <span>{fieldLabel(sub)}</span>\n {sub.validation?.required ? <span className=\"text-destructive\">*</span> : null}\n </Row>\n }\n >\n <FieldControl\n id={subId}\n field={sub}\n value={value[sub.name]}\n readOnly={readOnly}\n onChange={(next) => {\n const updated = { ...value }\n if (next === undefined) delete updated[sub.name]\n else updated[sub.name] = next\n onChange(updated)\n }}\n />\n </Field>\n )\n })}\n </Stack>\n )\n}\n\n/**\n * Object / repeater field control (EC-253), legibility-rebuilt (EC-305). A single\n * object (`multiple: false`) renders a depth-encoded card; an array (`'many'`)\n * dispatches by shape: a **flat** object type (all scalar subfields) → a compact\n * editable TABLE (C); a richer/nested type → a card list with depth encoding (B),\n * summary-first collapse (D), and reduced per-row chrome (E). Editing stays in the\n * shared field form — no canvas escape (unlike `blocks`) — so props and document\n * fields share this control. Depth flows via {@link ArrayDepthContext}.\n */\nexport function ObjectField({\n field,\n value,\n onChange,\n readOnly,\n id,\n}: {\n field: ObjectFieldDef\n value: unknown\n onChange: (value: unknown) => void\n readOnly: boolean\n id: string\n}) {\n const depth = useArrayDepth()\n if (field.multiple === false) {\n return (\n <Card className={cn('border-l-2 p-3', depthRail(depth), depthTint(depth))}>\n <ArrayDepthContext.Provider value={depth + 1}>\n <ObjectItemForm\n fields={field.fields}\n value={asRecord(value)}\n readOnly={readOnly}\n idPrefix={id}\n onChange={onChange}\n />\n </ArrayDepthContext.Provider>\n </Card>\n )\n }\n const props = { field, value, onChange, readOnly, id, depth }\n return isTabularObjectType(field) ? <ObjectTableField {...props} /> : <ObjectCardList {...props} />\n}\n\ninterface ObjectArrayProps {\n field: ObjectFieldDef\n value: unknown\n onChange: (value: unknown) => void\n readOnly: boolean\n id: string\n depth: number\n}\n\n/**\n * (B + D + E) The card list for a nested/rich object array: depth-encoded cards\n * (coloured rail + tint), items collapsed by default to a scannable summary outline\n * (D), accordion expansion (open one → collapse siblings), and the per-row controls\n * collapsed into one overflow menu (E). Drag-to-reorder; arrows live in the menu for\n * a11y. Nested arrays render one depth deeper via the context provider.\n */\nfunction ObjectCardList({ field, value, onChange, readOnly, id, depth }: ObjectArrayProps) {\n const arr = useObjectArray(field, value, onChange)\n const { items, keys } = arr\n // (D) Default COLLAPSED: track which keys are EXPANDED (empty = all collapsed).\n // (B) Accordion: expanding one collapses the siblings.\n const [expandedKeys, setExpandedKeys] = useState<ReadonlySet<number>>(() => new Set())\n const toggle = (key: number) =>\n setExpandedKeys((prev) => {\n if (prev.has(key)) {\n const next = new Set(prev)\n next.delete(key)\n return next\n }\n return new Set([key])\n })\n const allExpanded = items.length > 0 && keys.every((k) => expandedKeys.has(k))\n const onAdd = () => setExpandedKeys(new Set([arr.addItem()]))\n return (\n <ArrayDepthContext.Provider value={depth + 1}>\n <Stack gap=\"2\">\n {items.length > 1 && !readOnly ? (\n <Row justify=\"end\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => setExpandedKeys(allExpanded ? new Set() : new Set(keys))}\n >\n {translate(allExpanded ? 'cms.field.collapseAll' : 'cms.field.expandAll')}\n </Button>\n </Row>\n ) : null}\n {items.length === 0 ? (\n <Text size=\"sm\" tone=\"subtle\">\n {translate('cms.field.noItems')}\n </Text>\n ) : (\n items.map((item, i) => {\n // keysRef is reconciled to items.length, so keys[i] is always present;\n // `?? i` only satisfies noUncheckedIndexedAccess (never taken at runtime).\n const key = keys[i] ?? i\n const record = asRecord(item)\n const isExpanded = expandedKeys.has(key)\n return (\n <Card\n key={key}\n onDragOver={(e) => {\n if (arr.dragIndex === null) return\n e.preventDefault()\n if (arr.overIndex !== i) arr.setOverIndex(i)\n }}\n onDrop={() => arr.drop(i)}\n className={cn(\n 'border-l-2 p-0',\n depthRail(depth),\n depthTint(depth),\n arr.dragIndex === i ? 'opacity-50' : '',\n arr.overIndex === i && arr.dragIndex !== null && arr.dragIndex !== i\n ? 'border-l-primary ring-1 ring-primary/40'\n : '',\n )}\n >\n <Row className=\"select-none border-b border-border/60 bg-muted/20 px-2 py-1.5\">\n {!readOnly ? (\n <span\n draggable\n onDragStart={(e) => {\n e.dataTransfer.effectAllowed = 'move'\n e.dataTransfer.setData('text/plain', String(i))\n arr.setDragIndex(i)\n }}\n onDragEnd={arr.endDrag}\n aria-label={translate('cms.field.dragToReorder')}\n className=\"cursor-grab text-muted-foreground/50 hover:text-muted-foreground active:cursor-grabbing\"\n >\n <GripVertical className=\"size-3.5\" />\n </span>\n ) : null}\n <button\n type=\"button\"\n onClick={() => toggle(key)}\n aria-expanded={isExpanded}\n className=\"flex min-w-0 flex-1 items-center gap-1.5 text-left\"\n >\n <ChevronDown\n className={cn(\n 'size-3.5 shrink-0 text-muted-foreground transition-transform',\n isExpanded ? '' : '-rotate-90',\n )}\n aria-hidden\n />\n <Text size=\"sm\" className=\"min-w-0 truncate font-medium\">\n {objectItemSummary(field.fields, record, i)}\n </Text>\n </button>\n {!readOnly ? (\n <ItemOverflowMenu\n index={i}\n count={items.length}\n onMove={arr.move}\n onRemove={() => arr.removeAt(i)}\n />\n ) : null}\n </Row>\n {isExpanded ? (\n <div className=\"px-3 pb-3\">\n <ObjectItemForm\n fields={field.fields}\n value={record}\n readOnly={readOnly}\n idPrefix={`${id}-${i}`}\n onChange={(updated) => arr.setItem(i, updated)}\n />\n </div>\n ) : null}\n </Card>\n )\n })\n )}\n {!readOnly ? (\n <Button variant=\"outline\" size=\"sm\" onClick={onAdd}>\n <Plus className=\"size-3\" /> {translate('cms.field.addItem')}\n </Button>\n ) : null}\n </Stack>\n </ArrayDepthContext.Provider>\n )\n}\n\n/**\n * (C) A compact editable table for a FLAT object array — columns are the scalar\n * subfields, one row per item. Kills the card-stack sprawl for the commonest case\n * (option lists, opening hours, social links). Drag-reorder + an overflow menu per\n * row; cells reuse {@link FieldControl} so each subfield keeps its native control.\n */\nfunction ObjectTableField({ field, value, onChange, readOnly, id, depth }: ObjectArrayProps) {\n const arr = useObjectArray(field, value, onChange)\n const { items, keys } = arr\n const cols = field.fields\n const span = cols.length + (readOnly ? 0 : 2)\n return (\n <div className={cn('overflow-hidden rounded-md border border-border border-l-2', depthRail(depth), depthTint(depth))}>\n <table className=\"w-full text-sm\">\n <thead>\n <tr className=\"border-b border-border/60 bg-muted/30 text-left\">\n {!readOnly ? <th className=\"w-7\" /> : null}\n {cols.map((c) => (\n <th key={c.name} className=\"px-2 py-1.5 text-xs font-medium text-muted-foreground\">\n {fieldLabel(c)}\n {c.validation?.required ? <span className=\"text-destructive\"> *</span> : null}\n </th>\n ))}\n {!readOnly ? <th className=\"w-9\" /> : null}\n </tr>\n </thead>\n <tbody>\n {items.length === 0 ? (\n <tr>\n <td colSpan={span} className=\"px-2 py-2 text-muted-foreground\">\n <Text size=\"sm\" tone=\"subtle\">\n {translate('cms.field.noItems')}\n </Text>\n </td>\n </tr>\n ) : (\n items.map((item, i) => {\n const key = keys[i] ?? i\n const record = asRecord(item)\n return (\n <tr\n key={key}\n onDragOver={(e) => {\n if (arr.dragIndex === null) return\n e.preventDefault()\n if (arr.overIndex !== i) arr.setOverIndex(i)\n }}\n onDrop={() => arr.drop(i)}\n className={cn(\n 'border-b border-border/40 align-top last:border-0',\n arr.dragIndex === i ? 'opacity-50' : '',\n arr.overIndex === i && arr.dragIndex !== null && arr.dragIndex !== i\n ? 'bg-primary/5'\n : '',\n )}\n >\n {!readOnly ? (\n <td className=\"px-1 py-1\">\n <span\n draggable\n onDragStart={(e) => {\n e.dataTransfer.effectAllowed = 'move'\n e.dataTransfer.setData('text/plain', String(i))\n arr.setDragIndex(i)\n }}\n onDragEnd={arr.endDrag}\n aria-label={translate('cms.field.dragToReorder')}\n className=\"inline-flex cursor-grab text-muted-foreground/50 hover:text-muted-foreground active:cursor-grabbing\"\n >\n <GripVertical className=\"size-3.5\" />\n </span>\n </td>\n ) : null}\n {cols.map((c) => (\n <td key={c.name} className=\"px-2 py-1\">\n <FieldControl\n id={`${id}-${i}-${c.name}`}\n field={c}\n value={record[c.name]}\n readOnly={readOnly}\n onChange={(next) => {\n const updated = { ...record }\n if (next === undefined) delete updated[c.name]\n else updated[c.name] = next\n arr.setItem(i, updated)\n }}\n />\n </td>\n ))}\n {!readOnly ? (\n <td className=\"px-1 py-1\">\n <ItemOverflowMenu\n index={i}\n count={items.length}\n onMove={arr.move}\n onRemove={() => arr.removeAt(i)}\n />\n </td>\n ) : null}\n </tr>\n )\n })\n )}\n </tbody>\n </table>\n {!readOnly ? (\n <div className=\"border-t border-border/60 p-1.5\">\n <Button variant=\"ghost\" size=\"sm\" onClick={() => arr.addItem()}>\n <Plus className=\"size-3\" /> {translate('common.add')}\n </Button>\n </div>\n ) : null}\n </div>\n )\n}\n"]}
|
|
@@ -20,6 +20,20 @@ import type { HydratedCms, HydratedProjectSession, OperationResult } from '@elyt
|
|
|
20
20
|
* BEFORE children render, so "Preview with" lists and section previews show
|
|
21
21
|
* live entries, not fixture titles.
|
|
22
22
|
*
|
|
23
|
+
* EC-361 — the playground gets the SAME side effect, without the render gate:
|
|
24
|
+
* the seeded in-memory adapter IS a persisted backend behind the operations
|
|
25
|
+
* seam (AD-17), so the content session hydrates from it through the identical
|
|
26
|
+
* `loadHydratedCms` loader connected mode uses — fire-and-forget after the
|
|
27
|
+
* synchronous first render (EC-138 stays intact), refreshed per CMS surface
|
|
28
|
+
* mount exactly like connected's `CmsSessionGate` → `loadCms` rhythm. Canvas
|
|
29
|
+
* listing/relation population therefore resolves from in-memory session
|
|
30
|
+
* documents with connected-mode semantics (published-ness = the version
|
|
31
|
+
* pointer; the Draft view resolves live rows) instead of the blanket-empty
|
|
32
|
+
* session the playground used to run with. The React `session`/`loadCms`
|
|
33
|
+
* VALUES stay `null` in playground — the CMS surfaces keep their fixture
|
|
34
|
+
* baseline (EC-320's static-baseline rule); only the preview content engine
|
|
35
|
+
* reads persisted state.
|
|
36
|
+
*
|
|
23
37
|
* EC-308/AD-17 note — this gate deliberately does NOT read through the
|
|
24
38
|
* `useOperationQuery` cache seam. It is boot/session orchestration, not a
|
|
25
39
|
* cacheable server read: (1) the synchronous playground first-render is the
|
|
@@ -55,6 +69,11 @@ export { useProjectSession };
|
|
|
55
69
|
* `configureContentSession` side effect of its own — `loadCms` (from
|
|
56
70
|
* `useProjectSession`) already calls it internally when it resolves — so no
|
|
57
71
|
* `onResolved`/`enabled` is passed here.
|
|
72
|
+
*
|
|
73
|
+
* EC-361: in playground the gate stays synchronous (fixture baseline, EC-138),
|
|
74
|
+
* but `loadCms` is still fired per mount for its content-session side effect —
|
|
75
|
+
* the preview engine refreshes from the in-memory adapter on the same
|
|
76
|
+
* per-CMS-mount rhythm connected mode gets from the gated load.
|
|
58
77
|
*/
|
|
59
78
|
export declare function CmsSessionGate({ children, }: {
|
|
60
79
|
children: (cms: HydratedCms | null) => ReactNode;
|
|
@@ -29,15 +29,33 @@ export function ProjectSessionProvider({ projectId, children, }) {
|
|
|
29
29
|
// project's gate reconfigures it before rendering its own children.
|
|
30
30
|
useEffect(() => () => configureContentSession(null), []);
|
|
31
31
|
const loadCms = useCallback(async () => {
|
|
32
|
-
if (playground)
|
|
33
|
-
return { ok: true, value: null };
|
|
34
32
|
const { operations } = await getStudioOperations();
|
|
35
33
|
const result = await loadHydratedCms(operations, projectId);
|
|
36
34
|
// Keep the preview engine in sync with the freshest persisted content.
|
|
37
35
|
if (result.ok && result.value !== null)
|
|
38
36
|
configureContentSession({ ...result.value, routes: resolveDeploymentRoutes() });
|
|
37
|
+
// EC-361: the playground runs the SAME load (over the seeded in-memory
|
|
38
|
+
// adapter) for the content-session side effect above, but the GATE value
|
|
39
|
+
// stays `null` — CMS surfaces keep their config/fixture baseline (EC-320's
|
|
40
|
+
// static-baseline rule), matching the pre-EC-361 playground contract. An
|
|
41
|
+
// in-memory failure is loud, not a silent empty preview.
|
|
42
|
+
if (playground) {
|
|
43
|
+
if (!result.ok)
|
|
44
|
+
console.error('[elytra] playground content-session load failed', result.error);
|
|
45
|
+
return { ok: true, value: null };
|
|
46
|
+
}
|
|
39
47
|
return result;
|
|
40
48
|
}, [playground, projectId]);
|
|
49
|
+
// EC-361: playground boot hydration for the content preview engine — the same
|
|
50
|
+
// loader connected mode gates on, fired WITHOUT gating so the synchronous
|
|
51
|
+
// playground first render (EC-138) is preserved. `loadCms` owns the
|
|
52
|
+
// `configureContentSession` side effect; the resolved value is discarded here.
|
|
53
|
+
// Runs after `useHydrationPhase`'s fast-path effect (registered above) has
|
|
54
|
+
// reset the session, so the hydrated state always lands last.
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (playground)
|
|
57
|
+
void loadCms();
|
|
58
|
+
}, [playground, loadCms]);
|
|
41
59
|
return (_jsx(HydrationBoundary, { phase: phase, retry: retry, children: (session) => (_jsx(ProjectSessionContext.Provider, { value: { session, loadCms }, children: children })) }));
|
|
42
60
|
}
|
|
43
61
|
export { useProjectSession };
|
|
@@ -49,10 +67,19 @@ export { useProjectSession };
|
|
|
49
67
|
* `configureContentSession` side effect of its own — `loadCms` (from
|
|
50
68
|
* `useProjectSession`) already calls it internally when it resolves — so no
|
|
51
69
|
* `onResolved`/`enabled` is passed here.
|
|
70
|
+
*
|
|
71
|
+
* EC-361: in playground the gate stays synchronous (fixture baseline, EC-138),
|
|
72
|
+
* but `loadCms` is still fired per mount for its content-session side effect —
|
|
73
|
+
* the preview engine refreshes from the in-memory adapter on the same
|
|
74
|
+
* per-CMS-mount rhythm connected mode gets from the gated load.
|
|
52
75
|
*/
|
|
53
76
|
export function CmsSessionGate({ children, }) {
|
|
54
77
|
const { loadCms } = useProjectSession();
|
|
55
78
|
const playground = isPlaygroundMode();
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (playground)
|
|
81
|
+
void loadCms();
|
|
82
|
+
}, [playground, loadCms]);
|
|
56
83
|
const { phase, retry } = useHydrationPhase(loadCms, {
|
|
57
84
|
playground,
|
|
58
85
|
playgroundValue: null,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project-session.js","sourceRoot":"","sources":["../../src/components/project-session.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect } from 'react'\nimport type { ReactNode } from 'react'\nimport { getStudioOperations, loadHydratedCms, loadProjectSession } from '@elytracms/studio/lib/operations'\nimport type { HydratedCms, HydratedProjectSession, OperationResult } from '@elytracms/studio/lib/operations'\nimport { isPlaygroundMode } from '@elytracms/studio/lib/persistence'\nimport { configureContentSession } from '@elytracms/studio/lib/data'\nimport { resolveDeploymentRoutes } from '@elytracms/studio/lib/workspace'\nimport { createRequiredContext } from '@elytracms/studio/lib/required-context'\nimport { HydrationBoundary, useHydrationPhase } from './use-hydration-phase'\n\n/**\n * Project-session hydration gate (Milestone B sign-off fix, EC-177 gap 1).\n *\n * In Convex mode every project session must BOOT from persistence: the\n * builder from the latest persisted graph revision, the CMS from the saved\n * schema + documents, publishing from the persisted environment states.\n * This provider runs the `@elytracms/core/operations` hydration queries\n * (`lib/operations/session-hydration`) and — like `DeploymentSetupGate` — gates its\n * children on a deterministic loading state, so fixture content never\n * flashes (and is never editable) before the persisted state arrives.\n *\n * Playground mode renders children synchronously with `session: null`\n * (fixtures), byte-identical to the pre-hydration behavior — the EC-138\n * determinism contract.\n *\n * Side effect on hydration: the content preview engine\n * (`lib/data/collection-content`) is pointed at the hydrated documents\n * BEFORE children render, so \"Preview with\" lists and section previews show\n * live entries, not fixture titles.\n *\n * EC-308/AD-17 note — this gate deliberately does NOT read through the\n * `useOperationQuery` cache seam. It is boot/session orchestration, not a\n * cacheable server read: (1) the synchronous playground first-render is the\n * EC-138 determinism contract (a pull-query is async even when disabled);\n * (2) `configureContentSession` must run synchronously BEFORE children mount,\n * which a post-render query effect cannot guarantee; (3) `CmsSessionGate` is\n * intentionally fresh-per-visit (it remounts to show edits persisted since\n * project open) — the opposite of a cached read. It still reads through the\n * operations client (no transport bypass), and uses a user-driven retry epoch,\n * not freshness machinery — so it is outside the patchwork AD-17 retires.\n */\n\nexport interface ProjectSessionStore {\n /** Hydrated persisted state (Convex mode); `null` in playground (fixtures). */\n session: HydratedProjectSession | null\n /**\n * Load the CURRENT persisted CMS state (fresh query — the CMS section\n * remounts per visit and must show edits persisted since project open).\n * Playground resolves `null` immediately (fixtures).\n */\n loadCms(): Promise<OperationResult<HydratedCms | null>>\n}\n\nconst { Context: ProjectSessionContext, useRequired: useProjectSession } =\n createRequiredContext<ProjectSessionStore>('ProjectSession')\n\nexport function ProjectSessionProvider({\n projectId,\n children,\n}: {\n projectId: string\n children: ReactNode\n}) {\n const playground = isPlaygroundMode()\n\n const loader = useCallback(\n () => getStudioOperations().then(({ operations }) => loadProjectSession(operations, projectId)),\n [projectId],\n )\n // Point the preview content engine at the hydrated documents BEFORE any\n // child renders (panels read it module-level, synchronously). EC-291:\n // include the config-sourced routes so the canvas `resolveLink` derives a\n // rich-text link's URL exactly as delivery does. `enabled: true` below runs\n // this in the playground fast-path too (with `session: null`) — the one\n // divergence from `CmsSessionGate`, which owns no such side effect.\n const onResolved = useCallback((session: HydratedProjectSession | null) => {\n configureContentSession(\n session?.cms ? { ...session.cms, routes: resolveDeploymentRoutes() } : null,\n )\n }, [])\n const { phase, retry } = useHydrationPhase<HydratedProjectSession | null>(loader, {\n playground,\n playgroundValue: null,\n enabled: true,\n onResolved,\n })\n\n // Leaving the project resets the preview engine to fixtures; the next\n // project's gate reconfigures it before rendering its own children.\n useEffect(() => () => configureContentSession(null), [])\n\n const loadCms = useCallback(async (): Promise<OperationResult<HydratedCms | null>> => {\n if (playground) return { ok: true, value: null }\n const { operations } = await getStudioOperations()\n const result = await loadHydratedCms(operations, projectId)\n // Keep the preview engine in sync with the freshest persisted content.\n if (result.ok && result.value !== null)\n configureContentSession({ ...result.value, routes: resolveDeploymentRoutes() })\n return result\n }, [playground, projectId])\n\n return (\n <HydrationBoundary phase={phase} retry={retry}>\n {(session) => (\n <ProjectSessionContext.Provider value={{ session, loadCms }}>\n {children}\n </ProjectSessionContext.Provider>\n )}\n </HydrationBoundary>\n )\n}\n\nexport { useProjectSession }\n\n/**\n * Gate one CMS surface mount on a FRESH persisted CMS load (Convex mode).\n * Render-prop: hands the hydrated content (or `null` → fixture fallback,\n * first-run) to the caller once it resolved. Playground renders synchronously\n * with `null`. Unlike `ProjectSessionProvider`, this gate owns no\n * `configureContentSession` side effect of its own — `loadCms` (from\n * `useProjectSession`) already calls it internally when it resolves — so no\n * `onResolved`/`enabled` is passed here.\n */\nexport function CmsSessionGate({\n children,\n}: {\n children: (cms: HydratedCms | null) => ReactNode\n}) {\n const { loadCms } = useProjectSession()\n const playground = isPlaygroundMode()\n const { phase, retry } = useHydrationPhase<HydratedCms | null>(loadCms, {\n playground,\n playgroundValue: null,\n })\n return (\n <HydrationBoundary phase={phase} retry={retry}>\n {children}\n </HydrationBoundary>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"project-session.js","sourceRoot":"","sources":["../../src/components/project-session.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect } from 'react'\nimport type { ReactNode } from 'react'\nimport { getStudioOperations, loadHydratedCms, loadProjectSession } from '@elytracms/studio/lib/operations'\nimport type { HydratedCms, HydratedProjectSession, OperationResult } from '@elytracms/studio/lib/operations'\nimport { isPlaygroundMode } from '@elytracms/studio/lib/persistence'\nimport { configureContentSession } from '@elytracms/studio/lib/data'\nimport { resolveDeploymentRoutes } from '@elytracms/studio/lib/workspace'\nimport { createRequiredContext } from '@elytracms/studio/lib/required-context'\nimport { HydrationBoundary, useHydrationPhase } from './use-hydration-phase'\n\n/**\n * Project-session hydration gate (Milestone B sign-off fix, EC-177 gap 1).\n *\n * In Convex mode every project session must BOOT from persistence: the\n * builder from the latest persisted graph revision, the CMS from the saved\n * schema + documents, publishing from the persisted environment states.\n * This provider runs the `@elytracms/core/operations` hydration queries\n * (`lib/operations/session-hydration`) and — like `DeploymentSetupGate` — gates its\n * children on a deterministic loading state, so fixture content never\n * flashes (and is never editable) before the persisted state arrives.\n *\n * Playground mode renders children synchronously with `session: null`\n * (fixtures), byte-identical to the pre-hydration behavior — the EC-138\n * determinism contract.\n *\n * Side effect on hydration: the content preview engine\n * (`lib/data/collection-content`) is pointed at the hydrated documents\n * BEFORE children render, so \"Preview with\" lists and section previews show\n * live entries, not fixture titles.\n *\n * EC-361 — the playground gets the SAME side effect, without the render gate:\n * the seeded in-memory adapter IS a persisted backend behind the operations\n * seam (AD-17), so the content session hydrates from it through the identical\n * `loadHydratedCms` loader connected mode uses — fire-and-forget after the\n * synchronous first render (EC-138 stays intact), refreshed per CMS surface\n * mount exactly like connected's `CmsSessionGate` → `loadCms` rhythm. Canvas\n * listing/relation population therefore resolves from in-memory session\n * documents with connected-mode semantics (published-ness = the version\n * pointer; the Draft view resolves live rows) instead of the blanket-empty\n * session the playground used to run with. The React `session`/`loadCms`\n * VALUES stay `null` in playground — the CMS surfaces keep their fixture\n * baseline (EC-320's static-baseline rule); only the preview content engine\n * reads persisted state.\n *\n * EC-308/AD-17 note — this gate deliberately does NOT read through the\n * `useOperationQuery` cache seam. It is boot/session orchestration, not a\n * cacheable server read: (1) the synchronous playground first-render is the\n * EC-138 determinism contract (a pull-query is async even when disabled);\n * (2) `configureContentSession` must run synchronously BEFORE children mount,\n * which a post-render query effect cannot guarantee; (3) `CmsSessionGate` is\n * intentionally fresh-per-visit (it remounts to show edits persisted since\n * project open) — the opposite of a cached read. It still reads through the\n * operations client (no transport bypass), and uses a user-driven retry epoch,\n * not freshness machinery — so it is outside the patchwork AD-17 retires.\n */\n\nexport interface ProjectSessionStore {\n /** Hydrated persisted state (Convex mode); `null` in playground (fixtures). */\n session: HydratedProjectSession | null\n /**\n * Load the CURRENT persisted CMS state (fresh query — the CMS section\n * remounts per visit and must show edits persisted since project open).\n * Playground resolves `null` immediately (fixtures).\n */\n loadCms(): Promise<OperationResult<HydratedCms | null>>\n}\n\nconst { Context: ProjectSessionContext, useRequired: useProjectSession } =\n createRequiredContext<ProjectSessionStore>('ProjectSession')\n\nexport function ProjectSessionProvider({\n projectId,\n children,\n}: {\n projectId: string\n children: ReactNode\n}) {\n const playground = isPlaygroundMode()\n\n const loader = useCallback(\n () => getStudioOperations().then(({ operations }) => loadProjectSession(operations, projectId)),\n [projectId],\n )\n // Point the preview content engine at the hydrated documents BEFORE any\n // child renders (panels read it module-level, synchronously). EC-291:\n // include the config-sourced routes so the canvas `resolveLink` derives a\n // rich-text link's URL exactly as delivery does. `enabled: true` below runs\n // this in the playground fast-path too (with `session: null`) — the one\n // divergence from `CmsSessionGate`, which owns no such side effect.\n const onResolved = useCallback((session: HydratedProjectSession | null) => {\n configureContentSession(\n session?.cms ? { ...session.cms, routes: resolveDeploymentRoutes() } : null,\n )\n }, [])\n const { phase, retry } = useHydrationPhase<HydratedProjectSession | null>(loader, {\n playground,\n playgroundValue: null,\n enabled: true,\n onResolved,\n })\n\n // Leaving the project resets the preview engine to fixtures; the next\n // project's gate reconfigures it before rendering its own children.\n useEffect(() => () => configureContentSession(null), [])\n\n const loadCms = useCallback(async (): Promise<OperationResult<HydratedCms | null>> => {\n const { operations } = await getStudioOperations()\n const result = await loadHydratedCms(operations, projectId)\n // Keep the preview engine in sync with the freshest persisted content.\n if (result.ok && result.value !== null)\n configureContentSession({ ...result.value, routes: resolveDeploymentRoutes() })\n // EC-361: the playground runs the SAME load (over the seeded in-memory\n // adapter) for the content-session side effect above, but the GATE value\n // stays `null` — CMS surfaces keep their config/fixture baseline (EC-320's\n // static-baseline rule), matching the pre-EC-361 playground contract. An\n // in-memory failure is loud, not a silent empty preview.\n if (playground) {\n if (!result.ok) console.error('[elytra] playground content-session load failed', result.error)\n return { ok: true, value: null }\n }\n return result\n }, [playground, projectId])\n\n // EC-361: playground boot hydration for the content preview engine — the same\n // loader connected mode gates on, fired WITHOUT gating so the synchronous\n // playground first render (EC-138) is preserved. `loadCms` owns the\n // `configureContentSession` side effect; the resolved value is discarded here.\n // Runs after `useHydrationPhase`'s fast-path effect (registered above) has\n // reset the session, so the hydrated state always lands last.\n useEffect(() => {\n if (playground) void loadCms()\n }, [playground, loadCms])\n\n return (\n <HydrationBoundary phase={phase} retry={retry}>\n {(session) => (\n <ProjectSessionContext.Provider value={{ session, loadCms }}>\n {children}\n </ProjectSessionContext.Provider>\n )}\n </HydrationBoundary>\n )\n}\n\nexport { useProjectSession }\n\n/**\n * Gate one CMS surface mount on a FRESH persisted CMS load (Convex mode).\n * Render-prop: hands the hydrated content (or `null` → fixture fallback,\n * first-run) to the caller once it resolved. Playground renders synchronously\n * with `null`. Unlike `ProjectSessionProvider`, this gate owns no\n * `configureContentSession` side effect of its own — `loadCms` (from\n * `useProjectSession`) already calls it internally when it resolves — so no\n * `onResolved`/`enabled` is passed here.\n *\n * EC-361: in playground the gate stays synchronous (fixture baseline, EC-138),\n * but `loadCms` is still fired per mount for its content-session side effect —\n * the preview engine refreshes from the in-memory adapter on the same\n * per-CMS-mount rhythm connected mode gets from the gated load.\n */\nexport function CmsSessionGate({\n children,\n}: {\n children: (cms: HydratedCms | null) => ReactNode\n}) {\n const { loadCms } = useProjectSession()\n const playground = isPlaygroundMode()\n useEffect(() => {\n if (playground) void loadCms()\n }, [playground, loadCms])\n const { phase, retry } = useHydrationPhase<HydratedCms | null>(loadCms, {\n playground,\n playgroundValue: null,\n })\n return (\n <HydrationBoundary phase={phase} retry={retry}>\n {children}\n </HydrationBoundary>\n )\n}\n"]}
|
|
@@ -9,10 +9,12 @@ import type { OperationError, OperationResult } from '@elytracms/studio/lib/oper
|
|
|
9
9
|
* flattened away:
|
|
10
10
|
*
|
|
11
11
|
* - `ProjectSessionProvider`'s playground branch calls its `onResolved` side
|
|
12
|
-
* effect (`configureContentSession(null)`
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* effect (`configureContentSession(null)` — the reset; EC-361's separate
|
|
13
|
+
* playground effect then hydrates the content session from the in-memory
|
|
14
|
+
* adapter without gating); `CmsSessionGate` passes no `onResolved` at all
|
|
15
|
+
* (it owns no such side effect — `loadCms`'s own body already calls it).
|
|
16
|
+
* `enabled` toggles whether `onResolved` fires in the playground fast-path —
|
|
17
|
+
* the ONE point of divergence between the two.
|
|
16
18
|
* - `onResolved` runs synchronously BEFORE the phase flips to `'ready'` (both
|
|
17
19
|
* the playground fast-path and the real load), so children reading a
|
|
18
20
|
* module global (e.g. `configureContentSession`) see it updated on the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-hydration-phase.js","sourceRoot":"","sources":["../../src/components/use-hydration-phase.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAEhE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AAC1D,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AAEtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"use-hydration-phase.js","sourceRoot":"","sources":["../../src/components/use-hydration-phase.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { Button, Stack, Text } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport type { OperationError, OperationResult } from '@elytracms/studio/lib/operations'\nimport { OperationErrorBanner } from './operation-error'\n\n/**\n * The shared hydration state machine (EC-329) behind both\n * `ProjectSessionProvider` and `CmsSessionGate`: playground short-circuit,\n * phase union, attempt-driven retry, cancelled-flag cleanup — a specific\n * multi-part shape the two providers used to hand-roll identically. Real\n * asymmetries between them stay explicit HOOK OPTIONS rather than being\n * flattened away:\n *\n * - `ProjectSessionProvider`'s playground branch calls its `onResolved` side\n * effect (`configureContentSession(null)` — the reset; EC-361's separate\n * playground effect then hydrates the content session from the in-memory\n * adapter without gating); `CmsSessionGate` passes no `onResolved` at all\n * (it owns no such side effect — `loadCms`'s own body already calls it).\n * `enabled` toggles whether `onResolved` fires in the playground fast-path —\n * the ONE point of divergence between the two.\n * - `onResolved` runs synchronously BEFORE the phase flips to `'ready'` (both\n * the playground fast-path and the real load), so children reading a\n * module global (e.g. `configureContentSession`) see it updated on the\n * SAME render they first see `'ready'` — a bare `loader().then(setPhase)`\n * cannot guarantee that ordering.\n */\nexport type HydrationPhase<T> =\n | { kind: 'loading' }\n | { kind: 'ready'; value: T }\n | { kind: 'error'; error: OperationError }\n\nexport interface UseHydrationPhaseOptions<T> {\n /** Playground (fixture) sessions resolve synchronously to `playgroundValue` — no loader call. */\n playground: boolean\n /** The value `phase` resolves to in playground mode. */\n playgroundValue: T\n /** Call `onResolved` for the playground fast-path too (default `false`). */\n enabled?: boolean\n /**\n * Run synchronously before the phase flips to `'ready'` — in BOTH the\n * playground fast-path (when `enabled`) and the real load. Read via a ref\n * internally, so an inline (non-memoized) callback never re-triggers a fetch.\n */\n onResolved?: (value: T) => void\n}\n\nexport interface UseHydrationPhaseResult<T> {\n phase: HydrationPhase<T>\n /** Re-run the loader (the retry button's handler). */\n retry: () => void\n}\n\n/**\n * `loader`'s IDENTITY is the refetch trigger — memoize it (`useCallback`) over\n * whatever inputs should re-hydrate (e.g. `projectId`), exactly like the\n * original two effects keyed their dependency arrays.\n */\nexport function useHydrationPhase<T>(\n loader: () => Promise<OperationResult<T>>,\n options: UseHydrationPhaseOptions<T>,\n): UseHydrationPhaseResult<T> {\n const { playground, playgroundValue, enabled = false } = options\n const onResolvedRef = useRef(options.onResolved)\n onResolvedRef.current = options.onResolved\n\n const [phase, setPhase] = useState<HydrationPhase<T>>(() =>\n playground ? { kind: 'ready', value: playgroundValue } : { kind: 'loading' },\n )\n const [attempt, setAttempt] = useState(0)\n\n useEffect(() => {\n if (playground) {\n if (enabled) onResolvedRef.current?.(playgroundValue)\n return\n }\n let cancelled = false\n setPhase({ kind: 'loading' })\n void loader().then((result) => {\n if (cancelled) return\n if (!result.ok) {\n setPhase({ kind: 'error', error: result.error })\n return\n }\n onResolvedRef.current?.(result.value)\n setPhase({ kind: 'ready', value: result.value })\n })\n return () => {\n cancelled = true\n }\n // `onResolved` is read via the ref above (see the doc comment) — its\n // identity intentionally does not retrigger this effect.\n }, [loader, playground, attempt, enabled, playgroundValue])\n\n const retry = useCallback(() => setAttempt((n) => n + 1), [])\n return { phase, retry }\n}\n\n/**\n * Renders `phase`'s loading/error frames, or `children(value)` once ready —\n * the identical 3-way branch both `ProjectSessionProvider` and\n * `CmsSessionGate` used to repeat verbatim.\n */\nexport function HydrationBoundary<T>({\n phase,\n retry,\n children,\n}: {\n phase: HydrationPhase<T>\n retry: () => void\n children: (value: T) => ReactNode\n}): ReactNode {\n if (phase.kind === 'loading') return <SessionHydrating />\n if (phase.kind === 'error') return <SessionHydrationError error={phase.error} onRetry={retry} />\n return <>{children(phase.value)}</>\n}\n\n/** Deterministic loading frame while persisted state is fetched (mirrors DeploymentSetupGate). */\nfunction SessionHydrating() {\n return (\n <div className=\"grid h-full min-h-64 flex-1 place-items-center p-8\" data-testid=\"session-hydrating\">\n <Text size=\"sm\" tone=\"muted\">\n {translate('shell.session.loading')}\n </Text>\n </div>\n )\n}\n\n/** A failed hydration is a visible, retryable state — never a fixture fallback. */\nfunction SessionHydrationError({\n error,\n onRetry,\n}: {\n error: OperationError\n onRetry: () => void\n}) {\n return (\n <div className=\"grid h-full min-h-64 flex-1 place-items-center p-8\">\n <Stack gap=\"3\" className=\"w-full max-w-xl\">\n <Text size=\"sm\" weight=\"semibold\">\n {translate('shell.session.error.title')}\n </Text>\n <OperationErrorBanner error={error} />\n <Text size=\"xs\" tone=\"muted\">\n {translate('shell.session.error.body')}\n </Text>\n <Button variant=\"outline\" size=\"sm\" onClick={onRetry} className=\"self-start\">\n {translate('common.retry')}\n </Button>\n </Stack>\n </div>\n )\n}\n"]}
|
|
@@ -11,14 +11,17 @@ import type { ListingQuery } from '@elytracms/core/runtime-renderer';
|
|
|
11
11
|
* client (closed AD-3 query vocabulary, published perspective, EC-018 locale
|
|
12
12
|
* fallback, depth-1 population, resolved assets) over a **content session**:
|
|
13
13
|
*
|
|
14
|
-
* - Playground mode (and tests): the deterministic EC-145 sample fixtures —
|
|
15
|
-
* byte-identical to the pre-hydration behavior, no backend.
|
|
16
14
|
* - Convex mode: the project-session gate (`components/studio/project-session`)
|
|
17
15
|
* configures the session with the project's HYDRATED schema + documents, so
|
|
18
16
|
* "Preview with" lists and section previews show the live content — never
|
|
19
17
|
* fixture titles (EC-177 gap 1).
|
|
18
|
+
* - Playground mode (EC-361): the SAME hydration loader runs over the seeded
|
|
19
|
+
* in-memory adapter (through operations — AD-17), so canvas listing/relation
|
|
20
|
+
* population resolves from the in-memory session documents with identical
|
|
21
|
+
* semantics. With no session configured (tests, boot instants, no persisted
|
|
22
|
+
* content) the engine serves nothing.
|
|
20
23
|
*
|
|
21
|
-
* Nothing here invents query semantics; both
|
|
24
|
+
* Nothing here invents query semantics; both modes go through the same
|
|
22
25
|
* canonical client.
|
|
23
26
|
*/
|
|
24
27
|
/** The schema + documents the preview engine serves (one project session). */
|
|
@@ -55,7 +58,9 @@ export interface ContentSessionState {
|
|
|
55
58
|
* Default (no-session) collection schema: EMPTY. The studio ships no demo content
|
|
56
59
|
* (Oli, 2026-06-19) — with no backend/content session configured, there is nothing
|
|
57
60
|
* to preview, and the studio says so rather than serving fixtures. A real project's
|
|
58
|
-
* collections + documents arrive via {@link configureContentSession}
|
|
61
|
+
* collections + documents arrive via {@link configureContentSession} in BOTH modes
|
|
62
|
+
* (EC-361): Convex hydration, or the playground's seeded in-memory adapter through
|
|
63
|
+
* the same loader.
|
|
59
64
|
*/
|
|
60
65
|
export declare const playgroundCollections: readonly CollectionDef[];
|
|
61
66
|
/**
|
|
@@ -68,9 +73,9 @@ export declare const playgroundCollections: readonly CollectionDef[];
|
|
|
68
73
|
*/
|
|
69
74
|
export type CanvasPerspective = 'draft' | 'published';
|
|
70
75
|
/**
|
|
71
|
-
* Point the preview engine at a project's hydrated content
|
|
72
|
-
*
|
|
73
|
-
*
|
|
76
|
+
* Point the preview engine at a project's hydrated content — Convex hydration
|
|
77
|
+
* or the playground's in-memory persisted state (EC-361), both through the same
|
|
78
|
+
* `loadHydratedCms` loader — or back at the empty no-session default (`null`).
|
|
74
79
|
*/
|
|
75
80
|
export declare function configureContentSession(next: ContentSessionState | null): void;
|
|
76
81
|
/** Whether a hydrated content session is configured (Convex mode). */
|
|
@@ -5,7 +5,9 @@ import { getStudioLocale } from '../locale/locale-store.js';
|
|
|
5
5
|
* Default (no-session) collection schema: EMPTY. The studio ships no demo content
|
|
6
6
|
* (Oli, 2026-06-19) — with no backend/content session configured, there is nothing
|
|
7
7
|
* to preview, and the studio says so rather than serving fixtures. A real project's
|
|
8
|
-
* collections + documents arrive via {@link configureContentSession}
|
|
8
|
+
* collections + documents arrive via {@link configureContentSession} in BOTH modes
|
|
9
|
+
* (EC-361): Convex hydration, or the playground's seeded in-memory adapter through
|
|
10
|
+
* the same loader.
|
|
9
11
|
*/
|
|
10
12
|
export const playgroundCollections = [];
|
|
11
13
|
let contentSession = null;
|
|
@@ -13,9 +15,9 @@ let lookup = createLookup(playgroundCollections, [], []);
|
|
|
13
15
|
// Keyed by `${locale}:${perspective}` (EC-300) — one deterministic client per pair.
|
|
14
16
|
const clients = new Map();
|
|
15
17
|
/**
|
|
16
|
-
* Point the preview engine at a project's hydrated content
|
|
17
|
-
*
|
|
18
|
-
*
|
|
18
|
+
* Point the preview engine at a project's hydrated content — Convex hydration
|
|
19
|
+
* or the playground's in-memory persisted state (EC-361), both through the same
|
|
20
|
+
* `loadHydratedCms` loader — or back at the empty no-session default (`null`).
|
|
19
21
|
*/
|
|
20
22
|
export function configureContentSession(next) {
|
|
21
23
|
contentSession = next;
|