@elytracms/studio 0.0.94 → 0.0.96
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/convex/asset-alt-migration.test.ts +2 -2
- package/convex/asset-meta-patch.test.ts +218 -0
- package/convex/asset-replace-delete-race.test.ts +127 -0
- package/convex/assets.ts +27 -2
- package/convex/cms.ts +124 -8
- package/convex/delivery-redirects.test.ts +167 -0
- package/convex/delivery.ts +15 -5
- package/convex/rail-write-parity.test.ts +6 -6
- package/convex/redirects.ts +1 -1
- package/convex/schema.ts +6 -0
- package/convex/strict-reference-race.test.ts +139 -0
- package/dist/addon.d.ts +1 -1
- package/dist/addon.js.map +1 -1
- package/dist/components/ai/ai-chat-panel.js +15 -2
- package/dist/components/ai/ai-chat-panel.js.map +1 -1
- package/dist/components/ai/translate-document-action.d.ts +6 -0
- package/dist/components/ai/translate-document-action.js.map +1 -1
- package/dist/components/ai/translate-document-dialog.d.ts +1 -1
- package/dist/components/ai/translate-document-dialog.js +5 -5
- package/dist/components/ai/translate-document-dialog.js.map +1 -1
- package/dist/components/assets/asset-detail.js +29 -11
- package/dist/components/assets/asset-detail.js.map +1 -1
- package/dist/components/assets/asset-locale.d.ts +16 -0
- package/dist/components/assets/asset-locale.js +30 -0
- package/dist/components/assets/asset-locale.js.map +1 -0
- package/dist/components/assets/asset-meta-drift-notice.d.ts +13 -0
- package/dist/components/assets/asset-meta-drift-notice.js +22 -0
- package/dist/components/assets/asset-meta-drift-notice.js.map +1 -0
- package/dist/components/assets/asset-meta-label.d.ts +13 -0
- package/dist/components/assets/asset-meta-label.js +17 -0
- package/dist/components/assets/asset-meta-label.js.map +1 -0
- package/dist/components/assets/asset-metadata.d.ts +89 -15
- package/dist/components/assets/asset-metadata.js +123 -11
- package/dist/components/assets/asset-metadata.js.map +1 -1
- package/dist/components/assets/asset-search.d.ts +6 -2
- package/dist/components/assets/asset-search.js +19 -3
- package/dist/components/assets/asset-search.js.map +1 -1
- package/dist/components/assets/asset-thumb.js +6 -2
- package/dist/components/assets/asset-thumb.js.map +1 -1
- package/dist/components/assets/assets-collection-view-source.d.ts +25 -9
- package/dist/components/assets/assets-collection-view-source.js +80 -26
- package/dist/components/assets/assets-collection-view-source.js.map +1 -1
- package/dist/components/assets/assets-store.js +17 -4
- package/dist/components/assets/assets-store.js.map +1 -1
- package/dist/components/builder/builder-store.d.ts +21 -0
- package/dist/components/builder/builder-store.js +22 -0
- package/dist/components/builder/builder-store.js.map +1 -1
- package/dist/components/builder/canvas-commit-boundary.d.ts +22 -0
- package/dist/components/builder/canvas-commit-boundary.js +27 -0
- package/dist/components/builder/canvas-commit-boundary.js.map +1 -0
- package/dist/components/builder/canvas-rebase.d.ts +101 -0
- package/dist/components/builder/canvas-rebase.js +179 -0
- package/dist/components/builder/canvas-rebase.js.map +1 -0
- package/dist/components/builder/canvas-render-context.d.ts +6 -3
- package/dist/components/builder/canvas-render-context.js +17 -8
- package/dist/components/builder/canvas-render-context.js.map +1 -1
- package/dist/components/builder/composition-field-editor.d.ts +26 -2
- package/dist/components/builder/composition-field-editor.js +46 -27
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/node-factory.js +15 -3
- package/dist/components/builder/node-factory.js.map +1 -1
- package/dist/components/builder/property-inspector.js +3 -1
- package/dist/components/builder/property-inspector.js.map +1 -1
- package/dist/components/builder/run-inspector.js +10 -6
- package/dist/components/builder/run-inspector.js.map +1 -1
- package/dist/components/cms/asset-field.d.ts +5 -2
- package/dist/components/cms/asset-field.js +34 -17
- package/dist/components/cms/asset-field.js.map +1 -1
- package/dist/components/cms/asset-picker-core.d.ts +2 -2
- package/dist/components/cms/asset-picker-core.js +2 -2
- package/dist/components/cms/asset-picker-core.js.map +1 -1
- package/dist/components/cms/asset-picker.js +6 -2
- package/dist/components/cms/asset-picker.js.map +1 -1
- package/dist/components/cms/canvas-commit.d.ts +62 -0
- package/dist/components/cms/canvas-commit.js +39 -0
- package/dist/components/cms/canvas-commit.js.map +1 -0
- package/dist/components/cms/cms-store.d.ts +21 -8
- package/dist/components/cms/cms-store.js +19 -10
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.js +27 -18
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/commit-policy.d.ts +6 -2
- package/dist/components/cms/commit-policy.js +1 -1
- package/dist/components/cms/commit-policy.js.map +1 -1
- package/dist/components/cms/detail-editor.js +12 -2
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/draft-commit.d.ts +16 -3
- package/dist/components/cms/draft-commit.js +3 -2
- package/dist/components/cms/draft-commit.js.map +1 -1
- package/dist/components/cms/use-document-draft.d.ts +121 -5
- package/dist/components/cms/use-document-draft.js +197 -54
- package/dist/components/cms/use-document-draft.js.map +1 -1
- package/dist/components/cms/use-live-url.d.ts +6 -1
- package/dist/components/cms/use-live-url.js +12 -9
- package/dist/components/cms/use-live-url.js.map +1 -1
- package/dist/components/collection-view/cms-collection-view-source.js +11 -5
- package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
- package/dist/components/components/transfer-dialog.js +6 -4
- package/dist/components/components/transfer-dialog.js.map +1 -1
- package/dist/components/deployment-setup-gate.js +10 -1
- package/dist/components/deployment-setup-gate.js.map +1 -1
- package/dist/components/records/record-lens.js +3 -8
- package/dist/components/records/record-lens.js.map +1 -1
- package/dist/lib/ai/chat-state.d.ts +7 -0
- package/dist/lib/ai/chat-state.js.map +1 -1
- package/dist/lib/ai/tools.d.ts +8 -0
- package/dist/lib/ai/tools.js +4 -0
- package/dist/lib/ai/tools.js.map +1 -1
- package/dist/lib/assets/upload.d.ts +25 -7
- package/dist/lib/assets/upload.js +30 -21
- package/dist/lib/assets/upload.js.map +1 -1
- package/dist/lib/cms/published-documents.d.ts +24 -6
- package/dist/lib/cms/published-documents.js +39 -6
- package/dist/lib/cms/published-documents.js.map +1 -1
- package/dist/lib/cms-views/view-model.d.ts +25 -105
- package/dist/lib/cms-views/view-model.js +36 -103
- package/dist/lib/cms-views/view-model.js.map +1 -1
- package/dist/lib/collection-view/__fixtures__/document-writes.js +1 -0
- package/dist/lib/collection-view/__fixtures__/document-writes.js.map +1 -1
- package/dist/lib/collection-view/collection-view-source.d.ts +21 -8
- package/dist/lib/collection-view/collection-view-source.js.map +1 -1
- package/dist/lib/data/collection-content.js +5 -0
- package/dist/lib/data/collection-content.js.map +1 -1
- package/dist/lib/engine-version.d.ts +1 -1
- package/dist/lib/engine-version.js +1 -1
- package/dist/lib/engine-version.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +20 -4
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +16 -1
- package/dist/lib/i18n/messages.en.js +22 -5
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/operations/cms-persistence.d.ts +80 -35
- package/dist/lib/operations/cms-persistence.js +348 -85
- package/dist/lib/operations/cms-persistence.js.map +1 -1
- package/dist/lib/operations/config-manifest-lookup.d.ts +1 -1
- package/dist/lib/operations/config-manifest-lookup.js +3 -3
- package/dist/lib/operations/config-manifest-lookup.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.js +54 -8
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/persistence/deployment-readiness.js +14 -11
- package/dist/lib/persistence/deployment-readiness.js.map +1 -1
- package/dist/lib/persona/labels.js +2 -0
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/query/index.d.ts +2 -2
- package/dist/lib/query/index.js +1 -1
- package/dist/lib/query/index.js.map +1 -1
- package/dist/lib/query/operations-query.d.ts +26 -0
- package/dist/lib/query/operations-query.js +12 -0
- package/dist/lib/query/operations-query.js.map +1 -1
- package/dist/lib/registry/index.js +2 -2
- package/dist/lib/registry/index.js.map +1 -1
- package/dist/lib/registry/preview.d.ts +10 -0
- package/dist/lib/registry/preview.js +13 -4
- package/dist/lib/registry/preview.js.map +1 -1
- package/dist/lib/routes/route-issues.js +6 -3
- package/dist/lib/routes/route-issues.js.map +1 -1
- package/dist/lib/workspace/workspace-source.js +6 -7
- package/dist/lib/workspace/workspace-source.js.map +1 -1
- package/dist/previews/entry-node.d.ts +14 -0
- package/dist/previews/entry-node.js +22 -0
- package/dist/previews/entry-node.js.map +1 -0
- package/dist/previews/harness.js +2 -16
- package/dist/previews/harness.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { DocumentPatch } from '@elytracms/core/cms-core';
|
|
2
|
+
import type { ComponentNode } from '@elytracms/core/project-graph';
|
|
3
|
+
import type { PreservedItem } from './canvas-runs.js';
|
|
4
|
+
/** One composition state: the synthetic root and the preserved typography (a history entry's shape). */
|
|
5
|
+
export interface CanvasValueState {
|
|
6
|
+
readonly root: ComponentNode;
|
|
7
|
+
readonly preserved: readonly PreservedItem[];
|
|
8
|
+
}
|
|
9
|
+
/** The stored v2 sequence a composition state stands for (THE merge order). */
|
|
10
|
+
export declare function canvasValueOf(state: CanvasValueState): unknown;
|
|
11
|
+
/** The external change between two values of the field (the canvas writer's delta; `[]` when equal). */
|
|
12
|
+
export declare function externalChange(from: unknown, to: unknown): DocumentPatch[];
|
|
13
|
+
/** A field value with the change absorbed, or `undefined` when it cannot take it. */
|
|
14
|
+
export declare function absorbIntoValue(value: unknown, change: readonly DocumentPatch[]): {
|
|
15
|
+
value: unknown;
|
|
16
|
+
} | undefined;
|
|
17
|
+
/** One composition state with the change absorbed, or `undefined` when it cannot take it. */
|
|
18
|
+
export declare function absorbChange(state: CanvasValueState, change: readonly DocumentPatch[]): CanvasValueState | undefined;
|
|
19
|
+
/** The editor state a rebase reads and writes (the builder state's composition, history and selection). */
|
|
20
|
+
export interface RebasableState {
|
|
21
|
+
readonly present: ComponentNode;
|
|
22
|
+
readonly preserved: readonly PreservedItem[];
|
|
23
|
+
/** Oldest first. */
|
|
24
|
+
readonly past: readonly CanvasValueState[];
|
|
25
|
+
/** Next redo first. */
|
|
26
|
+
readonly future: readonly CanvasValueState[];
|
|
27
|
+
readonly selectedId: string | null;
|
|
28
|
+
readonly selectedRun: number | null;
|
|
29
|
+
readonly hoveredId: string | null;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Rebase the editor onto the field's change `from` → `to` (see the module doc). Returns
|
|
33
|
+
* `undefined` when there is nothing to change.
|
|
34
|
+
*/
|
|
35
|
+
export declare function rebaseCanvas(state: RebasableState, from: unknown, to: unknown): RebasableState | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* A change of the canvas field's value made UNDERNEATH the canvas, as the document draft
|
|
38
|
+
* saw it happen: the field's value before and after the session's document moved, a
|
|
39
|
+
* sequence number (one per change, increasing), and the revision the draft is at after it
|
|
40
|
+
* (EC-647). `from` ≡ `to` when only the revision moved — the field held still while the
|
|
41
|
+
* document advanced (a write of another field landed, the canvas's own write was
|
|
42
|
+
* acknowledged): the canvas's agreed value is then valid at the newer revision too.
|
|
43
|
+
*/
|
|
44
|
+
export interface CanvasFieldChange {
|
|
45
|
+
readonly from: unknown;
|
|
46
|
+
readonly to: unknown;
|
|
47
|
+
readonly seq: number;
|
|
48
|
+
readonly revision: number | undefined;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* What the canvas lifts into the draft: its value, the value its delta is measured from,
|
|
52
|
+
* and the revision that base value was agreed at — the edit's AUTHORING revision (EC-647:
|
|
53
|
+
* a value and its revision travel together; the draft's revision may already be newer
|
|
54
|
+
* than anything the canvas has seen).
|
|
55
|
+
*/
|
|
56
|
+
export interface CanvasLift {
|
|
57
|
+
readonly value: unknown;
|
|
58
|
+
/** The value the store last agreed on with the draft — the canvas writer's delta is `base` → `value`. */
|
|
59
|
+
readonly base: unknown;
|
|
60
|
+
/** The revision `base` was agreed at (`undefined`: the draft's view had none). */
|
|
61
|
+
readonly revision: number | undefined;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* EC-630 — the two-way sync between the canvas store and the document draft (pure; the
|
|
65
|
+
* canvas's `CompositionSync` is its React shell).
|
|
66
|
+
*
|
|
67
|
+
* It tracks the value the store and the draft last AGREED on (`synced`) and — EC-647 —
|
|
68
|
+
* the revision it was agreed at, as one value:
|
|
69
|
+
* - {@link CanvasSync.stored} — the store's value changed (an edit, an undo, a rebase's
|
|
70
|
+
* adoption): lift it, with `synced` as the base of the canvas writer's delta and its
|
|
71
|
+
* revision as the edit's authoring revision, unless the draft already holds it.
|
|
72
|
+
* Measuring the delta from `synced` — never from the draft's current value — is what
|
|
73
|
+
* keeps a colleague's change that reached the draft a moment before an edit lifts out
|
|
74
|
+
* of that edit's patches (they would otherwise revert it); pairing it with `synced`'s
|
|
75
|
+
* revision — never the draft's — is what makes the store refuse that edit where it
|
|
76
|
+
* overlaps the colleague's change (Codex F12), instead of accepting it at a revision
|
|
77
|
+
* that already holds a change the canvas never saw. The canvas's own lift keeps the
|
|
78
|
+
* revision (its own edit is not a revision of the document).
|
|
79
|
+
* - {@link CanvasSync.changed} — the draft reports a change made underneath: `synced`
|
|
80
|
+
* absorbs it AND takes the change's revision (a rebase rebases the value and the
|
|
81
|
+
* revision), and the action to dispatch is a `rebase` — none when only the revision
|
|
82
|
+
* moved. Until the store has applied every rebase dispatched (its `rebased` count), a
|
|
83
|
+
* store value predates one and is not lifted (its edits are lifted from the rebased
|
|
84
|
+
* value right after).
|
|
85
|
+
*/
|
|
86
|
+
export interface CanvasSync {
|
|
87
|
+
changed(change: CanvasFieldChange): {
|
|
88
|
+
type: 'rebase';
|
|
89
|
+
from: unknown;
|
|
90
|
+
to: unknown;
|
|
91
|
+
} | undefined;
|
|
92
|
+
stored(value: unknown, rebased: number): CanvasLift | undefined;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The sync of a canvas mounted on `initial` — the draft's value of the field at `revision`
|
|
96
|
+
* (the revision the draft was at when the canvas mounted) — having seen the draft's
|
|
97
|
+
* changes up to `seenSeq`.
|
|
98
|
+
*/
|
|
99
|
+
export declare function createCanvasSync(initial: unknown, revision: number | undefined, seenSeq?: number): CanvasSync;
|
|
100
|
+
/** The selection after a rebase: kept while its node exists, else its nearest surviving ancestor, else the root. */
|
|
101
|
+
export declare function survivingSelection(before: ComponentNode, after: ComponentNode, selectedId: string | null): string | null;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { applyDocumentPatches, locateNode, semanticValueEqual, valuePathNodeIds } from '@elytracms/core/cms-core';
|
|
2
|
+
import { DEFAULT_SLOT, getNode, getParentInfo, mergeBlockRoots, splitBlockRoots } from '@elytracms/core/project-graph';
|
|
3
|
+
import { compositionPatches } from './composition-patches.js';
|
|
4
|
+
import { runByKey } from './canvas-runs.js';
|
|
5
|
+
/**
|
|
6
|
+
* EC-630 — the canvas REBASES onto a change of its field made underneath it (a colleague's
|
|
7
|
+
* patch arriving by subscription, the canonical row a write returned, a resolved
|
|
8
|
+
* conflict) instead of remounting: the change lands, and the editor's undo history and
|
|
9
|
+
* selection survive (EC-595's model — the canvas commits and rebases like every other
|
|
10
|
+
* editor). Pure; the builder reducer's `rebase` action calls {@link rebaseCanvas}.
|
|
11
|
+
*
|
|
12
|
+
* **The external change** is named by the document draft that saw it happen: the field's
|
|
13
|
+
* value before (`from`) and after (`to`) the session's document moved. Its delta is the
|
|
14
|
+
* one the canvas writer itself would emit between the two ({@link externalChange} — node
|
|
15
|
+
* patches keyed by id, or the coarser whole-field write when node verbs cannot say it: a
|
|
16
|
+
* typography change, prose being one position). Measured between two DRAFT values, the
|
|
17
|
+
* delta holds the colleague's change and nothing else — not this editor's own edits (both
|
|
18
|
+
* values hold them), not a canonicalisation of them.
|
|
19
|
+
*
|
|
20
|
+
* **The present absorbs the change** with THE merge (`applyDocumentPatches`, the function
|
|
21
|
+
* both stores run), so an edit of this editor's that the draft has not seen yet survives
|
|
22
|
+
* under it; when the present cannot take it, the incoming value wins. Never re-normalised
|
|
23
|
+
* (receiving a change never writes one back).
|
|
24
|
+
*
|
|
25
|
+
* **Undo never reverts an external change.** The history holds whole-state snapshots, so
|
|
26
|
+
* kept as they were, an undo would restore a pre-change snapshot and the next commit
|
|
27
|
+
* would write patches that silently revert the colleague. Instead every snapshot ABSORBS
|
|
28
|
+
* the same delta: an undo then restores the editor's own earlier state WITH the
|
|
29
|
+
* colleague's change in it.
|
|
30
|
+
* - A `remove` of a node the snapshot does not hold is already absorbed (nothing to
|
|
31
|
+
* resurrect).
|
|
32
|
+
* - Any other patch a snapshot cannot take (its node is not there — the colleague edited
|
|
33
|
+
* a node this editor inserted later, or moved one into it) means restoring it WOULD
|
|
34
|
+
* revert the colleague: the snapshot and every entry beyond it (older undo steps, later
|
|
35
|
+
* redo steps — reachable only through it) are dropped. Conservative: a lost undo step
|
|
36
|
+
* is recoverable, a silently destroyed colleague's edit is not.
|
|
37
|
+
* - Steps the change emptied (an undo that would now change nothing — the colleague wrote
|
|
38
|
+
* the same position later, or rewrote the whole field) are dropped, so every remaining
|
|
39
|
+
* undo does something.
|
|
40
|
+
*
|
|
41
|
+
* **The selection** survives while its node does. When the change removed it, it moves to
|
|
42
|
+
* the nearest surviving ancestor (the rule a local `remove` follows — the inspector stays
|
|
43
|
+
* where the editor was working), else the root. A selected typography RUN stays selected
|
|
44
|
+
* only while the typography is unchanged: runs carry no ids (EC-500 D1), so after a prose
|
|
45
|
+
* change a key no longer names the same run with certainty.
|
|
46
|
+
*/
|
|
47
|
+
/** The field name the canvas's own deltas are addressed at (internal — never stored, never sent). */
|
|
48
|
+
const CANVAS_FIELD = 'canvas';
|
|
49
|
+
/** The stored v2 sequence a composition state stands for (THE merge order). */
|
|
50
|
+
export function canvasValueOf(state) {
|
|
51
|
+
return mergeBlockRoots(state.root.slots?.[DEFAULT_SLOT] ?? [], state.preserved);
|
|
52
|
+
}
|
|
53
|
+
/** A composition state holding `value` under the given synthetic root (its children replaced). */
|
|
54
|
+
function stateOfValue(root, value) {
|
|
55
|
+
const split = splitBlockRoots(value);
|
|
56
|
+
return { root: { ...root, slots: { ...root.slots, [DEFAULT_SLOT]: split.roots } }, preserved: split.preserved };
|
|
57
|
+
}
|
|
58
|
+
/** The external change between two values of the field (the canvas writer's delta; `[]` when equal). */
|
|
59
|
+
export function externalChange(from, to) {
|
|
60
|
+
return compositionPatches(from, to, CANVAS_FIELD, undefined);
|
|
61
|
+
}
|
|
62
|
+
/** Whether a `remove` names a node this value does not hold (already absorbed). */
|
|
63
|
+
function removesAbsent(value, patch) {
|
|
64
|
+
if (patch.op !== 'remove')
|
|
65
|
+
return false;
|
|
66
|
+
const ids = valuePathNodeIds(patch.path);
|
|
67
|
+
const id = ids[ids.length - 1];
|
|
68
|
+
return id !== undefined && locateNode(value, id) === undefined;
|
|
69
|
+
}
|
|
70
|
+
/** A field value with the change absorbed, or `undefined` when it cannot take it. */
|
|
71
|
+
export function absorbIntoValue(value, change) {
|
|
72
|
+
const patches = change.filter((patch) => !removesAbsent(value, patch));
|
|
73
|
+
if (patches.length === 0)
|
|
74
|
+
return { value };
|
|
75
|
+
const document = { collection: CANVAS_FIELD, id: CANVAS_FIELD, values: { [CANVAS_FIELD]: value } };
|
|
76
|
+
const result = applyDocumentPatches({ document, patches, since: [] });
|
|
77
|
+
if (result.refused.length > 0)
|
|
78
|
+
return undefined;
|
|
79
|
+
return { value: result.document.values[CANVAS_FIELD] };
|
|
80
|
+
}
|
|
81
|
+
/** One composition state with the change absorbed, or `undefined` when it cannot take it. */
|
|
82
|
+
export function absorbChange(state, change) {
|
|
83
|
+
const absorbed = absorbIntoValue(canvasValueOf(state), change);
|
|
84
|
+
return absorbed ? stateOfValue(state.root, absorbed.value) : undefined;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Rebase one direction of the history (`chain` ordered from the present outward — the
|
|
88
|
+
* newest undo step first, or the next redo step first): each entry absorbs the change,
|
|
89
|
+
* the first that cannot ends the chain, and an entry equal to its neighbour toward the
|
|
90
|
+
* present is dropped. Returns the chain in the same order.
|
|
91
|
+
*/
|
|
92
|
+
function rebaseChain(present, chain, change) {
|
|
93
|
+
const out = [];
|
|
94
|
+
let neighbour = canvasValueOf(present);
|
|
95
|
+
for (const entry of chain) {
|
|
96
|
+
const absorbed = absorbChange(entry, change);
|
|
97
|
+
if (!absorbed)
|
|
98
|
+
break;
|
|
99
|
+
const value = canvasValueOf(absorbed);
|
|
100
|
+
if (semanticValueEqual(value, neighbour))
|
|
101
|
+
continue;
|
|
102
|
+
out.push(absorbed);
|
|
103
|
+
neighbour = value;
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Rebase the editor onto the field's change `from` → `to` (see the module doc). Returns
|
|
109
|
+
* `undefined` when there is nothing to change.
|
|
110
|
+
*/
|
|
111
|
+
export function rebaseCanvas(state, from, to) {
|
|
112
|
+
const change = externalChange(from, to);
|
|
113
|
+
if (change.length === 0)
|
|
114
|
+
return undefined;
|
|
115
|
+
const current = { root: state.present, preserved: state.preserved };
|
|
116
|
+
const next = absorbChange(current, change) ?? stateOfValue(state.present, to);
|
|
117
|
+
const past = rebaseChain(next, [...state.past].reverse(), change).reverse();
|
|
118
|
+
const future = rebaseChain(next, state.future, change);
|
|
119
|
+
const typographyKept = semanticValueEqual(state.preserved.map((item) => item.node), next.preserved.map((item) => item.node));
|
|
120
|
+
const roots = next.root.slots?.[DEFAULT_SLOT]?.length ?? 0;
|
|
121
|
+
return {
|
|
122
|
+
present: next.root,
|
|
123
|
+
preserved: next.preserved,
|
|
124
|
+
past,
|
|
125
|
+
future,
|
|
126
|
+
selectedId: survivingSelection(state.present, next.root, state.selectedId),
|
|
127
|
+
selectedRun: state.selectedRun !== null && typographyKept && runByKey(roots, next.preserved, state.selectedRun)
|
|
128
|
+
? state.selectedRun
|
|
129
|
+
: null,
|
|
130
|
+
hoveredId: state.hoveredId !== null && getNode(next.root, state.hoveredId) ? state.hoveredId : null,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* The sync of a canvas mounted on `initial` — the draft's value of the field at `revision`
|
|
135
|
+
* (the revision the draft was at when the canvas mounted) — having seen the draft's
|
|
136
|
+
* changes up to `seenSeq`.
|
|
137
|
+
*/
|
|
138
|
+
export function createCanvasSync(initial, revision, seenSeq = 0) {
|
|
139
|
+
let synced = initial;
|
|
140
|
+
let agreedAt = revision;
|
|
141
|
+
let seen = seenSeq;
|
|
142
|
+
let dispatched = 0;
|
|
143
|
+
return {
|
|
144
|
+
changed(change) {
|
|
145
|
+
if (change.seq <= seen)
|
|
146
|
+
return undefined;
|
|
147
|
+
seen = change.seq;
|
|
148
|
+
agreedAt = change.revision;
|
|
149
|
+
const delta = externalChange(change.from, change.to);
|
|
150
|
+
if (delta.length === 0)
|
|
151
|
+
return undefined;
|
|
152
|
+
synced = absorbIntoValue(synced, delta)?.value ?? change.to;
|
|
153
|
+
dispatched += 1;
|
|
154
|
+
return { type: 'rebase', from: change.from, to: change.to };
|
|
155
|
+
},
|
|
156
|
+
stored(value, rebased) {
|
|
157
|
+
if (rebased < dispatched)
|
|
158
|
+
return undefined;
|
|
159
|
+
if (semanticValueEqual(value, synced))
|
|
160
|
+
return undefined;
|
|
161
|
+
const base = synced;
|
|
162
|
+
synced = value;
|
|
163
|
+
return { value, base, revision: agreedAt };
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/** The selection after a rebase: kept while its node exists, else its nearest surviving ancestor, else the root. */
|
|
168
|
+
export function survivingSelection(before, after, selectedId) {
|
|
169
|
+
if (selectedId === null)
|
|
170
|
+
return null;
|
|
171
|
+
let id = selectedId;
|
|
172
|
+
while (id !== null) {
|
|
173
|
+
if (getNode(after, id))
|
|
174
|
+
return id;
|
|
175
|
+
id = getParentInfo(before, id)?.parent.id ?? null;
|
|
176
|
+
}
|
|
177
|
+
return after.id;
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=canvas-rebase.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canvas-rebase.js","sourceRoot":"","sources":["../../../src/components/builder/canvas-rebase.ts"],"names":[],"mappings":"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","sourcesContent":["import { applyDocumentPatches, locateNode, semanticValueEqual, valuePathNodeIds } from '@elytracms/core/cms-core'\nimport type { CmsDocument, DocumentPatch } from '@elytracms/core/cms-core'\nimport { DEFAULT_SLOT, getNode, getParentInfo, mergeBlockRoots, splitBlockRoots } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { compositionPatches } from './composition-patches'\nimport { runByKey } from './canvas-runs'\nimport type { PreservedItem } from './canvas-runs'\n\n/**\n * EC-630 — the canvas REBASES onto a change of its field made underneath it (a colleague's\n * patch arriving by subscription, the canonical row a write returned, a resolved\n * conflict) instead of remounting: the change lands, and the editor's undo history and\n * selection survive (EC-595's model — the canvas commits and rebases like every other\n * editor). Pure; the builder reducer's `rebase` action calls {@link rebaseCanvas}.\n *\n * **The external change** is named by the document draft that saw it happen: the field's\n * value before (`from`) and after (`to`) the session's document moved. Its delta is the\n * one the canvas writer itself would emit between the two ({@link externalChange} — node\n * patches keyed by id, or the coarser whole-field write when node verbs cannot say it: a\n * typography change, prose being one position). Measured between two DRAFT values, the\n * delta holds the colleague's change and nothing else — not this editor's own edits (both\n * values hold them), not a canonicalisation of them.\n *\n * **The present absorbs the change** with THE merge (`applyDocumentPatches`, the function\n * both stores run), so an edit of this editor's that the draft has not seen yet survives\n * under it; when the present cannot take it, the incoming value wins. Never re-normalised\n * (receiving a change never writes one back).\n *\n * **Undo never reverts an external change.** The history holds whole-state snapshots, so\n * kept as they were, an undo would restore a pre-change snapshot and the next commit\n * would write patches that silently revert the colleague. Instead every snapshot ABSORBS\n * the same delta: an undo then restores the editor's own earlier state WITH the\n * colleague's change in it.\n * - A `remove` of a node the snapshot does not hold is already absorbed (nothing to\n * resurrect).\n * - Any other patch a snapshot cannot take (its node is not there — the colleague edited\n * a node this editor inserted later, or moved one into it) means restoring it WOULD\n * revert the colleague: the snapshot and every entry beyond it (older undo steps, later\n * redo steps — reachable only through it) are dropped. Conservative: a lost undo step\n * is recoverable, a silently destroyed colleague's edit is not.\n * - Steps the change emptied (an undo that would now change nothing — the colleague wrote\n * the same position later, or rewrote the whole field) are dropped, so every remaining\n * undo does something.\n *\n * **The selection** survives while its node does. When the change removed it, it moves to\n * the nearest surviving ancestor (the rule a local `remove` follows — the inspector stays\n * where the editor was working), else the root. A selected typography RUN stays selected\n * only while the typography is unchanged: runs carry no ids (EC-500 D1), so after a prose\n * change a key no longer names the same run with certainty.\n */\n\n/** The field name the canvas's own deltas are addressed at (internal — never stored, never sent). */\nconst CANVAS_FIELD = 'canvas'\n\n/** One composition state: the synthetic root and the preserved typography (a history entry's shape). */\nexport interface CanvasValueState {\n readonly root: ComponentNode\n readonly preserved: readonly PreservedItem[]\n}\n\n/** The stored v2 sequence a composition state stands for (THE merge order). */\nexport function canvasValueOf(state: CanvasValueState): unknown {\n return mergeBlockRoots(state.root.slots?.[DEFAULT_SLOT] ?? [], state.preserved)\n}\n\n/** A composition state holding `value` under the given synthetic root (its children replaced). */\nfunction stateOfValue(root: ComponentNode, value: unknown): CanvasValueState {\n const split = splitBlockRoots(value)\n return { root: { ...root, slots: { ...root.slots, [DEFAULT_SLOT]: split.roots } }, preserved: split.preserved }\n}\n\n/** The external change between two values of the field (the canvas writer's delta; `[]` when equal). */\nexport function externalChange(from: unknown, to: unknown): DocumentPatch[] {\n return compositionPatches(from, to, CANVAS_FIELD, undefined)\n}\n\n/** Whether a `remove` names a node this value does not hold (already absorbed). */\nfunction removesAbsent(value: unknown, patch: DocumentPatch): boolean {\n if (patch.op !== 'remove') return false\n const ids = valuePathNodeIds(patch.path)\n const id = ids[ids.length - 1]\n return id !== undefined && locateNode(value, id) === undefined\n}\n\n/** A field value with the change absorbed, or `undefined` when it cannot take it. */\nexport function absorbIntoValue(value: unknown, change: readonly DocumentPatch[]): { value: unknown } | undefined {\n const patches = change.filter((patch) => !removesAbsent(value, patch))\n if (patches.length === 0) return { value }\n const document: CmsDocument = { collection: CANVAS_FIELD, id: CANVAS_FIELD, values: { [CANVAS_FIELD]: value } }\n const result = applyDocumentPatches({ document, patches, since: [] })\n if (result.refused.length > 0) return undefined\n return { value: result.document.values[CANVAS_FIELD] }\n}\n\n/** One composition state with the change absorbed, or `undefined` when it cannot take it. */\nexport function absorbChange(state: CanvasValueState, change: readonly DocumentPatch[]): CanvasValueState | undefined {\n const absorbed = absorbIntoValue(canvasValueOf(state), change)\n return absorbed ? stateOfValue(state.root, absorbed.value) : undefined\n}\n\n/**\n * Rebase one direction of the history (`chain` ordered from the present outward — the\n * newest undo step first, or the next redo step first): each entry absorbs the change,\n * the first that cannot ends the chain, and an entry equal to its neighbour toward the\n * present is dropped. Returns the chain in the same order.\n */\nfunction rebaseChain(\n present: CanvasValueState,\n chain: readonly CanvasValueState[],\n change: readonly DocumentPatch[],\n): CanvasValueState[] {\n const out: CanvasValueState[] = []\n let neighbour = canvasValueOf(present)\n for (const entry of chain) {\n const absorbed = absorbChange(entry, change)\n if (!absorbed) break\n const value = canvasValueOf(absorbed)\n if (semanticValueEqual(value, neighbour)) continue\n out.push(absorbed)\n neighbour = value\n }\n return out\n}\n\n/** The editor state a rebase reads and writes (the builder state's composition, history and selection). */\nexport interface RebasableState {\n readonly present: ComponentNode\n readonly preserved: readonly PreservedItem[]\n /** Oldest first. */\n readonly past: readonly CanvasValueState[]\n /** Next redo first. */\n readonly future: readonly CanvasValueState[]\n readonly selectedId: string | null\n readonly selectedRun: number | null\n readonly hoveredId: string | null\n}\n\n/**\n * Rebase the editor onto the field's change `from` → `to` (see the module doc). Returns\n * `undefined` when there is nothing to change.\n */\nexport function rebaseCanvas(state: RebasableState, from: unknown, to: unknown): RebasableState | undefined {\n const change = externalChange(from, to)\n if (change.length === 0) return undefined\n const current: CanvasValueState = { root: state.present, preserved: state.preserved }\n const next = absorbChange(current, change) ?? stateOfValue(state.present, to)\n const past = rebaseChain(next, [...state.past].reverse(), change).reverse()\n const future = rebaseChain(next, state.future, change)\n const typographyKept = semanticValueEqual(\n state.preserved.map((item) => item.node),\n next.preserved.map((item) => item.node),\n )\n const roots = next.root.slots?.[DEFAULT_SLOT]?.length ?? 0\n return {\n present: next.root,\n preserved: next.preserved,\n past,\n future,\n selectedId: survivingSelection(state.present, next.root, state.selectedId),\n selectedRun:\n state.selectedRun !== null && typographyKept && runByKey(roots, next.preserved, state.selectedRun)\n ? state.selectedRun\n : null,\n hoveredId: state.hoveredId !== null && getNode(next.root, state.hoveredId) ? state.hoveredId : null,\n }\n}\n\n/**\n * A change of the canvas field's value made UNDERNEATH the canvas, as the document draft\n * saw it happen: the field's value before and after the session's document moved, a\n * sequence number (one per change, increasing), and the revision the draft is at after it\n * (EC-647). `from` ≡ `to` when only the revision moved — the field held still while the\n * document advanced (a write of another field landed, the canvas's own write was\n * acknowledged): the canvas's agreed value is then valid at the newer revision too.\n */\nexport interface CanvasFieldChange {\n readonly from: unknown\n readonly to: unknown\n readonly seq: number\n readonly revision: number | undefined\n}\n\n/**\n * What the canvas lifts into the draft: its value, the value its delta is measured from,\n * and the revision that base value was agreed at — the edit's AUTHORING revision (EC-647:\n * a value and its revision travel together; the draft's revision may already be newer\n * than anything the canvas has seen).\n */\nexport interface CanvasLift {\n readonly value: unknown\n /** The value the store last agreed on with the draft — the canvas writer's delta is `base` → `value`. */\n readonly base: unknown\n /** The revision `base` was agreed at (`undefined`: the draft's view had none). */\n readonly revision: number | undefined\n}\n\n/**\n * EC-630 — the two-way sync between the canvas store and the document draft (pure; the\n * canvas's `CompositionSync` is its React shell).\n *\n * It tracks the value the store and the draft last AGREED on (`synced`) and — EC-647 —\n * the revision it was agreed at, as one value:\n * - {@link CanvasSync.stored} — the store's value changed (an edit, an undo, a rebase's\n * adoption): lift it, with `synced` as the base of the canvas writer's delta and its\n * revision as the edit's authoring revision, unless the draft already holds it.\n * Measuring the delta from `synced` — never from the draft's current value — is what\n * keeps a colleague's change that reached the draft a moment before an edit lifts out\n * of that edit's patches (they would otherwise revert it); pairing it with `synced`'s\n * revision — never the draft's — is what makes the store refuse that edit where it\n * overlaps the colleague's change (Codex F12), instead of accepting it at a revision\n * that already holds a change the canvas never saw. The canvas's own lift keeps the\n * revision (its own edit is not a revision of the document).\n * - {@link CanvasSync.changed} — the draft reports a change made underneath: `synced`\n * absorbs it AND takes the change's revision (a rebase rebases the value and the\n * revision), and the action to dispatch is a `rebase` — none when only the revision\n * moved. Until the store has applied every rebase dispatched (its `rebased` count), a\n * store value predates one and is not lifted (its edits are lifted from the rebased\n * value right after).\n */\nexport interface CanvasSync {\n changed(change: CanvasFieldChange): { type: 'rebase'; from: unknown; to: unknown } | undefined\n stored(value: unknown, rebased: number): CanvasLift | undefined\n}\n\n/**\n * The sync of a canvas mounted on `initial` — the draft's value of the field at `revision`\n * (the revision the draft was at when the canvas mounted) — having seen the draft's\n * changes up to `seenSeq`.\n */\nexport function createCanvasSync(initial: unknown, revision: number | undefined, seenSeq = 0): CanvasSync {\n let synced = initial\n let agreedAt = revision\n let seen = seenSeq\n let dispatched = 0\n return {\n changed(change) {\n if (change.seq <= seen) return undefined\n seen = change.seq\n agreedAt = change.revision\n const delta = externalChange(change.from, change.to)\n if (delta.length === 0) return undefined\n synced = absorbIntoValue(synced, delta)?.value ?? change.to\n dispatched += 1\n return { type: 'rebase', from: change.from, to: change.to }\n },\n stored(value, rebased) {\n if (rebased < dispatched) return undefined\n if (semanticValueEqual(value, synced)) return undefined\n const base = synced\n synced = value\n return { value, base, revision: agreedAt }\n },\n }\n}\n\n/** The selection after a rebase: kept while its node exists, else its nearest surviving ancestor, else the root. */\nexport function survivingSelection(before: ComponentNode, after: ComponentNode, selectedId: string | null): string | null {\n if (selectedId === null) return null\n let id: string | null = selectedId\n while (id !== null) {\n if (getNode(after, id)) return id\n id = getParentInfo(before, id)?.parent.id ?? null\n }\n return after.id\n}\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Perspective } from '@elytracms/core/cms-core';
|
|
2
|
-
import type { AssetRecord } from '@elytracms/core/persistence';
|
|
2
|
+
import type { AssetReadLocale, AssetRecord } from '@elytracms/core/persistence';
|
|
3
3
|
import type { ListingQuery, RenderAsset, RenderContext, ResolveAsset, ResolveLink, ResolveListing, ResolveRelation } from '@elytracms/core/runtime-renderer';
|
|
4
4
|
import type { ContentQueryError } from '@elytracms/core/content';
|
|
5
5
|
import type { ComponentNode } from '@elytracms/core/project-graph';
|
|
@@ -93,9 +93,12 @@ export declare function createCanvasRenderContext(deps: CanvasRenderContextDeps)
|
|
|
93
93
|
* editor-set `meta.alt` rendered on the site but not on the canvas. One reader
|
|
94
94
|
* (`resolvedAsset`, `content/assets.ts`) means the canvas and the host cannot
|
|
95
95
|
* disagree about an asset's own facts; `RenderAsset` IS `ResolvedAsset` (EC-587 —
|
|
96
|
-
* one resolved shape in every position).
|
|
96
|
+
* one resolved shape in every position). EC-632: in the canvas's locale `at` — the
|
|
97
|
+
* locale the relations, listings and links beside it resolve in — so a localized alt
|
|
98
|
+
* previews in the locale the editor has open, with delivery's fallback. Pure, exported
|
|
99
|
+
* for the node test.
|
|
97
100
|
*/
|
|
98
|
-
export declare function canvasRenderAsset(record: AssetRecord | undefined): RenderAsset | null;
|
|
101
|
+
export declare function canvasRenderAsset(record: AssetRecord | undefined, at: AssetReadLocale | null): RenderAsset | null;
|
|
99
102
|
export declare function useCanvasRenderContext(args: {
|
|
100
103
|
/** The edited composition root (`builder.present`). */
|
|
101
104
|
present: ComponentNode;
|
|
@@ -6,6 +6,7 @@ import { createRenderContext, listingFailureOf } from '@elytracms/core/runtime-r
|
|
|
6
6
|
import { translate } from '../../lib/i18n/index.js';
|
|
7
7
|
import { contentQueryErrorMessage } from '../../lib/persona/labels.js';
|
|
8
8
|
import { acquireContentSessionConsumer, contentSessionEpoch, resolveCanvasLink, resolveCanvasListing, resolveCanvasRelation, subscribeContentSession, validateCanvasListingQuery, } from '../../lib/data/index.js';
|
|
9
|
+
import { useAssetReadLocale } from '../assets/asset-locale.js';
|
|
9
10
|
import { useStudioLocale } from '../../lib/locale/index.js';
|
|
10
11
|
import { useOptionalAssets } from '../assets/assets-store.js';
|
|
11
12
|
import { useReuse } from './reuse-context.js';
|
|
@@ -113,12 +114,15 @@ export function createCanvasRenderContext(deps) {
|
|
|
113
114
|
* editor-set `meta.alt` rendered on the site but not on the canvas. One reader
|
|
114
115
|
* (`resolvedAsset`, `content/assets.ts`) means the canvas and the host cannot
|
|
115
116
|
* disagree about an asset's own facts; `RenderAsset` IS `ResolvedAsset` (EC-587 —
|
|
116
|
-
* one resolved shape in every position).
|
|
117
|
+
* one resolved shape in every position). EC-632: in the canvas's locale `at` — the
|
|
118
|
+
* locale the relations, listings and links beside it resolve in — so a localized alt
|
|
119
|
+
* previews in the locale the editor has open, with delivery's fallback. Pure, exported
|
|
120
|
+
* for the node test.
|
|
117
121
|
*/
|
|
118
|
-
export function canvasRenderAsset(record) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return resolvedAsset(record, record.url);
|
|
122
|
+
export function canvasRenderAsset(record, at) {
|
|
123
|
+
// EC-633: a record with no URL is decided by the constructor (no resolved asset), the
|
|
124
|
+
// same answer delivery gets — so both render the same empty state.
|
|
125
|
+
return record ? resolvedAsset(record, record.url, at) : null;
|
|
122
126
|
}
|
|
123
127
|
export function useCanvasRenderContext(args) {
|
|
124
128
|
const { present, selectedId, hoveredId, studio, perspective, richEditEpochs } = args;
|
|
@@ -139,9 +143,7 @@ export function useCanvasRenderContext(args) {
|
|
|
139
143
|
// library, so the canvas previews exactly what the host renders. A record
|
|
140
144
|
// without a resolved url (e.g. a never-served seed) → null → the
|
|
141
145
|
// implementation's visible empty state.
|
|
142
|
-
|
|
143
|
-
const resolveAsset = useCallback((assetId) => canvasRenderAsset(assetsStore?.assets.find((a) => a.id === assetId)), [assetsStore]);
|
|
144
|
-
// The canvas's active locale for relation/listing/link resolution below.
|
|
146
|
+
// The canvas's active locale for asset/relation/listing/link resolution below.
|
|
145
147
|
// EC-479: the ambient locale is `null` until a project's config is installed (it
|
|
146
148
|
// no longer opens on the engine's `en/de` fixture). `undefined` means "you pick" to
|
|
147
149
|
// the content engine, which then resolves in the SESSION's own default locale —
|
|
@@ -149,6 +151,13 @@ export function useCanvasRenderContext(args) {
|
|
|
149
151
|
// in the project's locale.
|
|
150
152
|
const { locale } = useStudioLocale();
|
|
151
153
|
const activeLocale = locale ?? undefined;
|
|
154
|
+
// EC-632: an asset's localized metadata (alt) reads in the same locale, falling back to
|
|
155
|
+
// the project's default — `null` (shared metadata only) before a config is installed.
|
|
156
|
+
// EC-659: the studio's ONE asset read locale, read through the project's asset collection
|
|
157
|
+
// as declared now — a translation of a field it no longer localizes is never previewed.
|
|
158
|
+
const assetLocale = useAssetReadLocale();
|
|
159
|
+
const assetsStore = useOptionalAssets();
|
|
160
|
+
const resolveAsset = useCallback((assetId) => canvasRenderAsset(assetsStore?.assets.find((a) => a.id === assetId), assetLocale), [assetsStore, assetLocale]);
|
|
152
161
|
// EC-300: the reuse capability identifies a transcluding embed + its relation prop,
|
|
153
162
|
// so the canvas can detect a HARD reference whose target doesn't resolve and show a
|
|
154
163
|
// placeholder instead of an empty section. Null-safe (no provider / no capability).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"canvas-render-context.js","sourceRoot":"","sources":["../../../src/components/builder/canvas-render-context.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useMemo, useSyncExternalStore } from 'react'\nimport { DRAFT_PERSPECTIVE } from '@elytracms/core/cms-core'\nimport type { Perspective } from '@elytracms/core/cms-core'\nimport { resolvedAsset } from '@elytracms/core/content'\nimport type { AssetRecord } from '@elytracms/core/persistence'\nimport { createRenderContext, listingFailureOf } from '@elytracms/core/runtime-renderer'\nimport type {\n ListingQuery,\n RenderAsset,\n RenderContext,\n ResolveAsset,\n ResolveLink,\n ResolveListing,\n ResolveRelation,\n} from '@elytracms/core/runtime-renderer'\nimport type { ContentQueryError } from '@elytracms/core/content'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport type { StudioRegistry } from '../../lib/registry'\nimport { translate } from '../../lib/i18n'\nimport { contentQueryErrorMessage } from '../../lib/persona/labels'\nimport {\n acquireContentSessionConsumer,\n contentSessionEpoch,\n resolveCanvasLink,\n resolveCanvasListing,\n resolveCanvasRelation,\n subscribeContentSession,\n validateCanvasListingQuery,\n} from '../../lib/data'\nimport { useStudioLocale } from '../../lib/locale'\nimport { useOptionalAssets } from '../assets/assets-store'\nimport { useReuse } from './reuse-context'\nimport { reuseEmbedRef } from './reuse'\nimport type { ReuseCapability } from './reuse'\n\n/**\n * The canvas's `RenderContext` (EC-330, extracted from `BuilderCanvas`): the\n * injected resolvers that make the iframe preview match delivery — assets\n * (EC-195), relations (EC-254), listings (EC-255), rich-text links (EC-291) —\n * plus the per-node decorator that stamps the editable wrappers (selection/hover\n * state, rich-text remount epochs, and the EC-300 unresolved-reusable\n * placeholder). The binding resolver and the EC-176 locale-fallback chips (which\n * read bound props) went with the data-binding subsystem (EC-574).\n *\n * The context assembly is a pure factory ({@link createCanvasRenderContext})\n * over a pure decision helper ({@link unresolvedReuseStatus}), so the decoration\n * and resolver wiring are unit-testable without providers;\n * {@link useCanvasRenderContext} binds them to the live stores.\n */\n\n/** Collect every node id under a root (the page subtree). */\nexport function collectIds(node: ComponentNode, out: Set<string> = new Set()): Set<string> {\n out.add(node.id)\n for (const children of Object.values(node.slots ?? {})) {\n for (const child of children) collectIds(child, out)\n }\n return out\n}\n\n/**\n * EC-300/AD-16: a transcluding embed whose target doesn't resolve in the active\n * perspective renders a placeholder, never an empty section. `unpublished` = the\n * target exists as a draft but isn't live (Published view of a draft-only reusable);\n * `missing` = no resolvable target at all (deleted). `null` = resolves fine, or the\n * node isn't an embed / has no target picked yet (its own empty state owns that).\n */\nexport function unresolvedReuseStatus(\n node: ComponentNode,\n capability: ReuseCapability | null | undefined,\n resolveAt: (ref: { collection: string; id: string }, perspective: Perspective) => unknown,\n perspective: Perspective,\n): 'unpublished' | 'missing' | null {\n if (!capability) return null\n const ref = reuseEmbedRef(node, capability)\n if (!ref) return null\n if (resolveAt(ref, perspective)) return null\n return resolveAt(ref, DRAFT_PERSPECTIVE) ? 'unpublished' : 'missing'\n}\n\n/**\n * Everything the pure context factory needs — the live SOURCE (resolvers over the\n * content session, and the session schema's verdict on a listing query) plus the\n * editor's decoration state. EC-592: the factory is a call of core's ONE\n * `createRenderContext` (`mode: 'canvas'`) — the delivery host calls the same one\n * over its baked source.\n */\nexport interface CanvasRenderContextDeps {\n registry: RenderContext['registry']\n implementations: RenderContext['implementations']\n resolveAsset: ResolveAsset\n resolveRelation: ResolveRelation\n resolveListing: ResolveListing\n resolveLink: ResolveLink\n /** Only page nodes are editable; layout chrome must not be decorated. */\n pageNodeIds: ReadonlySet<string>\n selectedId: string | null\n hoveredId: string | null\n /** Per-node remount epochs from the inline rich-text editing (EC-159). */\n richEditEpochs: Record<string, number>\n unresolvedReuse(node: ComponentNode): 'unpublished' | 'missing' | null\n /**\n * EC-460 (never-silent) / EC-581 6a: why a runnable listing query is STATICALLY\n * invalid against the active session schema (unknown collection, non-filterable\n * filter/sort field) — localized messages, or `null` when it can run. The render\n * context judges the declaration itself and shows the notice above the block —\n * the same judgment the delivery host reads from its baked refusals.\n */\n listingFailure(query: ListingQuery): readonly string[] | null\n}\n\n/**\n * The session schema's verdict on a listing query, localized (EC-494:\n * `ContentQueryError` carries a catalog ref) — the canvas's `listingFailure` source.\n */\nexport function sessionListingFailure(\n validate: (query: ListingQuery) => readonly ContentQueryError[],\n): (query: ListingQuery) => readonly string[] | null {\n return (query) => {\n const errors = validate(query)\n return errors.length === 0 ? null : errors.map((error) => contentQueryErrorMessage(error))\n }\n}\n\n/**\n * The canvas notice text for a listing node that cannot run, or `null` (EC-460, the\n * never-silent rule) — core's ONE judgment (`listingFailureOf`: the registry's verdict\n * on the declaration, then `validate`'s on the built query; a listing awaiting its\n * bound value keeps the block's own empty state). Pure, exported for the node test.\n */\nexport function canvasListingIssue(\n node: ComponentNode,\n registry: RenderContext['registry'],\n validate: (query: ListingQuery) => readonly ContentQueryError[],\n): string | null {\n const failure = listingFailureOf(node, registry, { listingFailure: sessionListingFailure(validate) })\n return failure ? failure.join(' ') : null\n}\n\n/**\n * Assemble the canvas `RenderContext` — core's ONE `createRenderContext` over the live\n * source, `mode: 'canvas'`, with the editor's decoration. Pure (no hooks): the same\n * deps produce the same context, so the decorator's wrapper markup is pinnable.\n */\nexport function createCanvasRenderContext(deps: CanvasRenderContextDeps): RenderContext {\n return createRenderContext({\n components: { registry: deps.registry, implementations: deps.implementations },\n mode: 'canvas',\n source: {\n resolveAsset: deps.resolveAsset,\n resolveRelation: deps.resolveRelation,\n resolveListing: deps.resolveListing,\n resolveLink: deps.resolveLink,\n listingFailure: deps.listingFailure,\n },\n // EC-460: a listing that cannot run gets the canvas's own (chrome-styled,\n // localized) notice ABOVE the block, which keeps its empty state below.\n listingNotice: (_node, messages) => <CanvasListingIssueNotice message={messages.join(' ')} />,\n decorateNode: (node, rendered) => {\n if (!deps.pageNodeIds.has(node.id)) return rendered\n // An unresolved hard reference shows the placeholder in place of the empty\n // transclusion; the node stays selectable, so its inspector relation chip\n // (EC-296) deep-links to the reusable to edit/publish it.\n const unresolved = deps.unresolvedReuse(node)\n return (\n <div\n // The epoch suffix forces a remount after a rich-text editing\n // session mutated this node's rendered DOM (see richEditEpochs).\n key={`${node.id}#${deps.richEditEpochs[node.id] ?? 0}`}\n data-ec-node-id={node.id}\n className=\"ec-editable\"\n data-selected={node.id === deps.selectedId || undefined}\n data-hovered={node.id === deps.hoveredId || undefined}\n >\n {unresolved ? <UnresolvedReusablePlaceholder status={unresolved} /> : rendered}\n </div>\n )\n },\n })\n}\n\n/**\n * The live canvas render context: binds the pure factory to the studio's\n * stores (asset library, content session, active locale, reuse capability) for\n * the given perspective. Returns the context plus the page-node id set the\n * canvas also needs for spacing-handle gating.\n */\n/**\n * The canvas's asset reader IS delivery's (NK-049 finding, 2026-10-23). This used to\n * hand-pick `url/width/height/alt/focalPoint`, read `alt` from the legacy column\n * only and drop `meta` — so a component's AI badge (`meta.aiGenerated`) and an\n * editor-set `meta.alt` rendered on the site but not on the canvas. One reader\n * (`resolvedAsset`, `content/assets.ts`) means the canvas and the host cannot\n * disagree about an asset's own facts; `RenderAsset` IS `ResolvedAsset` (EC-587 —\n * one resolved shape in every position). Pure, exported for the node test.\n */\nexport function canvasRenderAsset(record: AssetRecord | undefined): RenderAsset | null {\n if (!record?.url) return null\n return resolvedAsset(record, record.url)\n}\n\nexport function useCanvasRenderContext(args: {\n /** The edited composition root (`builder.present`). */\n present: ComponentNode\n selectedId: string | null\n hoveredId: string | null\n studio: StudioRegistry\n perspective: Perspective\n richEditEpochs: Record<string, number>\n}): { ctx: RenderContext; pageNodeIds: ReadonlySet<string> } {\n const { present, selectedId, hoveredId, studio, perspective, richEditEpochs } = args\n\n // EC-460: the canvas follows the preview content session reactively. The\n // epoch bumps whenever `configureContentSession` re-points the session (the\n // write-sync refresh, a CMS-surface hydration, boot) — it keys the resolver\n // callbacks below, so the context identity changes and the canvas re-renders\n // against the fresh session. Registering as a CONSUMER tells the write-sync a\n // preview surface is live, so a stale session actually refreshes (with no\n // consumer it only flags — connected mode never pays a per-autosave query\n // while nobody is previewing).\n const sessionEpoch = useSyncExternalStore(subscribeContentSession, contentSessionEpoch)\n useEffect(() => acquireContentSessionConsumer(), [])\n\n // Only page nodes are editable; layout chrome (rendered around the page) is\n // read-only context and must not be selectable (it isn't in the page tree).\n const pageNodeIds = useMemo(() => collectIds(present), [present])\n\n // EC-195: resolve an `asset`-typed Image prop (an asset id) from the live asset\n // library, so the canvas previews exactly what the host renders. A record\n // without a resolved url (e.g. a never-served seed) → null → the\n // implementation's visible empty state.\n const assetsStore = useOptionalAssets()\n const resolveAsset: ResolveAsset = useCallback(\n (assetId) => canvasRenderAsset(assetsStore?.assets.find((a) => a.id === assetId)),\n [assetsStore],\n )\n\n // The canvas's active locale for relation/listing/link resolution below.\n // EC-479: the ambient locale is `null` until a project's config is installed (it\n // no longer opens on the engine's `en/de` fixture). `undefined` means \"you pick\" to\n // the content engine, which then resolves in the SESSION's own default locale —\n // never a fabricated one — so a canvas mounted outside a CMS session still previews\n // in the project's locale.\n const { locale } = useStudioLocale()\n const activeLocale = locale ?? undefined\n // EC-300: the reuse capability identifies a transcluding embed + its relation prop,\n // so the canvas can detect a HARD reference whose target doesn't resolve and show a\n // placeholder instead of an empty section. Null-safe (no provider / no capability).\n const reuse = useReuse()\n\n // EC-254: populate a block's `relation` prop (the editor's document pick — a\n // ProductGrid's products, a Reusable's element) to delivery shape from the\n // active content session, in the canvas's active locale, so the canvas previews\n // picked documents exactly as the host renders them. The studio analog of the\n // host's baked relations map. A missing/invisible target → null → the block's\n // own empty state.\n const resolveRelation: ResolveRelation = useCallback(\n (ref) => resolveCanvasRelation(ref, activeLocale, perspective),\n // sessionEpoch: re-resolve when the content session was re-pointed (EC-460).\n [activeLocale, perspective, sessionEpoch],\n )\n\n // EC-255: resolve a listing block's query (a ProductGrid keyed to a category) to\n // its documents from the active content session, in the canvas's active locale, so\n // the canvas previews a dynamic grid exactly as the host renders it. The studio\n // analog of the host's baked listings map; an empty/invalid query → [] → the\n // block's own empty state.\n const resolveListing: ResolveListing = useCallback(\n (query) => resolveCanvasListing(query, activeLocale, perspective),\n [activeLocale, perspective, sessionEpoch],\n )\n\n // EC-291: resolve a rich-text internal link (`{type:'reference'}`) to its target's\n // URL path from the active content session, in the canvas's active locale — the\n // studio analog of the host's baked links map. A missing/unpublished/unroutable\n // target → null → the rich-text renderer shows the visible `unresolved-ref` issue.\n const resolveLink: ResolveLink = useCallback(\n (ref) => resolveCanvasLink(ref, activeLocale, perspective),\n [activeLocale, perspective, sessionEpoch],\n )\n\n const unresolvedReuse = useCallback(\n (node: ComponentNode): 'unpublished' | 'missing' | null =>\n unresolvedReuseStatus(\n node,\n reuse?.capability,\n (ref, p) => resolveCanvasRelation(ref, activeLocale, p),\n perspective,\n ),\n [reuse, activeLocale, perspective, sessionEpoch],\n )\n\n // EC-460 (never-silent): the session schema's verdict on a listing query.\n const listingFailure = useMemo(\n () => sessionListingFailure((query) => validateCanvasListingQuery(query)),\n [sessionEpoch],\n )\n\n const ctx: RenderContext = useMemo(\n () =>\n createCanvasRenderContext({\n registry: studio.registry,\n implementations: studio.implementations,\n resolveAsset,\n resolveRelation,\n resolveListing,\n resolveLink,\n pageNodeIds,\n selectedId,\n hoveredId,\n richEditEpochs,\n unresolvedReuse,\n listingFailure,\n }),\n [studio, resolveAsset, resolveRelation, resolveListing, resolveLink, unresolvedReuse, pageNodeIds, selectedId, hoveredId, richEditEpochs, listingFailure],\n )\n\n return { ctx, pageNodeIds }\n}\n\n/**\n * In-canvas placeholder for a transcluding embed whose target doesn't resolve\n * (EC-300/AD-16) — shown in place of the empty section so a hard reference never\n * silently vanishes. Rendered INTO the iframe (portal), which carries only the\n * project's stylesheet, so it uses inline styles rather than studio UI classes\n * (the EC-414 AD-B chrome contract), with the `.ec-chrome` containment recipe\n * on its root. Exported for the chrome contract test. The node stays\n * selectable; the inspector's `element` relation chip (EC-296) deep-links to\n * the reusable to edit or publish it.\n */\n/**\n * In-canvas notice for a listing block whose query is statically invalid\n * against the active session schema (EC-460 — the never-silent rule): the\n * message renders ABOVE the block (which keeps its own empty state), so a\n * misdeclared `listing` manifest (non-filterable sort/filter field, unknown\n * collection) is an explicit, visible state — never an empty grid an editor\n * can't tell apart from \"no content yet\". Rendered INTO the iframe (project\n * stylesheet only), so it uses inline styles + the `.ec-chrome` containment\n * recipe, exactly like {@link UnresolvedReusablePlaceholder}. Exported for the\n * chrome contract test.\n */\nexport function CanvasListingIssueNotice({ message }: { message: string }) {\n return (\n <div\n className=\"ec-chrome\"\n data-testid=\"canvas-listing-issue\"\n style={{\n boxSizing: 'border-box',\n border: '1px dashed rgba(200,140,40,0.65)',\n borderRadius: 8,\n padding: '10px 14px',\n margin: '8px 16px',\n background: 'rgba(230,160,40,0.08)',\n color: 'rgba(120,80,10,0.95)',\n font: '13px/1.5 system-ui, -apple-system, sans-serif',\n }}\n >\n <span style={{ fontWeight: 600 }}>⚠ {translate('builder.canvas.listingInvalidTitle')}</span>{' '}\n <span style={{ opacity: 0.9 }}>{message}</span>\n </div>\n )\n}\n\nexport function UnresolvedReusablePlaceholder({ status }: { status: 'unpublished' | 'missing' }) {\n const title =\n status === 'unpublished'\n ? translate('builder.canvas.reusableUnpublishedTitle')\n : translate('builder.canvas.reusableMissingTitle')\n const hint =\n status === 'unpublished'\n ? translate('builder.canvas.reusableUnpublishedHint')\n : translate('builder.canvas.reusableMissingHint')\n return (\n <div\n className=\"ec-chrome\"\n style={{\n boxSizing: 'border-box',\n border: '1px dashed rgba(120,120,120,0.5)',\n borderRadius: 8,\n padding: '24px 28px',\n margin: '12px 16px',\n background: 'rgba(127,127,127,0.06)',\n color: 'rgba(90,90,90,0.95)',\n font: '14px/1.5 system-ui, -apple-system, sans-serif',\n textAlign: 'center',\n }}\n >\n <div style={{ fontWeight: 600 }}>⚠ {title}</div>\n <div style={{ marginTop: 6, fontSize: 13, opacity: 0.85 }}>{hint}</div>\n </div>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"canvas-render-context.js","sourceRoot":"","sources":["../../../src/components/builder/canvas-render-context.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useMemo, useSyncExternalStore } from 'react'\nimport { DRAFT_PERSPECTIVE } from '@elytracms/core/cms-core'\nimport type { Perspective } from '@elytracms/core/cms-core'\nimport { resolvedAsset } from '@elytracms/core/content'\nimport type { AssetReadLocale, AssetRecord } from '@elytracms/core/persistence'\nimport { createRenderContext, listingFailureOf } from '@elytracms/core/runtime-renderer'\nimport type {\n ListingQuery,\n RenderAsset,\n RenderContext,\n ResolveAsset,\n ResolveLink,\n ResolveListing,\n ResolveRelation,\n} from '@elytracms/core/runtime-renderer'\nimport type { ContentQueryError } from '@elytracms/core/content'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport type { StudioRegistry } from '../../lib/registry'\nimport { translate } from '../../lib/i18n'\nimport { contentQueryErrorMessage } from '../../lib/persona/labels'\nimport {\n acquireContentSessionConsumer,\n contentSessionEpoch,\n resolveCanvasLink,\n resolveCanvasListing,\n resolveCanvasRelation,\n subscribeContentSession,\n validateCanvasListingQuery,\n} from '../../lib/data'\nimport { useAssetReadLocale } from '../assets/asset-locale'\nimport { useStudioLocale } from '../../lib/locale'\nimport { useOptionalAssets } from '../assets/assets-store'\nimport { useReuse } from './reuse-context'\nimport { reuseEmbedRef } from './reuse'\nimport type { ReuseCapability } from './reuse'\n\n/**\n * The canvas's `RenderContext` (EC-330, extracted from `BuilderCanvas`): the\n * injected resolvers that make the iframe preview match delivery — assets\n * (EC-195), relations (EC-254), listings (EC-255), rich-text links (EC-291) —\n * plus the per-node decorator that stamps the editable wrappers (selection/hover\n * state, rich-text remount epochs, and the EC-300 unresolved-reusable\n * placeholder). The binding resolver and the EC-176 locale-fallback chips (which\n * read bound props) went with the data-binding subsystem (EC-574).\n *\n * The context assembly is a pure factory ({@link createCanvasRenderContext})\n * over a pure decision helper ({@link unresolvedReuseStatus}), so the decoration\n * and resolver wiring are unit-testable without providers;\n * {@link useCanvasRenderContext} binds them to the live stores.\n */\n\n/** Collect every node id under a root (the page subtree). */\nexport function collectIds(node: ComponentNode, out: Set<string> = new Set()): Set<string> {\n out.add(node.id)\n for (const children of Object.values(node.slots ?? {})) {\n for (const child of children) collectIds(child, out)\n }\n return out\n}\n\n/**\n * EC-300/AD-16: a transcluding embed whose target doesn't resolve in the active\n * perspective renders a placeholder, never an empty section. `unpublished` = the\n * target exists as a draft but isn't live (Published view of a draft-only reusable);\n * `missing` = no resolvable target at all (deleted). `null` = resolves fine, or the\n * node isn't an embed / has no target picked yet (its own empty state owns that).\n */\nexport function unresolvedReuseStatus(\n node: ComponentNode,\n capability: ReuseCapability | null | undefined,\n resolveAt: (ref: { collection: string; id: string }, perspective: Perspective) => unknown,\n perspective: Perspective,\n): 'unpublished' | 'missing' | null {\n if (!capability) return null\n const ref = reuseEmbedRef(node, capability)\n if (!ref) return null\n if (resolveAt(ref, perspective)) return null\n return resolveAt(ref, DRAFT_PERSPECTIVE) ? 'unpublished' : 'missing'\n}\n\n/**\n * Everything the pure context factory needs — the live SOURCE (resolvers over the\n * content session, and the session schema's verdict on a listing query) plus the\n * editor's decoration state. EC-592: the factory is a call of core's ONE\n * `createRenderContext` (`mode: 'canvas'`) — the delivery host calls the same one\n * over its baked source.\n */\nexport interface CanvasRenderContextDeps {\n registry: RenderContext['registry']\n implementations: RenderContext['implementations']\n resolveAsset: ResolveAsset\n resolveRelation: ResolveRelation\n resolveListing: ResolveListing\n resolveLink: ResolveLink\n /** Only page nodes are editable; layout chrome must not be decorated. */\n pageNodeIds: ReadonlySet<string>\n selectedId: string | null\n hoveredId: string | null\n /** Per-node remount epochs from the inline rich-text editing (EC-159). */\n richEditEpochs: Record<string, number>\n unresolvedReuse(node: ComponentNode): 'unpublished' | 'missing' | null\n /**\n * EC-460 (never-silent) / EC-581 6a: why a runnable listing query is STATICALLY\n * invalid against the active session schema (unknown collection, non-filterable\n * filter/sort field) — localized messages, or `null` when it can run. The render\n * context judges the declaration itself and shows the notice above the block —\n * the same judgment the delivery host reads from its baked refusals.\n */\n listingFailure(query: ListingQuery): readonly string[] | null\n}\n\n/**\n * The session schema's verdict on a listing query, localized (EC-494:\n * `ContentQueryError` carries a catalog ref) — the canvas's `listingFailure` source.\n */\nexport function sessionListingFailure(\n validate: (query: ListingQuery) => readonly ContentQueryError[],\n): (query: ListingQuery) => readonly string[] | null {\n return (query) => {\n const errors = validate(query)\n return errors.length === 0 ? null : errors.map((error) => contentQueryErrorMessage(error))\n }\n}\n\n/**\n * The canvas notice text for a listing node that cannot run, or `null` (EC-460, the\n * never-silent rule) — core's ONE judgment (`listingFailureOf`: the registry's verdict\n * on the declaration, then `validate`'s on the built query; a listing awaiting its\n * bound value keeps the block's own empty state). Pure, exported for the node test.\n */\nexport function canvasListingIssue(\n node: ComponentNode,\n registry: RenderContext['registry'],\n validate: (query: ListingQuery) => readonly ContentQueryError[],\n): string | null {\n const failure = listingFailureOf(node, registry, { listingFailure: sessionListingFailure(validate) })\n return failure ? failure.join(' ') : null\n}\n\n/**\n * Assemble the canvas `RenderContext` — core's ONE `createRenderContext` over the live\n * source, `mode: 'canvas'`, with the editor's decoration. Pure (no hooks): the same\n * deps produce the same context, so the decorator's wrapper markup is pinnable.\n */\nexport function createCanvasRenderContext(deps: CanvasRenderContextDeps): RenderContext {\n return createRenderContext({\n components: { registry: deps.registry, implementations: deps.implementations },\n mode: 'canvas',\n source: {\n resolveAsset: deps.resolveAsset,\n resolveRelation: deps.resolveRelation,\n resolveListing: deps.resolveListing,\n resolveLink: deps.resolveLink,\n listingFailure: deps.listingFailure,\n },\n // EC-460: a listing that cannot run gets the canvas's own (chrome-styled,\n // localized) notice ABOVE the block, which keeps its empty state below.\n listingNotice: (_node, messages) => <CanvasListingIssueNotice message={messages.join(' ')} />,\n decorateNode: (node, rendered) => {\n if (!deps.pageNodeIds.has(node.id)) return rendered\n // An unresolved hard reference shows the placeholder in place of the empty\n // transclusion; the node stays selectable, so its inspector relation chip\n // (EC-296) deep-links to the reusable to edit/publish it.\n const unresolved = deps.unresolvedReuse(node)\n return (\n <div\n // The epoch suffix forces a remount after a rich-text editing\n // session mutated this node's rendered DOM (see richEditEpochs).\n key={`${node.id}#${deps.richEditEpochs[node.id] ?? 0}`}\n data-ec-node-id={node.id}\n className=\"ec-editable\"\n data-selected={node.id === deps.selectedId || undefined}\n data-hovered={node.id === deps.hoveredId || undefined}\n >\n {unresolved ? <UnresolvedReusablePlaceholder status={unresolved} /> : rendered}\n </div>\n )\n },\n })\n}\n\n/**\n * The live canvas render context: binds the pure factory to the studio's\n * stores (asset library, content session, active locale, reuse capability) for\n * the given perspective. Returns the context plus the page-node id set the\n * canvas also needs for spacing-handle gating.\n */\n/**\n * The canvas's asset reader IS delivery's (NK-049 finding, 2026-10-23). This used to\n * hand-pick `url/width/height/alt/focalPoint`, read `alt` from the legacy column\n * only and drop `meta` — so a component's AI badge (`meta.aiGenerated`) and an\n * editor-set `meta.alt` rendered on the site but not on the canvas. One reader\n * (`resolvedAsset`, `content/assets.ts`) means the canvas and the host cannot\n * disagree about an asset's own facts; `RenderAsset` IS `ResolvedAsset` (EC-587 —\n * one resolved shape in every position). EC-632: in the canvas's locale `at` — the\n * locale the relations, listings and links beside it resolve in — so a localized alt\n * previews in the locale the editor has open, with delivery's fallback. Pure, exported\n * for the node test.\n */\nexport function canvasRenderAsset(record: AssetRecord | undefined, at: AssetReadLocale | null): RenderAsset | null {\n // EC-633: a record with no URL is decided by the constructor (no resolved asset), the\n // same answer delivery gets — so both render the same empty state.\n return record ? resolvedAsset(record, record.url, at) : null\n}\n\nexport function useCanvasRenderContext(args: {\n /** The edited composition root (`builder.present`). */\n present: ComponentNode\n selectedId: string | null\n hoveredId: string | null\n studio: StudioRegistry\n perspective: Perspective\n richEditEpochs: Record<string, number>\n}): { ctx: RenderContext; pageNodeIds: ReadonlySet<string> } {\n const { present, selectedId, hoveredId, studio, perspective, richEditEpochs } = args\n\n // EC-460: the canvas follows the preview content session reactively. The\n // epoch bumps whenever `configureContentSession` re-points the session (the\n // write-sync refresh, a CMS-surface hydration, boot) — it keys the resolver\n // callbacks below, so the context identity changes and the canvas re-renders\n // against the fresh session. Registering as a CONSUMER tells the write-sync a\n // preview surface is live, so a stale session actually refreshes (with no\n // consumer it only flags — connected mode never pays a per-autosave query\n // while nobody is previewing).\n const sessionEpoch = useSyncExternalStore(subscribeContentSession, contentSessionEpoch)\n useEffect(() => acquireContentSessionConsumer(), [])\n\n // Only page nodes are editable; layout chrome (rendered around the page) is\n // read-only context and must not be selectable (it isn't in the page tree).\n const pageNodeIds = useMemo(() => collectIds(present), [present])\n\n // EC-195: resolve an `asset`-typed Image prop (an asset id) from the live asset\n // library, so the canvas previews exactly what the host renders. A record\n // without a resolved url (e.g. a never-served seed) → null → the\n // implementation's visible empty state.\n // The canvas's active locale for asset/relation/listing/link resolution below.\n // EC-479: the ambient locale is `null` until a project's config is installed (it\n // no longer opens on the engine's `en/de` fixture). `undefined` means \"you pick\" to\n // the content engine, which then resolves in the SESSION's own default locale —\n // never a fabricated one — so a canvas mounted outside a CMS session still previews\n // in the project's locale.\n const { locale } = useStudioLocale()\n const activeLocale = locale ?? undefined\n // EC-632: an asset's localized metadata (alt) reads in the same locale, falling back to\n // the project's default — `null` (shared metadata only) before a config is installed.\n // EC-659: the studio's ONE asset read locale, read through the project's asset collection\n // as declared now — a translation of a field it no longer localizes is never previewed.\n const assetLocale = useAssetReadLocale()\n const assetsStore = useOptionalAssets()\n const resolveAsset: ResolveAsset = useCallback(\n (assetId) => canvasRenderAsset(assetsStore?.assets.find((a) => a.id === assetId), assetLocale),\n [assetsStore, assetLocale],\n )\n // EC-300: the reuse capability identifies a transcluding embed + its relation prop,\n // so the canvas can detect a HARD reference whose target doesn't resolve and show a\n // placeholder instead of an empty section. Null-safe (no provider / no capability).\n const reuse = useReuse()\n\n // EC-254: populate a block's `relation` prop (the editor's document pick — a\n // ProductGrid's products, a Reusable's element) to delivery shape from the\n // active content session, in the canvas's active locale, so the canvas previews\n // picked documents exactly as the host renders them. The studio analog of the\n // host's baked relations map. A missing/invisible target → null → the block's\n // own empty state.\n const resolveRelation: ResolveRelation = useCallback(\n (ref) => resolveCanvasRelation(ref, activeLocale, perspective),\n // sessionEpoch: re-resolve when the content session was re-pointed (EC-460).\n [activeLocale, perspective, sessionEpoch],\n )\n\n // EC-255: resolve a listing block's query (a ProductGrid keyed to a category) to\n // its documents from the active content session, in the canvas's active locale, so\n // the canvas previews a dynamic grid exactly as the host renders it. The studio\n // analog of the host's baked listings map; an empty/invalid query → [] → the\n // block's own empty state.\n const resolveListing: ResolveListing = useCallback(\n (query) => resolveCanvasListing(query, activeLocale, perspective),\n [activeLocale, perspective, sessionEpoch],\n )\n\n // EC-291: resolve a rich-text internal link (`{type:'reference'}`) to its target's\n // URL path from the active content session, in the canvas's active locale — the\n // studio analog of the host's baked links map. A missing/unpublished/unroutable\n // target → null → the rich-text renderer shows the visible `unresolved-ref` issue.\n const resolveLink: ResolveLink = useCallback(\n (ref) => resolveCanvasLink(ref, activeLocale, perspective),\n [activeLocale, perspective, sessionEpoch],\n )\n\n const unresolvedReuse = useCallback(\n (node: ComponentNode): 'unpublished' | 'missing' | null =>\n unresolvedReuseStatus(\n node,\n reuse?.capability,\n (ref, p) => resolveCanvasRelation(ref, activeLocale, p),\n perspective,\n ),\n [reuse, activeLocale, perspective, sessionEpoch],\n )\n\n // EC-460 (never-silent): the session schema's verdict on a listing query.\n const listingFailure = useMemo(\n () => sessionListingFailure((query) => validateCanvasListingQuery(query)),\n [sessionEpoch],\n )\n\n const ctx: RenderContext = useMemo(\n () =>\n createCanvasRenderContext({\n registry: studio.registry,\n implementations: studio.implementations,\n resolveAsset,\n resolveRelation,\n resolveListing,\n resolveLink,\n pageNodeIds,\n selectedId,\n hoveredId,\n richEditEpochs,\n unresolvedReuse,\n listingFailure,\n }),\n [studio, resolveAsset, resolveRelation, resolveListing, resolveLink, unresolvedReuse, pageNodeIds, selectedId, hoveredId, richEditEpochs, listingFailure],\n )\n\n return { ctx, pageNodeIds }\n}\n\n/**\n * In-canvas placeholder for a transcluding embed whose target doesn't resolve\n * (EC-300/AD-16) — shown in place of the empty section so a hard reference never\n * silently vanishes. Rendered INTO the iframe (portal), which carries only the\n * project's stylesheet, so it uses inline styles rather than studio UI classes\n * (the EC-414 AD-B chrome contract), with the `.ec-chrome` containment recipe\n * on its root. Exported for the chrome contract test. The node stays\n * selectable; the inspector's `element` relation chip (EC-296) deep-links to\n * the reusable to edit or publish it.\n */\n/**\n * In-canvas notice for a listing block whose query is statically invalid\n * against the active session schema (EC-460 — the never-silent rule): the\n * message renders ABOVE the block (which keeps its own empty state), so a\n * misdeclared `listing` manifest (non-filterable sort/filter field, unknown\n * collection) is an explicit, visible state — never an empty grid an editor\n * can't tell apart from \"no content yet\". Rendered INTO the iframe (project\n * stylesheet only), so it uses inline styles + the `.ec-chrome` containment\n * recipe, exactly like {@link UnresolvedReusablePlaceholder}. Exported for the\n * chrome contract test.\n */\nexport function CanvasListingIssueNotice({ message }: { message: string }) {\n return (\n <div\n className=\"ec-chrome\"\n data-testid=\"canvas-listing-issue\"\n style={{\n boxSizing: 'border-box',\n border: '1px dashed rgba(200,140,40,0.65)',\n borderRadius: 8,\n padding: '10px 14px',\n margin: '8px 16px',\n background: 'rgba(230,160,40,0.08)',\n color: 'rgba(120,80,10,0.95)',\n font: '13px/1.5 system-ui, -apple-system, sans-serif',\n }}\n >\n <span style={{ fontWeight: 600 }}>⚠ {translate('builder.canvas.listingInvalidTitle')}</span>{' '}\n <span style={{ opacity: 0.9 }}>{message}</span>\n </div>\n )\n}\n\nexport function UnresolvedReusablePlaceholder({ status }: { status: 'unpublished' | 'missing' }) {\n const title =\n status === 'unpublished'\n ? translate('builder.canvas.reusableUnpublishedTitle')\n : translate('builder.canvas.reusableMissingTitle')\n const hint =\n status === 'unpublished'\n ? translate('builder.canvas.reusableUnpublishedHint')\n : translate('builder.canvas.reusableMissingHint')\n return (\n <div\n className=\"ec-chrome\"\n style={{\n boxSizing: 'border-box',\n border: '1px dashed rgba(120,120,120,0.5)',\n borderRadius: 8,\n padding: '24px 28px',\n margin: '12px 16px',\n background: 'rgba(127,127,127,0.06)',\n color: 'rgba(90,90,90,0.95)',\n font: '14px/1.5 system-ui, -apple-system, sans-serif',\n textAlign: 'center',\n }}\n >\n <div style={{ fontWeight: 600 }}>⚠ {title}</div>\n <div style={{ marginTop: 6, fontSize: 13, opacity: 0.85 }}>{hint}</div>\n </div>\n )\n}\n"]}
|
|
@@ -5,6 +5,8 @@ import type { ComponentRegistry } from '@elytracms/core/component-registry';
|
|
|
5
5
|
import type { BlockNode, BlockSequenceValue } from '@elytracms/core/rich-text';
|
|
6
6
|
import type { MessageKey } from '../../lib/i18n/index.js';
|
|
7
7
|
import type { CompositionFieldScope } from './builder-store.js';
|
|
8
|
+
import type { CanvasFieldChange, CanvasLift } from './canvas-rebase.js';
|
|
9
|
+
import type { CanvasCommitChannel } from './canvas-commit-boundary.js';
|
|
8
10
|
import type { InspectorPane } from '../inspector/inspector-sidebar.js';
|
|
9
11
|
/**
|
|
10
12
|
* A sequence item the canvas does not natively edit (EC-547 / EC-500 D3):
|
|
@@ -65,10 +67,32 @@ export declare function CanvasRoleNotices({ notices }: {
|
|
|
65
67
|
* canvas without it. The value is not touched — nothing mounts that could write.
|
|
66
68
|
*/
|
|
67
69
|
export declare function NoStackCanvasNotice(): import("react").JSX.Element;
|
|
68
|
-
export declare function CompositionFieldEditor({ field, value, onChange, readOnly, trailingPanes, conflicts, }: {
|
|
70
|
+
export declare function CompositionFieldEditor({ field, value, revision, change, onChange, commits, readOnly, trailingPanes, conflicts, }: {
|
|
69
71
|
field: BlocksFieldDef;
|
|
72
|
+
/** The field's value at mount (the store is the source of truth while open). */
|
|
70
73
|
value: unknown;
|
|
71
|
-
|
|
74
|
+
/**
|
|
75
|
+
* EC-647 — the revision the draft was at when {@link value} was read (its value and its
|
|
76
|
+
* revision travel together): the revision the canvas's first edit is authored at.
|
|
77
|
+
*/
|
|
78
|
+
revision: number | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* EC-630 — the latest change of the field made UNDERNEATH the canvas (the document
|
|
81
|
+
* draft reports it); the store rebases onto it — and, EC-647, onto its revision.
|
|
82
|
+
*/
|
|
83
|
+
change?: CanvasFieldChange;
|
|
84
|
+
/**
|
|
85
|
+
* The store's value, lifted, with the value it was last agreed on with the draft — the
|
|
86
|
+
* base the canvas writer's delta is measured from — and the revision that was agreed
|
|
87
|
+
* at (EC-647: the edit's authoring revision).
|
|
88
|
+
*/
|
|
89
|
+
onChange: (lift: CanvasLift) => void;
|
|
90
|
+
/**
|
|
91
|
+
* EC-630 — the document's committer, for the inspector controls' commit boundaries
|
|
92
|
+
* (focus / blur / Enter at a position): a burst of typing commits on its control kind's
|
|
93
|
+
* policy, never per keystroke. Absent → every edit commits as the canvas writes it.
|
|
94
|
+
*/
|
|
95
|
+
commits?: CanvasCommitChannel;
|
|
72
96
|
readOnly: boolean;
|
|
73
97
|
/**
|
|
74
98
|
* EC-595 A2 — the field's per-field conflicts (a refused node patch: "changed by X —
|