@elementor/editor-canvas 4.3.4 → 4.3.5

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.
@@ -0,0 +1,128 @@
1
+ import { createMockElement } from 'test-utils';
2
+ import { addModelToParent, findModelInDocument, type V1ElementModelProps } from '@elementor/editor-elements';
3
+
4
+ import { createPendingElement } from '../create-pending-element';
5
+ import { type ElementView } from '../types';
6
+
7
+ jest.mock( '@elementor/editor-elements', () => ( {
8
+ ...jest.requireActual( '@elementor/editor-elements' ),
9
+ addModelToParent: jest.fn(),
10
+ findModelInDocument: jest.fn(),
11
+ getContainer: jest.fn(),
12
+ } ) );
13
+
14
+ const mockAddModelToParent = jest.mocked( addModelToParent );
15
+ const mockFindModelInDocument = jest.mocked( findModelInDocument );
16
+
17
+ describe( 'createPendingElement', () => {
18
+ beforeEach( () => {
19
+ (
20
+ window as typeof window & {
21
+ elementorCommon?: { helpers?: { getUniqueId?: jest.Mock } };
22
+ }
23
+ ).elementorCommon = {
24
+ helpers: {
25
+ getUniqueId: jest
26
+ .fn()
27
+ .mockReturnValueOnce( 'cloned-parent-id' )
28
+ .mockReturnValueOnce( 'cloned-child-id' ),
29
+ },
30
+ };
31
+
32
+ mockAddModelToParent.mockClear();
33
+ mockFindModelInDocument.mockClear();
34
+ mockAddModelToParent.mockReturnValue( true );
35
+ mockFindModelInDocument.mockReturnValue( createMockElement( {} ) as never );
36
+ } );
37
+
38
+ afterEach( () => {
39
+ delete ( window as typeof window & { elementorCommon?: unknown } ).elementorCommon;
40
+ } );
41
+
42
+ it( 'regenerates ids before pending clone insertion', () => {
43
+ // Arrange.
44
+ const wrapperView = {
45
+ getContainer: () => ( { id: 'parent-id' } ),
46
+ once: jest.fn(),
47
+ model: {
48
+ trigger: jest.fn(),
49
+ },
50
+ } as unknown as ElementView;
51
+
52
+ const source = {
53
+ id: 'original-parent-id',
54
+ elType: 'container',
55
+ settings: { _element_id: 'original-parent-element-id' },
56
+ elements: [
57
+ {
58
+ id: 'original-child-id',
59
+ elType: 'widget',
60
+ widgetType: 'button',
61
+ settings: { _element_id: 'original-child-element-id' },
62
+ elements: [],
63
+ },
64
+ ],
65
+ } as unknown as Partial< V1ElementModelProps >;
66
+
67
+ // Act.
68
+ const pendingElement = createPendingElement( wrapperView, source, { clone: true } );
69
+
70
+ // Assert.
71
+ expect( pendingElement?.getContainer().id ).toBe( 'cloned-parent-id' );
72
+ expect( mockAddModelToParent ).toHaveBeenCalledWith(
73
+ 'parent-id',
74
+ expect.objectContaining( {
75
+ id: 'cloned-parent-id',
76
+ settings: expect.objectContaining( { _element_id: '' } ),
77
+ elements: [
78
+ expect.objectContaining( {
79
+ id: 'cloned-child-id',
80
+ settings: expect.objectContaining( { _element_id: '' } ),
81
+ } ),
82
+ ],
83
+ } ),
84
+ expect.objectContaining( {
85
+ clone: false,
86
+ } )
87
+ );
88
+ expect( mockFindModelInDocument ).toHaveBeenCalledWith( 'cloned-parent-id' );
89
+ } );
90
+
91
+ it( 'preserves the source id when clone is false', () => {
92
+ // Arrange.
93
+ const wrapperView = {
94
+ getContainer: () => ( { id: 'parent-id' } ),
95
+ once: jest.fn(),
96
+ model: {
97
+ trigger: jest.fn(),
98
+ },
99
+ } as unknown as ElementView;
100
+
101
+ const source = {
102
+ id: 'original-parent-id',
103
+ elType: 'widget',
104
+ widgetType: 'button',
105
+ settings: { _element_id: 'original-parent-element-id' },
106
+ elements: [],
107
+ };
108
+
109
+ // Act.
110
+ const pendingElement = createPendingElement( wrapperView, source, { clone: false } );
111
+
112
+ // Assert.
113
+ expect( pendingElement?.getContainer().id ).toBe( 'original-parent-id' );
114
+ expect( mockAddModelToParent ).toHaveBeenCalledWith(
115
+ 'parent-id',
116
+ expect.objectContaining( {
117
+ id: 'original-parent-id',
118
+ settings: expect.objectContaining( {
119
+ _element_id: 'original-parent-element-id',
120
+ } ),
121
+ } ),
122
+ expect.objectContaining( {
123
+ clone: false,
124
+ } )
125
+ );
126
+ expect( mockFindModelInDocument ).toHaveBeenCalledWith( 'original-parent-id' );
127
+ } );
128
+ } );
@@ -121,20 +121,7 @@ describe( 'createTemplatedElementView', () => {
121
121
  } );
122
122
 
123
123
  describe( 'render context', () => {
124
- it( 'should inject computed tag into the twig render context', async () => {
125
- // Arrange
126
- const utils = createMockRenderer();
127
- const ViewClass = createTemplatedElementView( {
128
- type: MOCK_ELEMENT_TYPE,
129
- renderer: utils,
130
- element: {
131
- ...createMockElementConfig(),
132
- twig_templates: {
133
- main: '<{{ tag | e("html_tag") }}></{{ tag | e("html_tag") }}>',
134
- },
135
- },
136
- } );
137
-
124
+ const createRenderableView = ( ViewClass: ReturnType< typeof createTemplatedElementView > ) => {
138
125
  const view = new ViewClass() as unknown as ElementView & {
139
126
  _abortController: AbortController;
140
127
  };
@@ -159,12 +146,61 @@ describe( 'createTemplatedElementView', () => {
159
146
  view._ensureViewIsIntact = jest.fn();
160
147
  view.resetChildViewContainer = jest.fn();
161
148
 
149
+ return view;
150
+ };
151
+
152
+ it( 'should inject computed tag into the twig render context', async () => {
153
+ // Arrange
154
+ const utils = createMockRenderer();
155
+ const ViewClass = createTemplatedElementView( {
156
+ type: MOCK_ELEMENT_TYPE,
157
+ renderer: utils,
158
+ element: {
159
+ ...createMockElementConfig(),
160
+ twig_templates: {
161
+ main: '<{{ tag | e("html_tag") }}></{{ tag | e("html_tag") }}>',
162
+ },
163
+ },
164
+ } );
165
+
166
+ const view = createRenderableView( ViewClass );
167
+
162
168
  // Act
163
169
  await view._renderTemplate();
164
170
 
165
171
  // Assert
166
172
  expect( utils.render ).toHaveBeenCalledWith( 'main', expect.objectContaining( { tag: 'div' } ) );
167
173
  } );
174
+
175
+ it( 'should merge element template_context into the twig render context without overriding core keys', async () => {
176
+ // Arrange
177
+ const utils = createMockRenderer();
178
+ const ViewClass = createTemplatedElementView( {
179
+ type: MOCK_ELEMENT_TYPE,
180
+ renderer: utils,
181
+ element: {
182
+ ...createMockElementConfig(),
183
+ template_context: {
184
+ provider: { key: 'google', api_key: 'test-key' },
185
+ id: 'should-not-override',
186
+ },
187
+ },
188
+ } );
189
+
190
+ const view = createRenderableView( ViewClass );
191
+
192
+ // Act
193
+ await view._renderTemplate();
194
+
195
+ // Assert
196
+ expect( utils.render ).toHaveBeenCalledWith(
197
+ 'main',
198
+ expect.objectContaining( {
199
+ id: 'test-id',
200
+ provider: { key: 'google', api_key: 'test-key' },
201
+ } )
202
+ );
203
+ } );
168
204
  } );
169
205
  } );
170
206
 
@@ -71,15 +71,17 @@ export function createNestedTemplatedElementType( {
71
71
  };
72
72
  }
73
73
 
74
- function buildEditorAttributes( model: ElementView[ 'model' ] ): string {
74
+ export function buildEditorAttributes( model: ElementView[ 'model' ] ): string {
75
75
  const id = model.get( 'id' );
76
76
  const originId = model.get( 'originId' );
77
77
  const cid = model.cid ?? '';
78
+ const isDecorative = model.get( 'editor_settings' )?.decorative === true;
78
79
 
79
80
  const attrs: Record< string, string > = {
80
81
  'data-model-cid': cid,
81
82
  'data-interaction-id': originId ?? id,
82
83
  'x-ignore': 'true',
84
+ ...( isDecorative ? { 'data-e-decorative': 'true' } : {} ),
83
85
  };
84
86
 
85
87
  return Object.entries( attrs )
@@ -420,7 +422,7 @@ export function createNestedTemplatedElementView( {
420
422
  this._doAfterRender( () => parentOpenEditingPanel.call( this, options ) );
421
423
  },
422
424
 
423
- addElement( data: Partial< V1ElementModelProps >, options?: { edit?: boolean; at?: number } ) {
425
+ addElement( data: Partial< V1ElementModelProps >, options?: { edit?: boolean; at?: number; clone?: boolean } ) {
424
426
  if ( this.isRendered ) {
425
427
  return parentAddElement.call( this, data, options );
426
428
  }
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  addModelToParent,
3
+ cloneElementTree,
3
4
  findModelInDocument,
4
5
  generateElementId,
5
6
  getContainer,
@@ -12,12 +13,12 @@ import { type ElementView } from './types';
12
13
  export function createPendingElement(
13
14
  wrapperView: ElementView,
14
15
  data: Partial< V1ElementModelProps >,
15
- options: { edit?: boolean; at?: number } = {}
16
+ options: { edit?: boolean; at?: number; clone?: boolean } = {}
16
17
  ): { getContainer: () => V1Element } | undefined {
17
18
  const parentContainer = wrapperView.getContainer();
18
- const model: Partial< V1ElementModelProps > = { ...data };
19
+ const model: Partial< V1ElementModelProps > = options.clone ? cloneElementTree( data ) : { ...data };
19
20
 
20
- if ( ! model.id ) {
21
+ if ( ! options.clone && ! model.id ) {
21
22
  model.id = generateElementId();
22
23
  }
23
24
 
@@ -25,13 +26,16 @@ export function createPendingElement(
25
26
  model.elements = [];
26
27
  }
27
28
 
28
- const added = addModelToParent( parentContainer.id, model as V1ElementModelProps, options );
29
+ const added = addModelToParent( parentContainer.id, model as V1ElementModelProps, {
30
+ ...options,
31
+ clone: false,
32
+ } );
29
33
 
30
34
  if ( ! added ) {
31
35
  return undefined;
32
36
  }
33
37
 
34
- const childId = model.id;
38
+ const childId = model.id as string;
35
39
  const childModel = findModelInDocument( childId );
36
40
 
37
41
  if ( ! childModel ) {
@@ -29,7 +29,7 @@ export type CreateTemplatedElementTypeOptions = {
29
29
  export type TemplatedElementConfig = Required<
30
30
  Pick< V1ElementConfig, 'twig_templates' | 'twig_main_template' | 'atomic_props_schema' | 'base_styles_dictionary' >
31
31
  > &
32
- Pick< V1ElementConfig, 'default_html_tag' | 'html_tag_follows_link' >;
32
+ Pick< V1ElementConfig, 'default_html_tag' | 'html_tag_follows_link' | 'template_context' >;
33
33
 
34
34
  export function createTemplatedElementType( {
35
35
  type,
@@ -163,6 +163,7 @@ export function createTemplatedElementView( {
163
163
  this._lastResolvedSettingsHash = settingsHash;
164
164
 
165
165
  const context = {
166
+ ...( element.template_context ?? {} ),
166
167
  id: this.model.get( 'id' ),
167
168
  interaction_id: this.getInteractionId(),
168
169
  type,
@@ -1,6 +1,7 @@
1
1
  import { type MCPRegistryEntry } from '@elementor/editor-mcp';
2
2
  import { Schema } from '@elementor/editor-props';
3
3
 
4
+ import { isIconLibraryActive } from './icon-library-experiment';
4
5
  import { initAvailableWidgetsResource } from './resources/available-widgets-resource';
5
6
  import { initBestPracticesResource } from './resources/best-practices-resource';
6
7
  import { initBreakpointsResource } from './resources/breakpoints-resource';
@@ -11,6 +12,7 @@ import { initGeneralContextResource } from './resources/general-context-resource
11
12
  import { initSelectedElementResource } from './resources/selected-element-resource';
12
13
  import { initWidgetsSchemaResource } from './resources/widgets-schema-resource';
13
14
  import { initConfigureElementTool } from './tools/configure-element/tool';
15
+ import { initFindIconsTool } from './tools/find-icons/tool';
14
16
  import { initGetPageStructureTool } from './tools/get-page-structure/tool';
15
17
  import { getDynamicTagNamesByCategories } from './utils/resolve-dynamic-tag';
16
18
 
@@ -28,6 +30,9 @@ export const initCanvasMcp = ( reg: MCPRegistryEntry ) => {
28
30
  initGeneralContextResource( reg );
29
31
  initBestPracticesResource( reg );
30
32
  initConfigureElementTool( reg );
33
+ if ( isIconLibraryActive() ) {
34
+ initFindIconsTool( reg );
35
+ }
31
36
  initGetPageStructureTool( reg );
32
37
  initBreakpointsResource( reg );
33
38
  };
@@ -0,0 +1,5 @@
1
+ import { isExperimentActive } from '@elementor/editor-v1-adapters';
2
+
3
+ const ICON_LIBRARY_EXPERIMENT = 'e_svg_library';
4
+
5
+ export const isIconLibraryActive = () => isExperimentActive( ICON_LIBRARY_EXPERIMENT );
@@ -1,11 +1,52 @@
1
1
  import { toolPrompts } from '@elementor/editor-mcp';
2
2
 
3
+ import { isIconLibraryActive } from '../../icon-library-experiment';
3
4
  import { DYNAMIC_TAGS_URI } from '../../resources/dynamic-tags-resource';
4
5
  import { WIDGET_SCHEMA_URI } from '../../resources/widgets-schema-resource';
5
6
 
7
+ const ICONS_SECTION = `# Icons (the \`svg\` property on e-svg)
8
+ The \`svg\` property accepts two variants. Prefer a library icon for iconography (carts, arrows, social marks, check marks) — it needs no upload and inherits \`color\` via \`currentColor\`. Use an uploaded file only for custom artwork such as a brand logo.
9
+
10
+ Resolve the icon with the \`find-icons\` tool FIRST, then copy its \`value\` and \`library\` verbatim. Never assemble those strings yourself: Font Awesome renamed many icons between major versions (\`shopping-cart\` became \`cart-shopping\`), so a plausible guess renders nothing. \`find-icons\` matches on names, aliases, human search terms, and categories, so describe the intent — "add to cart" finds \`cart-shopping\`. It takes a batch of \`queries\`, so resolve every icon you need in one call.
11
+
12
+ Library icon (note the nested PropValues — \`value\` and \`library\` are each a \`string\` PropValue):
13
+ { "$$type": "icon", "value": { "value": { "$$type": "string", "value": "fa-solid fa-cart-shopping" }, "library": { "$$type": "string", "value": "fa-solid" } } }
14
+
15
+ Uploaded SVG file:
16
+ { "$$type": "svg-src", "value": { "id": { "$$type": "image-attachment-id", "value": 123 } } }
17
+
18
+ Size and colour an e-svg through \`style\` (\`width\`, \`height\`, \`color\`), never through \`propertiesToChange\`.
19
+ `;
20
+
21
+ const ICON_EXAMPLE = `
22
+ Setting a Font Awesome icon on an e-svg, after resolving it with \`find-icons\`:
23
+
24
+ \`\`\`json
25
+ {
26
+ propertiesToChange: {
27
+ svg: {
28
+ $$type: 'icon',
29
+ value: {
30
+ value: { $$type: 'string', value: 'fa-solid fa-cart-shopping' },
31
+ library: { $$type: 'string', value: 'fa-solid' }
32
+ }
33
+ }
34
+ },
35
+ style: {
36
+ 'width': '1.25rem',
37
+ 'height': '1.25rem',
38
+ 'color': '#1a1a1a'
39
+ },
40
+ elementId: 'element-id',
41
+ elementType: 'e-svg'
42
+ };
43
+ \`\`\`
44
+ `;
45
+
6
46
  export const CONFIGURE_ELEMENT_GUIDE_URI = 'elementor://canvas/tools/configure-element-guide';
7
47
 
8
48
  export const generatePrompt = () => {
49
+ const iconLibraryActive = isIconLibraryActive();
9
50
  const configureElementToolPrompt = toolPrompts( 'configure-element' );
10
51
 
11
52
  configureElementToolPrompt.description( `
@@ -64,7 +105,8 @@ Put the dynamic object EXACTLY at the node whose schema offers the "dynamic" var
64
105
  }
65
106
  Image example: { "$$type": "image", "value": { "src": { "$$type": "dynamic", "value": { "name": "<image tag>", "settings": { ... } } } } }
66
107
  Do NOT send "group" (it is resolved automatically). Use { "settings": {} } only when the tag has no settings.
67
- ` );
108
+
109
+ ${ iconLibraryActive ? ICONS_SECTION : '' }` );
68
110
 
69
111
  configureElementToolPrompt.parameter( 'elementId', 'The ID of the element to configure. MANDATORY.' );
70
112
 
@@ -107,6 +149,10 @@ Do NOT send "group" (it is resolved automatically). Use { "settings": {} } only
107
149
  \`\`\`
108
150
  ` );
109
151
 
152
+ if ( iconLibraryActive ) {
153
+ configureElementToolPrompt.example( ICON_EXAMPLE );
154
+ }
155
+
110
156
  configureElementToolPrompt.instruction(
111
157
  'The $$type property is MANDATORY for every value in propertiesToChange; it is not used in the style parameter (raw CSS only).'
112
158
  );
@@ -0,0 +1,143 @@
1
+ import { httpService } from '@elementor/http-client';
2
+
3
+ import { initFindIconsTool } from '../tool';
4
+
5
+ jest.mock( '@elementor/http-client', () => ( {
6
+ httpService: jest.fn(),
7
+ AxiosError: class AxiosError extends Error {},
8
+ } ) );
9
+
10
+ const mockedHttpService = httpService as jest.MockedFunction< typeof httpService >;
11
+
12
+ type ToolConfig = {
13
+ name: string;
14
+ description: string;
15
+ schema: Record< string, unknown >;
16
+ handler: ( input: Record< string, unknown > ) => Promise< Record< string, unknown > >;
17
+ };
18
+
19
+ const captureTool = (): ToolConfig => {
20
+ const addTool = jest.fn();
21
+ initFindIconsTool( { addTool } as never );
22
+ return addTool.mock.calls[ 0 ][ 0 ] as ToolConfig;
23
+ };
24
+
25
+ const serverResponse = {
26
+ results: [
27
+ {
28
+ query: 'add to cart',
29
+ matches: [
30
+ {
31
+ value: 'fa-solid fa-cart-plus',
32
+ library: 'fa-solid',
33
+ name: 'cart-plus',
34
+ label: 'Cart Plus',
35
+ matched_on: 'term',
36
+ license: 'free',
37
+ },
38
+ ],
39
+ total: 1,
40
+ page: 1,
41
+ per_page: 10,
42
+ truncated: false,
43
+ },
44
+ ],
45
+ libraries: [ 'fa-brands', 'fa-regular', 'fa-solid' ],
46
+ categories: [ 'shopping' ],
47
+ font_awesome_version: '7.3.1',
48
+ };
49
+
50
+ const givenServerResponse = ( data: Record< string, unknown > ) => {
51
+ const post = jest.fn().mockResolvedValue( { data: { data } } );
52
+ mockedHttpService.mockReturnValue( { post } as never );
53
+ return post;
54
+ };
55
+
56
+ describe( 'find-icons tool', () => {
57
+ beforeEach( () => {
58
+ jest.clearAllMocks();
59
+ } );
60
+
61
+ it( 'proxies a batch of queries to the php ability', async () => {
62
+ // Arrange
63
+ const post = givenServerResponse( serverResponse );
64
+ const { handler } = captureTool();
65
+
66
+ // Act
67
+ await handler( { queries: [ 'add to cart', 'free shipping' ] } );
68
+
69
+ // Assert
70
+ expect( post ).toHaveBeenCalledWith( 'elementor/v1/mcp-proxy', {
71
+ tool: 'find-icons',
72
+ input: { queries: [ 'add to cart', 'free shipping' ] },
73
+ } );
74
+ } );
75
+
76
+ it( 'maps per-page to the snake-cased ability input', async () => {
77
+ // Arrange
78
+ const post = givenServerResponse( serverResponse );
79
+ const { handler } = captureTool();
80
+
81
+ // Act
82
+ await handler( {
83
+ queries: [ 'cart' ],
84
+ library: 'fa-solid',
85
+ category: 'shopping',
86
+ page: 2,
87
+ perPage: 25,
88
+ } );
89
+
90
+ // Assert
91
+ expect( post ).toHaveBeenCalledWith( 'elementor/v1/mcp-proxy', {
92
+ tool: 'find-icons',
93
+ input: {
94
+ queries: [ 'cart' ],
95
+ library: 'fa-solid',
96
+ category: 'shopping',
97
+ page: 2,
98
+ per_page: 25,
99
+ },
100
+ } );
101
+ } );
102
+
103
+ it( 'returns the writable icon pair and the site catalog facets', async () => {
104
+ // Arrange
105
+ givenServerResponse( serverResponse );
106
+ const { handler } = captureTool();
107
+
108
+ // Act
109
+ const result = await handler( { queries: [ 'add to cart' ] } );
110
+
111
+ // Assert
112
+ expect( result.results ).toEqual( serverResponse.results );
113
+ expect( result.libraries ).toEqual( serverResponse.libraries );
114
+ expect( result.categories ).toEqual( serverResponse.categories );
115
+ expect( result.fontAwesomeVersion ).toBe( '7.3.1' );
116
+ } );
117
+
118
+ it( 'omits facets the server did not send', async () => {
119
+ // Arrange
120
+ givenServerResponse( { results: serverResponse.results, font_awesome_version: '7.3.1' } );
121
+ const { handler } = captureTool();
122
+
123
+ // Act
124
+ const result = await handler( { queries: [ 'add to cart' ] } );
125
+
126
+ // Assert
127
+ expect( result ).not.toHaveProperty( 'libraries' );
128
+ expect( result ).not.toHaveProperty( 'customLibraries' );
129
+ expect( result ).not.toHaveProperty( 'categories' );
130
+ } );
131
+
132
+ it( 'surfaces a server failure as a tool error', async () => {
133
+ // Arrange
134
+ const post = jest.fn().mockRejectedValue( new Error( 'The icon catalog is missing on this site.' ) );
135
+ mockedHttpService.mockReturnValue( { post } as never );
136
+ const { handler } = captureTool();
137
+
138
+ // Act & Assert
139
+ await expect( handler( { queries: [ 'cart' ] } ) ).rejects.toThrow(
140
+ 'The icon catalog is missing on this site.'
141
+ );
142
+ } );
143
+ } );
@@ -0,0 +1,43 @@
1
+ import { toolPrompts } from '@elementor/editor-mcp';
2
+
3
+ export const findIconsToolPrompt = toolPrompts( 'find-icons' );
4
+
5
+ findIconsToolPrompt.description(
6
+ 'Finds icons for the `svg` property of an `e-svg` element, across Font Awesome and any icon packs uploaded to this site. ' +
7
+ 'Returns the exact `value` and `library` pair to write - never assemble those strings yourself, and never guess an icon name. ' +
8
+ 'Call this before placing or configuring an `e-svg`. Nothing has to be uploaded to the Media Library for an icon to work.'
9
+ );
10
+
11
+ findIconsToolPrompt.instruction(
12
+ 'Call it when the user names an icon ("use the cart-shopping icon") and take the first exact `name` match, ' +
13
+ 'and also when the user describes a page and you decide icons belong in it ("create a product page") - ' +
14
+ 'in that case pass one query per icon you intend to place, in a single call.'
15
+ );
16
+
17
+ findIconsToolPrompt.instruction(
18
+ 'Write queries the way a person would describe the icon ("shopping cart", "credit card", "phone"). Icons are matched on ' +
19
+ 'their name, aliases, Font Awesome search terms and category, so plain language works better than guessed icon names.'
20
+ );
21
+
22
+ findIconsToolPrompt.instruction(
23
+ 'Results are ordered best first; `matched_on: "name"` is a stronger signal than `matched_on: "term"`. ' +
24
+ "Check each result's `license` and only use `free` icons; skip any `pro` entry. " +
25
+ 'Prefer `fa-solid` for UI iconography and keep one style across a page; brand marks only exist in `fa-brands`. ' +
26
+ 'An empty `matches` array means no match was found: broaden the phrase instead of inventing a value.'
27
+ );
28
+
29
+ findIconsToolPrompt.instruction(
30
+ 'Pass the chosen pair to `configure-element` as the `svg` property: ' +
31
+ '{ "$$type": "icon", "value": { "value": { "$$type": "string", "value": "fa-solid fa-cart-shopping" }, ' +
32
+ '"library": { "$$type": "string", "value": "fa-solid" } } }. ' +
33
+ 'An `e-svg` has no colour or size of its own, so set `width`, `height` and `color` in the same `configure-element` call.'
34
+ );
35
+
36
+ findIconsToolPrompt.example( `Icon named by the user:
37
+ { "queries": ["cart-shopping"] }` );
38
+
39
+ findIconsToolPrompt.example( `Every icon a product page needs, in one call:
40
+ { "queries": ["add to cart", "free shipping", "secure payment", "instagram"] }` );
41
+
42
+ findIconsToolPrompt.example( `Browse a category, one style only:
43
+ { "category": "shopping", "library": "fa-solid", "perPage": 25 }` );