@elytracms/studio 0.0.94 → 0.0.95

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.
Files changed (126) hide show
  1. package/convex/asset-alt-migration.test.ts +2 -2
  2. package/convex/asset-meta-patch.test.ts +218 -0
  3. package/convex/asset-replace-delete-race.test.ts +127 -0
  4. package/convex/assets.ts +27 -2
  5. package/convex/delivery-redirects.test.ts +167 -0
  6. package/convex/delivery.ts +15 -5
  7. package/convex/rail-write-parity.test.ts +6 -6
  8. package/convex/redirects.ts +1 -1
  9. package/convex/schema.ts +6 -0
  10. package/dist/addon.d.ts +1 -1
  11. package/dist/addon.js.map +1 -1
  12. package/dist/components/assets/asset-detail.js +26 -11
  13. package/dist/components/assets/asset-detail.js.map +1 -1
  14. package/dist/components/assets/asset-locale.d.ts +9 -0
  15. package/dist/components/assets/asset-locale.js +16 -0
  16. package/dist/components/assets/asset-locale.js.map +1 -0
  17. package/dist/components/assets/asset-meta-label.d.ts +12 -0
  18. package/dist/components/assets/asset-meta-label.js +15 -0
  19. package/dist/components/assets/asset-meta-label.js.map +1 -0
  20. package/dist/components/assets/asset-metadata.d.ts +69 -15
  21. package/dist/components/assets/asset-metadata.js +99 -11
  22. package/dist/components/assets/asset-metadata.js.map +1 -1
  23. package/dist/components/assets/asset-search.js +8 -2
  24. package/dist/components/assets/asset-search.js.map +1 -1
  25. package/dist/components/assets/asset-thumb.js +6 -2
  26. package/dist/components/assets/asset-thumb.js.map +1 -1
  27. package/dist/components/assets/assets-collection-view-source.d.ts +23 -9
  28. package/dist/components/assets/assets-collection-view-source.js +78 -26
  29. package/dist/components/assets/assets-collection-view-source.js.map +1 -1
  30. package/dist/components/assets/assets-store.js +17 -4
  31. package/dist/components/assets/assets-store.js.map +1 -1
  32. package/dist/components/builder/builder-store.d.ts +21 -0
  33. package/dist/components/builder/builder-store.js +22 -0
  34. package/dist/components/builder/builder-store.js.map +1 -1
  35. package/dist/components/builder/canvas-commit-boundary.d.ts +22 -0
  36. package/dist/components/builder/canvas-commit-boundary.js +27 -0
  37. package/dist/components/builder/canvas-commit-boundary.js.map +1 -0
  38. package/dist/components/builder/canvas-rebase.d.ts +77 -0
  39. package/dist/components/builder/canvas-rebase.js +169 -0
  40. package/dist/components/builder/canvas-rebase.js.map +1 -0
  41. package/dist/components/builder/canvas-render-context.d.ts +6 -3
  42. package/dist/components/builder/canvas-render-context.js +17 -9
  43. package/dist/components/builder/canvas-render-context.js.map +1 -1
  44. package/dist/components/builder/composition-field-editor.d.ts +20 -2
  45. package/dist/components/builder/composition-field-editor.js +45 -27
  46. package/dist/components/builder/composition-field-editor.js.map +1 -1
  47. package/dist/components/builder/node-factory.js +15 -3
  48. package/dist/components/builder/node-factory.js.map +1 -1
  49. package/dist/components/builder/property-inspector.js +3 -1
  50. package/dist/components/builder/property-inspector.js.map +1 -1
  51. package/dist/components/builder/run-inspector.js +10 -6
  52. package/dist/components/builder/run-inspector.js.map +1 -1
  53. package/dist/components/cms/asset-field.d.ts +5 -2
  54. package/dist/components/cms/asset-field.js +34 -17
  55. package/dist/components/cms/asset-field.js.map +1 -1
  56. package/dist/components/cms/asset-picker.js +3 -1
  57. package/dist/components/cms/asset-picker.js.map +1 -1
  58. package/dist/components/cms/canvas-commit.d.ts +56 -0
  59. package/dist/components/cms/canvas-commit.js +35 -0
  60. package/dist/components/cms/canvas-commit.js.map +1 -0
  61. package/dist/components/cms/collection-table.js +23 -7
  62. package/dist/components/cms/collection-table.js.map +1 -1
  63. package/dist/components/cms/commit-policy.d.ts +6 -2
  64. package/dist/components/cms/commit-policy.js +1 -1
  65. package/dist/components/cms/commit-policy.js.map +1 -1
  66. package/dist/components/cms/detail-editor.js +11 -1
  67. package/dist/components/cms/detail-editor.js.map +1 -1
  68. package/dist/components/cms/use-document-draft.d.ts +62 -4
  69. package/dist/components/cms/use-document-draft.js +102 -32
  70. package/dist/components/cms/use-document-draft.js.map +1 -1
  71. package/dist/components/cms/use-live-url.d.ts +6 -1
  72. package/dist/components/cms/use-live-url.js +12 -9
  73. package/dist/components/cms/use-live-url.js.map +1 -1
  74. package/dist/components/collection-view/cms-collection-view-source.js +9 -5
  75. package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
  76. package/dist/components/deployment-setup-gate.js +10 -1
  77. package/dist/components/deployment-setup-gate.js.map +1 -1
  78. package/dist/components/records/record-lens.js +3 -8
  79. package/dist/components/records/record-lens.js.map +1 -1
  80. package/dist/lib/assets/upload.d.ts +25 -7
  81. package/dist/lib/assets/upload.js +30 -21
  82. package/dist/lib/assets/upload.js.map +1 -1
  83. package/dist/lib/cms/published-documents.d.ts +24 -6
  84. package/dist/lib/cms/published-documents.js +39 -6
  85. package/dist/lib/cms/published-documents.js.map +1 -1
  86. package/dist/lib/cms-views/view-model.d.ts +14 -101
  87. package/dist/lib/cms-views/view-model.js +20 -101
  88. package/dist/lib/cms-views/view-model.js.map +1 -1
  89. package/dist/lib/collection-view/collection-view-source.d.ts +6 -1
  90. package/dist/lib/collection-view/collection-view-source.js.map +1 -1
  91. package/dist/lib/engine-version.d.ts +1 -1
  92. package/dist/lib/engine-version.js +1 -1
  93. package/dist/lib/engine-version.js.map +1 -1
  94. package/dist/lib/i18n/messages.de.js +7 -3
  95. package/dist/lib/i18n/messages.de.js.map +1 -1
  96. package/dist/lib/i18n/messages.en.d.ts +5 -1
  97. package/dist/lib/i18n/messages.en.js +8 -4
  98. package/dist/lib/i18n/messages.en.js.map +1 -1
  99. package/dist/lib/operations/config-manifest-lookup.d.ts +1 -1
  100. package/dist/lib/operations/config-manifest-lookup.js +3 -3
  101. package/dist/lib/operations/config-manifest-lookup.js.map +1 -1
  102. package/dist/lib/persistence/deployment-readiness.js +14 -11
  103. package/dist/lib/persistence/deployment-readiness.js.map +1 -1
  104. package/dist/lib/persona/labels.js +1 -0
  105. package/dist/lib/persona/labels.js.map +1 -1
  106. package/dist/lib/query/index.d.ts +2 -2
  107. package/dist/lib/query/index.js +1 -1
  108. package/dist/lib/query/index.js.map +1 -1
  109. package/dist/lib/query/operations-query.d.ts +26 -0
  110. package/dist/lib/query/operations-query.js +12 -0
  111. package/dist/lib/query/operations-query.js.map +1 -1
  112. package/dist/lib/registry/index.js +2 -2
  113. package/dist/lib/registry/index.js.map +1 -1
  114. package/dist/lib/registry/preview.d.ts +10 -0
  115. package/dist/lib/registry/preview.js +13 -4
  116. package/dist/lib/registry/preview.js.map +1 -1
  117. package/dist/lib/routes/route-issues.js +6 -3
  118. package/dist/lib/routes/route-issues.js.map +1 -1
  119. package/dist/lib/workspace/workspace-source.js +6 -7
  120. package/dist/lib/workspace/workspace-source.js.map +1 -1
  121. package/dist/previews/entry-node.d.ts +14 -0
  122. package/dist/previews/entry-node.js +22 -0
  123. package/dist/previews/entry-node.js.map +1 -0
  124. package/dist/previews/harness.js +2 -16
  125. package/dist/previews/harness.js.map +1 -1
  126. package/package.json +3 -3
@@ -0,0 +1,27 @@
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useEffect, useRef } from 'react';
3
+ import { FieldCommitBoundary } from '../cms/field-collaboration.js';
4
+ const CanvasCommitContext = createContext(null);
5
+ export const CanvasCommitProvider = CanvasCommitContext.Provider;
6
+ export function CanvasCommitBoundary({ position, policy, children, }) {
7
+ const channel = useContext(CanvasCommitContext);
8
+ const focusedRef = useRef(false);
9
+ const latest = useRef({ channel, position });
10
+ latest.current = { channel, position };
11
+ // A control that unmounts while focused (its node removed underneath, the selection
12
+ // moved) never sees a blur: leave it here, so its burst commits and focus is released.
13
+ useEffect(() => () => {
14
+ if (focusedRef.current)
15
+ latest.current.channel?.blurred(latest.current.position);
16
+ }, []);
17
+ if (!channel)
18
+ return _jsx(_Fragment, { children: children });
19
+ return (_jsx(FieldCommitBoundary, { onFocusField: () => {
20
+ focusedRef.current = true;
21
+ channel.focused(position, policy);
22
+ }, onBlurField: () => {
23
+ focusedRef.current = false;
24
+ channel.blurred(position);
25
+ }, onEnter: () => channel.entered(position), children: children }));
26
+ }
27
+ //# sourceMappingURL=canvas-commit-boundary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canvas-commit-boundary.js","sourceRoot":"","sources":["../../../src/components/builder/canvas-commit-boundary.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;AAEpE,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AAkBhE,MAAM,mBAAmB,GAAG,aAAa,CAA6B,IAAI,CAAC,CAAA;AAE3E,MAAM,CAAC,MAAM,oBAAoB,GAAG,mBAAmB,CAAC,QAAQ,CAAA;AAEhE,MAAM,UAAU,oBAAoB,CAAC,EACnC,QAAQ,EACR,MAAM,EACN,QAAQ,GAKT;IACC,MAAM,OAAO,GAAG,UAAU,CAAC,mBAAmB,CAAC,CAAA;IAC/C,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;IAChC,MAAM,MAAM,GAAG,MAAM,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAA;IAC5C,MAAM,CAAC,OAAO,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAA;IACtC,oFAAoF;IACpF,uFAAuF;IACvF,SAAS,CACP,GAAG,EAAE,CAAC,GAAG,EAAE;QACT,IAAI,UAAU,CAAC,OAAO;YAAE,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;IAClF,CAAC,EACD,EAAE,CACH,CAAA;IACD,IAAI,CAAC,OAAO;QAAE,OAAO,4BAAG,QAAQ,GAAI,CAAA;IACpC,OAAO,CACL,KAAC,mBAAmB,IAClB,YAAY,EAAE,GAAG,EAAE;YACjB,UAAU,CAAC,OAAO,GAAG,IAAI,CAAA;YACzB,OAAO,CAAC,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAA;QACnC,CAAC,EACD,WAAW,EAAE,GAAG,EAAE;YAChB,UAAU,CAAC,OAAO,GAAG,KAAK,CAAA;YAC1B,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;QAC3B,CAAC,EACD,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,YAEvC,QAAQ,GACW,CACvB,CAAA;AACH,CAAC","sourcesContent":["import { createContext, useContext, useEffect, useRef } from 'react'\nimport type { ReactNode } from 'react'\nimport { FieldCommitBoundary } from '../cms/field-collaboration'\nimport type { CommitPolicy } from '../cms/commit-policy'\nimport type { CanvasControlPosition } from '../cms/canvas-commit'\n\n/**\n * EC-630 — the canvas inspector's commit boundary. The document editor hands the canvas a\n * channel to its committer ({@link CanvasCommitChannel}); every inspector control that\n * types (a prop's control, a typography run's editor) stands inside a boundary naming the\n * position it writes and its kind's policy (`commitPolicyOf`), so a burst of typing is\n * committed on blur / Enter / the idle pause like a form field's — never per keystroke.\n * Without a channel (a surface with no document — the component preview) it is inert.\n */\nexport interface CanvasCommitChannel {\n focused(position: CanvasControlPosition, policy: CommitPolicy): void\n blurred(position: CanvasControlPosition): void\n entered(position: CanvasControlPosition): void\n}\n\nconst CanvasCommitContext = createContext<CanvasCommitChannel | null>(null)\n\nexport const CanvasCommitProvider = CanvasCommitContext.Provider\n\nexport function CanvasCommitBoundary({\n position,\n policy,\n children,\n}: {\n position: CanvasControlPosition\n policy: CommitPolicy\n children: ReactNode\n}) {\n const channel = useContext(CanvasCommitContext)\n const focusedRef = useRef(false)\n const latest = useRef({ channel, position })\n latest.current = { channel, position }\n // A control that unmounts while focused (its node removed underneath, the selection\n // moved) never sees a blur: leave it here, so its burst commits and focus is released.\n useEffect(\n () => () => {\n if (focusedRef.current) latest.current.channel?.blurred(latest.current.position)\n },\n [],\n )\n if (!channel) return <>{children}</>\n return (\n <FieldCommitBoundary\n onFocusField={() => {\n focusedRef.current = true\n channel.focused(position, policy)\n }}\n onBlurField={() => {\n focusedRef.current = false\n channel.blurred(position)\n }}\n onEnter={() => channel.entered(position)}\n >\n {children}\n </FieldCommitBoundary>\n )\n}\n"]}
@@ -0,0 +1,77 @@
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, and a
39
+ * sequence number (one per change, increasing).
40
+ */
41
+ export interface CanvasFieldChange {
42
+ readonly from: unknown;
43
+ readonly to: unknown;
44
+ readonly seq: number;
45
+ }
46
+ /** What the canvas lifts into the draft: its value and the value its delta is measured from. */
47
+ export interface CanvasLift {
48
+ readonly value: unknown;
49
+ /** The value the store last agreed on with the draft — the canvas writer's delta is `base` → `value`. */
50
+ readonly base: unknown;
51
+ }
52
+ /**
53
+ * EC-630 — the two-way sync between the canvas store and the document draft (pure; the
54
+ * canvas's `CompositionSync` is its React shell).
55
+ *
56
+ * It tracks the value the store and the draft last AGREED on (`synced`):
57
+ * - {@link CanvasSync.stored} — the store's value changed (an edit, an undo, a rebase's
58
+ * adoption): lift it, with `synced` as the base of the canvas writer's delta, unless
59
+ * the draft already holds it. Measuring the delta from `synced` — never from the draft's
60
+ * current value — is what keeps a colleague's change that reached the draft a moment
61
+ * before an edit lifts out of that edit's patches (they would otherwise revert it).
62
+ * - {@link CanvasSync.changed} — the draft reports a change made underneath: `synced`
63
+ * absorbs it, and the action to dispatch is a `rebase`. Until the store has applied
64
+ * every rebase dispatched (its `rebased` count), a store value predates one and is not
65
+ * lifted (its edits are lifted from the rebased value right after).
66
+ */
67
+ export interface CanvasSync {
68
+ changed(change: CanvasFieldChange): {
69
+ type: 'rebase';
70
+ from: unknown;
71
+ to: unknown;
72
+ } | undefined;
73
+ stored(value: unknown, rebased: number): CanvasLift | undefined;
74
+ }
75
+ export declare function createCanvasSync(initial: unknown, seenSeq?: number): CanvasSync;
76
+ /** The selection after a rebase: kept while its node exists, else its nearest surviving ancestor, else the root. */
77
+ export declare function survivingSelection(before: ComponentNode, after: ComponentNode, selectedId: string | null): string | null;
@@ -0,0 +1,169 @@
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
+ export function createCanvasSync(initial, seenSeq = 0) {
134
+ let synced = initial;
135
+ let seen = seenSeq;
136
+ let dispatched = 0;
137
+ return {
138
+ changed(change) {
139
+ if (change.seq <= seen)
140
+ return undefined;
141
+ seen = change.seq;
142
+ synced = absorbIntoValue(synced, externalChange(change.from, change.to))?.value ?? change.to;
143
+ dispatched += 1;
144
+ return { type: 'rebase', from: change.from, to: change.to };
145
+ },
146
+ stored(value, rebased) {
147
+ if (rebased < dispatched)
148
+ return undefined;
149
+ if (semanticValueEqual(value, synced))
150
+ return undefined;
151
+ const base = synced;
152
+ synced = value;
153
+ return { value, base };
154
+ },
155
+ };
156
+ }
157
+ /** The selection after a rebase: kept while its node exists, else its nearest surviving ancestor, else the root. */
158
+ export function survivingSelection(before, after, selectedId) {
159
+ if (selectedId === null)
160
+ return null;
161
+ let id = selectedId;
162
+ while (id !== null) {
163
+ if (getNode(after, id))
164
+ return id;
165
+ id = getParentInfo(before, id)?.parent.id ?? null;
166
+ }
167
+ return after.id;
168
+ }
169
+ //# 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, and a\n * sequence number (one per change, increasing).\n */\nexport interface CanvasFieldChange {\n readonly from: unknown\n readonly to: unknown\n readonly seq: number\n}\n\n/** What the canvas lifts into the draft: its value and the value its delta is measured from. */\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}\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`):\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, unless\n * the draft already holds it. Measuring the delta from `synced` — never from the draft's\n * current value — is what keeps a colleague's change that reached the draft a moment\n * before an edit lifts out of that edit's patches (they would otherwise revert it).\n * - {@link CanvasSync.changed} — the draft reports a change made underneath: `synced`\n * absorbs it, and the action to dispatch is a `rebase`. Until the store has applied\n * every rebase dispatched (its `rebased` count), a store value predates one and is not\n * lifted (its edits are lifted from the rebased 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\nexport function createCanvasSync(initial: unknown, seenSeq = 0): CanvasSync {\n let synced = initial\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 synced = absorbIntoValue(synced, externalChange(change.from, change.to))?.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 }\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). Pure, exported for the node test.
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;
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useMemo, useSyncExternalStore } from 'react';
3
3
  import { DRAFT_PERSPECTIVE } from '@elytracms/core/cms-core';
4
4
  import { resolvedAsset } from '@elytracms/core/content';
5
+ import { assetReadLocale } from '@elytracms/core/persistence';
5
6
  import { createRenderContext, listingFailureOf } from '@elytracms/core/runtime-renderer';
6
7
  import { translate } from '../../lib/i18n/index.js';
7
8
  import { contentQueryErrorMessage } from '../../lib/persona/labels.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). Pure, exported for the node test.
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
- if (!record?.url)
120
- return null;
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,16 +143,20 @@ 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
- const assetsStore = useOptionalAssets();
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 —
148
150
  // never a fabricated one — so a canvas mounted outside a CMS session still previews
149
151
  // in the project's locale.
150
- const { locale } = useStudioLocale();
152
+ const { locale, config: localeConfig } = 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
+ const defaultLocale = localeConfig?.default;
157
+ const assetLocale = useMemo(() => (defaultLocale === undefined ? null : assetReadLocale({ default: defaultLocale }, activeLocale)), [defaultLocale, activeLocale]);
158
+ const assetsStore = useOptionalAssets();
159
+ const resolveAsset = useCallback((assetId) => canvasRenderAsset(assetsStore?.assets.find((a) => a.id === assetId), assetLocale), [assetsStore, assetLocale]);
152
160
  // EC-300: the reuse capability identifies a transcluding embed + its relation prop,
153
161
  // so the canvas can detect a HARD reference whose target doesn't resolve and show a
154
162
  // 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 { assetReadLocale } from '@elytracms/core/persistence'\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 { 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, config: localeConfig } = 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 const defaultLocale = localeConfig?.default\n const assetLocale = useMemo(\n () => (defaultLocale === undefined ? null : assetReadLocale({ default: defaultLocale }, activeLocale)),\n [defaultLocale, activeLocale],\n )\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 } 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,26 @@ 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, 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
- onChange: (value: unknown) => void;
74
+ /**
75
+ * EC-630 — the latest change of the field made UNDERNEATH the canvas (the document
76
+ * draft reports it); the store rebases onto it.
77
+ */
78
+ change?: CanvasFieldChange;
79
+ /**
80
+ * The store's value, lifted, and the value it was last agreed on with the draft — the
81
+ * base the canvas writer's delta is measured from.
82
+ */
83
+ onChange: (value: unknown, base: unknown) => void;
84
+ /**
85
+ * EC-630 — the document's committer, for the inspector controls' commit boundaries
86
+ * (focus / blur / Enter at a position): a burst of typing commits on its control kind's
87
+ * policy, never per keystroke. Absent → every edit commits as the canvas writes it.
88
+ */
89
+ commits?: CanvasCommitChannel;
72
90
  readOnly: boolean;
73
91
  /**
74
92
  * EC-595 A2 — the field's per-field conflicts (a refused node patch: "changed by X —