@elementor/editor-canvas 4.0.0-573 → 4.0.0-597

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.
@@ -1,26 +1,19 @@
1
1
  import * as React from 'react';
2
- import { useEffect, useState } from 'react';
2
+ import { useEffect, useLayoutEffect, useState } from 'react';
3
3
  import { InlineEditor, InlineEditorToolbar } from '@elementor/editor-controls';
4
4
  import { Box, ThemeProvider } from '@elementor/ui';
5
- import { FloatingPortal, useInteractions } from '@floating-ui/react';
5
+ import { autoUpdate, flip, FloatingPortal, useFloating } from '@floating-ui/react';
6
6
 
7
7
  import { CANVAS_WRAPPER_ID, OutlineOverlay } from '../../../components/outline-overlay';
8
- import { useBindReactPropsToElement } from '../../../hooks/use-bind-react-props-to-element';
9
- import { useFloatingOnElement } from '../../../hooks/use-floating-on-element';
10
8
  import {
11
- calcSelectionCenterOffsets,
12
9
  type Editor,
13
- type EditorView,
14
- getComputedStyle,
15
- type Offsets,
10
+ getInlineEditorElement,
11
+ horizontalShifterMiddleware as horizontalShifter,
12
+ removeToolbarAnchor,
13
+ useOnClickOutsideIframe,
14
+ useRenderToolbar,
16
15
  } from './inline-editing-utils';
17
16
 
18
- const TOP_BAR_SELECTOR = '#elementor-editor-wrapper-v2';
19
- const NAVIGATOR_SELECTOR = '#elementor-navigator';
20
- const EDITING_PANEL = '#elementor-panel';
21
-
22
- const EDITOR_ELEMENTS_OUT_OF_IFRAME = [ TOP_BAR_SELECTOR, NAVIGATOR_SELECTOR, EDITING_PANEL ];
23
-
24
17
  const EDITOR_WRAPPER_SELECTOR = 'inline-editor-wrapper';
25
18
 
26
19
  export const CanvasInlineEditor = ( {
@@ -30,7 +23,7 @@ export const CanvasInlineEditor = ( {
30
23
  rootElement,
31
24
  id,
32
25
  setValue,
33
- onBlur,
26
+ ...props
34
27
  }: {
35
28
  elementClasses: string;
36
29
  initialValue: string | null;
@@ -40,13 +33,13 @@ export const CanvasInlineEditor = ( {
40
33
  setValue: ( value: string | null ) => void;
41
34
  onBlur: () => void;
42
35
  } ) => {
43
- const [ selectionOffsets, setSelectionOffsets ] = useState< Offsets | null >( null );
44
36
  const [ editor, setEditor ] = useState< Editor | null >( null );
37
+ const { onSelectionEnd, anchor: toolbarAnchor } = useRenderToolbar( rootElement.ownerDocument, id );
45
38
 
46
- const onSelectionEnd = ( view: EditorView ) => {
47
- const hasSelection = ! view.state.selection.empty;
39
+ const onBlur = () => {
40
+ removeToolbarAnchor( rootElement.ownerDocument, id );
48
41
 
49
- setSelectionOffsets( hasSelection ? calcSelectionCenterOffsets( view ) : null );
42
+ props.onBlur();
50
43
  };
51
44
 
52
45
  useOnClickOutsideIframe( onBlur );
@@ -59,10 +52,10 @@ export const CanvasInlineEditor = ( {
59
52
  .ProseMirror > * {
60
53
  height: 100%;
61
54
  }
62
- .${ EDITOR_WRAPPER_SELECTOR } .ProseMirror > button[contenteditable="true"] {
63
- height: auto;
64
- cursor: text;
65
- }
55
+ .${ EDITOR_WRAPPER_SELECTOR } .ProseMirror > button[contenteditable="true"] {
56
+ height: auto;
57
+ cursor: text;
58
+ }
66
59
  ` }
67
60
  </style>
68
61
  <InlineEditor
@@ -78,18 +71,9 @@ export const CanvasInlineEditor = ( {
78
71
  onBlur={ onBlur }
79
72
  autofocus
80
73
  expectedTag={ expectedTag }
81
- wrapperClassName={ EDITOR_WRAPPER_SELECTOR }
82
74
  onSelectionEnd={ onSelectionEnd }
83
75
  />
84
- { selectionOffsets && editor && (
85
- <InlineEditingToolbarWrapper
86
- expectedTag={ expectedTag }
87
- editor={ editor }
88
- rootElement={ rootElement }
89
- id={ id }
90
- selectionOffsets={ selectionOffsets }
91
- />
92
- ) }
76
+ { toolbarAnchor && editor && <InlineEditingToolbar anchor={ toolbarAnchor } editor={ editor } id={ id } /> }
93
77
  </ThemeProvider>
94
78
  );
95
79
  };
@@ -113,104 +97,26 @@ const InlineEditingOverlay = ( {
113
97
  return overlayRefElement ? <OutlineOverlay element={ overlayRefElement } id={ id } isSelected /> : null;
114
98
  };
115
99
 
116
- const InlineEditingToolbarWrapper = ( {
117
- expectedTag,
118
- editor,
119
- rootElement,
120
- id,
121
- selectionOffsets,
122
- }: {
123
- expectedTag: string | null;
124
- editor: Editor;
125
- rootElement: HTMLElement;
126
- id: string;
127
- selectionOffsets: Offsets;
128
- } ) => {
129
- const [ element, setElement ] = useState< HTMLElement | null >( null );
130
-
131
- useEffect( () => {
132
- setElement( getInlineEditorElement( rootElement, expectedTag ) );
133
- }, [ expectedTag, rootElement ] );
134
-
135
- return element ? (
136
- <InlineEditingToolbar element={ element } editor={ editor } id={ id } selectionOffsets={ selectionOffsets } />
137
- ) : null;
138
- };
139
-
140
- const InlineEditingToolbar = ( {
141
- element,
142
- editor,
143
- id,
144
- selectionOffsets,
145
- }: {
146
- element: HTMLElement;
147
- editor: Editor;
148
- id: string;
149
- selectionOffsets: Offsets;
150
- } ) => {
151
- const { floating } = useFloatingOnElement( {
152
- element,
153
- isSelected: true,
100
+ const InlineEditingToolbar = ( { anchor, editor, id }: { anchor: HTMLElement; editor: Editor; id: string } ) => {
101
+ const { refs, floatingStyles } = useFloating( {
102
+ placement: 'top',
103
+ strategy: 'fixed',
104
+ transform: false,
105
+ whileElementsMounted: autoUpdate,
106
+ middleware: [ horizontalShifter, flip() ],
154
107
  } );
155
- const { getFloatingProps, getReferenceProps } = useInteractions();
156
- const style = getComputedStyle( floating.styles, selectionOffsets );
157
108
 
158
- useBindReactPropsToElement( element, getReferenceProps );
109
+ useLayoutEffect( () => {
110
+ refs.setReference( anchor );
111
+
112
+ return () => refs.setReference( null );
113
+ }, [ anchor, refs ] );
159
114
 
160
115
  return (
161
116
  <FloatingPortal id={ CANVAS_WRAPPER_ID }>
162
- <Box
163
- ref={ floating.setRef }
164
- style={ {
165
- ...floating.styles,
166
- pointerEvents: 'none',
167
- } }
168
- role="presentation"
169
- { ...getFloatingProps( { style } ) }
170
- >
171
- { floating.styles.transform && (
172
- <Box
173
- sx={ {
174
- position: 'relative',
175
- transform: 'translateY(-100%)',
176
- height: 'max-content',
177
- } }
178
- >
179
- <InlineEditorToolbar
180
- editor={ editor }
181
- elementId={ id }
182
- sx={ {
183
- transform: 'translateX(-50%)',
184
- } }
185
- />
186
- </Box>
187
- ) }
117
+ <Box ref={ refs.setFloating } role="presentation" style={ { ...floatingStyles, pointerEvents: 'none' } }>
118
+ <InlineEditorToolbar editor={ editor } elementId={ id } />
188
119
  </Box>
189
120
  </FloatingPortal>
190
121
  );
191
122
  };
192
-
193
- const getInlineEditorElement = ( elementWrapper: HTMLElement, expectedTag: string | null ) => {
194
- return ! expectedTag ? null : ( elementWrapper.querySelector( expectedTag ) as HTMLDivElement );
195
- };
196
-
197
- // Elements out of iframe and canvas don't trigger "onClickAway" which unmounts the editor
198
- // since they are not part of the iframes owner document.
199
- // We need to manually add listeners to these elements to unmount the editor when they are clicked.
200
- const useOnClickOutsideIframe = ( handleUnmount: () => void ) => {
201
- const asyncUnmountInlineEditor = React.useCallback( () => queueMicrotask( handleUnmount ), [ handleUnmount ] );
202
-
203
- useEffect( () => {
204
- EDITOR_ELEMENTS_OUT_OF_IFRAME.forEach(
205
- ( selector ) =>
206
- document?.querySelector( selector )?.addEventListener( 'mousedown', asyncUnmountInlineEditor )
207
- );
208
-
209
- return () =>
210
- EDITOR_ELEMENTS_OUT_OF_IFRAME.forEach(
211
- ( selector ) =>
212
- document?.querySelector( selector )?.removeEventListener( 'mousedown', asyncUnmountInlineEditor )
213
- );
214
- // eslint-disable-next-line react-hooks/exhaustive-deps
215
- }, [] );
216
- };
@@ -2,7 +2,7 @@ import * as React from 'react';
2
2
  import { createRoot, type Root } from 'react-dom/client';
3
3
  import { getContainer, getElementLabel, getElementType } from '@elementor/editor-elements';
4
4
  import {
5
- htmlV2PropTypeUtil,
5
+ htmlV3PropTypeUtil,
6
6
  parseHtmlChildren,
7
7
  type PropType,
8
8
  type PropValue,
@@ -127,8 +127,9 @@ export default class InlineEditingReplacement extends ReplacementBase {
127
127
 
128
128
  getExtractedContentValue() {
129
129
  const propValue = this.getInlineEditablePropValue();
130
+ const extracted = htmlV3PropTypeUtil.extract( propValue );
130
131
 
131
- return htmlV2PropTypeUtil.extract( propValue )?.content ?? '';
132
+ return stringPropTypeUtil.extract( extracted?.content ?? null ) ?? '';
132
133
  }
133
134
 
134
135
  setContentValue( value: string | null ) {
@@ -136,8 +137,8 @@ export default class InlineEditingReplacement extends ReplacementBase {
136
137
  const html = value || '';
137
138
  const parsed = parseHtmlChildren( html );
138
139
 
139
- const valueToSave = htmlV2PropTypeUtil.create( {
140
- content: parsed.content || null,
140
+ const valueToSave = htmlV3PropTypeUtil.create( {
141
+ content: parsed.content ? stringPropTypeUtil.create( parsed.content ) : null,
141
142
  children: parsed.children,
142
143
  } );
143
144
 
@@ -174,7 +175,7 @@ export default class InlineEditingReplacement extends ReplacementBase {
174
175
  }
175
176
 
176
177
  if ( propType.kind === 'union' ) {
177
- const textKeys = [ htmlV2PropTypeUtil.key, stringPropTypeUtil.key ];
178
+ const textKeys = [ htmlV3PropTypeUtil.key, stringPropTypeUtil.key ];
178
179
 
179
180
  for ( const key of textKeys ) {
180
181
  if ( propType.prop_types[ key ] ) {
@@ -1,4 +1,4 @@
1
- import { htmlV2PropTypeUtil, type PropType, stringPropTypeUtil } from '@elementor/editor-props';
1
+ import { htmlV3PropTypeUtil, type PropType, stringPropTypeUtil } from '@elementor/editor-props';
2
2
 
3
3
  type InlineEditingEligibilityArgs = {
4
4
  rawValue: unknown;
@@ -9,7 +9,7 @@ const hasKey = ( propType: PropType ): propType is PropType & { key: unknown } =
9
9
  return 'key' in propType;
10
10
  };
11
11
 
12
- const TEXT_PROP_TYPE_KEYS = new Set( [ htmlV2PropTypeUtil.key, stringPropTypeUtil.key ] );
12
+ const TEXT_PROP_TYPE_KEYS = new Set( [ htmlV3PropTypeUtil.key, stringPropTypeUtil.key ] );
13
13
 
14
14
  const isCoreTextPropTypeKey = ( key: unknown ): boolean => {
15
15
  return ( TEXT_PROP_TYPE_KEYS as Set< unknown > ).has( key );
@@ -36,5 +36,5 @@ export const isInlineEditingAllowed = ( { rawValue, propTypeFromSchema }: Inline
36
36
  return isAllowedBySchema( propTypeFromSchema );
37
37
  }
38
38
 
39
- return htmlV2PropTypeUtil.isValid( rawValue ) || stringPropTypeUtil.isValid( rawValue );
39
+ return htmlV3PropTypeUtil.isValid( rawValue ) || stringPropTypeUtil.isValid( rawValue );
40
40
  };
@@ -1,9 +1,35 @@
1
- import { type CSSProperties } from 'react';
1
+ import { type CSSProperties, useCallback, useEffect, useState } from 'react';
2
2
  import { type InlineEditorToolbarProps } from '@elementor/editor-controls';
3
3
  import { type V1Element } from '@elementor/editor-elements';
4
+ import { type MiddlewareReturn, type MiddlewareState } from '@floating-ui/react';
4
5
 
5
6
  import { type LegacyWindow } from '../../types';
6
7
 
8
+ const TOP_BAR_SELECTOR = '#elementor-editor-wrapper-v2';
9
+ const NAVIGATOR_SELECTOR = '#elementor-navigator';
10
+ const EDITING_PANEL = '#elementor-panel';
11
+
12
+ const EDITOR_ELEMENTS_OUT_OF_IFRAME = [ TOP_BAR_SELECTOR, NAVIGATOR_SELECTOR, EDITING_PANEL ];
13
+
14
+ export const EDITOR_WRAPPER_SELECTOR = 'inline-editor-wrapper';
15
+
16
+ const TOOLBAR_ANCHOR_ID_PREFIX = 'inline-editing-toolbar-anchor';
17
+
18
+ const TOOLBAR_ANCHOR_STATIC_STYLES: CSSProperties = {
19
+ backgroundColor: 'transparent',
20
+ border: 'none',
21
+ outline: 'none',
22
+ boxShadow: 'none',
23
+ padding: '0',
24
+ margin: '0',
25
+ borderRadius: '0',
26
+ overflow: 'hidden',
27
+ opacity: '0',
28
+ pointerEvents: 'none',
29
+ position: 'absolute',
30
+ display: 'block',
31
+ };
32
+
7
33
  export type Editor = InlineEditorToolbarProps[ 'editor' ];
8
34
  export type EditorView = Editor[ 'view' ];
9
35
 
@@ -25,62 +51,134 @@ export const getWidgetType = ( container: V1Element | null ) => {
25
51
  return container?.model?.get( 'widgetType' ) ?? container?.model?.get( 'elType' ) ?? null;
26
52
  };
27
53
 
28
- export const calcSelectionCenterOffsets = ( view: EditorView ): Offsets | null => {
29
- const frameWindow = ( view.root as Document )?.defaultView;
54
+ export const getInlineEditorElement = ( elementWrapper: HTMLElement, expectedTag: string | null ) => {
55
+ return ! expectedTag ? null : ( elementWrapper.querySelector( expectedTag ) as HTMLDivElement );
56
+ };
57
+
58
+ // Elements out of iframe and canvas don't trigger "onClickAway" which unmounts the editor
59
+ // since they are not part of the iframes owner document.
60
+ // We need to manually add listeners to these elements to unmount the editor when they are clicked.
61
+ export const useOnClickOutsideIframe = ( handleUnmount: () => void ) => {
62
+ const asyncUnmountInlineEditor = useCallback( () => queueMicrotask( handleUnmount ), [ handleUnmount ] );
63
+
64
+ useEffect( () => {
65
+ EDITOR_ELEMENTS_OUT_OF_IFRAME.forEach(
66
+ ( selector ) =>
67
+ document?.querySelector( selector )?.addEventListener( 'mousedown', asyncUnmountInlineEditor )
68
+ );
69
+
70
+ return () =>
71
+ EDITOR_ELEMENTS_OUT_OF_IFRAME.forEach(
72
+ ( selector ) =>
73
+ document?.querySelector( selector )?.removeEventListener( 'mousedown', asyncUnmountInlineEditor )
74
+ );
75
+ // eslint-disable-next-line react-hooks/exhaustive-deps
76
+ }, [] );
77
+ };
78
+
79
+ export const useRenderToolbar = ( ownerDocument: Document, id: string ) => {
80
+ const [ anchor, setAnchor ] = useState< HTMLElement | null >( null );
81
+
82
+ const onSelectionEnd = ( view: EditorView ) => {
83
+ const hasSelection = ! view.state.selection.empty;
84
+
85
+ removeToolbarAnchor( ownerDocument, id );
86
+
87
+ if ( hasSelection ) {
88
+ setAnchor( createAnchorBasedOnSelection( ownerDocument, id ) );
89
+ } else {
90
+ setAnchor( null );
91
+ }
92
+ };
93
+
94
+ return { onSelectionEnd, anchor };
95
+ };
96
+
97
+ const createAnchorBasedOnSelection = ( ownerDocument: Document, id: string ): HTMLElement | null => {
98
+ const frameWindow = ownerDocument.defaultView;
30
99
  const selection = frameWindow?.getSelection();
31
- const editorContainer = view.dom;
32
100
 
33
- if ( ! selection || ! editorContainer ) {
101
+ if ( ! selection ) {
34
102
  return null;
35
103
  }
36
104
 
37
105
  const range = selection.getRangeAt( 0 );
38
106
  const selectionRect = range.getBoundingClientRect();
39
- const editorContainerRect = editorContainer.getBoundingClientRect();
107
+ const bodyRect = ownerDocument.body.getBoundingClientRect();
108
+ const toolbarAnchor = ownerDocument.createElement( 'span' );
40
109
 
41
- if ( ! selectionRect || ! editorContainerRect ) {
42
- return null;
43
- }
110
+ styleToolbarAnchor( toolbarAnchor, selectionRect, bodyRect );
111
+ toolbarAnchor.setAttribute( 'id', getToolbarAnchorId( id ) );
44
112
 
45
- const verticalOffset = selectionRect.top - editorContainerRect.top;
113
+ ownerDocument.body.appendChild( toolbarAnchor );
46
114
 
47
- const selectionCenter = selectionRect?.left + selectionRect?.width / 2;
48
- const horizontalOffset = selectionCenter - editorContainerRect.left;
115
+ return toolbarAnchor;
116
+ };
117
+
118
+ export const removeToolbarAnchor = ( ownerDocument: Document, id: string ) => {
119
+ const toolbarAnchor = getToolbarAnchor( ownerDocument, id );
49
120
 
50
- return { left: horizontalOffset, top: verticalOffset };
121
+ if ( toolbarAnchor ) {
122
+ ownerDocument.body.removeChild( toolbarAnchor );
123
+ }
51
124
  };
52
125
 
53
- export const getComputedStyle = ( styles: CSSProperties, offsets: Offsets ): CSSProperties => {
54
- const transform = extractTransformValue( styles );
55
-
56
- return transform
57
- ? {
58
- ...styles,
59
- marginLeft: `${ offsets.left }px`,
60
- marginTop: `${ offsets.top }px`,
61
- pointerEvents: 'none',
62
- }
63
- : {
64
- display: 'none',
65
- };
126
+ const getToolbarAnchorId = ( id: string ) => `${ TOOLBAR_ANCHOR_ID_PREFIX }-${ id }`;
127
+
128
+ export const getToolbarAnchor = ( ownerDocument: Document, id: string ) =>
129
+ ownerDocument.getElementById( getToolbarAnchorId( id ) ) as HTMLElement | null;
130
+
131
+ const styleToolbarAnchor = ( anchor: HTMLElement, selectionRect: DOMRect, bodyRect: DOMRect ) => {
132
+ const { width, height } = selectionRect;
133
+
134
+ Object.assign( anchor.style, {
135
+ ...TOOLBAR_ANCHOR_STATIC_STYLES,
136
+ top: `${ selectionRect.top - bodyRect.top }px`,
137
+ left: `${ selectionRect.left - bodyRect.left }px`,
138
+ width: `${ width }px`,
139
+ height: `${ height }px`,
140
+ } );
66
141
  };
67
142
 
68
- const extractTransformValue = ( styles: CSSProperties ) => {
69
- const translateRegex = /translate\([^)]*\)\s?/g;
70
- const numericValuesRegex = /(-?\d+\.?\d*)/g;
143
+ export const horizontalShifterMiddleware: {
144
+ name: string;
145
+ fn: ( state: MiddlewareState ) => MiddlewareReturn;
146
+ } = {
147
+ name: 'horizontalShifter',
148
+ fn( state ) {
149
+ const {
150
+ x: left,
151
+ y: top,
152
+ elements: { reference: anchor, floating },
153
+ } = state;
71
154
 
72
- const translateValue = styles?.transform?.match( translateRegex )?.[ 0 ];
73
- const values = translateValue?.match( numericValuesRegex );
155
+ const newState: MiddlewareReturn = {
156
+ ...state,
157
+ x: left,
158
+ y: top,
159
+ };
74
160
 
75
- if ( ! translateValue || ! values ) {
76
- return null;
77
- }
161
+ const isLeftOverflown = left < 0;
78
162
 
79
- const [ numericX, numericY ] = values.map( Number );
163
+ if ( isLeftOverflown ) {
164
+ newState.x = 0;
80
165
 
81
- if ( ! numericX || ! numericY ) {
82
- return null;
83
- }
166
+ return newState;
167
+ }
168
+
169
+ const anchorRect = anchor.getBoundingClientRect();
170
+ const right = left + floating.offsetWidth;
171
+ const documentWidth = ( anchor as HTMLElement ).ownerDocument.body.offsetWidth;
172
+ const isRightOverflown = right > documentWidth && anchorRect.right < right;
173
+
174
+ if ( isRightOverflown ) {
175
+ const diff = right - documentWidth;
176
+
177
+ newState.x = left - diff;
178
+
179
+ return newState;
180
+ }
84
181
 
85
- return styles.transform;
182
+ return newState;
183
+ },
86
184
  };
@@ -1,4 +1,4 @@
1
- import { htmlV2PropTypeUtil } from '@elementor/editor-props';
1
+ import { htmlV3PropTypeUtil, stringPropTypeUtil } from '@elementor/editor-props';
2
2
 
3
3
  import { type ModelExtensions } from './create-nested-templated-element-type';
4
4
  import { registerModelExtensions } from './init-legacy-views';
@@ -23,7 +23,10 @@ const tabModelExtensions: ModelExtensions = {
23
23
  ...paragraphElement,
24
24
  settings: {
25
25
  ...paragraphElement.settings,
26
- paragraph: htmlV2PropTypeUtil.create( { content: `Tab ${ position }`, children: [] } ),
26
+ paragraph: htmlV3PropTypeUtil.create( {
27
+ content: stringPropTypeUtil.create( `Tab ${ position }` ),
28
+ children: [],
29
+ } ),
27
30
  },
28
31
  };
29
32
 
@@ -26,3 +26,17 @@ export const inputSchema = {
26
26
  )
27
27
  .default( {} ),
28
28
  };
29
+
30
+ export const outputSchema = {
31
+ errors: z.string().describe( 'Error message if the composition building failed' ).optional(),
32
+ xmlStructure: z
33
+ .string()
34
+ .describe(
35
+ 'The built XML structure as a string. Must use this XML after completion of building the composition, it contains real IDs.'
36
+ )
37
+ .optional(),
38
+ llm_instructions: z
39
+ .string()
40
+ .describe( 'Instructions what to do next, Important to follow these instructions!' )
41
+ .optional(),
42
+ };
@@ -11,7 +11,7 @@ import { CompositionBuilder } from '../../../composition-builder/composition-bui
11
11
  import { BEST_PRACTICES_URI, STYLE_SCHEMA_URI, WIDGET_SCHEMA_URI } from '../../resources/widgets-schema-resource';
12
12
  import { doUpdateElementProperty } from '../../utils/do-update-element-property';
13
13
  import { generatePrompt } from './prompt';
14
- import { inputSchema as schema } from './schema';
14
+ import { inputSchema as schema, outputSchema } from './schema';
15
15
 
16
16
  export const initBuildCompositionsTool = ( reg: MCPRegistryEntry ) => {
17
17
  const { addTool } = reg;
@@ -27,7 +27,7 @@ export const initBuildCompositionsTool = ( reg: MCPRegistryEntry ) => {
27
27
  { description: 'Global Variables', uri: 'elementor://global-variables' },
28
28
  { description: 'Styles best practices', uri: BEST_PRACTICES_URI },
29
29
  ],
30
- // outputSchema: '',
30
+ outputSchema,
31
31
  modelPreferences: {
32
32
  hints: [ { name: 'claude-sonnet-4-5' } ],
33
33
  },
@@ -116,11 +116,12 @@ export const initBuildCompositionsTool = ( reg: MCPRegistryEntry ) => {
116
116
  ) }\n\n"Missing $$type" errors indicate that the configuration objects are invalid. Try again and apply **ALL** object entries with correct $$type.\nNow that you have these errors, fix them and try again. Errors regarding configuration objects, please check against the PropType schemas`;
117
117
  throw new Error( errorText );
118
118
  }
119
- if ( errors.length ) {
120
- throw new Error( errors.map( ( e ) => ( typeof e === 'string' ? e : e.message ) ).join( '\n' ) );
121
- }
122
- // Why text? Until there will be a stable versioning to OutputSchema, it is better to send string to the response.
123
- return `The composition was built successfully with element IDs embedded in the XML.
119
+ return {
120
+ xmlStructure: generatedXML,
121
+ errors: errors?.length
122
+ ? errors.map( ( e ) => ( typeof e === 'string' ? e : e.message ) ).join( '\n\n' )
123
+ : undefined,
124
+ llm_instructions: `The composition was built successfully with element IDs embedded in the XML.
124
125
 
125
126
  **CRITICAL NEXT STEPS** (Follow in order):
126
127
  1. **Apply Global Classes**: Use "apply-global-class" tool to apply the global classes you created BEFORE building this composition
@@ -130,10 +131,8 @@ export const initBuildCompositionsTool = ( reg: MCPRegistryEntry ) => {
130
131
  2. **Fine-tune if needed**: Use "configure-element" tool only for element-specific adjustments that don't warrant global classes
131
132
 
132
133
  Remember: Global classes ensure design consistency and reusability. Don't skip applying them!
133
-
134
- Updated XML structure:
135
- ${ generatedXML }
136
- `;
134
+ `,
135
+ };
137
136
  },
138
137
  } );
139
138
  };
@@ -0,0 +1,10 @@
1
+ import { createTransformer } from '../create-transformer';
2
+
3
+ type HtmlV3Value = {
4
+ content: string | null;
5
+ children: unknown[];
6
+ };
7
+
8
+ export const htmlV3Transformer = createTransformer( ( value: HtmlV3Value ) => {
9
+ return value?.content ?? '';
10
+ } );