@elytracms/studio 0.0.100 → 0.0.101

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 (124) hide show
  1. package/convex/analytics-cost.test.ts +1 -1
  2. package/convex/redirect-migration.test.ts +1 -1
  3. package/convex/redirects.ts +2 -2
  4. package/convex/shared-capture.convex.test.ts +165 -0
  5. package/dist/components/ai/ai-chat-panel-lazy.js +7 -2
  6. package/dist/components/ai/ai-chat-panel-lazy.js.map +1 -1
  7. package/dist/components/ai/proposal-card.js +2 -1
  8. package/dist/components/ai/proposal-card.js.map +1 -1
  9. package/dist/components/ai/proposal-unfilled.d.ts +14 -0
  10. package/dist/components/ai/proposal-unfilled.js +33 -0
  11. package/dist/components/ai/proposal-unfilled.js.map +1 -0
  12. package/dist/components/ai/translate-document-action.js +8 -6
  13. package/dist/components/ai/translate-document-action.js.map +1 -1
  14. package/dist/components/app-views/studio-config.d.ts +12 -12
  15. package/dist/components/app-views/studio-config.js.map +1 -1
  16. package/dist/components/builder/builder-store.d.ts +37 -1
  17. package/dist/components/builder/builder-store.js +21 -6
  18. package/dist/components/builder/builder-store.js.map +1 -1
  19. package/dist/components/builder/canvas-chrome-css.d.ts +1 -1
  20. package/dist/components/builder/canvas-chrome-css.js +1 -1
  21. package/dist/components/builder/canvas-chrome-css.js.map +1 -1
  22. package/dist/components/builder/canvas-chrome.d.ts +1 -1
  23. package/dist/components/builder/canvas-chrome.js +1 -1
  24. package/dist/components/builder/canvas-chrome.js.map +1 -1
  25. package/dist/components/builder/canvas-rebase.d.ts +4 -4
  26. package/dist/components/builder/canvas-rebase.js +3 -2
  27. package/dist/components/builder/canvas-rebase.js.map +1 -1
  28. package/dist/components/builder/canvas-render-context.d.ts +1 -1
  29. package/dist/components/builder/canvas-render-context.js +1 -1
  30. package/dist/components/builder/canvas-render-context.js.map +1 -1
  31. package/dist/components/builder/composition-field-editor-lazy.js +2 -1
  32. package/dist/components/builder/composition-field-editor-lazy.js.map +1 -1
  33. package/dist/components/builder/reuse-context.d.ts +8 -2
  34. package/dist/components/builder/reuse-context.js +62 -7
  35. package/dist/components/builder/reuse-context.js.map +1 -1
  36. package/dist/components/builder/reuse.d.ts +106 -70
  37. package/dist/components/builder/reuse.js +125 -108
  38. package/dist/components/builder/reuse.js.map +1 -1
  39. package/dist/components/cms/cms-store.d.ts +9 -1
  40. package/dist/components/cms/cms-store.js +1 -0
  41. package/dist/components/cms/cms-store.js.map +1 -1
  42. package/dist/components/cms/detail-editor.js +13 -13
  43. package/dist/components/cms/detail-editor.js.map +1 -1
  44. package/dist/components/cms/prop-fields.js +1 -1
  45. package/dist/components/cms/prop-fields.js.map +1 -1
  46. package/dist/components/cms/rich-text/lazy.js +2 -1
  47. package/dist/components/cms/rich-text/lazy.js.map +1 -1
  48. package/dist/components/cms/rich-text/toolbar-state.d.ts +1 -1
  49. package/dist/components/cms/rich-text/toolbar-state.js +1 -1
  50. package/dist/components/cms/rich-text/toolbar-state.js.map +1 -1
  51. package/dist/components/cms/use-field-focus.js +3 -3
  52. package/dist/components/cms/use-field-focus.js.map +1 -1
  53. package/dist/components/cms/version-compare-lazy.js +2 -1
  54. package/dist/components/cms/version-compare-lazy.js.map +1 -1
  55. package/dist/components/components/components-index.js +1 -1
  56. package/dist/components/components/components-index.js.map +1 -1
  57. package/dist/components/deployment-setup-gate.d.ts +1 -1
  58. package/dist/components/deployment-setup-gate.js +1 -1
  59. package/dist/components/deployment-setup-gate.js.map +1 -1
  60. package/dist/components/load-failure.d.ts +62 -0
  61. package/dist/components/load-failure.js +110 -0
  62. package/dist/components/load-failure.js.map +1 -0
  63. package/dist/components/notifications/shell-findings.js +3 -3
  64. package/dist/components/notifications/shell-findings.js.map +1 -1
  65. package/dist/customization.d.ts +3 -3
  66. package/dist/customization.js.map +1 -1
  67. package/dist/lib/ai/apply-content.js +99 -28
  68. package/dist/lib/ai/apply-content.js.map +1 -1
  69. package/dist/lib/ai/chat-context.d.ts +73 -7
  70. package/dist/lib/ai/chat-context.js +133 -14
  71. package/dist/lib/ai/chat-context.js.map +1 -1
  72. package/dist/lib/ai/chat-state.d.ts +7 -1
  73. package/dist/lib/ai/chat-state.js.map +1 -1
  74. package/dist/lib/ai/constraints.d.ts +5 -0
  75. package/dist/lib/ai/constraints.js +21 -6
  76. package/dist/lib/ai/constraints.js.map +1 -1
  77. package/dist/lib/ai/fake-provider.d.ts +76 -26
  78. package/dist/lib/ai/fake-provider.js +210 -66
  79. package/dist/lib/ai/fake-provider.js.map +1 -1
  80. package/dist/lib/ai/index.d.ts +1 -0
  81. package/dist/lib/ai/index.js +1 -0
  82. package/dist/lib/ai/index.js.map +1 -1
  83. package/dist/lib/ai/keyless-assistant.d.ts +37 -0
  84. package/dist/lib/ai/keyless-assistant.js +193 -0
  85. package/dist/lib/ai/keyless-assistant.js.map +1 -0
  86. package/dist/lib/ai/shape.d.ts +25 -2
  87. package/dist/lib/ai/shape.js +58 -7
  88. package/dist/lib/ai/shape.js.map +1 -1
  89. package/dist/lib/ai/tools.d.ts +11 -1
  90. package/dist/lib/ai/tools.js +51 -6
  91. package/dist/lib/ai/tools.js.map +1 -1
  92. package/dist/lib/ai/translate-document.js +6 -5
  93. package/dist/lib/ai/translate-document.js.map +1 -1
  94. package/dist/lib/ai/types.d.ts +22 -7
  95. package/dist/lib/ai/types.js.map +1 -1
  96. package/dist/lib/ai/update-fields.js +9 -6
  97. package/dist/lib/ai/update-fields.js.map +1 -1
  98. package/dist/lib/ai/writer-form.d.ts +11 -3
  99. package/dist/lib/ai/writer-form.js +105 -18
  100. package/dist/lib/ai/writer-form.js.map +1 -1
  101. package/dist/lib/auth-errors.d.ts +1 -1
  102. package/dist/lib/auth-errors.js +1 -1
  103. package/dist/lib/auth-errors.js.map +1 -1
  104. package/dist/lib/engine-version.d.ts +1 -1
  105. package/dist/lib/engine-version.js +1 -1
  106. package/dist/lib/engine-version.js.map +1 -1
  107. package/dist/lib/i18n/messages.de.d.ts +1 -1
  108. package/dist/lib/i18n/messages.de.js +23 -7
  109. package/dist/lib/i18n/messages.de.js.map +1 -1
  110. package/dist/lib/i18n/messages.en.d.ts +18 -3
  111. package/dist/lib/i18n/messages.en.js +29 -9
  112. package/dist/lib/i18n/messages.en.js.map +1 -1
  113. package/dist/lib/operations/cms-persistence.d.ts +18 -0
  114. package/dist/lib/operations/cms-persistence.js +47 -0
  115. package/dist/lib/operations/cms-persistence.js.map +1 -1
  116. package/dist/lib/workspace/index.d.ts +3 -3
  117. package/dist/lib/workspace/index.js +3 -3
  118. package/dist/lib/workspace/index.js.map +1 -1
  119. package/dist/router/navigate-once.d.ts +1 -1
  120. package/dist/router/navigate-once.js.map +1 -1
  121. package/dist/router/root.d.ts +3 -2
  122. package/dist/router/root.js +25 -4
  123. package/dist/router/root.js.map +1 -1
  124. package/package.json +3 -3
@@ -1 +1 @@
1
- {"version":3,"file":"canvas-chrome.js","sourceRoot":"","sources":["../../../src/components/builder/canvas-chrome.ts"],"names":[],"mappings":"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","sourcesContent":["import type { StudioCanvasTheme } from '../app-views/studio-config'\nimport { CANVAS_BACKDROP_LAYER_STATEMENT, canvasChromeCss } from './canvas-chrome-css'\nimport type { CanvasWorld } from './canvas-chrome-css'\n\nexport { canvasChromeCss, CANVAS_BACKDROP_LAYER_STATEMENT } from './canvas-chrome-css'\nexport type { CanvasWorld } from './canvas-chrome-css'\n\n/**\n * The canvas iframe's document chrome (EC-389, reshaped by EC-414) — the\n * framework-free half of `BuilderCanvas`'s `wireUp` (house pattern, EC-330):\n * everything injected into the preview document lives here, in ONE pinned\n * cascade order, unit-testable without jsdom.\n *\n * EC-414 (AD-A/AD-C): the studio design system is leaving the canvas document.\n * Which world a project lives in is decided by a RUNTIME PROBE of the loaded\n * canvas stylesheet — no flag, no config, zero consumer breakage:\n *\n * - `legacy` (marker tokens present): the merged pre-EC-414 canvas.css that\n * bundles `@elytracms/studio/styles.css` with the consumer theme. The\n * EC-389 body-class default and today's behavior apply EXACTLY.\n * - `new` (markers absent): the consumer's standalone full-Tailwind theme\n * entry. No studio utilities exist in the document, so the body-class\n * default retires and the neutral hand-written backdrop applies instead.\n */\n\n/**\n * The studio design system's marker tokens (AD-C probe). These are the plain\n * `:root` custom properties from the studio's generated `tokens.css`, which any\n * merged legacy build carries VERBATIM — unlike `@theme inline` variables\n * (e.g. `--color-panel`), which Tailwind v4 inlines into utilities and never\n * emits as custom properties (verified against the repo's Tailwind 4.3.2; the\n * task file's `--color-panel` example probe would classify every legacy build\n * as new). Two tokens ANDed so a consumer theme that happens to define one\n * generic name (`--panel`) cannot be mistaken for the studio design system.\n */\nexport const CANVAS_LEGACY_MARKER_TOKENS = ['--panel', '--grid-line'] as const\n\n/**\n * A Tailwind v4 build unconditionally emits `--default-font-family` on\n * `:root, :host` (preflight consumes it) — its absence after a successful\n * stylesheet load means the canvas stylesheet is not a Tailwind theme entry at\n * all (AD-D item 2's \"defines no @theme tokens\" invalid state).\n */\nexport const CANVAS_TAILWIND_MARKER_TOKEN = '--default-font-family'\n\n/** The settled classification of the canvas stylesheet (AD-C + AD-D item 2). */\nexport type CanvasThemeStatus =\n /** Pre-EC-414 merged canvas.css — studio design system + consumer theme. */\n | { kind: 'legacy' }\n /** The EC-414 shape: a standalone consumer Tailwind theme entry. */\n | { kind: 'new' }\n /** The app never provided a canvas stylesheet (`setCanvasStylesheetUrl`). */\n | { kind: 'no-stylesheet' }\n /** The stylesheet `<link>` failed to load. */\n | { kind: 'stylesheet-error' }\n /** Loaded, but defines no Tailwind theme tokens at all. */\n | { kind: 'no-theme-tokens' }\n\n/** The raw computed-style sample the pure classifier decides from. */\nexport interface CanvasThemeProbeSample {\n /** The `ec-app-css` link fired `error` (or never resolved). */\n loadFailed: boolean\n /** Computed values of {@link CANVAS_LEGACY_MARKER_TOKENS}, in order. */\n legacyMarkers: readonly string[]\n /** Computed value of {@link CANVAS_TAILWIND_MARKER_TOKEN}. */\n tailwindMarker: string\n}\n\nconst present = (value: string | null | undefined): boolean =>\n typeof value === 'string' && value.trim() !== ''\n\n/**\n * AD-C pure core: classify the canvas stylesheet world from computed-token\n * presence. Legacy needs EVERY legacy marker; a loaded stylesheet with no\n * Tailwind marker is the explicit `no-theme-tokens` invalid state.\n */\nexport function classifyCanvasTheme(sample: CanvasThemeProbeSample): CanvasThemeStatus {\n if (sample.loadFailed) return { kind: 'stylesheet-error' }\n if (sample.legacyMarkers.length > 0 && sample.legacyMarkers.every(present)) {\n return { kind: 'legacy' }\n }\n if (present(sample.tailwindMarker)) return { kind: 'new' }\n return { kind: 'no-theme-tokens' }\n}\n\n/** The world a status renders as: only the legacy merged stylesheet keeps the\n * EC-389 body-class default; every other state gets the neutral backdrop. */\nexport function canvasWorldOf(status: CanvasThemeStatus): CanvasWorld {\n return status.kind === 'legacy' ? 'legacy' : 'new'\n}\n\n/** Sample the live document's computed root styles for {@link classifyCanvasTheme}. */\nexport function probeCanvasTheme(doc: Document, loadFailed = false): CanvasThemeStatus {\n const root = doc.documentElement\n const win = doc.defaultView\n if (!root || !win) return { kind: 'new' }\n const computed = win.getComputedStyle(root)\n return classifyCanvasTheme({\n loadFailed,\n legacyMarkers: CANVAS_LEGACY_MARKER_TOKENS.map((token) => computed.getPropertyValue(token)),\n tailwindMarker: computed.getPropertyValue(CANVAS_TAILWIND_MARKER_TOKEN),\n })\n}\n\n/**\n * The studio's DEFAULT canvas body classes — LEGACY world only (EC-389 → EC-414):\n * a consumer's `defineStudio({ canvas: { bodyClassName } })` REPLACES it\n * wholesale (including `''` to drop it), because these utility classes\n * out-cascade a site theme's `@layer base body` rules — keeping them additive\n * would make the site's own page background unreachable (the NK-004 bug). In\n * the NEW world the default retires entirely (the utilities don't exist there);\n * the chrome stylesheet's neutral backdrop takes over.\n */\nexport const DEFAULT_CANVAS_BODY_CLASS = 'bg-background text-foreground font-sans'\n\n/** Everything injected into the canvas preview document. */\nexport interface CanvasChromeInputs {\n /** The consuming app's compiled canvas stylesheet URL; `null` → no link (tests/SSR). */\n stylesheetUrl: string | null\n /** Which stylesheet world the document is in (AD-C probe; see `wireCanvasChrome`). */\n world: CanvasWorld\n /** The consumer's canvas theme from `defineStudio({ canvas })` (EC-389). */\n theme?: StudioCanvasTheme\n}\n\n/** The head element surface the chrome creates (link/style). */\nexport interface CanvasChromeElement {\n id: string\n rel?: string\n href?: string\n textContent: string | null\n remove(): void\n}\n\n/**\n * The document surface `applyCanvasChrome` needs (the `canvas-dnd` house\n * pattern: a narrow structural type so the core is unit-testable with a\n * hand-rolled fake — no jsdom). A real iframe `Document` satisfies it.\n */\nexport interface CanvasChromeDocument {\n getElementById(id: string): CanvasChromeElement | null\n createElement(tag: string): CanvasChromeElement\n // `object` (not CanvasChromeElement): the real DOM `appendChild<T extends Node>`\n // is only bivariance-compatible with a parameter every Node satisfies.\n head: { appendChild(node: object): void }\n body: { className: string }\n}\n\n/**\n * Apply the canvas document chrome in the PINNED cascade order (EC-389/EC-414):\n *\n * 1. `ec-layer-order` — the backdrop layer STATEMENT, pinning the chrome's\n * `ec-canvas-backdrop` layer to the bottom of the layer order (it must\n * precede the consumer stylesheet in document order to lose to it);\n * 2. `ec-app-css` — the canvas stylesheet `<link>` (the consumer theme; in\n * the legacy world also the studio design system);\n * 3. `ec-outline-css` — the studio's editing chrome (world-composed:\n * backdrop + selection/DnD/toolbar/containment styles);\n * 4. `ec-canvas-theme` — the consumer's `canvas.tokensCss` (consumer overrides).\n * (An `ec-kit-tokens` layer used to sit between 3 and 4 for an instantiated\n * starter kit's palette; the kits went in EC-574.)\n *\n * Later elements win the cascade at equal specificity, so studio defaults <\n * consumer config — the customization-seam ordering. The body class is the\n * consumer's `bodyClassName` when given (even `''`), else the legacy default in\n * the legacy world and empty in the new world (AD-C). Safe to run repeatedly\n * (iframe onLoad + the already-loaded fast path + the probe settling):\n * links/styles are deduped by id, the chrome CSS is re-composed in place when\n * the world flips, and the theme style is re-appended so a re-wire never\n * changes who wins.\n */\nexport function applyCanvasChrome(d: CanvasChromeDocument, inputs: CanvasChromeInputs): void {\n if (!d.getElementById('ec-layer-order')) {\n const style = d.createElement('style')\n style.id = 'ec-layer-order'\n style.textContent = CANVAS_BACKDROP_LAYER_STATEMENT\n d.head.appendChild(style)\n }\n if (inputs.stylesheetUrl && !d.getElementById('ec-app-css')) {\n const link = d.createElement('link')\n link.id = 'ec-app-css'\n link.rel = 'stylesheet'\n link.href = inputs.stylesheetUrl\n d.head.appendChild(link)\n }\n const chromeCss = canvasChromeCss(inputs.world)\n const outline = d.getElementById('ec-outline-css')\n if (outline) {\n // Probe settled on the other world: re-compose in place (position stable).\n if (outline.textContent !== chromeCss) outline.textContent = chromeCss\n } else {\n const style = d.createElement('style')\n style.id = 'ec-outline-css'\n style.textContent = chromeCss\n d.head.appendChild(style)\n }\n // The theme layer re-appends (remove + add) on every wire, so it always lands\n // last — after a re-composed chrome layer too.\n appendCssLayer(d, 'ec-canvas-theme', inputs.theme?.tokensCss)\n d.body.className =\n inputs.theme?.bodyClassName ?? (inputs.world === 'legacy' ? DEFAULT_CANVAS_BODY_CLASS : '')\n}\n\n/** Replace-append one identified `<style>` layer; absent CSS removes a stale element. */\nfunction appendCssLayer(d: CanvasChromeDocument, id: string, css: string | undefined): void {\n const existing = d.getElementById(id)\n if (existing) existing.remove()\n if (css === undefined || css === '') return\n const style = d.createElement('style')\n style.id = id\n style.textContent = css\n d.head.appendChild(style)\n}\n\n/** `applyCanvasChrome` inputs minus the probe-owned `world`, plus the settle callback. */\nexport interface WireCanvasChromeOptions {\n stylesheetUrl: string | null\n theme?: StudioCanvasTheme\n /**\n * Fired when the classification SETTLES — the stylesheet finished loading\n * (or failing), or there is no stylesheet to wait for. Feeds the AD-D dev\n * notice; may fire again on re-wires (callers dedupe by kind).\n */\n onStatus?: (status: CanvasThemeStatus) => void\n}\n\n/** Links whose settle listeners are already attached (re-wires must not stack them). */\nconst wiredLinks = new WeakSet<object>()\n\n/**\n * The DOM-level wiring around {@link applyCanvasChrome}: applies the chrome\n * immediately with the CURRENT classification, then re-applies once the\n * `ec-app-css` link settles (the AD-C probe reads computed custom properties,\n * which exist only after the stylesheet loads). Before the stylesheet arrives\n * neither world's styling is resolvable anyway (no consumer CSS yet), so the\n * pre-settle pass is visually neutral in both worlds.\n */\nexport function wireCanvasChrome(doc: Document, options: WireCanvasChromeOptions): void {\n const apply = (settled: boolean, loadFailed: boolean) => {\n const status: CanvasThemeStatus = options.stylesheetUrl\n ? probeCanvasTheme(doc, loadFailed)\n : { kind: 'no-stylesheet' }\n applyCanvasChrome(doc, {\n stylesheetUrl: options.stylesheetUrl,\n world: canvasWorldOf(status),\n ...(options.theme !== undefined ? { theme: options.theme } : {}),\n })\n if (settled) options.onStatus?.(status)\n }\n\n if (!options.stylesheetUrl) {\n apply(true, false)\n return\n }\n // First pass injects the link (and everything else) with the current sample.\n const already = doc.getElementById('ec-app-css') as HTMLLinkElement | null\n const loaded = already?.sheet != null\n apply(loaded, false)\n if (loaded) return\n const link = doc.getElementById('ec-app-css') as HTMLLinkElement | null\n if (!link || wiredLinks.has(link)) return\n wiredLinks.add(link)\n link.addEventListener('load', () => apply(true, false), { once: true })\n link.addEventListener('error', () => apply(true, true), { once: true })\n}\n"]}
1
+ {"version":3,"file":"canvas-chrome.js","sourceRoot":"","sources":["../../../src/components/builder/canvas-chrome.ts"],"names":[],"mappings":"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","sourcesContent":["import type { StudioCanvasTheme } from '../app-views/studio-config'\nimport { CANVAS_BACKDROP_LAYER_STATEMENT, canvasChromeCss } from './canvas-chrome-css'\nimport type { CanvasWorld } from './canvas-chrome-css'\n\nexport { canvasChromeCss, CANVAS_BACKDROP_LAYER_STATEMENT } from './canvas-chrome-css'\nexport type { CanvasWorld } from './canvas-chrome-css'\n\n/**\n * The canvas iframe's document chrome (EC-389, reshaped by EC-414) — the\n * framework-free half of `BuilderCanvas`'s `wireUp` (house pattern, EC-330):\n * everything injected into the preview document lives here, in ONE pinned\n * cascade order, unit-testable without jsdom.\n *\n * EC-414 (AD-A/AD-C): the studio design system is leaving the canvas document.\n * Which world a project lives in is decided by a RUNTIME PROBE of the loaded\n * canvas stylesheet — no flag, no config, zero consumer breakage:\n *\n * - `legacy` (marker tokens present): the merged pre-EC-414 canvas.css that\n * bundles `@elytracms/studio/styles.css` with the consumer theme. The\n * EC-389 body-class default and today's behavior apply EXACTLY.\n * - `new` (markers absent): the consumer's standalone full-Tailwind theme\n * entry. No studio utilities exist in the document, so the body-class\n * default retires and the neutral hand-written backdrop applies instead.\n */\n\n/**\n * The studio design system's marker tokens (AD-C probe). These are the plain\n * `:root` custom properties from the studio's generated `tokens.css`, which any\n * merged legacy build carries VERBATIM — unlike `@theme inline` variables\n * (e.g. `--color-panel`), which Tailwind v4 inlines into utilities and never\n * emits as custom properties (verified against the repo's Tailwind 4.3.2; the\n * task file's `--color-panel` example probe would classify every legacy build\n * as new). Two tokens ANDed so a consumer theme that happens to define one\n * generic name (`--panel`) cannot be mistaken for the studio design system.\n */\nexport const CANVAS_LEGACY_MARKER_TOKENS = ['--panel', '--grid-line'] as const\n\n/**\n * A Tailwind v4 build unconditionally emits `--default-font-family` on\n * `:root, :host` (preflight consumes it) — its absence after a successful\n * stylesheet load means the canvas stylesheet is not a Tailwind theme entry at\n * all (AD-D item 2's \"defines no @theme tokens\" invalid state).\n */\nexport const CANVAS_TAILWIND_MARKER_TOKEN = '--default-font-family'\n\n/** The settled classification of the canvas stylesheet (AD-C + AD-D item 2). */\nexport type CanvasThemeStatus =\n /** Pre-EC-414 merged canvas.css — studio design system + consumer theme. */\n | { kind: 'legacy' }\n /** The EC-414 shape: a standalone consumer Tailwind theme entry. */\n | { kind: 'new' }\n /** The app never provided a canvas stylesheet (`setCanvasStylesheetUrl`). */\n | { kind: 'no-stylesheet' }\n /** The stylesheet `<link>` failed to load. */\n | { kind: 'stylesheet-error' }\n /** Loaded, but defines no Tailwind theme tokens at all. */\n | { kind: 'no-theme-tokens' }\n\n/** The raw computed-style sample the pure classifier decides from. */\nexport interface CanvasThemeProbeSample {\n /** The `ec-app-css` link fired `error` (or never resolved). */\n loadFailed: boolean\n /** Computed values of {@link CANVAS_LEGACY_MARKER_TOKENS}, in order. */\n legacyMarkers: readonly string[]\n /** Computed value of {@link CANVAS_TAILWIND_MARKER_TOKEN}. */\n tailwindMarker: string\n}\n\nconst present = (value: string | null | undefined): boolean =>\n typeof value === 'string' && value.trim() !== ''\n\n/**\n * AD-C pure core: classify the canvas stylesheet world from computed-token\n * presence. Legacy needs EVERY legacy marker; a loaded stylesheet with no\n * Tailwind marker is the explicit `no-theme-tokens` invalid state.\n */\nexport function classifyCanvasTheme(sample: CanvasThemeProbeSample): CanvasThemeStatus {\n if (sample.loadFailed) return { kind: 'stylesheet-error' }\n if (sample.legacyMarkers.length > 0 && sample.legacyMarkers.every(present)) {\n return { kind: 'legacy' }\n }\n if (present(sample.tailwindMarker)) return { kind: 'new' }\n return { kind: 'no-theme-tokens' }\n}\n\n/** The world a status renders as: only the legacy merged stylesheet keeps the\n * EC-389 body-class default; every other state gets the neutral backdrop. */\nexport function canvasWorldOf(status: CanvasThemeStatus): CanvasWorld {\n return status.kind === 'legacy' ? 'legacy' : 'new'\n}\n\n/** Sample the live document's computed root styles for {@link classifyCanvasTheme}. */\nexport function probeCanvasTheme(doc: Document, loadFailed = false): CanvasThemeStatus {\n const root = doc.documentElement\n const win = doc.defaultView\n if (!root || !win) return { kind: 'new' }\n const computed = win.getComputedStyle(root)\n return classifyCanvasTheme({\n loadFailed,\n legacyMarkers: CANVAS_LEGACY_MARKER_TOKENS.map((token) => computed.getPropertyValue(token)),\n tailwindMarker: computed.getPropertyValue(CANVAS_TAILWIND_MARKER_TOKEN),\n })\n}\n\n/**\n * The studio's DEFAULT canvas body classes — LEGACY world only (EC-389 → EC-414):\n * a consumer's `defineStudio({ canvas: { bodyClassName } })` REPLACES it\n * wholesale (including `''` to drop it), because these utility classes\n * out-cascade a site theme's `@layer base body` rules — keeping them additive\n * would make the site's own page background unreachable (the bug a consumer reported). In\n * the NEW world the default retires entirely (the utilities don't exist there);\n * the chrome stylesheet's neutral backdrop takes over.\n */\nexport const DEFAULT_CANVAS_BODY_CLASS = 'bg-background text-foreground font-sans'\n\n/** Everything injected into the canvas preview document. */\nexport interface CanvasChromeInputs {\n /** The consuming app's compiled canvas stylesheet URL; `null` → no link (tests/SSR). */\n stylesheetUrl: string | null\n /** Which stylesheet world the document is in (AD-C probe; see `wireCanvasChrome`). */\n world: CanvasWorld\n /** The consumer's canvas theme from `defineStudio({ canvas })` (EC-389). */\n theme?: StudioCanvasTheme\n}\n\n/** The head element surface the chrome creates (link/style). */\nexport interface CanvasChromeElement {\n id: string\n rel?: string\n href?: string\n textContent: string | null\n remove(): void\n}\n\n/**\n * The document surface `applyCanvasChrome` needs (the `canvas-dnd` house\n * pattern: a narrow structural type so the core is unit-testable with a\n * hand-rolled fake — no jsdom). A real iframe `Document` satisfies it.\n */\nexport interface CanvasChromeDocument {\n getElementById(id: string): CanvasChromeElement | null\n createElement(tag: string): CanvasChromeElement\n // `object` (not CanvasChromeElement): the real DOM `appendChild<T extends Node>`\n // is only bivariance-compatible with a parameter every Node satisfies.\n head: { appendChild(node: object): void }\n body: { className: string }\n}\n\n/**\n * Apply the canvas document chrome in the PINNED cascade order (EC-389/EC-414):\n *\n * 1. `ec-layer-order` — the backdrop layer STATEMENT, pinning the chrome's\n * `ec-canvas-backdrop` layer to the bottom of the layer order (it must\n * precede the consumer stylesheet in document order to lose to it);\n * 2. `ec-app-css` — the canvas stylesheet `<link>` (the consumer theme; in\n * the legacy world also the studio design system);\n * 3. `ec-outline-css` — the studio's editing chrome (world-composed:\n * backdrop + selection/DnD/toolbar/containment styles);\n * 4. `ec-canvas-theme` — the consumer's `canvas.tokensCss` (consumer overrides).\n * (An `ec-kit-tokens` layer used to sit between 3 and 4 for an instantiated\n * starter kit's palette; the kits went in EC-574.)\n *\n * Later elements win the cascade at equal specificity, so studio defaults <\n * consumer config — the customization-seam ordering. The body class is the\n * consumer's `bodyClassName` when given (even `''`), else the legacy default in\n * the legacy world and empty in the new world (AD-C). Safe to run repeatedly\n * (iframe onLoad + the already-loaded fast path + the probe settling):\n * links/styles are deduped by id, the chrome CSS is re-composed in place when\n * the world flips, and the theme style is re-appended so a re-wire never\n * changes who wins.\n */\nexport function applyCanvasChrome(d: CanvasChromeDocument, inputs: CanvasChromeInputs): void {\n if (!d.getElementById('ec-layer-order')) {\n const style = d.createElement('style')\n style.id = 'ec-layer-order'\n style.textContent = CANVAS_BACKDROP_LAYER_STATEMENT\n d.head.appendChild(style)\n }\n if (inputs.stylesheetUrl && !d.getElementById('ec-app-css')) {\n const link = d.createElement('link')\n link.id = 'ec-app-css'\n link.rel = 'stylesheet'\n link.href = inputs.stylesheetUrl\n d.head.appendChild(link)\n }\n const chromeCss = canvasChromeCss(inputs.world)\n const outline = d.getElementById('ec-outline-css')\n if (outline) {\n // Probe settled on the other world: re-compose in place (position stable).\n if (outline.textContent !== chromeCss) outline.textContent = chromeCss\n } else {\n const style = d.createElement('style')\n style.id = 'ec-outline-css'\n style.textContent = chromeCss\n d.head.appendChild(style)\n }\n // The theme layer re-appends (remove + add) on every wire, so it always lands\n // last — after a re-composed chrome layer too.\n appendCssLayer(d, 'ec-canvas-theme', inputs.theme?.tokensCss)\n d.body.className =\n inputs.theme?.bodyClassName ?? (inputs.world === 'legacy' ? DEFAULT_CANVAS_BODY_CLASS : '')\n}\n\n/** Replace-append one identified `<style>` layer; absent CSS removes a stale element. */\nfunction appendCssLayer(d: CanvasChromeDocument, id: string, css: string | undefined): void {\n const existing = d.getElementById(id)\n if (existing) existing.remove()\n if (css === undefined || css === '') return\n const style = d.createElement('style')\n style.id = id\n style.textContent = css\n d.head.appendChild(style)\n}\n\n/** `applyCanvasChrome` inputs minus the probe-owned `world`, plus the settle callback. */\nexport interface WireCanvasChromeOptions {\n stylesheetUrl: string | null\n theme?: StudioCanvasTheme\n /**\n * Fired when the classification SETTLES — the stylesheet finished loading\n * (or failing), or there is no stylesheet to wait for. Feeds the AD-D dev\n * notice; may fire again on re-wires (callers dedupe by kind).\n */\n onStatus?: (status: CanvasThemeStatus) => void\n}\n\n/** Links whose settle listeners are already attached (re-wires must not stack them). */\nconst wiredLinks = new WeakSet<object>()\n\n/**\n * The DOM-level wiring around {@link applyCanvasChrome}: applies the chrome\n * immediately with the CURRENT classification, then re-applies once the\n * `ec-app-css` link settles (the AD-C probe reads computed custom properties,\n * which exist only after the stylesheet loads). Before the stylesheet arrives\n * neither world's styling is resolvable anyway (no consumer CSS yet), so the\n * pre-settle pass is visually neutral in both worlds.\n */\nexport function wireCanvasChrome(doc: Document, options: WireCanvasChromeOptions): void {\n const apply = (settled: boolean, loadFailed: boolean) => {\n const status: CanvasThemeStatus = options.stylesheetUrl\n ? probeCanvasTheme(doc, loadFailed)\n : { kind: 'no-stylesheet' }\n applyCanvasChrome(doc, {\n stylesheetUrl: options.stylesheetUrl,\n world: canvasWorldOf(status),\n ...(options.theme !== undefined ? { theme: options.theme } : {}),\n })\n if (settled) options.onStatus?.(status)\n }\n\n if (!options.stylesheetUrl) {\n apply(true, false)\n return\n }\n // First pass injects the link (and everything else) with the current sample.\n const already = doc.getElementById('ec-app-css') as HTMLLinkElement | null\n const loaded = already?.sheet != null\n apply(loaded, false)\n if (loaded) return\n const link = doc.getElementById('ec-app-css') as HTMLLinkElement | null\n if (!link || wiredLinks.has(link)) return\n wiredLinks.add(link)\n link.addEventListener('load', () => apply(true, false), { once: true })\n link.addEventListener('error', () => apply(true, true), { once: true })\n}\n"]}
@@ -17,13 +17,13 @@ export declare function absorbIntoValue(value: unknown, change: readonly Documen
17
17
  /** One composition state with the change absorbed, or `undefined` when it cannot take it. */
18
18
  export declare function absorbChange(state: CanvasValueState, change: readonly DocumentPatch[]): CanvasValueState | undefined;
19
19
  /** The editor state a rebase reads and writes (the builder state's composition, history and selection). */
20
- export interface RebasableState {
20
+ export interface RebasableState<Entry extends CanvasValueState = CanvasValueState> {
21
21
  readonly present: ComponentNode;
22
22
  readonly preserved: readonly PreservedItem[];
23
23
  /** Oldest first. */
24
- readonly past: readonly CanvasValueState[];
24
+ readonly past: readonly Entry[];
25
25
  /** Next redo first. */
26
- readonly future: readonly CanvasValueState[];
26
+ readonly future: readonly Entry[];
27
27
  readonly selectedId: string | null;
28
28
  readonly selectedRun: number | null;
29
29
  readonly hoveredId: string | null;
@@ -32,7 +32,7 @@ export interface RebasableState {
32
32
  * Rebase the editor onto the field's change `from` → `to` (see the module doc). Returns
33
33
  * `undefined` when there is nothing to change.
34
34
  */
35
- export declare function rebaseCanvas(state: RebasableState, from: unknown, to: unknown): RebasableState | undefined;
35
+ export declare function rebaseCanvas<Entry extends CanvasValueState>(state: RebasableState<Entry>, from: unknown, to: unknown): RebasableState<Entry> | undefined;
36
36
  /**
37
37
  * A change of the canvas field's value made UNDERNEATH the canvas, as the document draft
38
38
  * saw it happen: the field's value before and after the session's document moved, a
@@ -87,7 +87,8 @@ export function absorbChange(state, change) {
87
87
  * Rebase one direction of the history (`chain` ordered from the present outward — the
88
88
  * newest undo step first, or the next redo step first): each entry absorbs the change,
89
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.
90
+ * present is dropped. Returns the chain in the same order. An entry keeps what it carries
91
+ * beside its value (EC-728: the step's effect outside the canvas); a dropped entry drops it.
91
92
  */
92
93
  function rebaseChain(present, chain, change) {
93
94
  const out = [];
@@ -99,7 +100,7 @@ function rebaseChain(present, chain, change) {
99
100
  const value = canvasValueOf(absorbed);
100
101
  if (semanticValueEqual(value, neighbour))
101
102
  continue;
102
- out.push(absorbed);
103
+ out.push({ ...entry, ...absorbed });
103
104
  neighbour = value;
104
105
  }
105
106
  return out;
@@ -1 +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
+ {"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. An entry keeps what it carries\n * beside its value (EC-728: the step's effect outside the canvas); a dropped entry drops it.\n */\nfunction rebaseChain<Entry extends CanvasValueState>(\n present: CanvasValueState,\n chain: readonly Entry[],\n change: readonly DocumentPatch[],\n): Entry[] {\n const out: Entry[] = []\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({ ...entry, ...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<Entry extends CanvasValueState = CanvasValueState> {\n readonly present: ComponentNode\n readonly preserved: readonly PreservedItem[]\n /** Oldest first. */\n readonly past: readonly Entry[]\n /** Next redo first. */\n readonly future: readonly Entry[]\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<Entry extends CanvasValueState>(\n state: RebasableState<Entry>,\n from: unknown,\n to: unknown,\n): RebasableState<Entry> | 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"]}
@@ -87,7 +87,7 @@ export declare function createCanvasRenderContext(deps: CanvasRenderContextDeps)
87
87
  * canvas also needs for spacing-handle gating.
88
88
  */
89
89
  /**
90
- * The canvas's asset reader IS delivery's (NK-049 finding, 2026-10-23). This used to
90
+ * The canvas's asset reader IS delivery's (a consumer's finding, 2026-10-23). This used to
91
91
  * hand-pick `url/width/height/alt/focalPoint`, read `alt` from the legacy column
92
92
  * only and drop `meta` — so a component's AI badge (`meta.aiGenerated`) and an
93
93
  * editor-set `meta.alt` rendered on the site but not on the canvas. One reader
@@ -108,7 +108,7 @@ export function createCanvasRenderContext(deps) {
108
108
  * canvas also needs for spacing-handle gating.
109
109
  */
110
110
  /**
111
- * The canvas's asset reader IS delivery's (NK-049 finding, 2026-10-23). This used to
111
+ * The canvas's asset reader IS delivery's (a consumer's finding, 2026-10-23). This used to
112
112
  * hand-pick `url/width/height/alt/focalPoint`, read `alt` from the legacy column
113
113
  * only and drop `meta` — so a component's AI badge (`meta.aiGenerated`) and an
114
114
  * editor-set `meta.alt` rendered on the site but not on the canvas. One reader
@@ -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 { 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"]}
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 (a consumer's 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"]}
@@ -2,6 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Suspense, lazy } from 'react';
3
3
  import { Text } from '@elytracms/studio/ui';
4
4
  import { translate } from '../../lib/i18n/index.js';
5
+ import { LazySurfaceBoundary } from '../load-failure.js';
5
6
  /**
6
7
  * Lazy boundary for the block-field page builder (EC-322 load-time pass).
7
8
  *
@@ -24,6 +25,6 @@ function CanvasFallback() {
24
25
  return (_jsx("div", { className: "grid min-h-0 flex-1 place-items-center p-8", children: _jsx(Text, { size: "sm", tone: "muted", children: translate('builder.canvas.loading') }) }));
25
26
  }
26
27
  export function CompositionFieldEditor(props) {
27
- return (_jsx(Suspense, { fallback: _jsx(CanvasFallback, {}), children: _jsx(CompositionFieldEditorImpl, { ...props }) }));
28
+ return (_jsx(LazySurfaceBoundary, { children: _jsx(Suspense, { fallback: _jsx(CanvasFallback, {}), children: _jsx(CompositionFieldEditorImpl, { ...props }) }) }));
28
29
  }
29
30
  //# sourceMappingURL=composition-field-editor-lazy.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"composition-field-editor-lazy.js","sourceRoot":"","sources":["../../../src/components/builder/composition-field-editor-lazy.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,OAAO,CAAA;AAEtC,OAAO,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAE1C;;;;;;;;;;;;;;GAcG;AACH,MAAM,0BAA0B,GAAG,IAAI,CAAC,GAAG,EAAE,CAC3C,MAAM,CAAC,4BAA4B,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACrD,OAAO,EAAE,MAAM,CAAC,sBAAsB;CACvC,CAAC,CAAC,CACJ,CAAA;AAED,SAAS,cAAc;IACrB,OAAO,CACL,cAAK,SAAS,EAAC,4CAA4C,YACzD,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,YACzB,SAAS,CAAC,wBAAwB,CAAC,GAC/B,GACH,CACP,CAAA;AACH,CAAC;AAED,MAAM,UAAU,sBAAsB,CACpC,KAAwD;IAExD,OAAO,CACL,KAAC,QAAQ,IAAC,QAAQ,EAAE,KAAC,cAAc,KAAG,YACpC,KAAC,0BAA0B,OAAK,KAAK,GAAI,GAChC,CACZ,CAAA;AACH,CAAC","sourcesContent":["import { Suspense, lazy } from 'react'\nimport type { ComponentProps } from 'react'\nimport { Text } from '@elytracms/studio/ui'\nimport { translate } from '../../lib/i18n'\n\n/**\n * Lazy boundary for the block-field page builder (EC-322 load-time pass).\n *\n * `CompositionFieldEditor` mounts the full page-builder canvas — the canvas,\n * page tree, block inserter, property inspector, clipboard, drag/reorder and the\n * builder store (the studio's single heaviest UI subtree). The detail editor\n * imports it statically but only renders it for documents that actually carry a\n * `blocks` field, so every ordinary document editor was paying the whole builder's\n * download cost for a surface it never shows.\n *\n * Splitting it behind {@link lazy} keeps the builder out of the CMS detail-view\n * critical path; it downloads on demand the first time a `blocks` field opens. The\n * detail editor's seam is unchanged — it still imports `CompositionFieldEditor`,\n * now the lazy-wrapped one, and renders it behind the same `canvasField` gate.\n */\nconst CompositionFieldEditorImpl = lazy(() =>\n import('./composition-field-editor').then((module) => ({\n default: module.CompositionFieldEditor,\n })),\n)\n\nfunction CanvasFallback() {\n return (\n <div className=\"grid min-h-0 flex-1 place-items-center p-8\">\n <Text size=\"sm\" tone=\"muted\">\n {translate('builder.canvas.loading')}\n </Text>\n </div>\n )\n}\n\nexport function CompositionFieldEditor(\n props: ComponentProps<typeof CompositionFieldEditorImpl>,\n) {\n return (\n <Suspense fallback={<CanvasFallback />}>\n <CompositionFieldEditorImpl {...props} />\n </Suspense>\n )\n}\n"]}
1
+ {"version":3,"file":"composition-field-editor-lazy.js","sourceRoot":"","sources":["../../../src/components/builder/composition-field-editor-lazy.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,OAAO,CAAA;AAEtC,OAAO,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC1C,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAErD;;;;;;;;;;;;;;GAcG;AACH,MAAM,0BAA0B,GAAG,IAAI,CAAC,GAAG,EAAE,CAC3C,MAAM,CAAC,4BAA4B,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACrD,OAAO,EAAE,MAAM,CAAC,sBAAsB;CACvC,CAAC,CAAC,CACJ,CAAA;AAED,SAAS,cAAc;IACrB,OAAO,CACL,cAAK,SAAS,EAAC,4CAA4C,YACzD,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,YACzB,SAAS,CAAC,wBAAwB,CAAC,GAC/B,GACH,CACP,CAAA;AACH,CAAC;AAED,MAAM,UAAU,sBAAsB,CACpC,KAAwD;IAExD,OAAO,CACL,KAAC,mBAAmB,cAClB,KAAC,QAAQ,IAAC,QAAQ,EAAE,KAAC,cAAc,KAAG,YACpC,KAAC,0BAA0B,OAAK,KAAK,GAAI,GAChC,GACS,CACvB,CAAA;AACH,CAAC","sourcesContent":["import { Suspense, lazy } from 'react'\nimport type { ComponentProps } from 'react'\nimport { Text } from '@elytracms/studio/ui'\nimport { translate } from '../../lib/i18n'\nimport { LazySurfaceBoundary } from '../load-failure'\n\n/**\n * Lazy boundary for the block-field page builder (EC-322 load-time pass).\n *\n * `CompositionFieldEditor` mounts the full page-builder canvas — the canvas,\n * page tree, block inserter, property inspector, clipboard, drag/reorder and the\n * builder store (the studio's single heaviest UI subtree). The detail editor\n * imports it statically but only renders it for documents that actually carry a\n * `blocks` field, so every ordinary document editor was paying the whole builder's\n * download cost for a surface it never shows.\n *\n * Splitting it behind {@link lazy} keeps the builder out of the CMS detail-view\n * critical path; it downloads on demand the first time a `blocks` field opens. The\n * detail editor's seam is unchanged — it still imports `CompositionFieldEditor`,\n * now the lazy-wrapped one, and renders it behind the same `canvasField` gate.\n */\nconst CompositionFieldEditorImpl = lazy(() =>\n import('./composition-field-editor').then((module) => ({\n default: module.CompositionFieldEditor,\n })),\n)\n\nfunction CanvasFallback() {\n return (\n <div className=\"grid min-h-0 flex-1 place-items-center p-8\">\n <Text size=\"sm\" tone=\"muted\">\n {translate('builder.canvas.loading')}\n </Text>\n </div>\n )\n}\n\nexport function CompositionFieldEditor(\n props: ComponentProps<typeof CompositionFieldEditorImpl>,\n) {\n return (\n <LazySurfaceBoundary>\n <Suspense fallback={<CanvasFallback />}>\n <CompositionFieldEditorImpl {...props} />\n </Suspense>\n </LazySurfaceBoundary>\n )\n}\n"]}
@@ -11,8 +11,14 @@ import type { PromoteEffects, ReuseCapability } from './reuse.js';
11
11
  * The provider needs BOTH the builder store (the edited composition) and the CMS
12
12
  * store (to create/read the reusable document), so it mounts inside the
13
13
  * composition-field editor's `BuilderProvider`, itself inside the CMS document
14
- * editor. Where no provider is mounted (or the capability is absent), the menu
15
- * reads `null` and simply omits the gestures.
14
+ * editor. Where no provider is mounted (or the capability is absent — EC-728: the
15
+ * project declares no `reuse` embed), the menu reads `null` and simply omits the gestures.
16
+ *
17
+ * EC-728 — *Make reusable* is ONE canvas history entry that records the document it
18
+ * created. When an Undo or Redo crosses it, this provider performs the document side
19
+ * (`createPromoteHistory`): Undo deletes the document only when it is provably nobody
20
+ * else's (never published, referenced nowhere, unchanged since its create), else keeps
21
+ * it and says why; Redo re-inserts what Undo removed.
16
22
  */
17
23
  export interface ReuseGestures {
18
24
  capability: ReuseCapability | null;
@@ -1,17 +1,18 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { createContext, useCallback, useContext, useMemo, useState } from 'react';
2
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
3
3
  import { blockRootsOf, getNode, isStaticProp } from '@elytracms/core/project-graph';
4
- import { readDocumentRef } from '@elytracms/core/cms-core';
4
+ import { documentKey, readDocumentRef } from '@elytracms/core/cms-core';
5
5
  import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Field, Input, Stack, toast, } from '@elytracms/studio/ui';
6
6
  import { getStudioRegistry } from '../../lib/registry/index.js';
7
7
  import { getStudioOperations } from '../../lib/operations/index.js';
8
+ import { ensureReferenceIndex } from '../../lib/references/references-store.js';
8
9
  import { PublishRefusedError } from '../../lib/operations/publish-with-revision.js';
9
10
  import { resolveDeploymentReuse } from '../../lib/workspace/index.js';
10
11
  import { localizedLabel, translate } from '../../lib/i18n/index.js';
11
12
  import { useCms } from '../cms/cms-store.js';
12
13
  import { useProjectSession } from '../project-session.js';
13
14
  import { useBuilder } from './builder-store.js';
14
- import { detachRefusal, resolveReuseCapability, runPromote } from './reuse.js';
15
+ import { createPromoteHistory, detachRefusal, resolveReuseCapability, runPromote } from './reuse.js';
15
16
  import { placementRefusalMessage } from './placement-refusal.js';
16
17
  /**
17
18
  * EC-690 — the publish step of "Make reusable" and its failure notice, for ONE promote. The
@@ -69,12 +70,66 @@ export function ReuseProvider({ children }) {
69
70
  const { projectId } = useProjectSession();
70
71
  const [pending, setPending] = useState(null);
71
72
  const manifests = useMemo(() => getStudioRegistry().registry.list(), []);
72
- // EC-467: the optional `reuse` declaration wins; absent, the heuristic runs
73
- // exactly as before. The config holder is static per studio boot, so it is read
73
+ // EC-728: the `reuse` declaration is the only source of the capability (declared,
74
+ // never inferred). The config holder is static per studio boot, so it is read
74
75
  // inside the memo (like `useStudioNav` reads the sidebar holder). Resolution
75
- // ISSUES are surfaced once, by the shell rail — this provider is mounted per
76
+ // ISSUES are surfaced once, by the shell — this provider is mounted per
76
77
  // composition field, so reporting here would repeat them per field.
77
78
  const capability = useMemo(() => resolveReuseCapability(manifests, cms.collections, resolveDeploymentReuse()), [manifests, cms.collections]);
79
+ // EC-728 — the document side of an Undo/Redo across a *Make reusable* step. The work runs
80
+ // after the render that applied the step (the page's restore is already committed, the
81
+ // flush awaits it), so it reads the latest stores through refs.
82
+ const cmsRef = useRef(cms);
83
+ cmsRef.current = cms;
84
+ const projectRef = useRef(projectId);
85
+ projectRef.current = projectId;
86
+ const [promoteHistory] = useState(() => createPromoteHistory({
87
+ flush: () => cmsRef.current.flushPersistence(),
88
+ read: async (ref) => {
89
+ const project = projectRef.current;
90
+ if (project === undefined)
91
+ throw new Error('no project');
92
+ const { operations } = await getStudioOperations();
93
+ const record = await operations.documents.get({ projectId: project, ref });
94
+ if (record.ok)
95
+ return { published: record.value.publishedVersion !== undefined };
96
+ if (record.error.code === 'not-found')
97
+ return undefined;
98
+ throw new Error(record.error.message);
99
+ },
100
+ referrers: async (ref) => {
101
+ const project = projectRef.current;
102
+ if (project === undefined)
103
+ throw new Error('no project');
104
+ const studio = await getStudioOperations();
105
+ // A settled index, as the delete pre-flight reads it: a provider that does not
106
+ // maintain the index in its write path updates it after the write's event.
107
+ await ensureReferenceIndex(studio, project);
108
+ await studio.references.flush();
109
+ const inbound = await studio.operations.references.inbound({
110
+ projectId: project,
111
+ target: { type: 'document', id: documentKey(ref) },
112
+ });
113
+ if (!inbound.ok)
114
+ throw new Error(inbound.error.message);
115
+ return inbound.value.length;
116
+ },
117
+ deleteCreated: (ref) => cmsRef.current.deleteCreatedDocument(ref),
118
+ insert: (document) => cmsRef.current.insertDocument(document),
119
+ // A kept document the editor can act on is the outcome of a successful Undo; a removal
120
+ // that FAILED stays in the issues tray until dismissed (EC-690's error channel).
121
+ notify: (key, name) => key === 'builder.reuse.undoKeptFailed'
122
+ ? toast.error(translate(key, { name }))
123
+ : toast.success(translate(key, { name })),
124
+ }));
125
+ const handledSeq = useRef(builder.historyEffect?.seq ?? 0);
126
+ const step = builder.historyEffect;
127
+ useEffect(() => {
128
+ if (!step || step.seq <= handledSeq.current)
129
+ return;
130
+ handledSeq.current = step.seq;
131
+ void promoteHistory.crossed(step);
132
+ }, [step, promoteHistory]);
78
133
  const canMakeReusable = useCallback((nodeId) => {
79
134
  if (!capability)
80
135
  return false;
@@ -131,7 +186,7 @@ export function ReuseProvider({ children }) {
131
186
  effects: {
132
187
  newDocumentId: () => cms.newDocumentId(),
133
188
  insertDocument: (doc) => cms.insertDocument(doc),
134
- replaceInCanvas: (nodeId, embed) => builder.dispatch({ type: 'replace', id: nodeId, nodes: [embed] }),
189
+ replaceInCanvas: (nodeId, embed, effect) => builder.dispatch({ type: 'replace', id: nodeId, nodes: [embed], effect }),
135
190
  refused: toastRefusal,
136
191
  madeDraft: (label) => toast.success(translate('builder.reuse.madeReusableDraft', { name: label })),
137
192
  madePublished: (label) => toast.success(translate('builder.reuse.madeReusablePublished', { name: label })),