@elementor/editor-canvas 4.1.0 → 4.2.0-839

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 (41) hide show
  1. package/dist/index.d.mts +22 -4
  2. package/dist/index.d.ts +22 -4
  3. package/dist/index.js +773 -137
  4. package/dist/index.mjs +730 -86
  5. package/package.json +18 -18
  6. package/src/__tests__/settings-props-resolver.test.ts +3 -0
  7. package/src/composition-builder/composition-builder.ts +5 -5
  8. package/src/form-structure/utils.ts +4 -0
  9. package/src/hooks/__tests__/use-style-items.test.ts +55 -0
  10. package/src/hooks/use-style-items.ts +12 -14
  11. package/src/index.ts +2 -0
  12. package/src/init-settings-transformers.ts +4 -0
  13. package/src/init-style-transformers.ts +2 -0
  14. package/src/legacy/create-nested-templated-element-type.ts +11 -2
  15. package/src/legacy/create-pending-element.ts +74 -0
  16. package/src/legacy/create-templated-element-type.ts +2 -2
  17. package/src/legacy/types.ts +9 -1
  18. package/src/mcp/canvas-mcp.ts +8 -0
  19. package/src/mcp/resources/available-widgets-resource.ts +67 -0
  20. package/src/mcp/resources/document-structure-resource.ts +51 -36
  21. package/src/mcp/resources/editor-state-resource.ts +122 -0
  22. package/src/mcp/resources/general-context-resource.ts +99 -0
  23. package/src/mcp/resources/selected-element-resource.ts +217 -0
  24. package/src/mcp/resources/widgets-schema-resource.ts +74 -14
  25. package/src/mcp/tools/build-composition/prompt.ts +6 -0
  26. package/src/mcp/tools/build-composition/tool.ts +26 -0
  27. package/src/mcp/tools/configure-element/prompt.ts +6 -6
  28. package/src/mcp/tools/configure-element/schema.ts +1 -1
  29. package/src/mcp/tools/configure-element/tool.ts +12 -0
  30. package/src/mcp/tools/get-element-config/tool.ts +13 -3
  31. package/src/mcp/utils/do-update-element-property.ts +1 -1
  32. package/src/mcp/utils/element-data-util.ts +46 -0
  33. package/src/mcp/utils/validate-input.ts +1 -1
  34. package/src/sync/global-styles-imported-event.ts +8 -0
  35. package/src/transformers/settings/date-range-transformer.ts +12 -0
  36. package/src/transformers/settings/time-range-transformer.ts +12 -0
  37. package/src/transformers/shared/image-src-transformer.ts +2 -0
  38. package/src/transformers/shared/image-transformer.ts +4 -1
  39. package/src/transformers/styles/span-transformer.ts +5 -0
  40. package/src/utils/after-render.ts +26 -0
  41. package/src/mcp/utils/generate-available-tags.ts +0 -23
@@ -1,41 +1,43 @@
1
+ import {
2
+ type ExtendedWindow as BaseExtendedWindow,
3
+ type V1Document,
4
+ type V1DocumentsManager,
5
+ } from '@elementor/editor-documents';
6
+ import {
7
+ getWidgetsCache,
8
+ type V1Element,
9
+ type V1ElementEditorSettingsProps,
10
+ type V1ElementModelProps,
11
+ } from '@elementor/editor-elements';
1
12
  import { type MCPRegistryEntry } from '@elementor/editor-mcp';
2
13
  import { __privateListenTo as listenTo, commandEndEvent } from '@elementor/editor-v1-adapters';
3
14
 
4
- type ExtendedWindow = Window & {
5
- elementor?: {
6
- documents?: {
7
- getCurrent?: () => {
8
- id: number;
9
- config: {
10
- type: string;
11
- settings?: {
12
- post_title?: string;
13
- };
14
- };
15
- container: {
16
- children?: ElementorContainer[];
17
- };
18
- };
19
- };
15
+ type UnknownVersionElementInstanceData = V1Element & {
16
+ model: V1Element[ 'model' ] & {
17
+ attributes: V1ElementModelProps;
18
+ config?: { atomic?: boolean };
19
+ editor_settings?: V1ElementEditorSettingsProps;
20
20
  };
21
+ children?: V1Element[];
21
22
  };
22
23
 
23
- type ElementorContainer = {
24
- id: string;
25
- model: {
26
- attributes: {
27
- id: string;
28
- elType: string;
29
- widgetType?: string;
30
- title?: string;
31
- };
32
- editor_settings?: {
33
- title?: string;
34
- };
24
+ type ContainerWithStructure = V1Document & {
25
+ config: V1Document[ 'config' ] & {
26
+ settings?: { post_title?: string };
27
+ };
28
+ container: V1Document[ 'container' ] & {
29
+ children?: UnknownVersionElementInstanceData[];
35
30
  };
36
- children?: ElementorContainer[];
37
31
  };
38
32
 
33
+ interface ExtendedWindow extends BaseExtendedWindow {
34
+ elementor: Omit< BaseExtendedWindow[ 'elementor' ], 'documents' > & {
35
+ documents: Omit< V1DocumentsManager, 'getCurrent' > & {
36
+ getCurrent: () => ContainerWithStructure;
37
+ };
38
+ };
39
+ }
40
+
39
41
  export const DOCUMENT_STRUCTURE_URI = 'elementor://document/structure';
40
42
 
41
43
  export const initDocumentStructureResource = ( reg: MCPRegistryEntry ) => {
@@ -62,6 +64,7 @@ export const initDocumentStructureResource = ( reg: MCPRegistryEntry ) => {
62
64
  commandEndEvent( 'document/elements/copy' ),
63
65
  commandEndEvent( 'document/elements/paste' ),
64
66
  commandEndEvent( 'editor/documents/attach-preview' ),
67
+ commandEndEvent( 'editor/documents/switch' ),
65
68
  ],
66
69
  updateDocumentStructure
67
70
  );
@@ -89,7 +92,7 @@ export const initDocumentStructureResource = ( reg: MCPRegistryEntry ) => {
89
92
  };
90
93
 
91
94
  function getDocumentStructure() {
92
- const extendedWindow = window as ExtendedWindow;
95
+ const extendedWindow = window as unknown as ExtendedWindow;
93
96
  const document = extendedWindow.elementor?.documents?.getCurrent?.();
94
97
 
95
98
  if ( ! document ) {
@@ -97,9 +100,7 @@ function getDocumentStructure() {
97
100
  }
98
101
 
99
102
  const containers = document.container?.children || [];
100
- const elements = ( containers as ElementorContainer[] ).map( ( container: ElementorContainer ) =>
101
- extractElementData( container )
102
- );
103
+ const elements = containers.map( ( container ) => extractElementData( container ) );
103
104
 
104
105
  return {
105
106
  documentId: document.id,
@@ -109,7 +110,20 @@ function getDocumentStructure() {
109
110
  };
110
111
  }
111
112
 
112
- function extractElementData( element: ElementorContainer ): Record< string, unknown > | null {
113
+ function resolveElementVersion( element: UnknownVersionElementInstanceData ): 'v3' | 'v4' {
114
+ if ( element.model?.config?.atomic ) {
115
+ return 'v4';
116
+ }
117
+
118
+ const widgetType = element.model?.attributes?.widgetType;
119
+ if ( widgetType && getWidgetsCache()?.[ widgetType ]?.atomic_props_schema ) {
120
+ return 'v4';
121
+ }
122
+
123
+ return 'v3';
124
+ }
125
+
126
+ function extractElementData( element: UnknownVersionElementInstanceData ): Record< string, unknown > | null {
113
127
  if ( ! element || ! element.model ) {
114
128
  return null;
115
129
  }
@@ -119,6 +133,7 @@ function extractElementData( element: ElementorContainer ): Record< string, unkn
119
133
  id: model.id,
120
134
  elType: model.elType,
121
135
  widgetType: model.widgetType || undefined,
136
+ version: resolveElementVersion( element ),
122
137
  };
123
138
 
124
139
  const title = model.title || element.model?.editor_settings?.title;
@@ -129,8 +144,8 @@ function extractElementData( element: ElementorContainer ): Record< string, unkn
129
144
 
130
145
  if ( element.children && element.children.length > 0 ) {
131
146
  result.children = element.children
132
- .map( ( child: ElementorContainer ) => extractElementData( child ) )
133
- .filter( ( child: Record< string, unknown > | null ) => child !== null );
147
+ .map( ( child ) => extractElementData( child as UnknownVersionElementInstanceData ) )
148
+ .filter( ( child ) => child !== null );
134
149
  }
135
150
 
136
151
  return result;
@@ -0,0 +1,122 @@
1
+ import { type MCPRegistryEntry } from '@elementor/editor-mcp';
2
+ import { __privateListenTo as listenTo, commandEndEvent } from '@elementor/editor-v1-adapters';
3
+
4
+ const CURRENTLY_VIEWED_SCREEN = 'The user is currently viewing the Elementor editor';
5
+ const PAGE_CONTENT_CHARACTER_LIMIT = 500;
6
+ const PREVIEW_TEXT_NODE_MIN_LENGTH = 2;
7
+
8
+ export const EDITOR_STATE_URI = 'elementor://context/editor-state';
9
+
10
+ type ElementorWindow = Window & {
11
+ elementor?: {
12
+ $previewContents?: Element[];
13
+ documents?: {
14
+ getCurrent?: () => {
15
+ config?: {
16
+ settings?: {
17
+ post_title?: string;
18
+ };
19
+ };
20
+ };
21
+ };
22
+ };
23
+ };
24
+
25
+ export const initEditorStateResource = ( reg: MCPRegistryEntry ) => {
26
+ const { resource, sendResourceUpdated } = reg;
27
+
28
+ let lastSerializedState = '';
29
+
30
+ const buildState = () => ( {
31
+ currentlyViewedScreen: CURRENTLY_VIEWED_SCREEN,
32
+ pageContent: getPageContentFromPreview(),
33
+ pageTitle: getPageTitle(),
34
+ } );
35
+
36
+ const notifyIfChanged = () => {
37
+ const serialized = JSON.stringify( buildState() );
38
+ if ( serialized === lastSerializedState ) {
39
+ return;
40
+ }
41
+ lastSerializedState = serialized;
42
+ sendResourceUpdated( { uri: EDITOR_STATE_URI } );
43
+ };
44
+
45
+ listenTo(
46
+ [ commandEndEvent( 'editor/documents/switch' ), commandEndEvent( 'editor/documents/attach-preview' ) ],
47
+ notifyIfChanged
48
+ );
49
+
50
+ lastSerializedState = JSON.stringify( buildState() );
51
+
52
+ resource(
53
+ 'editor-state',
54
+ EDITOR_STATE_URI,
55
+ {
56
+ description: 'Editor page title, preview text snapshot, and viewed screen label.',
57
+ },
58
+ async () => {
59
+ return {
60
+ contents: [
61
+ {
62
+ uri: EDITOR_STATE_URI,
63
+ text: JSON.stringify( buildState(), null, 2 ),
64
+ },
65
+ ],
66
+ };
67
+ }
68
+ );
69
+ };
70
+
71
+ function getPageContentFromPreview(): string | null {
72
+ try {
73
+ const root = ( window as ElementorWindow ).elementor?.$previewContents?.[ 0 ];
74
+ if ( ! root ) {
75
+ return null;
76
+ }
77
+ const content: string[] = [];
78
+ const clone = root.cloneNode( true ) as HTMLElement;
79
+ clone.querySelectorAll( '.elementor-editor-element-settings, #elementor-add-new-section' ).forEach( ( el ) => {
80
+ el.remove();
81
+ } );
82
+ const walk = ( node: Node, insideElementorElement = false ) => {
83
+ const isInside = ( node as Element ).classList?.contains( 'elementor-element' ) || insideElementorElement;
84
+ if ( node.nodeType === Node.TEXT_NODE && isInside ) {
85
+ const text = node.textContent?.trim().replace( /\s+/g, ' ' );
86
+ if ( text && text.length > PREVIEW_TEXT_NODE_MIN_LENGTH ) {
87
+ content.push( text );
88
+ }
89
+ } else {
90
+ node.childNodes.forEach( ( child ) => {
91
+ walk( child, isInside );
92
+ } );
93
+ }
94
+ };
95
+ walk( clone );
96
+ const text = content.join( ' ' );
97
+ if ( text.length > PAGE_CONTENT_CHARACTER_LIMIT ) {
98
+ return text.slice( 0, PAGE_CONTENT_CHARACTER_LIMIT ) + '...';
99
+ }
100
+ return text;
101
+ } catch {
102
+ return null;
103
+ }
104
+ }
105
+
106
+ function getPageTitle(): string {
107
+ try {
108
+ const extendedWindow = window as ElementorWindow;
109
+ const currentDocument = extendedWindow.elementor?.documents?.getCurrent?.();
110
+ const postTitle = currentDocument?.config?.settings?.post_title;
111
+ if ( postTitle ) {
112
+ return postTitle;
113
+ }
114
+
115
+ let title = document.title || 'Page';
116
+ title = title.split( /\s*[‹»|–—-]\s*/ )[ 0 ];
117
+ const trimmed = title.trim();
118
+ return trimmed || 'Page';
119
+ } catch {
120
+ return 'Page';
121
+ }
122
+ }
@@ -0,0 +1,99 @@
1
+ import { type MCPRegistryEntry } from '@elementor/editor-mcp';
2
+ import { __privateListenTo as listenTo, commandEndEvent } from '@elementor/editor-v1-adapters';
3
+
4
+ type ExtendedWindow = Window & {
5
+ angieConfig?: {
6
+ plugins?: Record< string, unknown >;
7
+ };
8
+ elementor?: {
9
+ documents?: {
10
+ getCurrent?: () => {
11
+ config?: {
12
+ settings?: {
13
+ post_title?: string;
14
+ };
15
+ };
16
+ };
17
+ };
18
+ };
19
+ };
20
+
21
+ export const GENERAL_CONTEXT_URI = 'elementor://context/general';
22
+
23
+ export const initGeneralContextResource = ( reg: MCPRegistryEntry ) => {
24
+ const { resource, sendResourceUpdated } = reg;
25
+
26
+ let lastSerializedPayload: string | null = null;
27
+
28
+ const getPageTitle = (): string | null => {
29
+ const extendedWindow = window as ExtendedWindow;
30
+ const title = extendedWindow.elementor?.documents?.getCurrent?.()?.config?.settings?.post_title;
31
+ if ( ! title?.trim() ) {
32
+ return null;
33
+ }
34
+ return title;
35
+ };
36
+
37
+ const buildPayload = () => {
38
+ const extendedWindow = window as ExtendedWindow;
39
+ const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
40
+ const postParam = new URLSearchParams( location.search ).get( 'post' );
41
+ const parsedPostId = postParam ? Number( postParam ) : null;
42
+ const postId = parsedPostId !== null && Number.isFinite( parsedPostId ) ? parsedPostId : null;
43
+ const pageTitle = getPageTitle();
44
+ const urlObject = new URL( window.location.href );
45
+ const pageUrl = urlObject.pathname + urlObject.search;
46
+ const pageName = pageTitle || 'Elementor Editor';
47
+ const plugins = extendedWindow.angieConfig?.plugins;
48
+
49
+ return {
50
+ timezone,
51
+ postId,
52
+ currentPage: {
53
+ pageName,
54
+ pageTitle,
55
+ pageUrl,
56
+ },
57
+ ...( plugins && { plugins } ),
58
+ };
59
+ };
60
+
61
+ const pushUpdateIfChanged = () => {
62
+ const serialized = JSON.stringify( buildPayload() );
63
+ if ( serialized === lastSerializedPayload ) {
64
+ return;
65
+ }
66
+ lastSerializedPayload = serialized;
67
+ sendResourceUpdated( { uri: GENERAL_CONTEXT_URI } );
68
+ };
69
+
70
+ resource(
71
+ 'general-context',
72
+ GENERAL_CONTEXT_URI,
73
+ {
74
+ description: 'General context: timezone, post id, and current page.',
75
+ },
76
+ async () => {
77
+ return {
78
+ contents: [
79
+ {
80
+ uri: GENERAL_CONTEXT_URI,
81
+ mimeType: 'application/json',
82
+ text: JSON.stringify( buildPayload(), null, 2 ),
83
+ },
84
+ ],
85
+ };
86
+ }
87
+ );
88
+
89
+ listenTo(
90
+ [
91
+ commandEndEvent( 'editor/documents/switch' ),
92
+ commandEndEvent( 'editor/documents/attach-preview' ),
93
+ commandEndEvent( 'document/elements/settings' ),
94
+ ],
95
+ pushUpdateIfChanged
96
+ );
97
+
98
+ pushUpdateIfChanged();
99
+ };
@@ -0,0 +1,217 @@
1
+ import { getContainer, getSelectedElements, getWidgetsCache, type V1Element } from '@elementor/editor-elements';
2
+ import { type MCPRegistryEntry } from '@elementor/editor-mcp';
3
+ import {
4
+ __privateListenTo as listenTo,
5
+ commandEndEvent,
6
+ type CommandEvent,
7
+ type ListenerEvent,
8
+ } from '@elementor/editor-v1-adapters';
9
+
10
+ export const SELECTED_ELEMENT_URI = 'elementor://context/selected-element';
11
+
12
+ type WidgetVersion = 'v3' | 'v4';
13
+
14
+ type SelectionContainer = V1Element & {
15
+ type?: string;
16
+ label?: string;
17
+ model: V1Element[ 'model' ] & {
18
+ config?: { atomic?: boolean };
19
+ };
20
+ settings: V1Element[ 'settings' ] & {
21
+ toJSON?: () => Record< string, unknown >;
22
+ };
23
+ };
24
+
25
+ type SelectedElementPayload = {
26
+ elementDisplayName: string | null;
27
+ elementType: string | null;
28
+ properties: Record< string, unknown > | null;
29
+ selectedElementId: string | null;
30
+ selectedParentId: string | null;
31
+ version: WidgetVersion | null;
32
+ widgetType: string | null;
33
+ };
34
+
35
+ export const initSelectedElementResource = ( reg: MCPRegistryEntry ) => {
36
+ const { resource, sendResourceUpdated } = reg;
37
+
38
+ let currentPayloadText: string | null = null;
39
+
40
+ const publishIfChanged = ( payload: SelectedElementPayload ) => {
41
+ const nextText = JSON.stringify( payload );
42
+
43
+ if ( nextText !== currentPayloadText ) {
44
+ currentPayloadText = nextText;
45
+ sendResourceUpdated( { uri: SELECTED_ELEMENT_URI } );
46
+ }
47
+ };
48
+
49
+ const onCommand = ( e: ListenerEvent ) => {
50
+ if ( e.type !== 'command' ) {
51
+ return;
52
+ }
53
+
54
+ const commandEvent = e as CommandEvent< { container?: SelectionContainer } >;
55
+
56
+ if ( commandEvent.command === 'document/elements/deselect-all' ) {
57
+ publishIfChanged( createEmptySelectedElementPayload() );
58
+ return;
59
+ }
60
+
61
+ if (
62
+ commandEvent.command !== 'document/elements/select' &&
63
+ commandEvent.command !== 'document/elements/settings'
64
+ ) {
65
+ return;
66
+ }
67
+
68
+ const { container } = commandEvent.args || {};
69
+
70
+ if ( container?.id ) {
71
+ publishIfChanged( buildPayloadFromContainer( container ) );
72
+ return;
73
+ }
74
+
75
+ publishIfChanged( readSelectionFromEditor() );
76
+ };
77
+
78
+ listenTo(
79
+ [
80
+ commandEndEvent( 'document/elements/select' ),
81
+ commandEndEvent( 'document/elements/deselect-all' ),
82
+ commandEndEvent( 'document/elements/settings' ),
83
+ ],
84
+ onCommand
85
+ );
86
+
87
+ publishIfChanged( readSelectionFromEditor() );
88
+
89
+ resource(
90
+ 'selected-element',
91
+ SELECTED_ELEMENT_URI,
92
+ {
93
+ description: 'Currently selected Elementor element context.',
94
+ },
95
+ async () => {
96
+ return {
97
+ contents: [
98
+ {
99
+ uri: SELECTED_ELEMENT_URI,
100
+ text: JSON.stringify( readSelectionFromEditor(), null, 2 ),
101
+ },
102
+ ],
103
+ };
104
+ }
105
+ );
106
+ };
107
+
108
+ function createEmptySelectedElementPayload(): SelectedElementPayload {
109
+ return {
110
+ elementDisplayName: null,
111
+ elementType: null,
112
+ properties: null,
113
+ selectedElementId: null,
114
+ selectedParentId: null,
115
+ version: null,
116
+ widgetType: null,
117
+ };
118
+ }
119
+
120
+ function readSelectionFromEditor(): SelectedElementPayload {
121
+ const elements = getSelectedElements();
122
+
123
+ if ( elements.length !== 1 ) {
124
+ return createEmptySelectedElementPayload();
125
+ }
126
+
127
+ const container = getContainer( elements[ 0 ].id );
128
+
129
+ return buildPayloadFromContainer( container );
130
+ }
131
+
132
+ function buildPayloadFromContainer( container: SelectionContainer | null ): SelectedElementPayload {
133
+ if ( ! container?.id ) {
134
+ return createEmptySelectedElementPayload();
135
+ }
136
+
137
+ const widgetType = container.model.get( 'widgetType' ) ?? null;
138
+ const elementType = container.type ?? 'widget';
139
+
140
+ return {
141
+ elementDisplayName: getElementDisplayName( container ),
142
+ elementType,
143
+ properties: getElementProperties( container, widgetType ),
144
+ selectedElementId: container.id,
145
+ selectedParentId: container.parent?.id ?? null,
146
+ version: resolveElementVersion( container, widgetType ),
147
+ widgetType,
148
+ };
149
+ }
150
+
151
+ function resolveElementVersion( container: SelectionContainer, widgetType: string | null ): WidgetVersion {
152
+ if ( container.model?.config?.atomic ) {
153
+ return 'v4';
154
+ }
155
+ if ( widgetType && getWidgetsCache()?.[ widgetType ]?.atomic_props_schema ) {
156
+ return 'v4';
157
+ }
158
+ return 'v3';
159
+ }
160
+
161
+ function getElementProperties(
162
+ container: SelectionContainer,
163
+ widgetType: string | null
164
+ ): Record< string, unknown > | null {
165
+ const settings = container.settings?.toJSON?.();
166
+ if ( ! settings || typeof settings !== 'object' ) {
167
+ return null;
168
+ }
169
+
170
+ const widgetConfig = widgetType ? getWidgetsCache()?.[ widgetType ] : null;
171
+ const controls = widgetConfig?.controls as Record< string, { default?: unknown } > | undefined;
172
+
173
+ const filtered: Record< string, unknown > = {};
174
+
175
+ for ( const [ key, value ] of Object.entries( settings ) ) {
176
+ if ( value === undefined || value === null || value === '' ) {
177
+ continue;
178
+ }
179
+
180
+ const controlDefault = controls?.[ key ]?.default;
181
+ if ( controlDefault !== undefined && JSON.stringify( value ) === JSON.stringify( controlDefault ) ) {
182
+ continue;
183
+ }
184
+
185
+ filtered[ key ] = value;
186
+ }
187
+
188
+ return Object.keys( filtered ).length > 0 ? filtered : null;
189
+ }
190
+
191
+ function getElementDisplayName( container: SelectionContainer ): string {
192
+ try {
193
+ if ( container.label ) {
194
+ return container.label;
195
+ }
196
+
197
+ const widgetType = container.model?.get?.( 'widgetType' );
198
+
199
+ if ( widgetType ) {
200
+ const capitalizedType = widgetType.charAt( 0 ).toUpperCase() + widgetType.slice( 1 );
201
+
202
+ return capitalizedType.replace( /-/g, ' ' );
203
+ }
204
+
205
+ if ( container.type === 'container' ) {
206
+ return 'Container';
207
+ }
208
+
209
+ if ( container.type === 'section' ) {
210
+ return 'Section';
211
+ }
212
+
213
+ return `Element ${ container.id }`;
214
+ } catch {
215
+ return `Element ${ container.id }`;
216
+ }
217
+ }
@@ -11,6 +11,50 @@ import {
11
11
  } from '@elementor/editor-props';
12
12
  import { getStylesSchema } from '@elementor/editor-styles';
13
13
 
14
+ import { hasV3Controls, isWidgetAvailableForLLM } from '../utils/element-data-util';
15
+
16
+ const V3_LAYOUT_CONTROL_TYPES = new Set( [ 'section', 'tab', 'tabs' ] );
17
+
18
+ type V3ControlMetadataEntry = {
19
+ default?: unknown;
20
+ type?: string;
21
+ options?: unknown;
22
+ };
23
+
24
+ function extractV3ControlsMetadata( controls: unknown ): Record< string, V3ControlMetadataEntry > {
25
+ if ( ! hasV3Controls( controls ) ) {
26
+ return {};
27
+ }
28
+ const result: Record< string, V3ControlMetadataEntry > = {};
29
+ for ( const [ controlKey, raw ] of Object.entries( controls as Record< string, unknown > ) ) {
30
+ if ( ! raw || typeof raw !== 'object' ) {
31
+ continue;
32
+ }
33
+ const control = raw as Record< string, unknown >;
34
+ const controlType = typeof control.type === 'string' ? control.type : undefined;
35
+ if ( controlType && V3_LAYOUT_CONTROL_TYPES.has( controlType ) ) {
36
+ continue;
37
+ }
38
+ const entry: V3ControlMetadataEntry = {};
39
+ if ( Object.prototype.hasOwnProperty.call( control, 'default' ) ) {
40
+ entry.default = control.default;
41
+ }
42
+ if ( controlType ) {
43
+ entry.type = controlType;
44
+ }
45
+ if ( Object.prototype.hasOwnProperty.call( control, 'options' ) && control.options !== undefined ) {
46
+ const options = control.options;
47
+ if ( options && typeof options === 'object' && ! Array.isArray( options ) ) {
48
+ entry.options = Object.keys( options as Record< string, unknown > );
49
+ } else {
50
+ entry.options = options;
51
+ }
52
+ }
53
+ result[ controlKey ] = entry;
54
+ }
55
+ return result;
56
+ }
57
+
14
58
  export const WIDGET_SCHEMA_URI = 'elementor://widgets/schema/{widgetType}';
15
59
  export const STYLE_SCHEMA_URI = 'elementor://styles/schema/{category}';
16
60
  export const BEST_PRACTICES_URI = 'elementor://styles/best-practices';
@@ -61,7 +105,7 @@ Variables from the user context ARE NOT SUPPORTED AND WILL RESOLVE IN ERROR.
61
105
  },
62
106
  } ),
63
107
  {
64
- description: 'Common styles schema for the specified category',
108
+ description: 'Common styles schema for the specified category (applicable for V4 elements only)',
65
109
  },
66
110
  async ( uri, variables ) => {
67
111
  const category = typeof variables.category === 'string' ? variables.category : variables.category?.[ 0 ];
@@ -89,10 +133,10 @@ Variables from the user context ARE NOT SUPPORTED AND WILL RESOLVE IN ERROR.
89
133
  new ResourceTemplate( WIDGET_SCHEMA_URI, {
90
134
  list: () => {
91
135
  const cache = getWidgetsCache() || {};
92
- const availableWidgets = Object.keys( cache || {} ).filter(
93
- ( widgetType ) =>
94
- cache[ widgetType ]?.atomic_props_schema && cache[ widgetType ].meta?.llm_support !== false
136
+ const availableWidgets = Object.keys( cache ).filter( ( widgetType ) =>
137
+ isWidgetAvailableForLLM( cache[ widgetType ] )
95
138
  );
139
+
96
140
  return {
97
141
  resources: availableWidgets.map( ( widgetType ) => ( {
98
142
  uri: `elementor://widgets/schema/${ widgetType }`,
@@ -108,20 +152,36 @@ Variables from the user context ARE NOT SUPPORTED AND WILL RESOLVE IN ERROR.
108
152
  const widgetType =
109
153
  typeof variables.widgetType === 'string' ? variables.widgetType : variables.widgetType?.[ 0 ];
110
154
  const widgetData = getWidgetsCache()?.[ widgetType ];
111
- const propSchema = widgetData?.atomic_props_schema;
112
- if ( ! propSchema || ! widgetData ) {
155
+ if ( ! widgetData ) {
113
156
  throw new Error( `No prop schema found for element type: ${ widgetType }` );
114
157
  }
158
+ const propSchema = widgetData.atomic_props_schema;
159
+ if ( ! propSchema ) {
160
+ if ( ! hasV3Controls( widgetData.controls ) ) {
161
+ throw new Error( `No prop schema found for element type: ${ widgetType }` );
162
+ }
163
+ const controlMetadata = extractV3ControlsMetadata( widgetData.controls );
164
+ return {
165
+ contents: [
166
+ {
167
+ uri: uri.toString(),
168
+ mimeType: 'application/json',
169
+ text: JSON.stringify( {
170
+ widget_version: 'v3',
171
+ message:
172
+ 'This widget exists in the editor but has no atomic props schema (V4). Use control_metadata as non-authoritative hints from legacy controls.',
173
+ fields_note: 'All settings are optional; there is no JSON schema for this widget type.',
174
+ properties: controlMetadata,
175
+ } ),
176
+ },
177
+ ],
178
+ };
179
+ }
115
180
  const asJson = Object.fromEntries(
116
- Object.entries( propSchema ).map( ( [ key, propType ] ) => [
117
- key,
118
- Schema.propTypeToJsonSchema( propType ),
119
- ] )
181
+ Object.entries( propSchema )
182
+ .filter( ( [ key, propType ] ) => Schema.isPropKeyConfigurable( key, propType as PropType ) )
183
+ .map( ( [ key, propType ] ) => [ key, Schema.propTypeToJsonSchema( propType ) ] )
120
184
  );
121
- Schema.nonConfigurablePropKeys.forEach( ( key ) => {
122
- // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
123
- delete asJson[ key ];
124
- } );
125
185
 
126
186
  const description =
127
187
  typeof widgetData?.meta?.description === 'string' ? widgetData.meta.description : undefined;