@imenam/mcp-design 1.0.3 → 1.0.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.
- package/README.md +61 -8
- package/dist/shared/icons/icon-data.d.ts +2 -0
- package/dist/shared/icons/icon-data.js +5 -0
- package/dist/shared/icons/icon-keywords.d.ts +1 -0
- package/dist/shared/icons/icon-keywords.js +4 -0
- package/dist/shared/icons/index.d.ts +47 -0
- package/dist/shared/icons/index.js +107 -0
- package/dist/shared/icons/search.d.ts +31 -0
- package/dist/shared/icons/search.js +104 -0
- package/dist/shared/instances.d.ts +39 -0
- package/dist/shared/instances.js +94 -0
- package/dist/shared/node-defaults.d.ts +2 -0
- package/dist/shared/node-defaults.js +32 -4
- package/dist/shared/node-fields.d.ts +2 -0
- package/dist/shared/node-fields.js +17 -4
- package/dist/shared/render.d.ts +34 -1
- package/dist/shared/render.js +80 -7
- package/dist/shared/schemas/design.schema.d.ts +19 -1
- package/dist/shared/schemas/design.schema.js +13 -1
- package/dist/src/core/clone.d.ts +23 -0
- package/dist/src/core/clone.js +34 -0
- package/dist/src/core/diff.js +2 -2
- package/dist/src/core/export.js +69 -4
- package/dist/src/core/hierarchy.d.ts +7 -0
- package/dist/src/core/hierarchy.js +75 -5
- package/dist/src/core/node-factory.js +55 -5
- package/dist/src/core/screenshot.d.ts +12 -0
- package/dist/src/core/screenshot.js +17 -6
- package/dist/src/index.js +97 -7
- package/dist/src/tools/add-nodes.js +7 -1
- package/dist/src/tools/delete-nodes.js +10 -1
- package/dist/src/tools/detach-instances.d.ts +14 -0
- package/dist/src/tools/detach-instances.js +61 -0
- package/dist/src/tools/duplicate-nodes.js +20 -28
- package/dist/src/tools/search-icons.d.ts +12 -0
- package/dist/src/tools/search-icons.js +87 -0
- package/dist/src/tools/ungroup-nodes.js +8 -0
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/gui/dist/assets/{index-C6fXmzfu.css → index-BIeiaHA2.css} +1 -1
- package/gui/dist/assets/index-Dpd_cZgh.js +10 -0
- package/gui/dist/assets/search--VtHhIew.js +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +4 -2
- package/scripts/generate-icons.js +85 -0
- package/gui/dist/assets/index-ffF8_vFp.js +0 -10
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bibliotheque d'icones embarquee (Lucide, licence ISC).
|
|
3
|
+
*
|
|
4
|
+
* Un noeud `icon` stocke un NOM, jamais un trace : le JSON du design reste
|
|
5
|
+
* lisible — "shopping-cart" dit a l'agent qui le relit ce que la forme
|
|
6
|
+
* represente, ce qu'une grappe d'ellipses dessinees a la main ne dit plus — et
|
|
7
|
+
* une icone se restyle sans se redessiner.
|
|
8
|
+
*
|
|
9
|
+
* Le trace n'existe qu'au moment du rendu, resolu ici. Comme `render.ts`, ce
|
|
10
|
+
* module ne depend d'aucune librairie : il est charge par le serveur MCP comme
|
|
11
|
+
* par le bundle de la GUI.
|
|
12
|
+
*/
|
|
13
|
+
import { LUCIDE_VERSION } from "./icon-data.js";
|
|
14
|
+
export { LUCIDE_VERSION };
|
|
15
|
+
/**
|
|
16
|
+
* Cote de la grille dans laquelle chaque icone est dessinee.
|
|
17
|
+
*
|
|
18
|
+
* C'est le repere de `strokeWidth` : une icone posee dans une boite de 96px
|
|
19
|
+
* avec strokeWidth 2 rend un trait de 8px a l'ecran, parce que l'ensemble du
|
|
20
|
+
* trace est mis a l'echelle. L'unite est celle de la grille, pas celle du
|
|
21
|
+
* document — c'est la seule qui donne le meme rendu a toutes les tailles.
|
|
22
|
+
*/
|
|
23
|
+
export declare const ICON_GRID = 24;
|
|
24
|
+
/** Epaisseur de trait de reference de Lucide, dans la grille de 24. */
|
|
25
|
+
export declare const DEFAULT_ICON_STROKE_WIDTH = 2;
|
|
26
|
+
/** Un element SVG du trace : son nom de balise et ses attributs de geometrie. */
|
|
27
|
+
export type IconElement = [string, Record<string, string>];
|
|
28
|
+
export declare function hasIcon(name: string): boolean;
|
|
29
|
+
/** Trace de l'icone dans la grille de 24, ou `null` si le nom est inconnu. */
|
|
30
|
+
export declare function iconElements(name: string): IconElement[] | null;
|
|
31
|
+
export declare function iconNames(): string[];
|
|
32
|
+
export declare function iconCount(): number;
|
|
33
|
+
/**
|
|
34
|
+
* Trace affiche a la place d'une icone dont le nom n'existe pas.
|
|
35
|
+
*
|
|
36
|
+
* Un carre en pointilles et un point d'interrogation : le meme SVG sert au
|
|
37
|
+
* canvas, a l'export et a la capture, donc une exception y serait fatale
|
|
38
|
+
* partout a la fois. Un design ouvert apres un renommage de la bibliotheque
|
|
39
|
+
* doit rester ouvrable, et montrer OU le probleme se situe.
|
|
40
|
+
*/
|
|
41
|
+
export declare const UNKNOWN_ICON_ELEMENTS: IconElement[];
|
|
42
|
+
/**
|
|
43
|
+
* Selection couverte par la description de `add_nodes` : de quoi composer une
|
|
44
|
+
* interface complete sans aucun aller-retour. Le reste de la bibliotheque
|
|
45
|
+
* s'atteint par `search_icons`.
|
|
46
|
+
*/
|
|
47
|
+
export declare const CORE_ICONS: readonly string[];
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bibliotheque d'icones embarquee (Lucide, licence ISC).
|
|
3
|
+
*
|
|
4
|
+
* Un noeud `icon` stocke un NOM, jamais un trace : le JSON du design reste
|
|
5
|
+
* lisible — "shopping-cart" dit a l'agent qui le relit ce que la forme
|
|
6
|
+
* represente, ce qu'une grappe d'ellipses dessinees a la main ne dit plus — et
|
|
7
|
+
* une icone se restyle sans se redessiner.
|
|
8
|
+
*
|
|
9
|
+
* Le trace n'existe qu'au moment du rendu, resolu ici. Comme `render.ts`, ce
|
|
10
|
+
* module ne depend d'aucune librairie : il est charge par le serveur MCP comme
|
|
11
|
+
* par le bundle de la GUI.
|
|
12
|
+
*/
|
|
13
|
+
import { ICON_DATA_JSON, LUCIDE_VERSION } from "./icon-data.js";
|
|
14
|
+
export { LUCIDE_VERSION };
|
|
15
|
+
/**
|
|
16
|
+
* Cote de la grille dans laquelle chaque icone est dessinee.
|
|
17
|
+
*
|
|
18
|
+
* C'est le repere de `strokeWidth` : une icone posee dans une boite de 96px
|
|
19
|
+
* avec strokeWidth 2 rend un trait de 8px a l'ecran, parce que l'ensemble du
|
|
20
|
+
* trace est mis a l'echelle. L'unite est celle de la grille, pas celle du
|
|
21
|
+
* document — c'est la seule qui donne le meme rendu a toutes les tailles.
|
|
22
|
+
*/
|
|
23
|
+
export const ICON_GRID = 24;
|
|
24
|
+
/** Epaisseur de trait de reference de Lucide, dans la grille de 24. */
|
|
25
|
+
export const DEFAULT_ICON_STROKE_WIDTH = 2;
|
|
26
|
+
let cache = null;
|
|
27
|
+
function library() {
|
|
28
|
+
// Analyse au premier acces : un serveur stdio qui ne dessine aucune icone ne
|
|
29
|
+
// paie pas le cout des 1800 traces.
|
|
30
|
+
if (cache === null)
|
|
31
|
+
cache = JSON.parse(ICON_DATA_JSON);
|
|
32
|
+
return cache;
|
|
33
|
+
}
|
|
34
|
+
export function hasIcon(name) {
|
|
35
|
+
return Object.prototype.hasOwnProperty.call(library(), name);
|
|
36
|
+
}
|
|
37
|
+
/** Trace de l'icone dans la grille de 24, ou `null` si le nom est inconnu. */
|
|
38
|
+
export function iconElements(name) {
|
|
39
|
+
return library()[name] ?? null;
|
|
40
|
+
}
|
|
41
|
+
export function iconNames() {
|
|
42
|
+
return Object.keys(library());
|
|
43
|
+
}
|
|
44
|
+
export function iconCount() {
|
|
45
|
+
return iconNames().length;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Trace affiche a la place d'une icone dont le nom n'existe pas.
|
|
49
|
+
*
|
|
50
|
+
* Un carre en pointilles et un point d'interrogation : le meme SVG sert au
|
|
51
|
+
* canvas, a l'export et a la capture, donc une exception y serait fatale
|
|
52
|
+
* partout a la fois. Un design ouvert apres un renommage de la bibliotheque
|
|
53
|
+
* doit rester ouvrable, et montrer OU le probleme se situe.
|
|
54
|
+
*/
|
|
55
|
+
export const UNKNOWN_ICON_ELEMENTS = [
|
|
56
|
+
["rect", { x: "2", y: "2", width: "20", height: "20", rx: "4", "stroke-dasharray": "3 3" }],
|
|
57
|
+
["path", { d: "M9.5 9.5a2.5 2.5 0 1 1 3.5 2.3V14" }],
|
|
58
|
+
["circle", { cx: "12", cy: "17.5", r: ".5", fill: "currentColor" }],
|
|
59
|
+
];
|
|
60
|
+
/**
|
|
61
|
+
* Selection couverte par la description de `add_nodes` : de quoi composer une
|
|
62
|
+
* interface complete sans aucun aller-retour. Le reste de la bibliotheque
|
|
63
|
+
* s'atteint par `search_icons`.
|
|
64
|
+
*/
|
|
65
|
+
export const CORE_ICONS = [
|
|
66
|
+
// Navigation et mise en page
|
|
67
|
+
"house", "search", "menu", "x", "chevron-left", "chevron-right", "chevron-up", "chevron-down",
|
|
68
|
+
"arrow-left", "arrow-right", "arrow-up", "arrow-down", "arrow-up-right", "arrow-down-right",
|
|
69
|
+
"external-link", "ellipsis", "ellipsis-vertical", "funnel", "sliders-horizontal", "settings",
|
|
70
|
+
"layout-dashboard", "layout-grid", "panel-left", "panel-right", "list", "list-checks", "table",
|
|
71
|
+
"columns-3", "rows-3", "maximize", "minimize", "expand", "shrink", "move", "grip-vertical", "grip-horizontal",
|
|
72
|
+
// Actions
|
|
73
|
+
"plus", "minus", "check", "circle-check", "circle-x", "circle-alert", "circle-question-mark",
|
|
74
|
+
"triangle-alert", "info", "ban", "trash-2", "pencil", "pen-line", "copy", "clipboard", "clipboard-check",
|
|
75
|
+
"scissors", "save", "download", "upload", "share-2", "send", "link", "unlink", "refresh-cw", "rotate-cw",
|
|
76
|
+
"undo", "redo", "lock", "lock-open", "eye", "eye-off", "star", "heart", "bookmark", "flag",
|
|
77
|
+
"bell", "bell-off", "thumbs-up", "thumbs-down",
|
|
78
|
+
// Personnes et communication
|
|
79
|
+
"user", "users", "user-plus", "user-check", "circle-user", "contact", "mail", "mail-open",
|
|
80
|
+
"message-circle", "message-square", "messages-square", "phone", "phone-call", "video", "video-off",
|
|
81
|
+
"mic", "mic-off", "headphones", "at-sign",
|
|
82
|
+
// Documents et medias
|
|
83
|
+
"file", "file-text", "file-plus", "files", "folder", "folder-open", "folder-plus", "archive",
|
|
84
|
+
"book", "book-open", "newspaper", "notebook-pen", "sticky-note", "paperclip", "printer",
|
|
85
|
+
"image", "images", "camera", "film", "music", "play", "circle-play", "pause", "volume-2", "volume-x",
|
|
86
|
+
// Temps et lieux
|
|
87
|
+
"calendar", "calendar-days", "clock", "timer", "hourglass", "alarm-clock", "map-pin", "map",
|
|
88
|
+
"navigation", "compass", "globe", "plane", "car", "truck", "bike", "train-front", "ship",
|
|
89
|
+
// Commerce
|
|
90
|
+
"shopping-cart", "shopping-bag", "shopping-basket", "credit-card", "wallet", "banknote", "coins",
|
|
91
|
+
"dollar-sign", "euro", "percent", "receipt", "tag", "tags", "gift", "package", "package-check",
|
|
92
|
+
// Donnees et statuts
|
|
93
|
+
"chart-line", "chart-bar", "chart-pie", "chart-column", "trending-up", "trending-down", "activity",
|
|
94
|
+
"gauge", "target", "award", "trophy", "zap", "flame", "sparkles", "wand-sparkles", "lightbulb", "rocket",
|
|
95
|
+
// Technique
|
|
96
|
+
"database", "server", "cloud", "cloud-upload", "cloud-download", "hard-drive", "cpu", "monitor",
|
|
97
|
+
"smartphone", "tablet", "laptop", "keyboard", "mouse-pointer", "wifi", "wifi-off", "bluetooth",
|
|
98
|
+
"battery", "plug", "power", "terminal", "code", "code-xml", "braces", "bug", "git-branch",
|
|
99
|
+
"git-commit-horizontal", "shield", "shield-check", "key", "key-round", "fingerprint-pattern",
|
|
100
|
+
"scan", "qr-code", "barcode",
|
|
101
|
+
// Nature et divers
|
|
102
|
+
"sun", "moon", "cloud-rain", "snowflake", "wind", "droplet", "leaf", "tree-pine", "mountain",
|
|
103
|
+
"loader-circle", "circle-dot", "dot", "text-align-start", "text-align-center", "text-align-end",
|
|
104
|
+
"bold", "italic", "underline", "type", "palette", "brush", "pipette", "ruler", "crop", "layers",
|
|
105
|
+
"box", "shapes", "circle", "square", "triangle", "hexagon", "face-slightly-smiling",
|
|
106
|
+
"face-slightly-frowning", "face-neutral", "handshake", "hand", "languages", "accessibility",
|
|
107
|
+
];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recherche d'icones par nom et par synonyme.
|
|
3
|
+
*
|
|
4
|
+
* Separe de `index.ts` a dessein : le rendu n'a besoin que de la geometrie, et
|
|
5
|
+
* l'index de mots-cles (deux fois le poids du reste) ne doit peser ni sur le
|
|
6
|
+
* demarrage du serveur, ni sur le premier affichage du canvas. Il n'est charge
|
|
7
|
+
* que par l'outil `search_icons` et par le selecteur de la GUI.
|
|
8
|
+
*/
|
|
9
|
+
export declare function iconKeywords(name: string): string[];
|
|
10
|
+
export interface IconMatch {
|
|
11
|
+
name: string;
|
|
12
|
+
/** Synonymes qui ont fait ressortir l'icone — ce qui explique un resultat inattendu. */
|
|
13
|
+
keywords: string[];
|
|
14
|
+
score: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Icones correspondant a une requete en langage naturel.
|
|
18
|
+
*
|
|
19
|
+
* Un terme non reconnu ne vide pas le resultat : les scores s'additionnent, si
|
|
20
|
+
* bien qu'une requete de trois mots dont un seul existe dans la bibliotheque
|
|
21
|
+
* retourne quand meme quelque chose d'utile.
|
|
22
|
+
*/
|
|
23
|
+
export declare function searchIcons(query: string, limit?: number): IconMatch[];
|
|
24
|
+
/**
|
|
25
|
+
* Noms proches d'un nom d'icone refuse — ce qui transforme une erreur en indice.
|
|
26
|
+
*
|
|
27
|
+
* Les synonymes comptent autant que les noms : c'est ce qui rattrape les
|
|
28
|
+
* renommages de la bibliotheque, ou "home" ne designe plus rien mais reste le
|
|
29
|
+
* synonyme de "house".
|
|
30
|
+
*/
|
|
31
|
+
export declare function suggestIcons(name: string, limit?: number): string[];
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recherche d'icones par nom et par synonyme.
|
|
3
|
+
*
|
|
4
|
+
* Separe de `index.ts` a dessein : le rendu n'a besoin que de la geometrie, et
|
|
5
|
+
* l'index de mots-cles (deux fois le poids du reste) ne doit peser ni sur le
|
|
6
|
+
* demarrage du serveur, ni sur le premier affichage du canvas. Il n'est charge
|
|
7
|
+
* que par l'outil `search_icons` et par le selecteur de la GUI.
|
|
8
|
+
*/
|
|
9
|
+
import { hasIcon, iconNames } from "./index.js";
|
|
10
|
+
import { ICON_KEYWORDS_JSON } from "./icon-keywords.js";
|
|
11
|
+
let cache = null;
|
|
12
|
+
function keywords() {
|
|
13
|
+
if (cache === null)
|
|
14
|
+
cache = JSON.parse(ICON_KEYWORDS_JSON);
|
|
15
|
+
return cache;
|
|
16
|
+
}
|
|
17
|
+
export function iconKeywords(name) {
|
|
18
|
+
return keywords()[name] ?? [];
|
|
19
|
+
}
|
|
20
|
+
function tokenize(query) {
|
|
21
|
+
return query
|
|
22
|
+
.toLowerCase()
|
|
23
|
+
.normalize("NFD")
|
|
24
|
+
.replace(/\p{Diacritic}/gu, "")
|
|
25
|
+
.split(/[^a-z0-9]+/)
|
|
26
|
+
.filter((token) => token.length > 0);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Score d'une icone pour un terme : le nom prime sur les synonymes, et un debut
|
|
30
|
+
* de mot prime sur une occurrence au milieu. Sans cette hierarchie, "user"
|
|
31
|
+
* remonte d'abord les vingt variantes composees plutot que l'icone "user".
|
|
32
|
+
*
|
|
33
|
+
* Le RANG du synonyme compte autant que sa presence : les mots-cles sont
|
|
34
|
+
* ordonnes du plus au moins caracteristique. "home" est le premier synonyme de
|
|
35
|
+
* "house" et le quatrieme de "lamp" — sans cette decote, une recherche sur
|
|
36
|
+
* "home" sortait une lampe avant une maison.
|
|
37
|
+
*/
|
|
38
|
+
function scoreToken(name, tags, token) {
|
|
39
|
+
if (name === token)
|
|
40
|
+
return 1000;
|
|
41
|
+
const segments = name.split("-");
|
|
42
|
+
const byName = segments.includes(token) ? (segments[0] === token ? 400 : 250) : name.includes(token) ? 120 : 0;
|
|
43
|
+
const exact = tags.indexOf(token);
|
|
44
|
+
const prefix = exact >= 0 ? -1 : tags.findIndex((tag) => tag.startsWith(token));
|
|
45
|
+
const byTag = exact >= 0
|
|
46
|
+
? Math.max(80, 300 - exact * 25)
|
|
47
|
+
: prefix >= 0
|
|
48
|
+
? Math.max(40, 110 - prefix * 10)
|
|
49
|
+
: tags.some((tag) => tag.includes(token))
|
|
50
|
+
? 30
|
|
51
|
+
: 0;
|
|
52
|
+
// Le meilleur des deux, jamais leur somme : "close" doit sortir "x", dont
|
|
53
|
+
// c'est le premier synonyme, avant "panel-top-close" qui le porte dans son nom.
|
|
54
|
+
return Math.max(byName, byTag);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Icones correspondant a une requete en langage naturel.
|
|
58
|
+
*
|
|
59
|
+
* Un terme non reconnu ne vide pas le resultat : les scores s'additionnent, si
|
|
60
|
+
* bien qu'une requete de trois mots dont un seul existe dans la bibliotheque
|
|
61
|
+
* retourne quand meme quelque chose d'utile.
|
|
62
|
+
*/
|
|
63
|
+
export function searchIcons(query, limit = 24) {
|
|
64
|
+
const tokens = tokenize(query);
|
|
65
|
+
if (tokens.length === 0)
|
|
66
|
+
return [];
|
|
67
|
+
const index = keywords();
|
|
68
|
+
const matches = [];
|
|
69
|
+
for (const name of iconNames()) {
|
|
70
|
+
const tags = index[name] ?? [];
|
|
71
|
+
let score = 0;
|
|
72
|
+
let matched = 0;
|
|
73
|
+
for (const token of tokens) {
|
|
74
|
+
const value = scoreToken(name, tags, token);
|
|
75
|
+
if (value > 0)
|
|
76
|
+
matched += 1;
|
|
77
|
+
score += value;
|
|
78
|
+
}
|
|
79
|
+
if (score === 0)
|
|
80
|
+
continue;
|
|
81
|
+
// Une icone qui repond a TOUS les termes passe devant : "credit card" doit
|
|
82
|
+
// sortir "credit-card", pas les trente icones contenant "card". Le bonus est
|
|
83
|
+
// proportionnel et non forfaitaire, sinon deux correspondances tiedes
|
|
84
|
+
// passeraient devant une icone dont le NOM est exactement le terme cherche.
|
|
85
|
+
if (matched === tokens.length && tokens.length > 1)
|
|
86
|
+
score *= 1.5;
|
|
87
|
+
matches.push({ name, keywords: tags, score });
|
|
88
|
+
}
|
|
89
|
+
return matches
|
|
90
|
+
.sort((a, b) => b.score - a.score || a.name.length - b.name.length || a.name.localeCompare(b.name))
|
|
91
|
+
.slice(0, Math.max(1, limit));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Noms proches d'un nom d'icone refuse — ce qui transforme une erreur en indice.
|
|
95
|
+
*
|
|
96
|
+
* Les synonymes comptent autant que les noms : c'est ce qui rattrape les
|
|
97
|
+
* renommages de la bibliotheque, ou "home" ne designe plus rien mais reste le
|
|
98
|
+
* synonyme de "house".
|
|
99
|
+
*/
|
|
100
|
+
export function suggestIcons(name, limit = 6) {
|
|
101
|
+
if (hasIcon(name))
|
|
102
|
+
return [];
|
|
103
|
+
return searchIcons(name, limit).map((match) => match.name);
|
|
104
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { RenderNode } from "./render.js";
|
|
2
|
+
/**
|
|
3
|
+
* Groupes reutilisables : composants et instances.
|
|
4
|
+
*
|
|
5
|
+
* Un noeud `instance` est une FEUILLE en stockage : il ne porte qu'une
|
|
6
|
+
* reference (`componentId`) vers le groupe qu'il reproduit. Son contenu n'est
|
|
7
|
+
* jamais persiste — il est reconstruit ici, a la volee, par `expandInstances`.
|
|
8
|
+
*
|
|
9
|
+
* C'est ce qui donne la propagation : editer le groupe source met a jour toutes
|
|
10
|
+
* ses instances d'un coup, et `get_changes` ne signale qu'un seul changement au
|
|
11
|
+
* lieu de N copies. En contrepartie, rien ne se surcharge dans une instance —
|
|
12
|
+
* une copie qui doit diverger se detache (`detach_instances`).
|
|
13
|
+
*
|
|
14
|
+
* Comme `render.ts` et `tree.ts`, ce module ne depend d'aucune librairie : il
|
|
15
|
+
* est importe aussi bien par le serveur MCP que par le canvas du navigateur.
|
|
16
|
+
*/
|
|
17
|
+
/** Separe l'id de l'instance de celui du noeud du composant : "n_ab12~n_title". */
|
|
18
|
+
export declare const INSTANCE_ID_SEPARATOR = "~";
|
|
19
|
+
/** Vrai si l'id designe un noeud issu d'une expansion : il n'existe pas en stockage. */
|
|
20
|
+
export declare function isExpandedId(id: string): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Id du noeud reellement stocke derriere un id rendu : l'instance elle-meme
|
|
23
|
+
* pour un noeud expanse, l'id inchange sinon. C'est par la que passe toute
|
|
24
|
+
* selection venue du canvas — cliquer dans une instance selectionne l'instance.
|
|
25
|
+
*/
|
|
26
|
+
export declare function instanceRootOf(id: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Remplace chaque noeud `instance` par un `group` equivalent suivi d'une copie
|
|
29
|
+
* du sous-arbre de son composant, les ids des copies etant prefixes par celui de
|
|
30
|
+
* l'instance.
|
|
31
|
+
*
|
|
32
|
+
* Le resultat ne contient plus aucun noeud `instance` : tous les consommateurs
|
|
33
|
+
* en aval (calcul de disposition, rendu SVG, export HTML/JSX, canvas) travaillent
|
|
34
|
+
* sur des groupes ordinaires, sans une seule branche supplementaire.
|
|
35
|
+
*
|
|
36
|
+
* Le groupe source, lui, reste dans la liste : il est visible sur la planche,
|
|
37
|
+
* comme un composant principal dans Figma.
|
|
38
|
+
*/
|
|
39
|
+
export declare function expandInstances<T extends RenderNode>(nodes: T[]): T[];
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { childrenByParent } from "./tree.js";
|
|
2
|
+
/**
|
|
3
|
+
* Groupes reutilisables : composants et instances.
|
|
4
|
+
*
|
|
5
|
+
* Un noeud `instance` est une FEUILLE en stockage : il ne porte qu'une
|
|
6
|
+
* reference (`componentId`) vers le groupe qu'il reproduit. Son contenu n'est
|
|
7
|
+
* jamais persiste — il est reconstruit ici, a la volee, par `expandInstances`.
|
|
8
|
+
*
|
|
9
|
+
* C'est ce qui donne la propagation : editer le groupe source met a jour toutes
|
|
10
|
+
* ses instances d'un coup, et `get_changes` ne signale qu'un seul changement au
|
|
11
|
+
* lieu de N copies. En contrepartie, rien ne se surcharge dans une instance —
|
|
12
|
+
* une copie qui doit diverger se detache (`detach_instances`).
|
|
13
|
+
*
|
|
14
|
+
* Comme `render.ts` et `tree.ts`, ce module ne depend d'aucune librairie : il
|
|
15
|
+
* est importe aussi bien par le serveur MCP que par le canvas du navigateur.
|
|
16
|
+
*/
|
|
17
|
+
/** Separe l'id de l'instance de celui du noeud du composant : "n_ab12~n_title". */
|
|
18
|
+
export const INSTANCE_ID_SEPARATOR = "~";
|
|
19
|
+
/**
|
|
20
|
+
* Profondeur maximale d'imbrication d'instances. La garde anti-recursion de
|
|
21
|
+
* `validateHierarchy` rend le cas impossible en stockage ; ce plafond protege
|
|
22
|
+
* le rendu d'un fichier ecrit a la main.
|
|
23
|
+
*/
|
|
24
|
+
const MAX_INSTANCE_DEPTH = 8;
|
|
25
|
+
/** Vrai si l'id designe un noeud issu d'une expansion : il n'existe pas en stockage. */
|
|
26
|
+
export function isExpandedId(id) {
|
|
27
|
+
return id.includes(INSTANCE_ID_SEPARATOR);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Id du noeud reellement stocke derriere un id rendu : l'instance elle-meme
|
|
31
|
+
* pour un noeud expanse, l'id inchange sinon. C'est par la que passe toute
|
|
32
|
+
* selection venue du canvas — cliquer dans une instance selectionne l'instance.
|
|
33
|
+
*/
|
|
34
|
+
export function instanceRootOf(id) {
|
|
35
|
+
const separator = id.indexOf(INSTANCE_ID_SEPARATOR);
|
|
36
|
+
return separator === -1 ? id : id.slice(0, separator);
|
|
37
|
+
}
|
|
38
|
+
function derivedId(prefix, id) {
|
|
39
|
+
return prefix === "" ? id : `${prefix}${INSTANCE_ID_SEPARATOR}${id}`;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Remplace chaque noeud `instance` par un `group` equivalent suivi d'une copie
|
|
43
|
+
* du sous-arbre de son composant, les ids des copies etant prefixes par celui de
|
|
44
|
+
* l'instance.
|
|
45
|
+
*
|
|
46
|
+
* Le resultat ne contient plus aucun noeud `instance` : tous les consommateurs
|
|
47
|
+
* en aval (calcul de disposition, rendu SVG, export HTML/JSX, canvas) travaillent
|
|
48
|
+
* sur des groupes ordinaires, sans une seule branche supplementaire.
|
|
49
|
+
*
|
|
50
|
+
* Le groupe source, lui, reste dans la liste : il est visible sur la planche,
|
|
51
|
+
* comme un composant principal dans Figma.
|
|
52
|
+
*/
|
|
53
|
+
export function expandInstances(nodes) {
|
|
54
|
+
if (!nodes.some((node) => node.type === "instance"))
|
|
55
|
+
return nodes;
|
|
56
|
+
const children = childrenByParent(nodes);
|
|
57
|
+
const byId = new Map(nodes.map((node) => [node.id, node]));
|
|
58
|
+
const result = [];
|
|
59
|
+
/** Ids d'origine deja emis a leur place normale, pour le filet de fin. */
|
|
60
|
+
const emitted = new Set();
|
|
61
|
+
const visit = (node, prefix, parentId, chain) => {
|
|
62
|
+
const id = derivedId(prefix, node.id);
|
|
63
|
+
if (prefix === "")
|
|
64
|
+
emitted.add(node.id);
|
|
65
|
+
if (node.type !== "instance") {
|
|
66
|
+
result.push({ ...node, id, parentId });
|
|
67
|
+
for (const child of children.get(node.id) ?? [])
|
|
68
|
+
visit(child, prefix, id, chain);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
// L'instance devient le groupe qu'elle represente : sa position, sa
|
|
72
|
+
// rotation et son opacite s'appliquent au contenu du composant.
|
|
73
|
+
result.push({ ...node, id, parentId, type: "group", componentId: undefined });
|
|
74
|
+
const source = node.componentId === undefined ? undefined : byId.get(node.componentId);
|
|
75
|
+
// Reference invalide, recursive ou trop profonde : l'instance se rend vide
|
|
76
|
+
// plutot que de boucler. La validation, elle, refuse d'ecrire ces cas.
|
|
77
|
+
if (!source || source.type !== "group")
|
|
78
|
+
return;
|
|
79
|
+
if (chain.includes(source.id) || chain.length >= MAX_INSTANCE_DEPTH)
|
|
80
|
+
return;
|
|
81
|
+
const nested = [...chain, source.id];
|
|
82
|
+
for (const child of children.get(source.id) ?? [])
|
|
83
|
+
visit(child, id, id, nested);
|
|
84
|
+
};
|
|
85
|
+
for (const node of children.get(null) ?? [])
|
|
86
|
+
visit(node, "", null, []);
|
|
87
|
+
// Meme filet que `flattenPreOrder` : un noeud dont le parent a disparu est
|
|
88
|
+
// rendu a la racine plutot que de devenir invisible.
|
|
89
|
+
for (const node of nodes) {
|
|
90
|
+
if (!emitted.has(node.id))
|
|
91
|
+
visit(node, "", null, []);
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
@@ -20,6 +20,8 @@ export declare const NODE_STYLE_FIELDS: Record<NodeKind, ReadonlyArray<keyof Ren
|
|
|
20
20
|
* et ne peuvent pas etre ecrits directement — ni par l'agent, ni par la GUI.
|
|
21
21
|
*/
|
|
22
22
|
export declare const GROUP_DERIVED_FIELDS: ReadonlyArray<keyof RenderNode>;
|
|
23
|
+
/** Types dont la taille est deduite du contenu, donc jamais ecrite directement. */
|
|
24
|
+
export declare const SIZE_DERIVED_KINDS: readonly NodeKind[];
|
|
23
25
|
export declare const NODE_GEOMETRY_FIELDS: ReadonlyArray<keyof RenderNode>;
|
|
24
26
|
export declare function defaultNodeName(type: NodeKind, index: number): string;
|
|
25
27
|
export declare function nodeKindLabel(type: NodeKind): string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
import { DEFAULT_ICON_STROKE_WIDTH, ICON_GRID } from "./icons/index.js";
|
|
2
|
+
export const NODE_KINDS = ["rect", "ellipse", "line", "text", "icon", "group", "instance"];
|
|
2
3
|
/** Types que l'utilisateur peut dessiner directement ; un groupe se cree a partir d'une selection. */
|
|
3
|
-
export const DRAWABLE_KINDS = ["rect", "ellipse", "line", "text"];
|
|
4
|
+
export const DRAWABLE_KINDS = ["rect", "ellipse", "line", "text", "icon"];
|
|
4
5
|
export const NODE_DEFAULTS = {
|
|
5
6
|
rect: { width: 200, height: 120, fill: "#e8e8e6", stroke: "#1a1a18", strokeWidth: 1, radius: 8 },
|
|
6
7
|
ellipse: { width: 160, height: 160, fill: "#e8e8e6", stroke: "#1a1a18", strokeWidth: 1 },
|
|
@@ -18,8 +19,19 @@ export const NODE_DEFAULTS = {
|
|
|
18
19
|
// En pixels, comme x/y/fontSize — jamais un multiplicateur.
|
|
19
20
|
lineHeight: 32,
|
|
20
21
|
},
|
|
22
|
+
// Taille de la grille de Lucide : une icone posee sans consigne rend au
|
|
23
|
+
// format pour lequel son trace a ete dessine.
|
|
24
|
+
icon: {
|
|
25
|
+
width: ICON_GRID,
|
|
26
|
+
height: ICON_GRID,
|
|
27
|
+
fill: "none",
|
|
28
|
+
stroke: "#1a1a18",
|
|
29
|
+
strokeWidth: DEFAULT_ICON_STROKE_WIDTH,
|
|
30
|
+
},
|
|
21
31
|
// Un groupe n'a pas de taille propre : elle est deduite de son contenu.
|
|
22
32
|
group: { width: 0, height: 0 },
|
|
33
|
+
// Une instance non plus : sa taille est celle du composant qu'elle reproduit.
|
|
34
|
+
instance: { width: 0, height: 0 },
|
|
23
35
|
};
|
|
24
36
|
/** Champs stylistiques acceptés par type — tout le reste est refusé à la création comme au patch. */
|
|
25
37
|
export const NODE_STYLE_FIELDS = {
|
|
@@ -27,13 +39,19 @@ export const NODE_STYLE_FIELDS = {
|
|
|
27
39
|
ellipse: ["fill", "stroke", "strokeWidth"],
|
|
28
40
|
line: ["stroke", "strokeWidth"],
|
|
29
41
|
text: ["text", "color", "fontSize", "fontFamily", "fontWeight", "fontStyle", "textAlign", "lineHeight"],
|
|
42
|
+
icon: ["icon", "fill", "stroke", "strokeWidth"],
|
|
30
43
|
group: [],
|
|
44
|
+
// Rien ne se surcharge dans une instance : son apparence est celle de son
|
|
45
|
+
// composant. Seul le lien lui-meme se change, ce qui l'echange de composant.
|
|
46
|
+
instance: ["componentId"],
|
|
31
47
|
};
|
|
32
48
|
/**
|
|
33
49
|
* Champs deduits du contenu d'un groupe. Ils sont recalcules a chaque mutation
|
|
34
50
|
* et ne peuvent pas etre ecrits directement — ni par l'agent, ni par la GUI.
|
|
35
51
|
*/
|
|
36
52
|
export const GROUP_DERIVED_FIELDS = ["width", "height"];
|
|
53
|
+
/** Types dont la taille est deduite du contenu, donc jamais ecrite directement. */
|
|
54
|
+
export const SIZE_DERIVED_KINDS = ["group", "instance"];
|
|
37
55
|
export const NODE_GEOMETRY_FIELDS = [
|
|
38
56
|
"x",
|
|
39
57
|
"y",
|
|
@@ -47,7 +65,9 @@ const LABELS = {
|
|
|
47
65
|
ellipse: "Ellipse",
|
|
48
66
|
line: "Ligne",
|
|
49
67
|
text: "Texte",
|
|
68
|
+
icon: "Icône",
|
|
50
69
|
group: "Groupe",
|
|
70
|
+
instance: "Instance",
|
|
51
71
|
};
|
|
52
72
|
export function defaultNodeName(type, index) {
|
|
53
73
|
return `${LABELS[type]} ${index}`;
|
|
@@ -57,10 +77,18 @@ export function nodeKindLabel(type) {
|
|
|
57
77
|
}
|
|
58
78
|
/** Taille minimale garantissant qu'un noeud reste sélectionnable à la souris. */
|
|
59
79
|
export function clampNodeSize(type, width, height) {
|
|
60
|
-
|
|
61
|
-
|
|
80
|
+
// Groupe et instance : taille deduite du contenu.
|
|
81
|
+
if (SIZE_DERIVED_KINDS.includes(type))
|
|
82
|
+
return { width, height };
|
|
62
83
|
if (type === "line")
|
|
63
84
|
return { width, height }; // les deltas négatifs encodent la direction
|
|
85
|
+
// Une icone est carree : son trace l'est, et une boite rectangulaire ne
|
|
86
|
+
// ferait que laisser du vide sur les cotes. Le cote suit la plus petite
|
|
87
|
+
// dimension demandee, ce qui rend un cadrage a la souris previsible.
|
|
88
|
+
if (type === "icon") {
|
|
89
|
+
const side = Math.max(1, Math.min(Math.abs(width), Math.abs(height)));
|
|
90
|
+
return { width: side, height: side };
|
|
91
|
+
}
|
|
64
92
|
if (type === "text")
|
|
65
93
|
return { width: Math.max(20, Math.abs(width)), height: Math.max(16, Math.abs(height)) };
|
|
66
94
|
return { width: Math.max(1, Math.abs(width)), height: Math.max(1, Math.abs(height)) };
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
export const NODE_FIELD_DOCS = {
|
|
10
10
|
id: "Explicit id. Generated when omitted; must be unique within the design.",
|
|
11
|
-
type: "Kind of node: rect, ellipse, line, text or
|
|
11
|
+
type: "Kind of node: rect, ellipse, line, text, icon, group or instance.",
|
|
12
12
|
name: "Layer name shown in the GUI. Purely descriptive.",
|
|
13
13
|
parentId: "Id of the containing group, or null for the root level of the artboard. " +
|
|
14
14
|
"Changing it reparents the node — its x/y are then read in the new parent's space, so it moves on screen. " +
|
|
@@ -25,10 +25,23 @@ export const NODE_FIELD_DOCS = {
|
|
|
25
25
|
"On a group, derived from the content and read-only.",
|
|
26
26
|
rotation: "Rotation in degrees, clockwise, around the centre of the bounding box. 0 = upright.",
|
|
27
27
|
opacity: "Opacity, from 0 (fully transparent) to 1 (fully opaque).",
|
|
28
|
-
fill: 'CSS colour filling the interior, e.g. "#ffffff" or "rgba(0,0,0,0.4)". rect and
|
|
29
|
-
stroke: "CSS colour of the outline — of the stroke itself on a line node.",
|
|
30
|
-
strokeWidth: "Thickness of the outline, in pixels. 0 or omitted means no outline."
|
|
28
|
+
fill: 'CSS colour filling the interior, e.g. "#ffffff" or "rgba(0,0,0,0.4)". rect, ellipse and icon. On an icon it is "none" by default — the shape is drawn by its stroke.',
|
|
29
|
+
stroke: "CSS colour of the outline — of the stroke itself on a line node, of the whole drawing on an icon.",
|
|
30
|
+
strokeWidth: "Thickness of the outline, in pixels. 0 or omitted means no outline. " +
|
|
31
|
+
"On an ICON this is the only exception to the pixel rule: it is expressed in the icon's own 24x24 grid " +
|
|
32
|
+
"(2 = the library's regular weight, 1.5 = light, 2.5 = bold), so it scales with the icon. " +
|
|
33
|
+
"An icon 96px wide with strokeWidth 2 draws an 8px line on screen — never raise it to compensate for a large size.",
|
|
31
34
|
radius: "Corner radius, in pixels. rect only; clamped to half of the shortest side.",
|
|
35
|
+
icon: "Name of the icon to draw, from the bundled Lucide library, e.g. \"shopping-cart\", \"circle-check\", \"user\". " +
|
|
36
|
+
"Required on an icon node. NEVER approximate an icon out of rects, ellipses and lines: name it here instead — " +
|
|
37
|
+
"the result is sharp at any size, restyles in one field, and stays readable to whoever reads the design later. " +
|
|
38
|
+
"Call search_icons to find a name from a description (it also understands synonyms, so \"basket\" finds \"shopping-cart\"). " +
|
|
39
|
+
"The node's box is square: the drawing is scaled uniformly to fit it and centred, never stretched.",
|
|
40
|
+
componentId: "Instance nodes only: id of the GROUP this instance mirrors. The group's content is NOT copied — it is resolved " +
|
|
41
|
+
"at render time, so editing the group updates every instance of it at once, and a repeated block stays a single " +
|
|
42
|
+
"node to read and to change. Whenever the same block appears more than once (cards, list rows, menu items, " +
|
|
43
|
+
"buttons), build it once as a group and add instances of it instead of duplicating it. " +
|
|
44
|
+
"Change this field to swap an instance over to another group; use detach_instances when one copy must diverge.",
|
|
32
45
|
text: "Text content. It is NEVER auto-wrapped: only explicit \\n characters start a new line.",
|
|
33
46
|
color: "CSS colour of the text.",
|
|
34
47
|
fontSize: "Font size, in pixels (same meaning as the CSS font-size property).",
|
package/dist/shared/render.d.ts
CHANGED
|
@@ -7,14 +7,17 @@
|
|
|
7
7
|
* importable depuis le bundle GUI.
|
|
8
8
|
*/
|
|
9
9
|
import { normalizedBox } from "./tree.js";
|
|
10
|
+
import { type IconElement } from "./icons/index.js";
|
|
10
11
|
export { normalizedBox };
|
|
11
12
|
export type { Box, Point } from "./tree.js";
|
|
12
13
|
export interface RenderNode {
|
|
13
14
|
id: string;
|
|
14
|
-
type: "rect" | "ellipse" | "line" | "text" | "group";
|
|
15
|
+
type: "rect" | "ellipse" | "line" | "text" | "icon" | "group" | "instance";
|
|
15
16
|
name: string;
|
|
16
17
|
/** Groupe conteneur, ou null a la racine. Les coordonnees sont relatives a ce parent. */
|
|
17
18
|
parentId: string | null;
|
|
19
|
+
/** Instance : id du groupe reproduit. Resolu par `expandInstances` avant tout rendu. */
|
|
20
|
+
componentId?: string;
|
|
18
21
|
x: number;
|
|
19
22
|
y: number;
|
|
20
23
|
width: number;
|
|
@@ -25,6 +28,8 @@ export interface RenderNode {
|
|
|
25
28
|
stroke?: string;
|
|
26
29
|
strokeWidth?: number;
|
|
27
30
|
radius?: number;
|
|
31
|
+
/** Nom de l'icone dans la bibliotheque embarquee, p.ex. "shopping-cart". Type `icon` uniquement. */
|
|
32
|
+
icon?: string;
|
|
28
33
|
text?: string;
|
|
29
34
|
color?: string;
|
|
30
35
|
fontSize?: number;
|
|
@@ -66,6 +71,9 @@ export declare function defaultLineHeight(fontSize: number): number;
|
|
|
66
71
|
export declare function textMetrics(node: RenderNode): {
|
|
67
72
|
fontSize: number;
|
|
68
73
|
lineStep: number;
|
|
74
|
+
/** Hauteur occupée par les lignes, interligne compris. */
|
|
75
|
+
blockHeight: number;
|
|
76
|
+
blockOffset: number;
|
|
69
77
|
/** Ligne de base de la première ligne, mesurée depuis le haut de la boîte. */
|
|
70
78
|
firstBaseline: number;
|
|
71
79
|
anchor: string;
|
|
@@ -81,6 +89,31 @@ export declare function groupTransform(node: RenderNode, localCenter: {
|
|
|
81
89
|
cx: number;
|
|
82
90
|
cy: number;
|
|
83
91
|
}): string;
|
|
92
|
+
/**
|
|
93
|
+
* Placement du trace d'une icone dans sa boite.
|
|
94
|
+
*
|
|
95
|
+
* L'icone est dessinee dans une grille carree : elle est mise a l'echelle
|
|
96
|
+
* uniformement pour tenir dans la boite, puis centree. Jamais deformee — une
|
|
97
|
+
* icone etiree se lit immediatement comme un defaut, alors qu'une icone centree
|
|
98
|
+
* dans une boite trop large reste correcte.
|
|
99
|
+
*/
|
|
100
|
+
export declare function iconPlacement(node: RenderNode): {
|
|
101
|
+
x: number;
|
|
102
|
+
y: number;
|
|
103
|
+
scale: number;
|
|
104
|
+
size: number;
|
|
105
|
+
};
|
|
106
|
+
/** Trace du noeud, ou le carre en pointilles si le nom d'icone est inconnu. */
|
|
107
|
+
export declare function iconElementsOf(node: RenderNode): IconElement[];
|
|
108
|
+
/**
|
|
109
|
+
* Attributs de trait d'une icone, exprimes dans la grille de 24.
|
|
110
|
+
*
|
|
111
|
+
* `color` fait resoudre les rares `fill="currentColor"` de la bibliotheque vers
|
|
112
|
+
* la couleur du trait : sans lui, le point d'un graphique en nuage ressortirait
|
|
113
|
+
* en noir sur une icone blanche.
|
|
114
|
+
*/
|
|
115
|
+
export declare function iconStyle(node: RenderNode): Record<string, string>;
|
|
116
|
+
export declare function iconElementToSvg([tag, attributes]: IconElement): string;
|
|
84
117
|
/**
|
|
85
118
|
* Markup SVG d'un seul noeud, sans wrapper — utilisé par l'export et par le canvas.
|
|
86
119
|
* Un groupe ne produit rien par lui-meme : c'est un conteneur, rendu par
|