@imenam/mcp-design 1.0.4 → 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.
Files changed (46) hide show
  1. package/README.md +61 -8
  2. package/dist/shared/icons/icon-data.d.ts +2 -0
  3. package/dist/shared/icons/icon-data.js +5 -0
  4. package/dist/shared/icons/icon-keywords.d.ts +1 -0
  5. package/dist/shared/icons/icon-keywords.js +4 -0
  6. package/dist/shared/icons/index.d.ts +47 -0
  7. package/dist/shared/icons/index.js +107 -0
  8. package/dist/shared/icons/search.d.ts +31 -0
  9. package/dist/shared/icons/search.js +104 -0
  10. package/dist/shared/instances.d.ts +39 -0
  11. package/dist/shared/instances.js +94 -0
  12. package/dist/shared/node-defaults.d.ts +2 -0
  13. package/dist/shared/node-defaults.js +32 -4
  14. package/dist/shared/node-fields.d.ts +2 -0
  15. package/dist/shared/node-fields.js +17 -4
  16. package/dist/shared/render.d.ts +31 -1
  17. package/dist/shared/render.js +65 -6
  18. package/dist/shared/schemas/design.schema.d.ts +19 -1
  19. package/dist/shared/schemas/design.schema.js +13 -1
  20. package/dist/src/core/clone.d.ts +23 -0
  21. package/dist/src/core/clone.js +34 -0
  22. package/dist/src/core/diff.js +2 -2
  23. package/dist/src/core/export.js +63 -3
  24. package/dist/src/core/hierarchy.d.ts +7 -0
  25. package/dist/src/core/hierarchy.js +75 -5
  26. package/dist/src/core/node-factory.js +55 -5
  27. package/dist/src/core/screenshot.d.ts +12 -0
  28. package/dist/src/core/screenshot.js +17 -6
  29. package/dist/src/index.js +97 -7
  30. package/dist/src/tools/add-nodes.js +7 -1
  31. package/dist/src/tools/delete-nodes.js +10 -1
  32. package/dist/src/tools/detach-instances.d.ts +14 -0
  33. package/dist/src/tools/detach-instances.js +61 -0
  34. package/dist/src/tools/duplicate-nodes.js +20 -28
  35. package/dist/src/tools/search-icons.d.ts +12 -0
  36. package/dist/src/tools/search-icons.js +87 -0
  37. package/dist/src/tools/ungroup-nodes.js +8 -0
  38. package/dist/src/version.d.ts +1 -1
  39. package/dist/src/version.js +1 -1
  40. package/gui/dist/assets/{index-C6fXmzfu.css → index-BIeiaHA2.css} +1 -1
  41. package/gui/dist/assets/index-Dpd_cZgh.js +10 -0
  42. package/gui/dist/assets/search--VtHhIew.js +1 -0
  43. package/gui/dist/index.html +2 -2
  44. package/package.json +4 -2
  45. package/scripts/generate-icons.js +85 -0
  46. package/gui/dist/assets/index-Cmo-X8Zs.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
- export const NODE_KINDS = ["rect", "ellipse", "line", "text", "group"];
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
- if (type === "group")
61
- return { width, height }; // taille deduite du contenu
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)) };
@@ -21,6 +21,8 @@ export declare const NODE_FIELD_DOCS: {
21
21
  stroke: string;
22
22
  strokeWidth: string;
23
23
  radius: string;
24
+ icon: string;
25
+ componentId: string;
24
26
  text: string;
25
27
  color: string;
26
28
  fontSize: string;
@@ -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 group.",
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 ellipse only.',
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).",
@@ -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;
@@ -84,6 +89,31 @@ export declare function groupTransform(node: RenderNode, localCenter: {
84
89
  cx: number;
85
90
  cy: number;
86
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;
87
117
  /**
88
118
  * Markup SVG d'un seul noeud, sans wrapper — utilisé par l'export et par le canvas.
89
119
  * Un groupe ne produit rien par lui-meme : c'est un conteneur, rendu par