openvibers/openvibe.design Commits
MCP: the icons for agents (search, SVG, code, migrate) (#78)
Changes
@@@ -7,6 +7,7 @@* words(glyph) → the search words for a glyph (its synonyms, then every other set's names for it)* resolve(name) → our glyph for "gear", "fa-gear", "lucide:settings", "material:settings" or "settings", or null* scan(text) → every icon a page or a component asks for, by set: [{ set, name, count, glyph, brand }]++ * search(query, n) → the glyphs that best answer a search: its own name, then any set's name, then its words* LUCIDE, MATERIAL → { theirName: glyph } (only names whose glyph exists)*/const glyphs = require('openvibe-shared/glyphs');@@@ -233,4 +234,24 @@ function scan(text) {return [...found.values()].sort((a, b) => (a.glyph || a.brand ? 0 : 1) - (b.glyph || b.brand ? 0 : 1) || order[a.set] - order[b.set] || b.count - a.count || (a.name < b.name ? -1 : 1));}--module.exports = { words, resolve, scan, LUCIDE, MATERIAL };++/** Ranked: the exact glyph a name resolves to, then names that start with or hold the query, then any set's names and++ * the words. Every word of a several-word query must match somewhere. */++function search(query, n = 24) {++ const q = String(query || '').trim().toLowerCase().slice(0, 100);++ if (!q) return glyphs.NAMES.slice(0, n);++ const exact = resolve(q);++ const terms = q.split(/\s+/).filter(Boolean);++ const scored = [];++ for (const g of glyphs.NAMES) {++ const fa = Object.entries(glyphs.FA).filter(([, x]) => x === g).map(([f]) => f);++ const hay = [g, ...fa, ...words(g)];++ if (!terms.every((t) => hay.some((h) => h.includes(t)))) continue;++ let score = g === exact ? 100 : g === q ? 90 : g.startsWith(q) ? 70 : g.includes(q) ? 60 : 0;++ if (!score) score = fa.some((f) => f === q) ? 50 : words(g).some((w) => w === q) ? 45 : fa.some((f) => f.includes(q)) ? 30 : 20;++ scored.push([g, score]);++ }++ if (exact && !scored.some(([g]) => g === exact)) scored.push([exact, 100]);++ return scored.sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1)).slice(0, n).map(([g]) => g);++}++++module.exports = { words, resolve, scan, search, LUCIDE, MATERIAL };@@@ -4,6 +4,13 @@const express = require('express');const components = require('openvibe-shared/components');const themeTokens = require('../tokens');++const glyphs = require('openvibe-shared/glyphs');++const aliases = require('../glyphs/aliases');++const ex = require('../glyphs/export');++const formats = require('../glyphs/formats');++++const BASE = 'https://openvibe.design';++const FORMATS = { svg: null, inline_svg: 'inlineSvg', jsx: 'jsx', data_uri: 'dataUri', css_mask: 'cssMask', android: 'vectorDrawable', esm: 'esModule' };const { version } = require('../../package.json');const PROTOCOL = '2025-06-18';@@@ -26,6 +33,26 @@ const TOOLS = [{ name: 'openvibe_design_validate_kit_document', description: 'Validate a kit document against the component contract and its size limits.', inputSchema: schema(['doc'], {doc: { type: 'object', description: 'A kit component document.' },}) },++ // The OpenVibe glyphs (plan L50 step 6): the same answers as openvibe.design/icons, for agents.++ { name: 'openvibe_design_icon_search', description: 'Find OpenVibe icons (free, MIT) by a word ("settings", "delete"), our name, or a Font Awesome, Lucide or Material name. Best match first, with each icon\'s page and SVG address.', inputSchema: schema(['query'], {++ query: { type: 'string', minLength: 1, maxLength: 100 },++ limit: { type: 'integer', minimum: 1, maximum: 60, default: 12 },++ }) },++ { name: 'openvibe_design_icon_svg', description: 'One OpenVibe icon as a standalone SVG. name may be ours or a Font Awesome (fa-gear), Lucide (lucide:settings) or Material (material:delete) name. style auto draws the solid drawing at 18 px and under.', inputSchema: schema(['name'], {++ name: { type: 'string', minLength: 1, maxLength: 60 },++ size: { type: 'integer', minimum: 16, maximum: 1024, default: 24 },++ color: { type: 'string', pattern: '^(mark|[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$', description: 'Hex without #, or mark for the two OpenVibe colours. Default 0f172a.' },++ style: { type: 'string', enum: ['auto', 'line', 'solid'], default: 'auto' },++ }) },++ { name: 'openvibe_design_icon_code', description: 'One OpenVibe icon as code to paste: inline_svg (HTML, Vue, Svelte, Solid; currentColor), jsx (a React component), data_uri, css_mask (a class painted in the text colour), android (VectorDrawable XML), esm (an ES module with the drawings) or svg.', inputSchema: schema(['name', 'format'], {++ name: { type: 'string', minLength: 1, maxLength: 60 },++ format: { type: 'string', enum: Object.keys(FORMATS) },++ size: { type: 'integer', minimum: 16, maximum: 1024, default: 24 },++ style: { type: 'string', enum: ['auto', 'line', 'solid'], default: 'auto' },++ }) },++ { name: 'openvibe_design_icon_migrate', description: 'Read HTML or code and list every Font Awesome, Lucide, Material or MUI icon it uses, with its OpenVibe twin and how often it appears. Read only.', inputSchema: schema(['text'], {++ text: { type: 'string', minLength: 1, maxLength: 400000 },++ }) },].map(tool => ({ ...tool, annotations: { readOnlyHint: true, openWorldHint: false } }));const rpc = (id, result) => ({ jsonrpc: '2.0', id, result });@@@ -76,6 +103,33 @@ async function callTool(name, args, { catalog }) {case 'openvibe_design_validate_kit_document':if (!Object.prototype.hasOwnProperty.call(args, 'doc')) return toolError('doc is required.');return toolResult(components.validate(args.doc));++ case 'openvibe_design_icon_search': {++ if (typeof args.query !== 'string' || !args.query.trim() || args.query.length > 100) return toolError('query must be a nonempty string of at most 100 characters.');++ const limit = args.limit === undefined ? 12 : args.limit;++ if (!Number.isInteger(limit) || limit < 1 || limit > 60) return toolError('limit must be a whole number from 1 to 60.');++ const fa = (g) => Object.entries(glyphs.FA).filter(([, x]) => x === g).map(([f]) => `fa-${f}`);++ return toolResult({ icons: aliases.search(args.query, limit).map((g) => ({++ name: g, page: `${BASE}/icons/${g}`, svg: `${BASE}/icons/${g}.svg`, solid: !!glyphs.solid(g),++ font_awesome: fa(g).slice(0, 6), words: aliases.words(g).slice(0, 8),++ })), use: `<script src="${BASE}/ov-icon.js" defer></script> then <ov-icon name="…"></ov-icon>` });++ }++ case 'openvibe_design_icon_svg':++ case 'openvibe_design_icon_code': {++ const g = typeof args.name === 'string' && args.name.length <= 60 ? aliases.resolve(args.name) : null;++ if (!g) return toolError('No OpenVibe icon by that name. Try openvibe_design_icon_search.');++ if (args.size !== undefined && (!Number.isInteger(args.size) || args.size < 16 || args.size > 1024)) return toolError('size must be a whole number from 16 to 1024.');++ if (args.style !== undefined && !['auto', 'line', 'solid'].includes(args.style)) return toolError('style is auto, line or solid.');++ if (args.color !== undefined && (typeof args.color !== 'string' || !/^(mark|[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(args.color))) return toolError('color is a hex colour without # (0f172a), or mark.');++ const opts = ex.options({ size: args.size || 24, style: args.style || 'auto', ...(args.color ? { color: args.color } : {}) });++ if (name === 'openvibe_design_icon_svg') return toolResult({ name: g, style: ex.drawnAs(g, opts), svg: ex.svg(g, opts), page: `${BASE}/icons/${g}` });++ if (!Object.prototype.hasOwnProperty.call(FORMATS, args.format)) return toolError(`format is one of ${Object.keys(FORMATS).join(', ')}.`);++ const fn = FORMATS[args.format];++ const code = fn === null ? ex.svg(g, opts) : fn === 'esModule' ? formats.esModule(g) : formats[fn](g, opts);++ return toolResult({ name: g, format: args.format, code });++ }++ case 'openvibe_design_icon_migrate':++ if (typeof args.text !== 'string' || !args.text || args.text.length > 400000) return toolError('text must be a nonempty string of at most 400,000 characters.');++ return toolResult({ icons: aliases.scan(args.text), page: `${BASE}/icons/migrate` });default:return null;}@@@ -22,6 +22,7 @@ const components = require('openvibe-shared/components');'openvibe_design_list_themes', 'openvibe_design_get_theme_tokens','openvibe_design_list_components', 'openvibe_design_get_component','openvibe_design_validate_kit_document',++ 'openvibe_design_icon_search', 'openvibe_design_icon_svg', 'openvibe_design_icon_code', 'openvibe_design_icon_migrate',]);assert.ok(listed.every(x => x.inputSchema && x.annotations.readOnlyHint));assert.strictEqual((await t.get('/mcp', { method: 'OPTIONS' })).status, 204);@@@ -32,6 +33,31 @@ const components = require('openvibe-shared/components');assert.strictEqual((await t.get('/.well-known/agent.json')).json().mcp.url, 'https://openvibe.design/mcp');assert.match((await t.get('/llms.txt')).text, /https:\/\/openvibe\.design\/mcp/);});++ await check('the icon tools (plan L50 step 6): search by any word or set\'s name, an SVG, code to paste, a migration', async () => {++ const found = (await call('openvibe_design_icon_search', { query: 'settings', limit: 3 })).structuredContent;++ assert.strictEqual(found.icons[0].name, 'gear');++ assert.strictEqual(found.icons[0].svg, 'https://openvibe.design/icons/gear.svg');++ assert.ok(found.icons[0].font_awesome.includes('fa-gear') && found.icons[0].words.includes('settings'));++ assert.match(found.use, /ov-icon\.js/);++ assert.strictEqual((await call('openvibe_design_icon_search', { query: 'lucide:trash-2' })).structuredContent.icons[0].name, 'trash');++ assert.strictEqual((await call('openvibe_design_icon_search', { query: '' })).isError, true);++ assert.strictEqual((await call('openvibe_design_icon_search', { query: 'x', limit: 99 })).isError, true);++ assert.strictEqual((await call('openvibe_design_icon_svg', { name: 'material:mic', size: 16 })).structuredContent.name, 'mic', 'a Material name');++ const mic = (await call('openvibe_design_icon_svg', { name: 'mic', size: 16, color: 'ff0000' })).structuredContent;++ assert.strictEqual(mic.style, 'solid', 'small and auto: the solid drawing');++ assert.ok(mic.svg.startsWith('<svg xmlns="http://www.w3.org/2000/svg"') && mic.svg.includes('fill="#ff0000"'));++ assert.strictEqual((await call('openvibe_design_icon_svg', { name: 'mic', size: 48 })).structuredContent.style, 'line');++ assert.strictEqual((await call('openvibe_design_icon_svg', { name: 'no-such-icon' })).isError, true);++ assert.strictEqual((await call('openvibe_design_icon_svg', { name: 'mic', color: 'red;}' })).isError, true);++ const jsx = (await call('openvibe_design_icon_code', { name: 'fa-trash-can', format: 'jsx' })).structuredContent;++ assert.strictEqual(jsx.name, 'trash');++ assert.match(jsx.code, /^export function TrashIcon\(props\)/);++ assert.match((await call('openvibe_design_icon_code', { name: 'robot', format: 'android' })).structuredContent.code, /<vector xmlns:android=/);++ assert.match((await call('openvibe_design_icon_code', { name: 'heart', format: 'esm' })).structuredContent.code, /export default \{ name, line, solid \};/);++ assert.strictEqual((await call('openvibe_design_icon_code', { name: 'heart', format: 'png' })).isError, true);++ const moved = (await call('openvibe_design_icon_migrate', { text: '<i data-lucide="settings"></i><i class="fa-solid fa-house fa-fw"></i>' })).structuredContent;++ assert.deepStrictEqual(moved.icons.map((f) => [f.set, f.name, f.glyph]), [['fa', 'house', 'home'], ['lucide', 'settings', 'gear']]);++ });await check('list themes good and bad arguments', async () => {const good = await call('openvibe_design_list_themes', { query: 'nig', mode: 'dark' });assert.strictEqual(good.structuredContent.themes.length, 1);
@@ -7,6 +7,7 @@
@@ -7,6 +7,7 @@
* words(glyph) → the search words for a glyph (its synonyms, then every other set's names for it)
* words(glyph) → the search words for a glyph (its synonyms, then every other set's names for it)
* resolve(name) → our glyph for "gear", "fa-gear", "lucide:settings", "material:settings" or "settings", or null
* resolve(name) → our glyph for "gear", "fa-gear", "lucide:settings", "material:settings" or "settings", or null
* scan(text) → every icon a page or a component asks for, by set: [{ set, name, count, glyph, brand }]
* scan(text) → every icon a page or a component asks for, by set: [{ set, name, count, glyph, brand }]
+ * search(query, n) → the glyphs that best answer a search: its own name, then any set's name, then its words
* LUCIDE, MATERIAL → { theirName: glyph } (only names whose glyph exists)
* LUCIDE, MATERIAL → { theirName: glyph } (only names whose glyph exists)
*/
*/
const glyphs = require('openvibe-shared/glyphs');
const glyphs = require('openvibe-shared/glyphs');
@@ -233,4 +234,24 @@ function scan(text) {
@@ -233,4 +234,24 @@ function scan(text) {
return [...found.values()].sort((a, b) => (a.glyph || a.brand ? 0 : 1) - (b.glyph || b.brand ? 0 : 1) || order[a.set] - order[b.set] || b.count - a.count || (a.name < b.name ? -1 : 1));
return [...found.values()].sort((a, b) => (a.glyph || a.brand ? 0 : 1) - (b.glyph || b.brand ? 0 : 1) || order[a.set] - order[b.set] || b.count - a.count || (a.name < b.name ? -1 : 1));
}
}
-module.exports = { words, resolve, scan, LUCIDE, MATERIAL };
+/** Ranked: the exact glyph a name resolves to, then names that start with or hold the query, then any set's names and
+ * the words. Every word of a several-word query must match somewhere. */
+function search(query, n = 24) {
+ const q = String(query || '').trim().toLowerCase().slice(0, 100);
+ if (!q) return glyphs.NAMES.slice(0, n);
+ const exact = resolve(q);
+ const terms = q.split(/\s+/).filter(Boolean);
+ const scored = [];
+ for (const g of glyphs.NAMES) {
+ const fa = Object.entries(glyphs.FA).filter(([, x]) => x === g).map(([f]) => f);
+ const hay = [g, ...fa, ...words(g)];
+ if (!terms.every((t) => hay.some((h) => h.includes(t)))) continue;
+ let score = g === exact ? 100 : g === q ? 90 : g.startsWith(q) ? 70 : g.includes(q) ? 60 : 0;
+ if (!score) score = fa.some((f) => f === q) ? 50 : words(g).some((w) => w === q) ? 45 : fa.some((f) => f.includes(q)) ? 30 : 20;
+ scored.push([g, score]);
+ }
+ if (exact && !scored.some(([g]) => g === exact)) scored.push([exact, 100]);
+ return scored.sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1)).slice(0, n).map(([g]) => g);
+}
+
+module.exports = { words, resolve, scan, search, LUCIDE, MATERIAL };
@@ -4,6 +4,13 @@
@@ -4,6 +4,13 @@
const express = require('express');
const express = require('express');
const components = require('openvibe-shared/components');
const components = require('openvibe-shared/components');
const themeTokens = require('../tokens');
const themeTokens = require('../tokens');
+const glyphs = require('openvibe-shared/glyphs');
+const aliases = require('../glyphs/aliases');
+const ex = require('../glyphs/export');
+const formats = require('../glyphs/formats');
+
+const BASE = 'https://openvibe.design';
+const FORMATS = { svg: null, inline_svg: 'inlineSvg', jsx: 'jsx', data_uri: 'dataUri', css_mask: 'cssMask', android: 'vectorDrawable', esm: 'esModule' };
const { version } = require('../../package.json');
const { version } = require('../../package.json');
const PROTOCOL = '2025-06-18';
const PROTOCOL = '2025-06-18';
@@ -26,6 +33,26 @@ const TOOLS = [
@@ -26,6 +33,26 @@ const TOOLS = [
{ name: 'openvibe_design_validate_kit_document', description: 'Validate a kit document against the component contract and its size limits.', inputSchema: schema(['doc'], {
{ name: 'openvibe_design_validate_kit_document', description: 'Validate a kit document against the component contract and its size limits.', inputSchema: schema(['doc'], {
doc: { type: 'object', description: 'A kit component document.' },
doc: { type: 'object', description: 'A kit component document.' },
}) },
}) },
+ // The OpenVibe glyphs (plan L50 step 6): the same answers as openvibe.design/icons, for agents.
+ { name: 'openvibe_design_icon_search', description: 'Find OpenVibe icons (free, MIT) by a word ("settings", "delete"), our name, or a Font Awesome, Lucide or Material name. Best match first, with each icon\'s page and SVG address.', inputSchema: schema(['query'], {
+ query: { type: 'string', minLength: 1, maxLength: 100 },
+ limit: { type: 'integer', minimum: 1, maximum: 60, default: 12 },
+ }) },
+ { name: 'openvibe_design_icon_svg', description: 'One OpenVibe icon as a standalone SVG. name may be ours or a Font Awesome (fa-gear), Lucide (lucide:settings) or Material (material:delete) name. style auto draws the solid drawing at 18 px and under.', inputSchema: schema(['name'], {
+ name: { type: 'string', minLength: 1, maxLength: 60 },
+ size: { type: 'integer', minimum: 16, maximum: 1024, default: 24 },
+ color: { type: 'string', pattern: '^(mark|[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$', description: 'Hex without #, or mark for the two OpenVibe colours. Default 0f172a.' },
+ style: { type: 'string', enum: ['auto', 'line', 'solid'], default: 'auto' },
+ }) },
+ { name: 'openvibe_design_icon_code', description: 'One OpenVibe icon as code to paste: inline_svg (HTML, Vue, Svelte, Solid; currentColor), jsx (a React component), data_uri, css_mask (a class painted in the text colour), android (VectorDrawable XML), esm (an ES module with the drawings) or svg.', inputSchema: schema(['name', 'format'], {
+ name: { type: 'string', minLength: 1, maxLength: 60 },
+ format: { type: 'string', enum: Object.keys(FORMATS) },
+ size: { type: 'integer', minimum: 16, maximum: 1024, default: 24 },
+ style: { type: 'string', enum: ['auto', 'line', 'solid'], default: 'auto' },
+ }) },
+ { name: 'openvibe_design_icon_migrate', description: 'Read HTML or code and list every Font Awesome, Lucide, Material or MUI icon it uses, with its OpenVibe twin and how often it appears. Read only.', inputSchema: schema(['text'], {
+ text: { type: 'string', minLength: 1, maxLength: 400000 },
+ }) },
].map(tool => ({ ...tool, annotations: { readOnlyHint: true, openWorldHint: false } }));
].map(tool => ({ ...tool, annotations: { readOnlyHint: true, openWorldHint: false } }));
const rpc = (id, result) => ({ jsonrpc: '2.0', id, result });
const rpc = (id, result) => ({ jsonrpc: '2.0', id, result });
@@ -76,6 +103,33 @@ async function callTool(name, args, { catalog }) {
@@ -76,6 +103,33 @@ async function callTool(name, args, { catalog }) {
case 'openvibe_design_validate_kit_document':
case 'openvibe_design_validate_kit_document':
if (!Object.prototype.hasOwnProperty.call(args, 'doc')) return toolError('doc is required.');
if (!Object.prototype.hasOwnProperty.call(args, 'doc')) return toolError('doc is required.');
return toolResult(components.validate(args.doc));
return toolResult(components.validate(args.doc));
+ case 'openvibe_design_icon_search': {
+ if (typeof args.query !== 'string' || !args.query.trim() || args.query.length > 100) return toolError('query must be a nonempty string of at most 100 characters.');
+ const limit = args.limit === undefined ? 12 : args.limit;
+ if (!Number.isInteger(limit) || limit < 1 || limit > 60) return toolError('limit must be a whole number from 1 to 60.');
+ const fa = (g) => Object.entries(glyphs.FA).filter(([, x]) => x === g).map(([f]) => `fa-${f}`);
+ return toolResult({ icons: aliases.search(args.query, limit).map((g) => ({
+ name: g, page: `${BASE}/icons/${g}`, svg: `${BASE}/icons/${g}.svg`, solid: !!glyphs.solid(g),
+ font_awesome: fa(g).slice(0, 6), words: aliases.words(g).slice(0, 8),
+ })), use: `<script src="${BASE}/ov-icon.js" defer></script> then <ov-icon name="…"></ov-icon>` });
+ }
+ case 'openvibe_design_icon_svg':
+ case 'openvibe_design_icon_code': {
+ const g = typeof args.name === 'string' && args.name.length <= 60 ? aliases.resolve(args.name) : null;
+ if (!g) return toolError('No OpenVibe icon by that name. Try openvibe_design_icon_search.');
+ if (args.size !== undefined && (!Number.isInteger(args.size) || args.size < 16 || args.size > 1024)) return toolError('size must be a whole number from 16 to 1024.');
+ if (args.style !== undefined && !['auto', 'line', 'solid'].includes(args.style)) return toolError('style is auto, line or solid.');
+ if (args.color !== undefined && (typeof args.color !== 'string' || !/^(mark|[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(args.color))) return toolError('color is a hex colour without # (0f172a), or mark.');
+ const opts = ex.options({ size: args.size || 24, style: args.style || 'auto', ...(args.color ? { color: args.color } : {}) });
+ if (name === 'openvibe_design_icon_svg') return toolResult({ name: g, style: ex.drawnAs(g, opts), svg: ex.svg(g, opts), page: `${BASE}/icons/${g}` });
+ if (!Object.prototype.hasOwnProperty.call(FORMATS, args.format)) return toolError(`format is one of ${Object.keys(FORMATS).join(', ')}.`);
+ const fn = FORMATS[args.format];
+ const code = fn === null ? ex.svg(g, opts) : fn === 'esModule' ? formats.esModule(g) : formats[fn](g, opts);
+ return toolResult({ name: g, format: args.format, code });
+ }
+ case 'openvibe_design_icon_migrate':
+ if (typeof args.text !== 'string' || !args.text || args.text.length > 400000) return toolError('text must be a nonempty string of at most 400,000 characters.');
+ return toolResult({ icons: aliases.scan(args.text), page: `${BASE}/icons/migrate` });
default:
default:
return null;
return null;
}
}
@@ -22,6 +22,7 @@ const components = require('openvibe-shared/components');
@@ -22,6 +22,7 @@ const components = require('openvibe-shared/components');
'openvibe_design_list_themes', 'openvibe_design_get_theme_tokens',
'openvibe_design_list_themes', 'openvibe_design_get_theme_tokens',
'openvibe_design_list_components', 'openvibe_design_get_component',
'openvibe_design_list_components', 'openvibe_design_get_component',
'openvibe_design_validate_kit_document',
'openvibe_design_validate_kit_document',
+ 'openvibe_design_icon_search', 'openvibe_design_icon_svg', 'openvibe_design_icon_code', 'openvibe_design_icon_migrate',
]);
]);
assert.ok(listed.every(x => x.inputSchema && x.annotations.readOnlyHint));
assert.ok(listed.every(x => x.inputSchema && x.annotations.readOnlyHint));
assert.strictEqual((await t.get('/mcp', { method: 'OPTIONS' })).status, 204);
assert.strictEqual((await t.get('/mcp', { method: 'OPTIONS' })).status, 204);
@@ -32,6 +33,31 @@ const components = require('openvibe-shared/components');
@@ -32,6 +33,31 @@ const components = require('openvibe-shared/components');
assert.strictEqual((await t.get('/.well-known/agent.json')).json().mcp.url, 'https://openvibe.design/mcp');
assert.strictEqual((await t.get('/.well-known/agent.json')).json().mcp.url, 'https://openvibe.design/mcp');
assert.match((await t.get('/llms.txt')).text, /https:\/\/openvibe\.design\/mcp/);
assert.match((await t.get('/llms.txt')).text, /https:\/\/openvibe\.design\/mcp/);
});
});
+ await check('the icon tools (plan L50 step 6): search by any word or set\'s name, an SVG, code to paste, a migration', async () => {
+ const found = (await call('openvibe_design_icon_search', { query: 'settings', limit: 3 })).structuredContent;
+ assert.strictEqual(found.icons[0].name, 'gear');
+ assert.strictEqual(found.icons[0].svg, 'https://openvibe.design/icons/gear.svg');
+ assert.ok(found.icons[0].font_awesome.includes('fa-gear') && found.icons[0].words.includes('settings'));
+ assert.match(found.use, /ov-icon\.js/);
+ assert.strictEqual((await call('openvibe_design_icon_search', { query: 'lucide:trash-2' })).structuredContent.icons[0].name, 'trash');
+ assert.strictEqual((await call('openvibe_design_icon_search', { query: '' })).isError, true);
+ assert.strictEqual((await call('openvibe_design_icon_search', { query: 'x', limit: 99 })).isError, true);
+ assert.strictEqual((await call('openvibe_design_icon_svg', { name: 'material:mic', size: 16 })).structuredContent.name, 'mic', 'a Material name');
+ const mic = (await call('openvibe_design_icon_svg', { name: 'mic', size: 16, color: 'ff0000' })).structuredContent;
+ assert.strictEqual(mic.style, 'solid', 'small and auto: the solid drawing');
+ assert.ok(mic.svg.startsWith('<svg xmlns="http://www.w3.org/2000/svg"') && mic.svg.includes('fill="#ff0000"'));
+ assert.strictEqual((await call('openvibe_design_icon_svg', { name: 'mic', size: 48 })).structuredContent.style, 'line');
+ assert.strictEqual((await call('openvibe_design_icon_svg', { name: 'no-such-icon' })).isError, true);
+ assert.strictEqual((await call('openvibe_design_icon_svg', { name: 'mic', color: 'red;}' })).isError, true);
+ const jsx = (await call('openvibe_design_icon_code', { name: 'fa-trash-can', format: 'jsx' })).structuredContent;
+ assert.strictEqual(jsx.name, 'trash');
+ assert.match(jsx.code, /^export function TrashIcon\(props\)/);
+ assert.match((await call('openvibe_design_icon_code', { name: 'robot', format: 'android' })).structuredContent.code, /<vector xmlns:android=/);
+ assert.match((await call('openvibe_design_icon_code', { name: 'heart', format: 'esm' })).structuredContent.code, /export default \{ name, line, solid \};/);
+ assert.strictEqual((await call('openvibe_design_icon_code', { name: 'heart', format: 'png' })).isError, true);
+ const moved = (await call('openvibe_design_icon_migrate', { text: '<i data-lucide="settings"></i><i class="fa-solid fa-house fa-fw"></i>' })).structuredContent;
+ assert.deepStrictEqual(moved.icons.map((f) => [f.set, f.name, f.glyph]), [['fa', 'house', 'home'], ['lucide', 'settings', 'gear']]);
+ });
await check('list themes good and bad arguments', async () => {
await check('list themes good and bad arguments', async () => {
const good = await call('openvibe_design_list_themes', { query: 'nig', mode: 'dark' });
const good = await call('openvibe_design_list_themes', { query: 'nig', mode: 'dark' });
assert.strictEqual(good.structuredContent.themes.length, 1);
assert.strictEqual(good.structuredContent.themes.length, 1);