@elementor/editor-canvas 4.2.0-beta2 → 4.3.0-1001
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +449 -1319
- package/dist/index.mjs +314 -1197
- package/package.json +22 -22
- package/src/index.ts +1 -1
- package/src/legacy/__tests__/init-legacy-views.test.ts +1 -1
- package/src/mcp/canvas-mcp.ts +4 -2
- package/src/mcp/resources/__tests__/available-widgets-resource.test.ts +85 -0
- package/src/mcp/resources/__tests__/dynamic-tags-resource.test.ts +27 -37
- package/src/mcp/resources/__tests__/widgets-schema-resource.test.ts +82 -74
- package/src/mcp/resources/available-widgets-resource.ts +35 -41
- package/src/mcp/resources/best-practices-resource.ts +34 -0
- package/src/mcp/resources/dynamic-tags-resource.ts +28 -29
- package/src/mcp/resources/widgets-schema-resource.ts +37 -177
- package/src/mcp/tools/build-composition/tool.ts +107 -218
- package/src/sync/element-added-event.ts +8 -0
- package/src/utils/__tests__/grid-outline-utils.test.ts +39 -0
- package/src/utils/grid-outline-utils.ts +13 -2
- package/src/composition-builder/__tests__/composition-builder.test.ts +0 -352
- package/src/composition-builder/composition-builder.ts +0 -350
- package/src/composition-builder/utils/__tests__/required-children-enforcer.test.ts +0 -79
- package/src/composition-builder/utils/__tests__/required-default-child-tags.test.ts +0 -35
- package/src/composition-builder/utils/required-children-enforcer.ts +0 -56
- package/src/composition-builder/utils/required-default-child-tags.ts +0 -22
- package/src/mcp/resources/best-practices.ts +0 -159
- package/src/mcp/resources/build-llm-guidance.ts +0 -110
- package/src/mcp/tools/build-composition/__tests__/xml-leaf-wrapper.test.ts +0 -117
- package/src/mcp/tools/build-composition/prompt.ts +0 -167
- package/src/mcp/tools/build-composition/schema.ts +0 -42
- package/src/mcp/tools/build-composition/xml-leaf-wrapper.ts +0 -68
- package/src/mcp/utils/__tests__/get-composition-target-container.test.ts +0 -59
- package/src/mcp/utils/get-composition-target-container.ts +0 -15
|
@@ -1,350 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createElement,
|
|
3
|
-
type CreateElementParams,
|
|
4
|
-
deleteElement,
|
|
5
|
-
generateElementId,
|
|
6
|
-
getContainer,
|
|
7
|
-
getWidgetsCache,
|
|
8
|
-
type V1Element,
|
|
9
|
-
type V1ElementConfig,
|
|
10
|
-
type V1ElementModelProps,
|
|
11
|
-
} from '@elementor/editor-elements';
|
|
12
|
-
import { type z } from '@elementor/schema';
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
collectEmptyMessageErrors,
|
|
16
|
-
collectFormAncestorErrors,
|
|
17
|
-
collectSubmitButtonErrors,
|
|
18
|
-
} from '../form-structure/utils';
|
|
19
|
-
import { doUpdateElementProperty } from '../mcp/utils/do-update-element-property';
|
|
20
|
-
import { mergeCustomCssText } from '../mcp/utils/merge-custom-css';
|
|
21
|
-
import { RequiredChildrenEnforcer } from './utils/required-children-enforcer';
|
|
22
|
-
import { getRequiredDefaultChildTemplates } from './utils/required-default-child-tags';
|
|
23
|
-
|
|
24
|
-
type AnyValue = z.infer< z.ZodTypeAny >;
|
|
25
|
-
type AnyConfig = Record< string, Record< string, AnyValue > >;
|
|
26
|
-
|
|
27
|
-
const CREATE_ELEMENT_INVALID_CONTAINER_MESSAGE = 'createElement did not return an element container with a model.';
|
|
28
|
-
|
|
29
|
-
type API = {
|
|
30
|
-
createElement: typeof createElement;
|
|
31
|
-
deleteElement: typeof deleteElement;
|
|
32
|
-
getWidgetsCache: typeof getWidgetsCache;
|
|
33
|
-
generateElementId: typeof generateElementId;
|
|
34
|
-
getContainer: typeof getContainer;
|
|
35
|
-
doUpdateElementProperty: typeof doUpdateElementProperty;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
type CtorOptions = {
|
|
39
|
-
xml: Document;
|
|
40
|
-
api?: Partial< API >;
|
|
41
|
-
elementConfig?: AnyConfig;
|
|
42
|
-
stylesConfig?: AnyConfig;
|
|
43
|
-
customCSS?: Record< string, string >;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
export class CompositionBuilder {
|
|
47
|
-
private elementConfig: Record< string, Record< string, AnyValue > > = {};
|
|
48
|
-
private elementStylesConfig: Record< string, Record< string, AnyValue > > = {};
|
|
49
|
-
private elementCustomCSS: Record< string, string > = {};
|
|
50
|
-
private rootContainers: V1Element[] = [];
|
|
51
|
-
private api: API = {
|
|
52
|
-
createElement,
|
|
53
|
-
deleteElement,
|
|
54
|
-
getWidgetsCache,
|
|
55
|
-
generateElementId,
|
|
56
|
-
getContainer,
|
|
57
|
-
doUpdateElementProperty,
|
|
58
|
-
};
|
|
59
|
-
private xml: Document;
|
|
60
|
-
|
|
61
|
-
public static fromXMLString( xmlString: string, api: Partial< API > = {} ): CompositionBuilder {
|
|
62
|
-
const parser = new DOMParser();
|
|
63
|
-
const xmlDoc = parser.parseFromString( xmlString, 'application/xml' );
|
|
64
|
-
const errorNode = xmlDoc.querySelector( 'parsererror' );
|
|
65
|
-
if ( errorNode ) {
|
|
66
|
-
throw new Error( 'Failed to parse XML string: ' + errorNode.textContent );
|
|
67
|
-
}
|
|
68
|
-
return new CompositionBuilder( {
|
|
69
|
-
xml: xmlDoc,
|
|
70
|
-
api,
|
|
71
|
-
} );
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
constructor( opts: CtorOptions ) {
|
|
75
|
-
const { api = {}, elementConfig = {}, stylesConfig = {}, customCSS = {}, xml } = opts;
|
|
76
|
-
this.xml = xml;
|
|
77
|
-
Object.assign( this.api, api );
|
|
78
|
-
this.setElementConfig( elementConfig );
|
|
79
|
-
this.setStylesConfig( stylesConfig );
|
|
80
|
-
this.setCustomCSS( customCSS );
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
setElementConfig( config: Record< string, Record< string, AnyValue > > ) {
|
|
84
|
-
this.elementConfig = config;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
setStylesConfig( config: Record< string, Record< string, AnyValue > > ) {
|
|
88
|
-
this.elementStylesConfig = config;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
setCustomCSS( config: Record< string, string > ) {
|
|
92
|
-
this.elementCustomCSS = config;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
getXML() {
|
|
96
|
-
return this.xml;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
private buildModelTree(
|
|
100
|
-
node: Element,
|
|
101
|
-
widgetsCache: Record< string, V1ElementConfig >
|
|
102
|
-
): Record< string, unknown > {
|
|
103
|
-
const elementTag = node.tagName;
|
|
104
|
-
const isWidget = widgetsCache[ elementTag ]?.elType === 'widget';
|
|
105
|
-
const id = this.api.generateElementId();
|
|
106
|
-
const children = Array.from( node.children ).map( ( child ) => this.buildModelTree( child, widgetsCache ) );
|
|
107
|
-
|
|
108
|
-
node.setAttribute( 'id', id );
|
|
109
|
-
|
|
110
|
-
const base: V1ElementModelProps = {
|
|
111
|
-
id,
|
|
112
|
-
skipDefaultChildren: true,
|
|
113
|
-
elements: children as V1ElementModelProps[ 'elements' ],
|
|
114
|
-
editor_settings: {
|
|
115
|
-
title: node.getAttribute( 'configuration-id' ) ?? undefined,
|
|
116
|
-
},
|
|
117
|
-
elType: 'widget',
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
// TODO: Restore this code once components are working in compositions
|
|
121
|
-
// if ( elementTag === 'e-component' ) {
|
|
122
|
-
// // apply component id before applying values
|
|
123
|
-
// const elementConfig = this.elementConfig[ String( node.getAttribute( 'configuration-id' ) ) ];
|
|
124
|
-
// if ( elementConfig ) {
|
|
125
|
-
// base.settings = base.settings || {};
|
|
126
|
-
// base.settings.component_instance = elementConfig.component_instance;
|
|
127
|
-
// }
|
|
128
|
-
// }
|
|
129
|
-
|
|
130
|
-
if ( isWidget ) {
|
|
131
|
-
return { ...base, elType: 'widget' as const, widgetType: elementTag };
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return { ...base, elType: elementTag };
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
private async awaitViewRender( element: V1Element ) {
|
|
138
|
-
const view = element.view as Record< string, unknown > | undefined;
|
|
139
|
-
if ( view?._currentRenderPromise instanceof Promise ) {
|
|
140
|
-
await view._currentRenderPromise;
|
|
141
|
-
} else {
|
|
142
|
-
await Promise.resolve();
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
private validateChildTypes( node: Element, widgetsCache: Record< string, V1ElementConfig > ): string[] {
|
|
147
|
-
const errors: string[] = [];
|
|
148
|
-
const allowedChildTypes = widgetsCache[ node.tagName ]?.allowed_child_types;
|
|
149
|
-
|
|
150
|
-
if ( allowedChildTypes?.length ) {
|
|
151
|
-
for ( const child of Array.from( node.children ) ) {
|
|
152
|
-
if ( ! allowedChildTypes.includes( child.tagName ) ) {
|
|
153
|
-
errors.push(
|
|
154
|
-
`"${ child.tagName }" is not allowed as a child of "${
|
|
155
|
-
node.tagName
|
|
156
|
-
}". Allowed: ${ allowedChildTypes.join( ', ' ) }`
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
for ( const child of Array.from( node.children ) ) {
|
|
163
|
-
errors.push( ...this.validateChildTypes( child, widgetsCache ) );
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return errors;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
private matchNodeByConfigId( configId: string ) {
|
|
170
|
-
const node = this.xml.querySelector( `[configuration-id="${ configId }"]` );
|
|
171
|
-
if ( ! node ) {
|
|
172
|
-
throw new Error( `Configuration id "${ configId }" does not have target node.` );
|
|
173
|
-
}
|
|
174
|
-
const id = node.getAttribute( 'id' );
|
|
175
|
-
if ( ! id ) {
|
|
176
|
-
throw new Error( `Node with configuration id "${ configId }" does not have element id.` );
|
|
177
|
-
}
|
|
178
|
-
const element = this.api.getContainer( id );
|
|
179
|
-
if ( ! element ) {
|
|
180
|
-
throw new Error( `Element with id "${ id }" not found but should exist.` );
|
|
181
|
-
}
|
|
182
|
-
return {
|
|
183
|
-
element,
|
|
184
|
-
node,
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
private async applyProperties() {
|
|
189
|
-
const configErrors: string[] = [];
|
|
190
|
-
const styleErrors: string[] = [];
|
|
191
|
-
|
|
192
|
-
const allConfigIds = new Set( [
|
|
193
|
-
...Object.keys( this.elementConfig ),
|
|
194
|
-
...Object.keys( this.elementStylesConfig ),
|
|
195
|
-
...Object.keys( this.elementCustomCSS ),
|
|
196
|
-
] );
|
|
197
|
-
|
|
198
|
-
for ( const configId of allConfigIds ) {
|
|
199
|
-
let element, node;
|
|
200
|
-
try {
|
|
201
|
-
( { element, node } = this.matchNodeByConfigId( configId ) );
|
|
202
|
-
} catch ( matchErr ) {
|
|
203
|
-
const msg = ( matchErr as Error ).message;
|
|
204
|
-
if ( this.elementConfig[ configId ] ) {
|
|
205
|
-
configErrors.push( msg );
|
|
206
|
-
}
|
|
207
|
-
if ( this.elementStylesConfig[ configId ] || this.elementCustomCSS[ configId ] ) {
|
|
208
|
-
styleErrors.push( msg );
|
|
209
|
-
}
|
|
210
|
-
continue;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const config = this.elementConfig[ configId ];
|
|
214
|
-
if ( config ) {
|
|
215
|
-
for ( const [ propertyName, propertyValue ] of Object.entries( config ) ) {
|
|
216
|
-
try {
|
|
217
|
-
this.api.doUpdateElementProperty( {
|
|
218
|
-
elementId: element.id,
|
|
219
|
-
propertyName,
|
|
220
|
-
propertyValue,
|
|
221
|
-
elementType: node.tagName,
|
|
222
|
-
} );
|
|
223
|
-
} catch ( error ) {
|
|
224
|
-
configErrors.push( ( error as Error ).message );
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const styleConfig = this.elementStylesConfig[ configId ];
|
|
230
|
-
const hasInvalidStyles = false;
|
|
231
|
-
if ( styleConfig ) {
|
|
232
|
-
const validStylesPropValues: Record< string, AnyValue > = {};
|
|
233
|
-
for ( const [ styleName, stylePropValue ] of Object.entries( styleConfig ) ) {
|
|
234
|
-
if ( styleName === '$intention' ) {
|
|
235
|
-
continue;
|
|
236
|
-
} else {
|
|
237
|
-
// skipping actual validation - properies comes from the server
|
|
238
|
-
validStylesPropValues[ styleName ] = stylePropValue;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
if ( Object.keys( validStylesPropValues ).length > 0 ) {
|
|
242
|
-
try {
|
|
243
|
-
this.api.doUpdateElementProperty( {
|
|
244
|
-
elementId: element.id,
|
|
245
|
-
propertyName: '_styles',
|
|
246
|
-
propertyValue: validStylesPropValues,
|
|
247
|
-
elementType: node.tagName,
|
|
248
|
-
} );
|
|
249
|
-
} catch ( error ) {
|
|
250
|
-
styleErrors.push( String( error ) );
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const intentionCss = typeof styleConfig?.$intention === 'string' ? styleConfig.$intention.trim() : '';
|
|
256
|
-
const fallbackCss = hasInvalidStyles && intentionCss ? intentionCss : '';
|
|
257
|
-
const mergedCustomCss = mergeCustomCssText( this.elementCustomCSS[ configId ], fallbackCss );
|
|
258
|
-
if ( mergedCustomCss ) {
|
|
259
|
-
try {
|
|
260
|
-
this.api.doUpdateElementProperty( {
|
|
261
|
-
elementId: element.id,
|
|
262
|
-
propertyName: '_styles',
|
|
263
|
-
propertyValue: { custom_css: mergedCustomCss },
|
|
264
|
-
elementType: node.tagName,
|
|
265
|
-
} );
|
|
266
|
-
} catch ( cssErr ) {
|
|
267
|
-
styleErrors.push( String( cssErr ) );
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
await this.awaitViewRender( element );
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
return { configErrors, styleErrors };
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
async build( rootContainer: V1Element ) {
|
|
278
|
-
const widgetsCache = this.api.getWidgetsCache() || {};
|
|
279
|
-
|
|
280
|
-
new Set( this.xml.querySelectorAll( '*' ) ).forEach( ( node ) => {
|
|
281
|
-
if ( ! widgetsCache[ node.tagName ] ) {
|
|
282
|
-
throw new Error( `Unknown widget type: ${ node.tagName }` );
|
|
283
|
-
}
|
|
284
|
-
} );
|
|
285
|
-
|
|
286
|
-
const typesWithRequiredChildren = Object.keys( widgetsCache ).filter(
|
|
287
|
-
( elementType ) => getRequiredDefaultChildTemplates( widgetsCache[ elementType ] ).length > 0
|
|
288
|
-
);
|
|
289
|
-
|
|
290
|
-
typesWithRequiredChildren.forEach( ( elementType ) => {
|
|
291
|
-
new RequiredChildrenEnforcer( elementType, widgetsCache ).enforce( this.xml );
|
|
292
|
-
} );
|
|
293
|
-
|
|
294
|
-
const childTypeErrors: string[] = [];
|
|
295
|
-
for ( const rootChild of Array.from( this.xml.children ) ) {
|
|
296
|
-
childTypeErrors.push( ...this.validateChildTypes( rootChild, widgetsCache ) );
|
|
297
|
-
}
|
|
298
|
-
if ( childTypeErrors.length ) {
|
|
299
|
-
throw new Error( `Invalid element structure:\n${ childTypeErrors.join( '\n' ) }` );
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const formErrors = [
|
|
303
|
-
...collectFormAncestorErrors( this.xml ),
|
|
304
|
-
...collectSubmitButtonErrors( this.xml ),
|
|
305
|
-
...collectEmptyMessageErrors( this.xml ),
|
|
306
|
-
];
|
|
307
|
-
|
|
308
|
-
const children = Array.from( this.xml.children );
|
|
309
|
-
for ( const childNode of children ) {
|
|
310
|
-
const modelTree = this.buildModelTree( childNode, widgetsCache );
|
|
311
|
-
|
|
312
|
-
try {
|
|
313
|
-
const newElement = this.api.createElement( {
|
|
314
|
-
container: rootContainer,
|
|
315
|
-
model: modelTree as CreateElementParams[ 'model' ],
|
|
316
|
-
options: { useHistory: false },
|
|
317
|
-
} );
|
|
318
|
-
if ( ! newElement?.model ) {
|
|
319
|
-
throw new Error( CREATE_ELEMENT_INVALID_CONTAINER_MESSAGE );
|
|
320
|
-
}
|
|
321
|
-
this.rootContainers.push( newElement );
|
|
322
|
-
await this.awaitViewRender( newElement );
|
|
323
|
-
} catch ( e: unknown ) {
|
|
324
|
-
const attempToRestoreInvalidContainer = this.api.getContainer( modelTree.id as string );
|
|
325
|
-
if ( attempToRestoreInvalidContainer ) {
|
|
326
|
-
this.api.deleteElement( { container: attempToRestoreInvalidContainer } );
|
|
327
|
-
}
|
|
328
|
-
throw e;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
const { configErrors, styleErrors } = await this.applyProperties();
|
|
333
|
-
|
|
334
|
-
if ( typeof window !== 'undefined' ) {
|
|
335
|
-
const targetWindow = window.top || window;
|
|
336
|
-
targetWindow.dispatchEvent(
|
|
337
|
-
new CustomEvent( 'elementor/composition/built', {
|
|
338
|
-
detail: { rootContainers: this.rootContainers.map( ( c ) => c.id ) },
|
|
339
|
-
} )
|
|
340
|
-
);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
return {
|
|
344
|
-
configErrors,
|
|
345
|
-
styleErrors,
|
|
346
|
-
formErrors,
|
|
347
|
-
rootContainers: [ ...this.rootContainers ],
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
}
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { type V1ElementConfig } from '@elementor/editor-elements';
|
|
2
|
-
|
|
3
|
-
import { RequiredChildrenEnforcer } from '../required-children-enforcer';
|
|
4
|
-
|
|
5
|
-
describe( 'RequiredChildrenEnforcer', () => {
|
|
6
|
-
const FULL_FORM_DIRECT_CHILD_COUNT = 3;
|
|
7
|
-
|
|
8
|
-
const createWidgetsCache = (): Record< string, V1ElementConfig > => ( {
|
|
9
|
-
'e-form': {
|
|
10
|
-
title: 'Form',
|
|
11
|
-
controls: {},
|
|
12
|
-
elType: 'widget',
|
|
13
|
-
default_children: [
|
|
14
|
-
{
|
|
15
|
-
elType: 'e-form-success-message',
|
|
16
|
-
meta: { required: true },
|
|
17
|
-
elements: [],
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
elType: 'e-form-error-message',
|
|
21
|
-
meta: { required: true },
|
|
22
|
-
elements: [],
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
elType: 'widget',
|
|
26
|
-
widgetType: 'e-form-input',
|
|
27
|
-
elements: [],
|
|
28
|
-
},
|
|
29
|
-
],
|
|
30
|
-
} as V1ElementConfig,
|
|
31
|
-
'e-form-input': { title: 'Input', controls: {}, elType: 'widget' } as V1ElementConfig,
|
|
32
|
-
'e-form-success-message': {
|
|
33
|
-
title: 'Success',
|
|
34
|
-
controls: {},
|
|
35
|
-
elType: 'e-form-success-message',
|
|
36
|
-
} as V1ElementConfig,
|
|
37
|
-
'e-form-error-message': {
|
|
38
|
-
title: 'Error',
|
|
39
|
-
controls: {},
|
|
40
|
-
elType: 'e-form-error-message',
|
|
41
|
-
} as V1ElementConfig,
|
|
42
|
-
} );
|
|
43
|
-
|
|
44
|
-
it( 'throws when required direct children are missing', () => {
|
|
45
|
-
// Arrange
|
|
46
|
-
const xml = new DOMParser().parseFromString( '<e-form><e-form-input /></e-form>', 'application/xml' );
|
|
47
|
-
const enforcer = new RequiredChildrenEnforcer( 'e-form', createWidgetsCache() );
|
|
48
|
-
|
|
49
|
-
// Act & Assert
|
|
50
|
-
expect( () => enforcer.enforce( xml ) ).toThrow(
|
|
51
|
-
/Missing required direct child element tag\(s\): e-form-success-message, e-form-error-message/
|
|
52
|
-
);
|
|
53
|
-
} );
|
|
54
|
-
|
|
55
|
-
it( 'throws when only some required direct children exist', () => {
|
|
56
|
-
// Arrange
|
|
57
|
-
const xml = new DOMParser().parseFromString( '<e-form><e-form-success-message /></e-form>', 'application/xml' );
|
|
58
|
-
const enforcer = new RequiredChildrenEnforcer( 'e-form', createWidgetsCache() );
|
|
59
|
-
|
|
60
|
-
// Act & Assert
|
|
61
|
-
expect( () => enforcer.enforce( xml ) ).toThrow(
|
|
62
|
-
/Missing required direct child element tag\(s\): e-form-error-message/
|
|
63
|
-
);
|
|
64
|
-
} );
|
|
65
|
-
|
|
66
|
-
it( 'does not throw when all required direct children exist', () => {
|
|
67
|
-
// Arrange
|
|
68
|
-
const xmlStr = '<e-form><e-form-success-message /><e-form-error-message /><e-form-input /></e-form>';
|
|
69
|
-
const xml = new DOMParser().parseFromString( xmlStr, 'application/xml' );
|
|
70
|
-
const enforcer = new RequiredChildrenEnforcer( 'e-form', createWidgetsCache() );
|
|
71
|
-
|
|
72
|
-
// Act
|
|
73
|
-
expect( () => enforcer.enforce( xml ) ).not.toThrow();
|
|
74
|
-
|
|
75
|
-
// Assert
|
|
76
|
-
const form = xml.querySelector( 'e-form' );
|
|
77
|
-
expect( form?.children.length ).toBe( FULL_FORM_DIRECT_CHILD_COUNT );
|
|
78
|
-
} );
|
|
79
|
-
} );
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { type V1ElementConfig } from '@elementor/editor-elements';
|
|
2
|
-
|
|
3
|
-
import { getRequiredDefaultChildTypes } from '../required-default-child-tags';
|
|
4
|
-
|
|
5
|
-
describe( 'required-default-child-tags', () => {
|
|
6
|
-
it( 'returns XML tag names for default children marked meta.required', () => {
|
|
7
|
-
// Arrange
|
|
8
|
-
const config = {
|
|
9
|
-
title: 'Form',
|
|
10
|
-
controls: {},
|
|
11
|
-
default_children: [
|
|
12
|
-
{
|
|
13
|
-
elType: 'e-form-success-message',
|
|
14
|
-
meta: { required: true },
|
|
15
|
-
elements: [],
|
|
16
|
-
},
|
|
17
|
-
{
|
|
18
|
-
elType: 'widget',
|
|
19
|
-
widgetType: 'e-form-input',
|
|
20
|
-
meta: { required: true },
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
elType: 'e-form-label',
|
|
24
|
-
elements: [],
|
|
25
|
-
},
|
|
26
|
-
],
|
|
27
|
-
} as unknown as V1ElementConfig;
|
|
28
|
-
|
|
29
|
-
// Act
|
|
30
|
-
const tags = getRequiredDefaultChildTypes( config );
|
|
31
|
-
|
|
32
|
-
// Assert
|
|
33
|
-
expect( tags ).toEqual( [ 'e-form-success-message', 'e-form-input' ] );
|
|
34
|
-
} );
|
|
35
|
-
} );
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { type V1ElementConfig } from '@elementor/editor-elements';
|
|
2
|
-
|
|
3
|
-
import { type ChildTemplate, getRequiredDefaultChildTemplates } from './required-default-child-tags';
|
|
4
|
-
|
|
5
|
-
const REQUIRED_CHILD_SCHEMA_HINT =
|
|
6
|
-
'Use the widget schema resource; under llm_guidance.required_direct_children for V4 widgets.';
|
|
7
|
-
|
|
8
|
-
export class RequiredChildrenEnforcer {
|
|
9
|
-
private readonly elementType: string;
|
|
10
|
-
private readonly requiredTemplates: ChildTemplate[];
|
|
11
|
-
|
|
12
|
-
constructor( elementType: string, widgetsCache: Record< string, V1ElementConfig > ) {
|
|
13
|
-
this.elementType = elementType;
|
|
14
|
-
this.requiredTemplates = getRequiredDefaultChildTemplates( widgetsCache[ elementType ] );
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
enforce( xml: Document ) {
|
|
18
|
-
if ( this.requiredTemplates.length === 0 ) {
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const errors: string[] = [];
|
|
23
|
-
|
|
24
|
-
for ( const rootNode of Array.from( xml.children ) ) {
|
|
25
|
-
this.collectMissingRequiredErrors( rootNode, errors );
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if ( errors.length ) {
|
|
29
|
-
throw new Error( `${ errors.join( '\n' ) }\n${ REQUIRED_CHILD_SCHEMA_HINT }` );
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
private collectMissingRequiredErrors( node: Element, errors: string[] ) {
|
|
34
|
-
if ( node.tagName === this.elementType ) {
|
|
35
|
-
const existingChildTags = new Set( Array.from( node.children ).map( ( child ) => child.tagName ) );
|
|
36
|
-
const missingTags = this.requiredTemplates
|
|
37
|
-
.map( ( child ) => child.widgetType ?? child.elType ?? '' )
|
|
38
|
-
.filter( ( type ) => type && ! existingChildTags.has( type ) ) as string[];
|
|
39
|
-
|
|
40
|
-
if ( missingTags.length ) {
|
|
41
|
-
const configurationId = node.getAttribute( 'configuration-id' );
|
|
42
|
-
const location = configurationId
|
|
43
|
-
? `<${ node.tagName } configuration-id="${ configurationId }">`
|
|
44
|
-
: `<${ node.tagName }>`;
|
|
45
|
-
|
|
46
|
-
errors.push(
|
|
47
|
-
`${ location } Missing required direct child element tag(s): ${ missingTags.join( ', ' ) }.`
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
for ( const childNode of Array.from( node.children ) ) {
|
|
53
|
-
this.collectMissingRequiredErrors( childNode, errors );
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { type V1ElementConfig } from '@elementor/editor-elements';
|
|
2
|
-
|
|
3
|
-
export type ChildTemplate = {
|
|
4
|
-
widgetType?: string;
|
|
5
|
-
elType?: string;
|
|
6
|
-
meta?: { required?: boolean };
|
|
7
|
-
};
|
|
8
|
-
export function getRequiredDefaultChildTemplates( elementConfig: V1ElementConfig | undefined ): ChildTemplate[] {
|
|
9
|
-
const defaultChildren = elementConfig?.default_children as ChildTemplate[];
|
|
10
|
-
|
|
11
|
-
if ( ! Array.isArray( defaultChildren ) ) {
|
|
12
|
-
return [];
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
return defaultChildren.filter( ( child ) => child?.meta?.required ?? false );
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function getRequiredDefaultChildTypes( elementConfig: V1ElementConfig | undefined ): string[] {
|
|
19
|
-
return getRequiredDefaultChildTemplates( elementConfig )
|
|
20
|
-
.map( ( child ) => child.widgetType ?? child.elType ?? '' )
|
|
21
|
-
.filter( ( type ) => Boolean( type ) );
|
|
22
|
-
}
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
export const BEST_PRACTICES_PROMPT = `
|
|
2
|
-
# DESIGN QUALITY IMPERATIVE
|
|
3
|
-
|
|
4
|
-
You are generating designs for real users who expect distinctive, intentional aesthetics - NOT generic AI output.
|
|
5
|
-
|
|
6
|
-
**The Core Challenge**: Large language models naturally converge toward statistically common design patterns during generation. This creates predictable, uninspired results that users describe as "AI slop": safe color schemes, default typography hierarchies, minimal contrast, and timid spacing.
|
|
7
|
-
|
|
8
|
-
**Your Mission**: Actively resist distributional convergence by making intentional, distinctive design choices across all aesthetic dimensions. Every design decision should have a clear purpose tied to visual hierarchy, brand personality, or user experience goals.
|
|
9
|
-
|
|
10
|
-
When in doubt between "safe" and "distinctive," choose distinctive - users can always request refinements, but they cannot salvage generic foundations.
|
|
11
|
-
|
|
12
|
-
---
|
|
13
|
-
|
|
14
|
-
# DESIGN VECTORS - Concrete Implementation Guidance
|
|
15
|
-
|
|
16
|
-
## 1. Typography & Visual Hierarchy
|
|
17
|
-
|
|
18
|
-
### Avoid Distributional Defaults:
|
|
19
|
-
- NO generic sans-serifs as primary typefaces (Inter, Roboto, Arial, Helvetica)
|
|
20
|
-
- NO timid size ratios (1.2x, 1.5x scaling)
|
|
21
|
-
- NO uniform font weights (everything at 400 or 600)
|
|
22
|
-
|
|
23
|
-
### Intentional Alternatives:
|
|
24
|
-
- **For Technical/Modern**: Consider monospace headlines (JetBrains Mono, SF Mono) paired with clean body text
|
|
25
|
-
- **For Editorial/Elegant**: Consider serif headlines (Playfair Display, Crimson Text) with sans-serif body
|
|
26
|
-
- **For Playful/Creative**: Consider display fonts with character, paired with highly legible body text
|
|
27
|
-
|
|
28
|
-
### Scale & Contrast Implementation:
|
|
29
|
-
- Headline-to-body size ratios: 3x minimum (e.g., 48px headline vs 16px body)
|
|
30
|
-
- Use extreme weight contrasts: pair weight-100 or 200 with weight-800 or 900
|
|
31
|
-
- Line height contrasts: tight headlines (1.1) vs. generous body (1.7)
|
|
32
|
-
- Letter spacing: compressed headlines (-0.02em to -0.05em) vs. open small text (0.03em+)
|
|
33
|
-
|
|
34
|
-
## 2. Color & Theme Strategy
|
|
35
|
-
|
|
36
|
-
### Avoid Distributional Defaults:
|
|
37
|
-
- NO purple gradients or blue-purple color schemes (massively overrepresented in AI output)
|
|
38
|
-
- NO evenly-distributed color palettes (3-4 colors used equally)
|
|
39
|
-
- NO timid pastels or all-neutral schemes
|
|
40
|
-
- NO #333333, #666666, #999999 grays
|
|
41
|
-
|
|
42
|
-
### Intentional Alternatives:
|
|
43
|
-
- **Commit to a Dominant Color**: Choose ONE primary brand color that appears in 60-70% of colored elements
|
|
44
|
-
- **Sharp Accent Strategy**: Use 1-2 high-contrast accent colors sparingly (10-15% of colored elements)
|
|
45
|
-
- **Neutrals with Personality**: Replace pure grays with warm (#3d3228, #f5f1ed) or cool (#2a2f3d, #f0f2f5) tinted neutrals
|
|
46
|
-
|
|
47
|
-
### Color Psychology Mapping:
|
|
48
|
-
- Energy/Action → Warm reds, oranges, yellows (NOT purple/blue)
|
|
49
|
-
- Trust/Calm → Deep teals, forest greens (NOT generic blue)
|
|
50
|
-
- Luxury/Premium → Deep burgundy, emerald, charcoal with gold accents
|
|
51
|
-
- Playful/Creative → Unexpected combinations (coral + mint, mustard + navy)
|
|
52
|
-
|
|
53
|
-
## 3. Spatial Design & White Space
|
|
54
|
-
|
|
55
|
-
### Avoid Distributional Defaults:
|
|
56
|
-
- NO uniform spacing (everything 16px or 24px)
|
|
57
|
-
- NO cramped layouts that maximize content density
|
|
58
|
-
- NO default container widths (1200px, 1440px)
|
|
59
|
-
|
|
60
|
-
### Intentional Alternatives:
|
|
61
|
-
- **Breathing Room**: Use generous white space as a design element (80-120px vertical spacing between sections)
|
|
62
|
-
- **Asymmetric Spacing**: Vary padding dramatically (small: 12px, medium: 48px, large: 96px)
|
|
63
|
-
- **Content Width Strategy**:
|
|
64
|
-
- Reading content: max 65-75 characters (600-700px)
|
|
65
|
-
- Hero sections: asymmetric layouts, not centered blocks
|
|
66
|
-
- Cards/components: vary sizes intentionally, not uniform grids
|
|
67
|
-
|
|
68
|
-
## 4. Motion & Interaction Design
|
|
69
|
-
|
|
70
|
-
### Avoid Distributional Defaults:
|
|
71
|
-
- NO scattered micro-interactions on every element
|
|
72
|
-
- NO generic fade-in animations
|
|
73
|
-
- NO 0.3s ease-in-out transitions everywhere
|
|
74
|
-
|
|
75
|
-
### Intentional Alternatives:
|
|
76
|
-
- **High-Impact Moments**: Use animation for 2-3 key moments (page load hero, primary CTA, section reveals)
|
|
77
|
-
- **Staggered Reveals**: When animating multiple items, use staggered delays (0.1s increments)
|
|
78
|
-
- **Purposeful Timing**: Fast interactions (0.15s) for responsiveness, slow reveals (0.6s+) for drama
|
|
79
|
-
|
|
80
|
-
## 5. Backgrounds & Atmospheric Depth
|
|
81
|
-
|
|
82
|
-
### Avoid Distributional Defaults:
|
|
83
|
-
- NO solid white or light gray backgrounds
|
|
84
|
-
- NO single-color backgrounds
|
|
85
|
-
- NO generic gradient overlays
|
|
86
|
-
|
|
87
|
-
### Intentional Alternatives:
|
|
88
|
-
- **Layered Gradients**: Combine 2-3 subtle gradients for depth
|
|
89
|
-
- **Geometric Patterns**: SVG patterns, mesh gradients, or subtle noise textures
|
|
90
|
-
- **Strategic Contrast**: Alternate between light and dark sections for rhythm
|
|
91
|
-
|
|
92
|
-
## 6. Visual Hierarchy Principles
|
|
93
|
-
|
|
94
|
-
### Clear Priority System:
|
|
95
|
-
1. **Primary Focus (1 element)**: Largest, highest contrast, most visual weight
|
|
96
|
-
2. **Secondary Elements (2-3 elements)**: 40-60% of primary size, reduced contrast
|
|
97
|
-
3. **Tertiary/Support (everything else)**: Minimal visual weight, muted colors
|
|
98
|
-
|
|
99
|
-
### Contrast Techniques:
|
|
100
|
-
- Size: 3x+ differences between hierarchy levels
|
|
101
|
-
- Weight: 300+ difference in font-weight values
|
|
102
|
-
- Color: Primary gets brand color, secondary gets neutral, tertiary gets muted
|
|
103
|
-
- Space: Primary gets 2x+ surrounding white space vs. secondary
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
# IMPLEMENTATION WORKFLOW
|
|
107
|
-
|
|
108
|
-
When building a composition:
|
|
109
|
-
|
|
110
|
-
1. **Define the Visual Goal First**
|
|
111
|
-
- What emotion/brand personality? (Professional, playful, elegant, bold)
|
|
112
|
-
- What's the single most important element?
|
|
113
|
-
- What color family (warm/cool/neutral) supports the goal?
|
|
114
|
-
|
|
115
|
-
2. **Choose Typography Personality**
|
|
116
|
-
- Select font pairings that match the personality (NOT defaults)
|
|
117
|
-
- Define scale (3x+ headline ratio)
|
|
118
|
-
- Set weight extremes (light/heavy contrast)
|
|
119
|
-
|
|
120
|
-
3. **Commit to Color Strategy**
|
|
121
|
-
- Pick ONE dominant color (NOT purple, NOT generic blue)
|
|
122
|
-
- Choose 1 sharp accent
|
|
123
|
-
- Define warm or cool neutrals
|
|
124
|
-
|
|
125
|
-
4. **Design Spatial Rhythm**
|
|
126
|
-
- Use generous white space (3-6rem between sections)
|
|
127
|
-
- Create asymmetry (not everything centered)
|
|
128
|
-
- Vary component sizes intentionally
|
|
129
|
-
|
|
130
|
-
5. **Add Strategic Motion**
|
|
131
|
-
- Identify 2-3 high-impact animation moments
|
|
132
|
-
- Use staggered timing for multiple elements
|
|
133
|
-
- Keep interactions purposeful, not decorative
|
|
134
|
-
|
|
135
|
-
6. **Layer Atmospheric Depth**
|
|
136
|
-
- Use gradient combinations for backgrounds
|
|
137
|
-
- Add subtle patterns or textures
|
|
138
|
-
- Alternate light/dark sections for rhythm
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
# CONTEXT-AWARE DESIGN PERSONALITIES
|
|
142
|
-
|
|
143
|
-
Brand personality should influence your choices:
|
|
144
|
-
|
|
145
|
-
- **Corporate/Professional**: Deep neutrals, serif headlines, structured spacing
|
|
146
|
-
- **Creative/Agency**: Bold color, display fonts, asymmetric layouts
|
|
147
|
-
- **Tech/Startup**: Monospace accents, sharp colors, modern spacing
|
|
148
|
-
- **Elegant/Luxury**: Serif dominance, muted colors with metallic accents, generous white space
|
|
149
|
-
|
|
150
|
-
# USER OVERRIDE CAPABILITY
|
|
151
|
-
|
|
152
|
-
Always respect user specifications:
|
|
153
|
-
|
|
154
|
-
If user specifies:
|
|
155
|
-
- Specific colors → use them
|
|
156
|
-
- Specific fonts → use them
|
|
157
|
-
- Specific spacing → use it
|
|
158
|
-
- "Minimal/simple" → reduce ornamentation but maintain quality principles
|
|
159
|
-
`;
|