@imenam/mcp-design 1.0.13 → 1.0.15
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 +454 -403
- package/dist/shared/feedback.d.ts +3 -2
- package/dist/shared/feedback.js +4 -3
- package/dist/src/core/design-model.d.ts +54 -0
- package/dist/src/core/design-model.js +129 -0
- package/dist/src/core/design-store.d.ts +3 -18
- package/dist/src/core/design-store.js +15 -103
- package/dist/src/core/exchange.d.ts +86 -0
- package/dist/src/core/exchange.js +430 -0
- package/dist/src/core/feedback-model.d.ts +64 -0
- package/dist/src/core/feedback-model.js +211 -0
- package/dist/src/core/feedback-store.d.ts +4 -31
- package/dist/src/core/feedback-store.js +14 -195
- package/dist/src/core/ids.js +1 -2
- package/dist/src/gui-worker.js +42 -53
- package/dist/src/index.js +0 -7
- package/gui/dist/assets/index-DTIGOSpK.js +15 -0
- package/gui/dist/assets/{search-DHQx5ZT0.js → search-CH3uadrp.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +3 -2
- package/gui/dist/assets/index-SUmtOh8T.js +0 -15
package/README.md
CHANGED
|
@@ -1,403 +1,454 @@
|
|
|
1
|
-
# @imenam/mcp-design
|
|
2
|
-
|
|
3
|
-
Un canvas de design pilotable par un agent, avec une interface graphique où l'utilisateur reprend la main.
|
|
4
|
-
|
|
5
|
-
L'agent compose une planche (formes, texte) via des outils MCP. L'utilisateur ouvre la GUI, déplace, retaille, recolore, réécrit. L'agent relit ensuite **exactement ce qui a changé** et l'intègre dans le code de l'application.
|
|
6
|
-
|
|
7
|
-
L'utilisateur peut aussi, sans toucher au design, **épingler des remarques** sur une page, un élément ou une zone. Il les transmet en un retour nommé que l'agent lit avec `get_feedback`, captures comprises ; l'agent déclare ensuite ce qu'il a traité avec `mark_feedback_processed`, et l'utilisateur suit l'avancement de chaque retour dans la page « Retours ».
|
|
8
|
-
|
|
9
|
-
```
|
|
10
|
-
Agent ──add_nodes/update_nodes──▶ Design ◀──glisser-déposer── Utilisateur (GUI)
|
|
11
|
-
▲ │
|
|
12
|
-
├──────────── get_changes ──── diff depuis la dernière baseline ──────┤
|
|
13
|
-
└──────────── get_feedback ─── remarques transmises (retour n°N) ─────┘
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
---
|
|
17
|
-
|
|
18
|
-
## Installation
|
|
19
|
-
|
|
20
|
-
```bash
|
|
21
|
-
npm install -g @imenam/mcp-design
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
Ou directement via `npx`, sans installation.
|
|
25
|
-
|
|
26
|
-
### Déclaration dans un projet
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
npx -y @imenam/mcp-design --claude-setup-mcp
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
La commande crée (ou complète) le `.mcp.json` du répertoire courant avec une entrée `mcp-design` dont les variables sont pré-remplies. Ajoutez `--force` pour remplacer une entrée existante ; les autres serveurs sont préservés.
|
|
33
|
-
|
|
34
|
-
Deux flags permettent de choisir directement comment la GUI sera exposée, sans éditer le JSON ensuite :
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
# GUI derrière le proxy central
|
|
38
|
-
npx -y @imenam/mcp-design --claude-setup-mcp --proxy_url=http://localhost:3000
|
|
39
|
-
|
|
40
|
-
# GUI en accès direct, sans proxy : http://localhost:4500
|
|
41
|
-
npx -y @imenam/mcp-design --claude-setup-mcp --app_port=4500
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
Les deux flags sont exclusifs — la commande échoue si les deux sont passés.
|
|
45
|
-
|
|
46
|
-
Exemple d'entrée générée :
|
|
47
|
-
|
|
48
|
-
```json
|
|
49
|
-
{
|
|
50
|
-
"mcpServers": {
|
|
51
|
-
"mcp-design": {
|
|
52
|
-
"command": "npx",
|
|
53
|
-
"args": ["-y", "@imenam/mcp-design"],
|
|
54
|
-
"env": {
|
|
55
|
-
"PROXY_URL": "http://localhost:3000",
|
|
56
|
-
"APP_PORT": "",
|
|
57
|
-
"APP_GROUP": "Mon Projet",
|
|
58
|
-
"APP_NAME": "Design Mon Projet",
|
|
59
|
-
"APP_PATH": "/design-mon-projet",
|
|
60
|
-
"MCP_DESIGN_DATA_DIR": "C:/dev/mon-projet/.design-data",
|
|
61
|
-
"MCP_LOG_DIR": ""
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
### Variables d'environnement
|
|
69
|
-
|
|
70
|
-
| Variable | Rôle |
|
|
71
|
-
|---|---|
|
|
72
|
-
| `PROXY_URL` | URL du [proxy central](https://github.com/imenam/proxy-setup) : le proxy attribue le port et monte la GUI sous `APP_PATH`. |
|
|
73
|
-
| `APP_PORT` | Port local utilisé **quand `PROXY_URL` est absent** : la GUI est servie directement sur `http://localhost:<port>`, à la racine. Permet d'utiliser l'interface sans le hub de centralisation. |
|
|
74
|
-
| `APP_PATH` | Chemin de montage de la GUI derrière le proxy (défaut `/design-gui`). Ignoré en mode `APP_PORT`. `PROXY_APP_PATH` puis `GUI_PATH` sont acceptés en repli. |
|
|
75
|
-
| `APP_NAME` | Nom affiché dans le dashboard du proxy. `PROXY_APP_NAME` puis `GUI_NAME` sont acceptés en repli. |
|
|
76
|
-
| `APP_PATH_PREFIX` | Préfixe de namespace ajouté au chemin **par défaut** (ex. `/abc123` → `/abc123/design-gui`). Sert à isoler plusieurs instances sous un même proxy. |
|
|
77
|
-
| `APP_GROUP` | Section repliable du dashboard du proxy (facultatif). |
|
|
78
|
-
| `MCP_DESIGN_DATA_DIR` | Répertoire de stockage des designs et des remarques. `MCP_DATA_DIR` est accepté en repli. Défaut : `<package>/.design-data`. |
|
|
79
|
-
| `MCP_LOG_DIR` | Répertoire des logs. Défaut : `<data>/logs`. |
|
|
80
|
-
|
|
81
|
-
`PROXY_URL` et `APP_PORT` s'excluent : `PROXY_URL` prime si les deux sont renseignés. **Si aucune des deux n'est définie, la GUI est simplement désactivée** — les outils MCP continuent de fonctionner.
|
|
82
|
-
|
|
83
|
-
`APP_PATH` / `APP_NAME` sont les noms canoniques ; `PROXY_APP_*` puis `GUI_*` restent acceptés, dans cet ordre de priorité décroissante. La règle de `APP_PATH_PREFIX` est simple : **il ne préfixe que le chemin par défaut**. Dès que `APP_PATH` (ou l'un de ses replis) est renseigné, cette valeur est prise telle quelle, sans préfixe — la saisie manuelle l'emporte toujours. Les chemins sont normalisés (un seul `/` de séparation, pas de `/` final) et le préfixage est idempotent. Le `<base href>` de l'interface suit le chemin réellement enregistré, assets compris.
|
|
84
|
-
|
|
85
|
-
La résolution est assurée par le paquet partagé [`@imenam/mcp-gui-interface`](https://www.npmjs.com/package/@imenam/mcp-gui-interface), qui documente la règle en détail.
|
|
86
|
-
|
|
87
|
-
### Exposition réseau
|
|
88
|
-
|
|
89
|
-
La GUI écoute sur `127.0.0.1` uniquement, en mode proxy comme en mode `APP_PORT` : elle n'est pas joignable directement depuis une autre machine. Pour y accéder à distance, passez par le [proxy central](https://github.com/imenam/proxy-setup) ou le gateway manager, qui relaient vers `localhost:<port>`.
|
|
90
|
-
|
|
91
|
-
Chaque requête traverse la garde de [`@imenam/mcp-gui-interface`](https://www.npmjs.com/package/@imenam/mcp-gui-interface) (`honoGuiGuard`), qui répond `403` :
|
|
92
|
-
|
|
93
|
-
- à tout en-tête `Host` autre que `localhost`, `127.0.0.1` ou `::1` (protection contre le DNS rebinding) ;
|
|
94
|
-
- aux modifications (méthodes autres que `GET`, `HEAD`, `OPTIONS`) venues d'un autre site, d'après `Sec-Fetch-Site` ou, à défaut, `Origin` (sont admis : la machine elle-même, le proxy de `PROXY_URL`).
|
|
95
|
-
|
|
96
|
-
Un design est un simple fichier JSON dans `<data>/designs/<id>.json` — lisible, diffable, versionnable. Les images qu'il affiche sont dans `<data>/images/`.
|
|
97
|
-
|
|
98
|
-
---
|
|
99
|
-
|
|
100
|
-
## Les outils
|
|
101
|
-
|
|
102
|
-
### Planches
|
|
103
|
-
|
|
104
|
-
| Outil | Effet |
|
|
105
|
-
|---|---|
|
|
106
|
-
| `list_designs` | Liste les planches, leur taille, leur nombre d'éléments et le nombre de modifications non encore récupérées. |
|
|
107
|
-
| `create_design` | Crée une planche vide (`name`, et facultativement `width`, `height`, `background`). |
|
|
108
|
-
| `duplicate_design` | Crée une nouvelle planche reprenant toute une planche existante — dimensions, fond, éléments, groupes et instances. Les identifiants des éléments sont conservés ; `name` et `group` sont facultatifs (par défaut, le nom d'origine suivi de « (copy) » et le même groupe de pages). La copie part sans modification en attente. |
|
|
109
|
-
| `get_design` | Lit une planche : `format: "summary"` (inventaire compact, défaut) ou `format: "json"` (valeurs exactes). |
|
|
110
|
-
| `update_design` | Renomme la planche, change ses dimensions ou son fond. |
|
|
111
|
-
| `delete_design` | Supprime définitivement une planche. |
|
|
112
|
-
|
|
113
|
-
### Éléments
|
|
114
|
-
|
|
115
|
-
| Outil | Effet |
|
|
116
|
-
|---|---|
|
|
117
|
-
| `add_nodes` | Ajoute un ou plusieurs éléments par-dessus les autres. |
|
|
118
|
-
| `add_image` | Importe une image (chemin absolu d'un fichier PNG, JPEG, GIF, WebP ou SVG) et la pose par-dessus les autres éléments. |
|
|
119
|
-
| `update_nodes` | Patche des éléments existants — seuls les champs fournis changent. |
|
|
120
|
-
| `delete_nodes` | Supprime des éléments par identifiant. Supprimer un groupe emporte son contenu. |
|
|
121
|
-
| `duplicate_nodes` | Copie des éléments ou des groupes avec tout leur contenu, sous de nouveaux identifiants. `offset_x`/`offset_y` et `count` disposent une série de copies, `parent_id` les dépose dans un groupe. Avec `target_design_id`, la copie va dans une **autre planche** : les éléments gardent leur position à l'écran, et les composants reproduits par les instances copiées sont copiés avec elles, une seule fois, à la racine. |
|
|
122
|
-
| `group_nodes` | Enveloppe des éléments dans un nouveau groupe, sans rien déplacer à l'écran. |
|
|
123
|
-
| `ungroup_nodes` | Dissout des groupes : leur contenu remonte d'un niveau, toujours sans bouger. |
|
|
124
|
-
| `detach_instances` | Rompt le lien d'une instance avec son composant : son contenu devient des éléments ordinaires. |
|
|
125
|
-
|
|
126
|
-
Huit types d'éléments : `rect`, `ellipse`, `line`, `text`, `icon`, `image`, `group` et `instance`. Seul `type` est obligatoire — sauf pour `icon`, qui a besoin du nom de l'icône, et `image`, qui a besoin du nom d'une image importée ; tout le reste tombe sur des valeurs par défaut visibles, donc `{ "type": "rect" }` suffit à créer quelque chose qu'on voit à l'écran.
|
|
127
|
-
|
|
128
|
-
**Champs communs** — `name`, `parentId`, `x`, `y`, `width`, `height` (en pixels, origine en haut à gauche), `rotation` (degrés, autour du centre de la boîte), `opacity` (0–1).
|
|
129
|
-
|
|
130
|
-
**Toute longueur est en pixels**, sans exception : `x`, `y`, `width`, `height`, `strokeWidth`, `radius`, `fontSize` et `lineHeight`. Chaque champ porte sa description et son unité dans [`shared/node-fields.ts`](shared/node-fields.ts), d'où le schéma zod **et** l'`inputSchema` MCP les tirent tous les deux — une unité n'est écrite qu'à un seul endroit.
|
|
131
|
-
|
|
132
|
-
| Type | Champs propres |
|
|
133
|
-
|---|---|
|
|
134
|
-
| `rect` | `fill`, `stroke`, `strokeWidth`, `radius` |
|
|
135
|
-
| `ellipse` | `fill`, `stroke`, `strokeWidth` |
|
|
136
|
-
| `line` | `stroke`, `strokeWidth` — le trait va de `(x, y)` à `(x + width, y + height)`, donc les deltas négatifs encodent la direction |
|
|
137
|
-
| `text` | `text`, `color`, `fontSize`, `fontFamily`, `fontWeight`, `fontStyle`, `textAlign`, `lineHeight` (en px) |
|
|
138
|
-
| `icon` | `icon` (obligatoire — le nom du dessin), `fill`, `stroke`, `strokeWidth` |
|
|
139
|
-
| `image` | `image` (obligatoire — le nom de l'image dans la médiathèque), `fit` (`cover` par défaut, `contain`, `fill`) |
|
|
140
|
-
| `group` | aucun — c'est un pur conteneur |
|
|
141
|
-
| `instance` | `componentId` (obligatoire — le groupe reproduit) ; rien d'autre, son apparence vient de ce groupe |
|
|
142
|
-
|
|
143
|
-
### Icônes
|
|
144
|
-
|
|
145
|
-
Un agent qui n'a que des formes à sa disposition finit par approximer une icône avec trois ellipses et deux traits : le rendu est médiocre, et la maquette relue plus tard ne dit plus ce que ce tas de formes représentait. Le type `icon` répond exactement à ça — **1791 icônes [Lucide](https://lucide.dev) (licence ISC) sont embarquées** dans le paquet.
|
|
146
|
-
|
|
147
|
-
```json
|
|
148
|
-
{ "type": "icon", "icon": "shopping-cart", "x": 24, "y": 18, "width": 24, "height": 24, "stroke": "#1a1a18" }
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
Le nœud stocke le **nom**, jamais le tracé : le JSON reste lisible — `shopping-cart` dit à l'agent qui le relit ce que la forme représente —, un nœud `icon` pèse moins qu'un `rect`, et changer d'icône est un `update_nodes` d'un seul champ. Le SVG est résolu au rendu depuis [`shared/icons/`](shared/icons/), donc le canvas, l'export et la capture dessinent rigoureusement la même chose.
|
|
152
|
-
|
|
153
|
-
| Outil | Effet |
|
|
154
|
-
|---|---|
|
|
155
|
-
| `search_icons` | Cherche une icône par description (`query`), en anglais et par synonymes — « basket » sort `shopping-cart`. Renvoie les noms **et** une planche de vignettes, sauf si `preview: false`. |
|
|
156
|
-
|
|
157
|
-
Les icônes les plus courantes sont listées directement dans la description d'`add_nodes` (`CORE_ICONS` dans [`shared/icons/index.ts`](shared/icons/index.ts)) : habiller une interface ordinaire ne demande aucun aller-retour. `search_icons` sert à la traîne longue. Un nom refusé repart avec des suggestions issues des synonymes, ce qui rattrape aussi bien une faute qu'un renommage de la bibliothèque (`home` → `house`).
|
|
158
|
-
|
|
159
|
-
Trois règles à connaître :
|
|
160
|
-
|
|
161
|
-
- **`strokeWidth` est la seule exception à la règle du pixel** : il s'exprime dans la grille 24 × 24 de l'icône (2 = graisse normale, 1.5 = fine, 2.5 = grasse). Une icône de 96 px avec `strokeWidth: 2` dessine un trait de 8 px à l'écran — le trait suit la taille, il n'y a rien à compenser.
|
|
162
|
-
- **La boîte est carrée** : `width` et `height` sont ramenés au plus petit des deux, et le tracé est mis à l'échelle uniformément puis centré. Jamais déformé.
|
|
163
|
-
- **Un nom inconnu ne casse rien** : l'icône est remplacée par un carré en pointillés avec un `?`. Le même SVG servant au canvas, à l'export et à la capture, une exception y serait fatale partout à la fois — un design ouvert après une mise à jour de la bibliothèque doit rester ouvrable, et montrer *où* est le problème.
|
|
164
|
-
|
|
165
|
-
Côté GUI, l'outil « Icône » de la barre d'outils (raccourci `I`) ouvre un sélecteur cherchable, et l'inspecteur permet de remplacer l'icône d'un élément existant. L'index de synonymes y est chargé à la demande, pour ne pas peser sur le premier affichage du canvas.
|
|
166
|
-
|
|
167
|
-
La bibliothèque est figée dans le dépôt par `npm run generate-icons` ([`scripts/generate-icons.js`](scripts/generate-icons.js)) : ni le serveur publié ni le bundle de la GUI ne dépendent de Lucide à l'exécution, et la géométrie d'un design ne peut pas changer au gré d'un `npm install`.
|
|
168
|
-
|
|
169
|
-
### Images
|
|
170
|
-
|
|
171
|
-
`add_image` pose une vraie image — photo, logo, illustration, capture — là où une icône ou des formes ne suffisent pas.
|
|
172
|
-
|
|
173
|
-
```json
|
|
174
|
-
{ "design_id": "landing", "file_path": "C:/projets/site/assets/hero.jpg", "x": 0, "y": 80, "width": 1440 }
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
- **Le fichier est copié** dans `<MCP_DESIGN_DATA_DIR>/images/`, sous un nom tiré de l'empreinte de son contenu (`img_3f9a1c2b7d4e5f60.jpg`). Le design ne stocke que ce nom : le JSON reste lisible, une même image importée deux fois n'est stockée qu'une fois, et la planche ne dépend plus de l'emplacement du fichier d'origine. La médiathèque est commune à toutes les planches du dossier de données.
|
|
178
|
-
- **Taille** : sans `width` ni `height`, la boîte prend les dimensions naturelles de l'image ; avec une seule des deux, l'autre suit ses proportions.
|
|
179
|
-
- **`fit`** dit comment l'image remplit sa boîte : `cover` la remplit en rognant le débordement, `contain` la montre en entier, `fill` l'étire.
|
|
180
|
-
- **Réutiliser une image** : la réponse d'`add_image` donne son nom ; un nœud `{ "type": "image", "image": "img_…" }` dans `add_nodes` la repose sans second import. Un nom absent de la médiathèque est refusé à l'écriture.
|
|
181
|
-
- Le format est reconnu d'après le contenu du fichier, pas d'après son extension ; 20 Mo au plus.
|
|
182
|
-
- Derrière [`@imenam/mcp-http-gateway`](https://www.npmjs.com/package/@imenam/mcp-http-gateway), l'outil déclare la paire `file_path` / `file_content` : le relais lit le fichier sur la machine de l'agent et en transmet le contenu (dossier autorisé par `GATEWAY_INLINE_ROOTS`).
|
|
183
|
-
|
|
184
|
-
Le canvas charge les images depuis le worker (`GET /api/images/<nom>`). `screenshot_design`, les captures des remarques et `export_design` les intègrent en data URI : un export `svg`, `html` ou `jsx` s'ouvre tel quel, sans la médiathèque — mais il pèse le poids des images qu'il contient ; pour une planche illustrée, préférez `destination_path` à un retour en ligne.
|
|
185
|
-
|
|
186
|
-
Dans la GUI, une image se déplace, se redimensionne et se pivote comme les autres éléments ; l'inspecteur règle son cadrage.
|
|
187
|
-
|
|
188
|
-
### Hiérarchie
|
|
189
|
-
|
|
190
|
-
Chaque élément porte un `parentId` : `null` à la racine de la planche, sinon l'identifiant d'un `group`. **Les coordonnées sont exprimées dans l'espace du parent** : un élément à la racine se positionne par rapport à la planche, un élément dans un groupe par rapport à l'origine de ce groupe.
|
|
191
|
-
|
|
192
|
-
C'est ce qui rend un déplacement de groupe lisible côté agent — un seul champ change, pas les coordonnées de chaque membre :
|
|
193
|
-
|
|
194
|
-
```
|
|
195
|
-
- g_a1b2c3 — "Carte produit" (group)
|
|
196
|
-
x: 100 -> 260
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
La taille d'un groupe est **déduite de son contenu** : `width` et `height` sont recalculés à chaque mutation, ne peuvent pas être écrits, et n'apparaissent jamais dans un diff. Le stockage reste une liste plate, ordonnée en parcours préfixe — chaque parent suivi de son sous-arbre — ce qui garde le fichier JSON lisible et diffable.
|
|
200
|
-
|
|
201
|
-
Le texte n'est **jamais re-wrappé automatiquement** : seuls les `\n` explicites créent une nouvelle ligne. Le comportement est ainsi identique à l'écran et à l'export.
|
|
202
|
-
|
|
203
|
-
`lineHeight` est une **distance en pixels entre deux lignes de base**, pas un multiplicateur de `fontSize` : avec `fontSize: 64`, on écrit `76`, pas `1.2`. C'est l'unité que les agents emploient spontanément, et la seule qui se lise dans la même échelle que le reste. Une valeur sous 4 est donc refusée à l'écriture avec un message explicite, et les fichiers écrits avant ce changement sont convertis à la lecture (`ratio x fontSize`) par [`src/core/migrate.ts`](src/core/migrate.ts). Champ omis = `fontSize x 1.35`.
|
|
204
|
-
|
|
205
|
-
#### La boîte d'un texte épouse son texte
|
|
206
|
-
|
|
207
|
-
Le bloc de lignes est **centré verticalement dans la boîte** du nœud : c'est ce qui permet de centrer un libellé dans un bouton sans calculer de décalage. Encore faut-il que la boîte soit celle qu'on croit — une hauteur par défaut plus grande que le texte le fait descendre sous le point posé.
|
|
208
|
-
|
|
209
|
-
Donc : **un nœud `text` créé sans `height` prend la hauteur exacte de son texte** (`lignes x lineHeight`). Le centrage n'a alors plus rien à décaler, et la première ligne commence bien à `y`, à gauche de `x`. Ajouter une ligne agrandit la boîte au lieu de la faire déborder : une boîte qui épousait son texte continue de l'épouser, à chaque changement de `text`, `fontSize` ou `lineHeight`, à l'écriture par l'agent comme à l'édition dans la GUI.
|
|
210
|
-
|
|
211
|
-
Une hauteur **explicite**, elle, n'est jamais retouchée — c'est le geste qui dit « centre ce libellé » :
|
|
212
|
-
|
|
213
|
-
```jsonc
|
|
214
|
-
{ "type": "text", "x": 40, "y": 120, "text": "Titre", "fontSize": 32 } // boîte = 43.2px, texte posé à y
|
|
215
|
-
{ "type": "text", "x": 40, "y": 100, "height": 26, "text": "OK", "fontSize": 12 } // libellé centré dans son bouton
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
### Groupes réutilisables
|
|
219
|
-
|
|
220
|
-
Quand un même bloc revient plusieurs fois — une carte, une ligne de liste, un bouton —, il ne se recopie pas : il se **construit une fois** en groupe, puis s'**instancie**. Une `instance` ne stocke aucun contenu, seulement l'identifiant du groupe qu'elle reproduit ; le sous-arbre est reconstitué au rendu. Éditer le groupe met donc à jour **toutes** ses instances d'un coup, et un bloc répété reste une seule chose à lire et à modifier. C'est le fonctionnement d'un composant principal dans Figma.
|
|
221
|
-
|
|
222
|
-
```jsonc
|
|
223
|
-
// 1. le composant : un groupe ordinaire
|
|
224
|
-
{ "type": "group", "id": "card", "name": "Carte" }
|
|
225
|
-
// 2. ses copies liées
|
|
226
|
-
{ "type": "instance", "componentId": "card", "x": 320, "y": 80 }
|
|
227
|
-
{ "type": "instance", "componentId": "card", "x": 640, "y": 80 }
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
Règles, volontairement peu nombreuses :
|
|
231
|
-
|
|
232
|
-
- **Rien ne se surcharge dans une instance** — c'est ce qui garantit qu'elles restent identiques. Sa position, sa rotation et son opacité lui appartiennent ; tout le reste vient du composant. Une copie qui doit diverger devient un second groupe, ou se détache (`detach_instances`).
|
|
233
|
-
- La **taille** d'une instance est déduite de son composant, comme celle d'un groupe.
|
|
234
|
-
- Changer `componentId` **échange** l'instance de composant (le *swap* de Figma).
|
|
235
|
-
- Les instances s'**imbriquent** (une instance de carte contenant une instance de bouton), mais un composant ne peut pas se contenir lui-même : le cas est refusé à l'écriture.
|
|
236
|
-
- Un groupe encore instancié ne peut être ni supprimé ni dissous — il faut d'abord détacher ou supprimer ses instances.
|
|
237
|
-
- Détacher une instance ne détache **pas** les instances imbriquées dans son composant : elles restent liées.
|
|
238
|
-
|
|
239
|
-
Côté agent, le gain se lit dans `get_changes` : une retouche du composant apparaît comme **un** changement, pas comme N copies modifiées.
|
|
240
|
-
|
|
241
|
-
### Ordre d'empilement
|
|
242
|
-
|
|
243
|
-
L'index dans la liste **est** l'ordre d'empilement : le dernier élément d'une fratrie est celui du dessus. `add_nodes` empile au-dessus, et `reorder_nodes` déplace ensuite ce qui existe déjà.
|
|
244
|
-
|
|
245
|
-
| Outil | Effet |
|
|
246
|
-
|---|---|
|
|
247
|
-
| `reorder_nodes` | `position: front / back / forward / backward` sur un ou plusieurs éléments. Un groupe emporte son contenu ; plusieurs éléments gardent leur ordre relatif. |
|
|
248
|
-
|
|
249
|
-
L'empilement est **relatif à la fratrie** : un élément ne passe jamais devant le contenu d'un autre groupe — c'est ce groupe qu'il faut alors réordonner, ou l'élément qu'il faut en sortir via `parentId`. La logique vit dans [`shared/tree.ts`](shared/tree.ts) et sert aussi bien l'outil que le panneau de calques de la GUI : les deux ne peuvent pas diverger.
|
|
250
|
-
|
|
251
|
-
Un lot est intégralement validé avant la moindre écriture : une entrée invalide ne laisse jamais un design à moitié modifié.
|
|
252
|
-
|
|
253
|
-
### Récupérer le travail de l'utilisateur
|
|
254
|
-
|
|
255
|
-
| Outil | Effet |
|
|
256
|
-
|---|---|
|
|
257
|
-
| `get_changes` | Liste tout ce qui a été ajouté, modifié, supprimé ou réordonné depuis la dernière baseline, champ par champ (`x: 40 -> 240`). |
|
|
258
|
-
| `mark_synced` | Fige l'état courant comme nouvelle baseline. |
|
|
259
|
-
| `export_design` | Rend la planche en `html`, `jsx`, `svg`, `json` ou `summary`. `destination_path` + `filename` écrivent un fichier **sur la machine de l'agent** au lieu de renvoyer le contenu. |
|
|
260
|
-
| `screenshot_design` | Rasterise la planche en PNG et le renvoie **en image** à l'agent. `node_id` cadre la capture sur un groupe (ou n'importe quel élément), `scale` zoome, `padding` ajoute de la marge, `destination_path` + `filename` écrivent un `.png` **sur la machine de l'agent** au lieu de renvoyer l'image. |
|
|
261
|
-
|
|
262
|
-
> **Où le fichier est écrit.** `destination_path` est un **dossier absolu** sur la machine de l'agent, `filename` le nom que le fichier y prend. Omis, les deux outils se comportent comme avant : le contenu (ou l'image) revient dans la réponse, sans fichier.
|
|
263
|
-
>
|
|
264
|
-
> En stdio, ce MCP tourne à côté de l'agent et écrit lui-même. Derrière [`@imenam/mcp-http-gateway`](https://www.npmjs.com/package/@imenam/mcp-http-gateway), il tourne sur la machine du gateway et ne peut pas atteindre le disque de l'agent : il déclare alors la paire `destination_path` / `destination_relay`, renvoie les octets, et le relais — qui tourne, lui, sur la machine de l'agent — les y écrit. `destination_relay` est rempli par le relais, jamais par l'agent, et cette voie exige `GATEWAY_INLINE_WRITE_ROOTS` dans le bloc `env` du relais.
|
|
265
|
-
|
|
266
|
-
**`get_changes` n'acquitte jamais ce qu'il montre.** Seul `mark_synced` déplace la baseline, et il ne doit être appelé qu'une fois les modifications réellement reportées dans le code. Un simple appel de lecture ne peut donc pas faire disparaître un diff que l'agent n'a pas encore traité.
|
|
267
|
-
|
|
268
|
-
### Lire les remarques de l'utilisateur
|
|
269
|
-
|
|
270
|
-
| Outil | Effet |
|
|
271
|
-
|---|---|
|
|
272
|
-
| `get_feedback` | Renvoie le plus ancien retour qui a encore des remarques à traiter (le dernier quand tout est traité), ou celui demandé par `round`. Pour chaque remarque : son identifiant, la page, puis la page entière, l'élément visé (id, nom, chemin, boîte) ou la zone et les éléments cités, le texte et son état de traitement. Les captures d'éléments et de zones suivent **en images**, numérotées comme dans le texte ; `captures: false` les omet. La réponse se termine par la liste de tous les retours et de leur avancement. |
|
|
273
|
-
| `mark_feedback_processed` | Déclare traitées des remarques d'un retour (`round`, `remark_ids` — toutes si omis), avec une `note` sur ce qui a été fait. C'est ce que l'utilisateur voit dans la page « Retours ». |
|
|
274
|
-
|
|
275
|
-
Un retour est **figé** au moment où l'utilisateur clique sur « Terminer les retours » : `get_feedback` peut le relire autant de fois que nécessaire, rien ne change. Seuls son nom (renommable dans la GUI) et le suivi du traitement évoluent ; **lire un retour ne le marque jamais traité**, seul `mark_feedback_processed` (ou l'utilisateur) le fait. Le texte est celui que la GUI affiche en aperçu avant l'enregistrement — même fonction de mise en forme, donc même contenu. Tant qu'aucun retour n'existe, l'outil dit combien de remarques attendent encore dans la GUI.
|
|
276
|
-
|
|
277
|
-
Les remarques vivent dans `<MCP_DESIGN_DATA_DIR>/feedback/remarks.json`, les captures PNG dans `<MCP_DESIGN_DATA_DIR>/feedback/captures/` : le même dossier de données que les designs, celui fixé par la configuration du projet.
|
|
278
|
-
|
|
279
|
-
### GUI
|
|
280
|
-
|
|
281
|
-
| Outil | Effet |
|
|
282
|
-
|---|---|
|
|
283
|
-
| `reconnect_gui` | Relance le worker GUI et le réenregistre auprès du proxy. `force: true` redémarre même s'il semble vivant. |
|
|
284
|
-
|
|
285
|
-
---
|
|
286
|
-
|
|
287
|
-
## Boucle de travail type
|
|
288
|
-
|
|
289
|
-
```
|
|
290
|
-
create_design { name: "Landing" }
|
|
291
|
-
add_nodes { design_id: "landing", nodes: [...] }
|
|
292
|
-
mark_synced { design_id: "landing" } ← point de départ de la comparaison
|
|
293
|
-
|
|
294
|
-
… l'utilisateur ouvre la GUI et retouche la maquette …
|
|
295
|
-
|
|
296
|
-
get_changes { design_id: "landing" } ← ce qu'il a changé, champ par champ
|
|
297
|
-
screenshot_design { design_id: "landing" } ← ce que ça donne réellement, en image
|
|
298
|
-
export_design { design_id: "landing", format: "html" }
|
|
299
|
-
… l'agent reporte les valeurs dans le code de l'application …
|
|
300
|
-
mark_synced { design_id: "landing" } ← acquittement
|
|
301
|
-
|
|
302
|
-
… l'utilisateur passe en mode Remarques, commente, clique « Terminer les retours »,
|
|
303
|
-
puis dit à l'agent qu'il a terminé …
|
|
304
|
-
|
|
305
|
-
get_feedback {} ← retour à traiter : remarques + captures
|
|
306
|
-
… l'agent applique les remarques …
|
|
307
|
-
mark_feedback_processed { round: 1, remark_ids: ["rm_…"], note: "…" } ← suivi visible dans la GUI
|
|
308
|
-
```
|
|
309
|
-
|
|
310
|
-
---
|
|
311
|
-
|
|
312
|
-
## L'interface
|
|
313
|
-
|
|
314
|
-
Trois colonnes : la liste des planches, le canvas, l'inspecteur et les calques.
|
|
315
|
-
|
|
316
|
-
- **Pages** — la barre de gauche présente l'arbre des groupes : chevron, dossier ouvert ou fermé et nombre de pages par groupe, avec un trait vertical qui relie chaque niveau à son parent. À chaque niveau, les sous-groupes viennent d'abord, puis les pages, chacun par ordre alphabétique ; les pages hors groupe sont regroupées en bas sous « Hors groupe ». À l'ouverture, l'interface rouvre la dernière page consultée dans ce navigateur (mémorisée localement, propre à chaque poste) ; à défaut, la première page de la liste.
|
|
317
|
-
- **Groupe affiché** — le sélecteur en tête montre « Tous les groupes » ou un seul groupe de premier niveau, avec son nombre de pages et de pages à synchroniser. Le choix est mémorisé dans le navigateur.
|
|
318
|
-
- **Recherche et filtres** — le champ de recherche porte sur les noms de pages et de dossiers, à toute profondeur, sans tenir compte des accents ni de la casse (`Échap` le vide) : un dossier dont le nom correspond reste affiché en entier, sinon seules ses pages et sous-dossiers qui correspondent restent. Les filtres « Toutes », « À synchroniser » et « Remarques » (remarques non transmises) portent leur compteur. Une recherche ou un filtre actif déplie tous les groupes.
|
|
319
|
-
- **Tout replier** — le bouton à droite de la recherche replie tous les dossiers du groupe affiché ; quand ils le sont déjà tous, il les déplie. Il est inactif pendant une recherche ou un filtre, qui gardent tout déplié.
|
|
320
|
-
- **Cartes de page** — un aperçu aux proportions de la planche, le nom, le format et la date de dernière modification ; à droite, la bulle violette des remarques non transmises et le point ambre des modifications à synchroniser. Le menu « … », visible au survol, propose **Déplacer vers…** (vide : hors groupe), **Dupliquer** (la copie s'ouvre aussitôt) et **Supprimer**. Une page se range aussi par glisser-déposer sur un groupe.
|
|
321
|
-
- **Nouvelle page** — le bouton en pied ouvre le formulaire de création ; le groupe est prérempli avec le groupe affiché.
|
|
322
|
-
|
|
323
|
-
- **Outils** — sélection, rectangle, ellipse, ligne, texte, icône. Glisser pour dimensionner, cliquer pour poser à la taille par défaut. L'outil icône ouvre d'abord son sélecteur : poser une icône suppose d'avoir choisi laquelle.
|
|
324
|
-
- **Édition** — déplacement au glisser, 8 poignées de redimensionnement (2 extrémités pour une ligne), sélection multiple au rectangle d'encadrement ou au Maj+clic.
|
|
325
|
-
- **Groupes** — `Ctrl+G` groupe la sélection, `Ctrl+Maj+G` la dissout. Un clic dans le canvas sélectionne le **groupe le plus haut** ; `Alt+clic` vise l'élément exact à l'intérieur. Déplacer un groupe déplace tout son contenu.
|
|
326
|
-
- **Texte** — double-clic pour éditer en place, avec la même métrique qu'au rendu final. Une boîte à la taille de son texte le reste : ajouter une ligne l'agrandit d'autant.
|
|
327
|
-
- **Inspecteur** — position, taille, rotation, opacité, couleurs, épaisseur, arrondi, contenu, taille et graisse de police, interligne (en px), alignement, choix de l'icône. Pour un groupe, la taille est affichée en lecture seule puisqu'elle suit son contenu ; pour une icône, un seul champ de taille, puisqu'elle est carrée.
|
|
328
|
-
- **Calques** — arbre repliable, ordre d'empilement, sélection, montée/descente d'un cran.
|
|
329
|
-
|
|
330
|
-
### Naviguer dans la planche
|
|
331
|
-
|
|
332
|
-
- **Zoom** : `Ctrl`/`Cmd` + molette, ancré sous le curseur — le point visé ne bouge pas.
|
|
333
|
-
- **Déplacement** : molette seule, ou **barre espace maintenue + glisser**. Tant que la barre espace est enfoncée, aucun geste ne peut déplacer un élément, même en partant de l'un d'eux. Le clic molette fait la même chose.
|
|
334
|
-
- Le sélecteur de zoom de la barre d'outils reste disponible pour revenir à une valeur ronde.
|
|
335
|
-
|
|
336
|
-
Raccourcis : `V` `R` `O` `L` `T` `I` (outils), `Ctrl+Z` / `Ctrl+Y` (annuler/rétablir), `Ctrl+G` / `Ctrl+Maj+G` (grouper / dissoudre), `Ctrl+D` (dupliquer), `Ctrl+A` (tout sélectionner), `Suppr`, flèches (déplacer d'1 px, 10 px avec `Maj`), `Échap`.
|
|
337
|
-
|
|
338
|
-
La sauvegarde est automatique et différée. Une modification faite par l'agent pendant que la GUI est ouverte y apparaît
|
|
339
|
-
|
|
340
|
-
Le point ambre d'une page indique qu'elle porte des modifications que l'agent n'a pas encore acquittées avec `mark_synced` ; leur nombre s'affiche au survol.
|
|
341
|
-
|
|
342
|
-
### Mode Remarques
|
|
343
|
-
|
|
344
|
-
La bascule **Édition / Remarques** de la barre d'outils (raccourci `M`) passe le canvas en lecture : rien ne s'y déplace, et le rail de droite affiche les remarques à la place de l'inspecteur.
|
|
345
|
-
|
|
346
|
-
- **Remarque sur un élément** — un clic vise le **groupe le plus haut**, comme en édition ; un nouveau clic à l'intérieur descend d'un niveau, `Alt+clic` vise l'élément exact. La bulle de saisie affiche le chemin de l'élément : un clic sur un niveau vise ce parent.
|
|
347
|
-
- **Remarque sur une zone** — glisser trace un rectangle. La bulle montre l'aperçu de la zone et les **éléments détectés** : un élément entièrement compris est cité tel quel, un groupe qui déborde est ouvert pour citer ceux de ses éléments que la zone touche, un élément qui déborde est marqué « en partie » et décoché par défaut.
|
|
348
|
-
- **Remarque sur la page** — depuis le panneau, une seule remarque en cours par page.
|
|
349
|
-
- **Captures** — pour un élément ou une zone, « Joindre une capture » (coché par défaut) enregistre un PNG de ce que l'utilisateur commentait, rendu par le serveur au moment de la remarque.
|
|
350
|
-
- **Pastilles** — chaque remarque d'élément ou de zone porte un numéro sur la planche ; survoler une carte du panneau met sa cible en évidence, cliquer une pastille désigne sa carte.
|
|
351
|
-
- **Terminer les retours** — demande un **nom** pour le retour (« Retour n°N » proposé), choisit le périmètre (cette page ou toutes) et les remarques à transmettre, et montre exactement le texte que l'agent recevra. L'enregistrement fige un **retour numéroté et nommé**.
|
|
352
|
-
- **Après l'envoi** — les remarques restent visibles en lecture seule, marquées « Transmise · *nom du retour* », puis « Traitée » une fois traitées : pastille creuse, zone en pointillés. Une case du panneau les masque. Une remarque dont l'élément a disparu depuis le signale.
|
|
353
|
-
|
|
354
|
-
### Page « Retours »
|
|
355
|
-
|
|
356
|
-
Le bouton **Retours** de la barre d'outils (avec le nombre de retours pas encore entièrement traités) ouvre la liste de tous les retours, du plus récent au plus ancien, chacun avec son état — *À traiter*, *En cours* (n/total), *Traité* — et une barre d'avancement. Le détail d'un retour reprend ses remarques page par page, avec leur capture, et pour chacune : *À traiter*, ou *Traitée par l'agent / par vous* avec la date et la note de l'agent. Le nom du retour se modifie en place ; une case coche ou rouvre une remarque à la main, et « Tout marquer comme traité » (ou « Tout rouvrir ») agit sur tout le retour affiché ; le même bouton en haut de la page traite d'un coup tous les retours en attente, après confirmation. Une remarque déjà traitée par l'agent garde son auteur et sa note ; « Ouvrir la page » ramène sur la planche en mode Remarques. `Échap` ferme la page.
|
|
357
|
-
|
|
358
|
-
Les retours créés avant l'arrivée des noms reçoivent « Retour n°N », et aucune de leurs remarques n'est considérée comme traitée.
|
|
359
|
-
|
|
360
|
-
`Ctrl+Entrée` enregistre une remarque, `Échap` la ferme, puis quitte le mode. Le badge violet d'une page dans la liste compte ses remarques non transmises.
|
|
361
|
-
|
|
362
|
-
---
|
|
363
|
-
|
|
364
|
-
##
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
**
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
###
|
|
402
|
-
|
|
403
|
-
|
|
1
|
+
# @imenam/mcp-design
|
|
2
|
+
|
|
3
|
+
Un canvas de design pilotable par un agent, avec une interface graphique où l'utilisateur reprend la main.
|
|
4
|
+
|
|
5
|
+
L'agent compose une planche (formes, texte) via des outils MCP. L'utilisateur ouvre la GUI, déplace, retaille, recolore, réécrit. L'agent relit ensuite **exactement ce qui a changé** et l'intègre dans le code de l'application.
|
|
6
|
+
|
|
7
|
+
L'utilisateur peut aussi, sans toucher au design, **épingler des remarques** sur une page, un élément ou une zone. Il les transmet en un retour nommé que l'agent lit avec `get_feedback`, captures comprises ; l'agent déclare ensuite ce qu'il a traité avec `mark_feedback_processed`, et l'utilisateur suit l'avancement de chaque retour dans la page « Retours ».
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Agent ──add_nodes/update_nodes──▶ Design ◀──glisser-déposer── Utilisateur (GUI)
|
|
11
|
+
▲ │
|
|
12
|
+
├──────────── get_changes ──── diff depuis la dernière baseline ──────┤
|
|
13
|
+
└──────────── get_feedback ─── remarques transmises (retour n°N) ─────┘
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install -g @imenam/mcp-design
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Ou directement via `npx`, sans installation.
|
|
25
|
+
|
|
26
|
+
### Déclaration dans un projet
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx -y @imenam/mcp-design --claude-setup-mcp
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
La commande crée (ou complète) le `.mcp.json` du répertoire courant avec une entrée `mcp-design` dont les variables sont pré-remplies. Ajoutez `--force` pour remplacer une entrée existante ; les autres serveurs sont préservés.
|
|
33
|
+
|
|
34
|
+
Deux flags permettent de choisir directement comment la GUI sera exposée, sans éditer le JSON ensuite :
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# GUI derrière le proxy central
|
|
38
|
+
npx -y @imenam/mcp-design --claude-setup-mcp --proxy_url=http://localhost:3000
|
|
39
|
+
|
|
40
|
+
# GUI en accès direct, sans proxy : http://localhost:4500
|
|
41
|
+
npx -y @imenam/mcp-design --claude-setup-mcp --app_port=4500
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Les deux flags sont exclusifs — la commande échoue si les deux sont passés.
|
|
45
|
+
|
|
46
|
+
Exemple d'entrée générée :
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"mcpServers": {
|
|
51
|
+
"mcp-design": {
|
|
52
|
+
"command": "npx",
|
|
53
|
+
"args": ["-y", "@imenam/mcp-design"],
|
|
54
|
+
"env": {
|
|
55
|
+
"PROXY_URL": "http://localhost:3000",
|
|
56
|
+
"APP_PORT": "",
|
|
57
|
+
"APP_GROUP": "Mon Projet",
|
|
58
|
+
"APP_NAME": "Design Mon Projet",
|
|
59
|
+
"APP_PATH": "/design-mon-projet",
|
|
60
|
+
"MCP_DESIGN_DATA_DIR": "C:/dev/mon-projet/.design-data",
|
|
61
|
+
"MCP_LOG_DIR": ""
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Variables d'environnement
|
|
69
|
+
|
|
70
|
+
| Variable | Rôle |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `PROXY_URL` | URL du [proxy central](https://github.com/imenam/proxy-setup) : le proxy attribue le port et monte la GUI sous `APP_PATH`. |
|
|
73
|
+
| `APP_PORT` | Port local utilisé **quand `PROXY_URL` est absent** : la GUI est servie directement sur `http://localhost:<port>`, à la racine. Permet d'utiliser l'interface sans le hub de centralisation. |
|
|
74
|
+
| `APP_PATH` | Chemin de montage de la GUI derrière le proxy (défaut `/design-gui`). Ignoré en mode `APP_PORT`. `PROXY_APP_PATH` puis `GUI_PATH` sont acceptés en repli. |
|
|
75
|
+
| `APP_NAME` | Nom affiché dans le dashboard du proxy. `PROXY_APP_NAME` puis `GUI_NAME` sont acceptés en repli. |
|
|
76
|
+
| `APP_PATH_PREFIX` | Préfixe de namespace ajouté au chemin **par défaut** (ex. `/abc123` → `/abc123/design-gui`). Sert à isoler plusieurs instances sous un même proxy. |
|
|
77
|
+
| `APP_GROUP` | Section repliable du dashboard du proxy (facultatif). |
|
|
78
|
+
| `MCP_DESIGN_DATA_DIR` | Répertoire de stockage des designs et des remarques. `MCP_DATA_DIR` est accepté en repli. Défaut : `<package>/.design-data`. |
|
|
79
|
+
| `MCP_LOG_DIR` | Répertoire des logs. Défaut : `<data>/logs`. |
|
|
80
|
+
|
|
81
|
+
`PROXY_URL` et `APP_PORT` s'excluent : `PROXY_URL` prime si les deux sont renseignés. **Si aucune des deux n'est définie, la GUI est simplement désactivée** — les outils MCP continuent de fonctionner.
|
|
82
|
+
|
|
83
|
+
`APP_PATH` / `APP_NAME` sont les noms canoniques ; `PROXY_APP_*` puis `GUI_*` restent acceptés, dans cet ordre de priorité décroissante. La règle de `APP_PATH_PREFIX` est simple : **il ne préfixe que le chemin par défaut**. Dès que `APP_PATH` (ou l'un de ses replis) est renseigné, cette valeur est prise telle quelle, sans préfixe — la saisie manuelle l'emporte toujours. Les chemins sont normalisés (un seul `/` de séparation, pas de `/` final) et le préfixage est idempotent. Le `<base href>` de l'interface suit le chemin réellement enregistré, assets compris.
|
|
84
|
+
|
|
85
|
+
La résolution est assurée par le paquet partagé [`@imenam/mcp-gui-interface`](https://www.npmjs.com/package/@imenam/mcp-gui-interface), qui documente la règle en détail.
|
|
86
|
+
|
|
87
|
+
### Exposition réseau
|
|
88
|
+
|
|
89
|
+
La GUI écoute sur `127.0.0.1` uniquement, en mode proxy comme en mode `APP_PORT` : elle n'est pas joignable directement depuis une autre machine. Pour y accéder à distance, passez par le [proxy central](https://github.com/imenam/proxy-setup) ou le gateway manager, qui relaient vers `localhost:<port>`.
|
|
90
|
+
|
|
91
|
+
Chaque requête traverse la garde de [`@imenam/mcp-gui-interface`](https://www.npmjs.com/package/@imenam/mcp-gui-interface) (`honoGuiGuard`), qui répond `403` :
|
|
92
|
+
|
|
93
|
+
- à tout en-tête `Host` autre que `localhost`, `127.0.0.1` ou `::1` (protection contre le DNS rebinding) ;
|
|
94
|
+
- aux modifications (méthodes autres que `GET`, `HEAD`, `OPTIONS`) venues d'un autre site, d'après `Sec-Fetch-Site` ou, à défaut, `Origin` (sont admis : la machine elle-même, le proxy de `PROXY_URL`).
|
|
95
|
+
|
|
96
|
+
Un design est un simple fichier JSON dans `<data>/designs/<id>.json` — lisible, diffable, versionnable. Les images qu'il affiche sont dans `<data>/images/`.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Les outils
|
|
101
|
+
|
|
102
|
+
### Planches
|
|
103
|
+
|
|
104
|
+
| Outil | Effet |
|
|
105
|
+
|---|---|
|
|
106
|
+
| `list_designs` | Liste les planches, leur taille, leur nombre d'éléments et le nombre de modifications non encore récupérées. |
|
|
107
|
+
| `create_design` | Crée une planche vide (`name`, et facultativement `width`, `height`, `background`). |
|
|
108
|
+
| `duplicate_design` | Crée une nouvelle planche reprenant toute une planche existante — dimensions, fond, éléments, groupes et instances. Les identifiants des éléments sont conservés ; `name` et `group` sont facultatifs (par défaut, le nom d'origine suivi de « (copy) » et le même groupe de pages). La copie part sans modification en attente. |
|
|
109
|
+
| `get_design` | Lit une planche : `format: "summary"` (inventaire compact, défaut) ou `format: "json"` (valeurs exactes). |
|
|
110
|
+
| `update_design` | Renomme la planche, change ses dimensions ou son fond. |
|
|
111
|
+
| `delete_design` | Supprime définitivement une planche. |
|
|
112
|
+
|
|
113
|
+
### Éléments
|
|
114
|
+
|
|
115
|
+
| Outil | Effet |
|
|
116
|
+
|---|---|
|
|
117
|
+
| `add_nodes` | Ajoute un ou plusieurs éléments par-dessus les autres. |
|
|
118
|
+
| `add_image` | Importe une image (chemin absolu d'un fichier PNG, JPEG, GIF, WebP ou SVG) et la pose par-dessus les autres éléments. |
|
|
119
|
+
| `update_nodes` | Patche des éléments existants — seuls les champs fournis changent. |
|
|
120
|
+
| `delete_nodes` | Supprime des éléments par identifiant. Supprimer un groupe emporte son contenu. |
|
|
121
|
+
| `duplicate_nodes` | Copie des éléments ou des groupes avec tout leur contenu, sous de nouveaux identifiants. `offset_x`/`offset_y` et `count` disposent une série de copies, `parent_id` les dépose dans un groupe. Avec `target_design_id`, la copie va dans une **autre planche** : les éléments gardent leur position à l'écran, et les composants reproduits par les instances copiées sont copiés avec elles, une seule fois, à la racine. |
|
|
122
|
+
| `group_nodes` | Enveloppe des éléments dans un nouveau groupe, sans rien déplacer à l'écran. |
|
|
123
|
+
| `ungroup_nodes` | Dissout des groupes : leur contenu remonte d'un niveau, toujours sans bouger. |
|
|
124
|
+
| `detach_instances` | Rompt le lien d'une instance avec son composant : son contenu devient des éléments ordinaires. |
|
|
125
|
+
|
|
126
|
+
Huit types d'éléments : `rect`, `ellipse`, `line`, `text`, `icon`, `image`, `group` et `instance`. Seul `type` est obligatoire — sauf pour `icon`, qui a besoin du nom de l'icône, et `image`, qui a besoin du nom d'une image importée ; tout le reste tombe sur des valeurs par défaut visibles, donc `{ "type": "rect" }` suffit à créer quelque chose qu'on voit à l'écran.
|
|
127
|
+
|
|
128
|
+
**Champs communs** — `name`, `parentId`, `x`, `y`, `width`, `height` (en pixels, origine en haut à gauche), `rotation` (degrés, autour du centre de la boîte), `opacity` (0–1).
|
|
129
|
+
|
|
130
|
+
**Toute longueur est en pixels**, sans exception : `x`, `y`, `width`, `height`, `strokeWidth`, `radius`, `fontSize` et `lineHeight`. Chaque champ porte sa description et son unité dans [`shared/node-fields.ts`](shared/node-fields.ts), d'où le schéma zod **et** l'`inputSchema` MCP les tirent tous les deux — une unité n'est écrite qu'à un seul endroit.
|
|
131
|
+
|
|
132
|
+
| Type | Champs propres |
|
|
133
|
+
|---|---|
|
|
134
|
+
| `rect` | `fill`, `stroke`, `strokeWidth`, `radius` |
|
|
135
|
+
| `ellipse` | `fill`, `stroke`, `strokeWidth` |
|
|
136
|
+
| `line` | `stroke`, `strokeWidth` — le trait va de `(x, y)` à `(x + width, y + height)`, donc les deltas négatifs encodent la direction |
|
|
137
|
+
| `text` | `text`, `color`, `fontSize`, `fontFamily`, `fontWeight`, `fontStyle`, `textAlign`, `lineHeight` (en px) |
|
|
138
|
+
| `icon` | `icon` (obligatoire — le nom du dessin), `fill`, `stroke`, `strokeWidth` |
|
|
139
|
+
| `image` | `image` (obligatoire — le nom de l'image dans la médiathèque), `fit` (`cover` par défaut, `contain`, `fill`) |
|
|
140
|
+
| `group` | aucun — c'est un pur conteneur |
|
|
141
|
+
| `instance` | `componentId` (obligatoire — le groupe reproduit) ; rien d'autre, son apparence vient de ce groupe |
|
|
142
|
+
|
|
143
|
+
### Icônes
|
|
144
|
+
|
|
145
|
+
Un agent qui n'a que des formes à sa disposition finit par approximer une icône avec trois ellipses et deux traits : le rendu est médiocre, et la maquette relue plus tard ne dit plus ce que ce tas de formes représentait. Le type `icon` répond exactement à ça — **1791 icônes [Lucide](https://lucide.dev) (licence ISC) sont embarquées** dans le paquet.
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
{ "type": "icon", "icon": "shopping-cart", "x": 24, "y": 18, "width": 24, "height": 24, "stroke": "#1a1a18" }
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Le nœud stocke le **nom**, jamais le tracé : le JSON reste lisible — `shopping-cart` dit à l'agent qui le relit ce que la forme représente —, un nœud `icon` pèse moins qu'un `rect`, et changer d'icône est un `update_nodes` d'un seul champ. Le SVG est résolu au rendu depuis [`shared/icons/`](shared/icons/), donc le canvas, l'export et la capture dessinent rigoureusement la même chose.
|
|
152
|
+
|
|
153
|
+
| Outil | Effet |
|
|
154
|
+
|---|---|
|
|
155
|
+
| `search_icons` | Cherche une icône par description (`query`), en anglais et par synonymes — « basket » sort `shopping-cart`. Renvoie les noms **et** une planche de vignettes, sauf si `preview: false`. |
|
|
156
|
+
|
|
157
|
+
Les icônes les plus courantes sont listées directement dans la description d'`add_nodes` (`CORE_ICONS` dans [`shared/icons/index.ts`](shared/icons/index.ts)) : habiller une interface ordinaire ne demande aucun aller-retour. `search_icons` sert à la traîne longue. Un nom refusé repart avec des suggestions issues des synonymes, ce qui rattrape aussi bien une faute qu'un renommage de la bibliothèque (`home` → `house`).
|
|
158
|
+
|
|
159
|
+
Trois règles à connaître :
|
|
160
|
+
|
|
161
|
+
- **`strokeWidth` est la seule exception à la règle du pixel** : il s'exprime dans la grille 24 × 24 de l'icône (2 = graisse normale, 1.5 = fine, 2.5 = grasse). Une icône de 96 px avec `strokeWidth: 2` dessine un trait de 8 px à l'écran — le trait suit la taille, il n'y a rien à compenser.
|
|
162
|
+
- **La boîte est carrée** : `width` et `height` sont ramenés au plus petit des deux, et le tracé est mis à l'échelle uniformément puis centré. Jamais déformé.
|
|
163
|
+
- **Un nom inconnu ne casse rien** : l'icône est remplacée par un carré en pointillés avec un `?`. Le même SVG servant au canvas, à l'export et à la capture, une exception y serait fatale partout à la fois — un design ouvert après une mise à jour de la bibliothèque doit rester ouvrable, et montrer *où* est le problème.
|
|
164
|
+
|
|
165
|
+
Côté GUI, l'outil « Icône » de la barre d'outils (raccourci `I`) ouvre un sélecteur cherchable, et l'inspecteur permet de remplacer l'icône d'un élément existant. L'index de synonymes y est chargé à la demande, pour ne pas peser sur le premier affichage du canvas.
|
|
166
|
+
|
|
167
|
+
La bibliothèque est figée dans le dépôt par `npm run generate-icons` ([`scripts/generate-icons.js`](scripts/generate-icons.js)) : ni le serveur publié ni le bundle de la GUI ne dépendent de Lucide à l'exécution, et la géométrie d'un design ne peut pas changer au gré d'un `npm install`.
|
|
168
|
+
|
|
169
|
+
### Images
|
|
170
|
+
|
|
171
|
+
`add_image` pose une vraie image — photo, logo, illustration, capture — là où une icône ou des formes ne suffisent pas.
|
|
172
|
+
|
|
173
|
+
```json
|
|
174
|
+
{ "design_id": "landing", "file_path": "C:/projets/site/assets/hero.jpg", "x": 0, "y": 80, "width": 1440 }
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
- **Le fichier est copié** dans `<MCP_DESIGN_DATA_DIR>/images/`, sous un nom tiré de l'empreinte de son contenu (`img_3f9a1c2b7d4e5f60.jpg`). Le design ne stocke que ce nom : le JSON reste lisible, une même image importée deux fois n'est stockée qu'une fois, et la planche ne dépend plus de l'emplacement du fichier d'origine. La médiathèque est commune à toutes les planches du dossier de données.
|
|
178
|
+
- **Taille** : sans `width` ni `height`, la boîte prend les dimensions naturelles de l'image ; avec une seule des deux, l'autre suit ses proportions.
|
|
179
|
+
- **`fit`** dit comment l'image remplit sa boîte : `cover` la remplit en rognant le débordement, `contain` la montre en entier, `fill` l'étire.
|
|
180
|
+
- **Réutiliser une image** : la réponse d'`add_image` donne son nom ; un nœud `{ "type": "image", "image": "img_…" }` dans `add_nodes` la repose sans second import. Un nom absent de la médiathèque est refusé à l'écriture.
|
|
181
|
+
- Le format est reconnu d'après le contenu du fichier, pas d'après son extension ; 20 Mo au plus.
|
|
182
|
+
- Derrière [`@imenam/mcp-http-gateway`](https://www.npmjs.com/package/@imenam/mcp-http-gateway), l'outil déclare la paire `file_path` / `file_content` : le relais lit le fichier sur la machine de l'agent et en transmet le contenu (dossier autorisé par `GATEWAY_INLINE_ROOTS`).
|
|
183
|
+
|
|
184
|
+
Le canvas charge les images depuis le worker (`GET /api/images/<nom>`). `screenshot_design`, les captures des remarques et `export_design` les intègrent en data URI : un export `svg`, `html` ou `jsx` s'ouvre tel quel, sans la médiathèque — mais il pèse le poids des images qu'il contient ; pour une planche illustrée, préférez `destination_path` à un retour en ligne.
|
|
185
|
+
|
|
186
|
+
Dans la GUI, une image se déplace, se redimensionne et se pivote comme les autres éléments ; l'inspecteur règle son cadrage.
|
|
187
|
+
|
|
188
|
+
### Hiérarchie
|
|
189
|
+
|
|
190
|
+
Chaque élément porte un `parentId` : `null` à la racine de la planche, sinon l'identifiant d'un `group`. **Les coordonnées sont exprimées dans l'espace du parent** : un élément à la racine se positionne par rapport à la planche, un élément dans un groupe par rapport à l'origine de ce groupe.
|
|
191
|
+
|
|
192
|
+
C'est ce qui rend un déplacement de groupe lisible côté agent — un seul champ change, pas les coordonnées de chaque membre :
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
- g_a1b2c3 — "Carte produit" (group)
|
|
196
|
+
x: 100 -> 260
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
La taille d'un groupe est **déduite de son contenu** : `width` et `height` sont recalculés à chaque mutation, ne peuvent pas être écrits, et n'apparaissent jamais dans un diff. Le stockage reste une liste plate, ordonnée en parcours préfixe — chaque parent suivi de son sous-arbre — ce qui garde le fichier JSON lisible et diffable.
|
|
200
|
+
|
|
201
|
+
Le texte n'est **jamais re-wrappé automatiquement** : seuls les `\n` explicites créent une nouvelle ligne. Le comportement est ainsi identique à l'écran et à l'export.
|
|
202
|
+
|
|
203
|
+
`lineHeight` est une **distance en pixels entre deux lignes de base**, pas un multiplicateur de `fontSize` : avec `fontSize: 64`, on écrit `76`, pas `1.2`. C'est l'unité que les agents emploient spontanément, et la seule qui se lise dans la même échelle que le reste. Une valeur sous 4 est donc refusée à l'écriture avec un message explicite, et les fichiers écrits avant ce changement sont convertis à la lecture (`ratio x fontSize`) par [`src/core/migrate.ts`](src/core/migrate.ts). Champ omis = `fontSize x 1.35`.
|
|
204
|
+
|
|
205
|
+
#### La boîte d'un texte épouse son texte
|
|
206
|
+
|
|
207
|
+
Le bloc de lignes est **centré verticalement dans la boîte** du nœud : c'est ce qui permet de centrer un libellé dans un bouton sans calculer de décalage. Encore faut-il que la boîte soit celle qu'on croit — une hauteur par défaut plus grande que le texte le fait descendre sous le point posé.
|
|
208
|
+
|
|
209
|
+
Donc : **un nœud `text` créé sans `height` prend la hauteur exacte de son texte** (`lignes x lineHeight`). Le centrage n'a alors plus rien à décaler, et la première ligne commence bien à `y`, à gauche de `x`. Ajouter une ligne agrandit la boîte au lieu de la faire déborder : une boîte qui épousait son texte continue de l'épouser, à chaque changement de `text`, `fontSize` ou `lineHeight`, à l'écriture par l'agent comme à l'édition dans la GUI.
|
|
210
|
+
|
|
211
|
+
Une hauteur **explicite**, elle, n'est jamais retouchée — c'est le geste qui dit « centre ce libellé » :
|
|
212
|
+
|
|
213
|
+
```jsonc
|
|
214
|
+
{ "type": "text", "x": 40, "y": 120, "text": "Titre", "fontSize": 32 } // boîte = 43.2px, texte posé à y
|
|
215
|
+
{ "type": "text", "x": 40, "y": 100, "height": 26, "text": "OK", "fontSize": 12 } // libellé centré dans son bouton
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Groupes réutilisables
|
|
219
|
+
|
|
220
|
+
Quand un même bloc revient plusieurs fois — une carte, une ligne de liste, un bouton —, il ne se recopie pas : il se **construit une fois** en groupe, puis s'**instancie**. Une `instance` ne stocke aucun contenu, seulement l'identifiant du groupe qu'elle reproduit ; le sous-arbre est reconstitué au rendu. Éditer le groupe met donc à jour **toutes** ses instances d'un coup, et un bloc répété reste une seule chose à lire et à modifier. C'est le fonctionnement d'un composant principal dans Figma.
|
|
221
|
+
|
|
222
|
+
```jsonc
|
|
223
|
+
// 1. le composant : un groupe ordinaire
|
|
224
|
+
{ "type": "group", "id": "card", "name": "Carte" }
|
|
225
|
+
// 2. ses copies liées
|
|
226
|
+
{ "type": "instance", "componentId": "card", "x": 320, "y": 80 }
|
|
227
|
+
{ "type": "instance", "componentId": "card", "x": 640, "y": 80 }
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Règles, volontairement peu nombreuses :
|
|
231
|
+
|
|
232
|
+
- **Rien ne se surcharge dans une instance** — c'est ce qui garantit qu'elles restent identiques. Sa position, sa rotation et son opacité lui appartiennent ; tout le reste vient du composant. Une copie qui doit diverger devient un second groupe, ou se détache (`detach_instances`).
|
|
233
|
+
- La **taille** d'une instance est déduite de son composant, comme celle d'un groupe.
|
|
234
|
+
- Changer `componentId` **échange** l'instance de composant (le *swap* de Figma).
|
|
235
|
+
- Les instances s'**imbriquent** (une instance de carte contenant une instance de bouton), mais un composant ne peut pas se contenir lui-même : le cas est refusé à l'écriture.
|
|
236
|
+
- Un groupe encore instancié ne peut être ni supprimé ni dissous — il faut d'abord détacher ou supprimer ses instances.
|
|
237
|
+
- Détacher une instance ne détache **pas** les instances imbriquées dans son composant : elles restent liées.
|
|
238
|
+
|
|
239
|
+
Côté agent, le gain se lit dans `get_changes` : une retouche du composant apparaît comme **un** changement, pas comme N copies modifiées.
|
|
240
|
+
|
|
241
|
+
### Ordre d'empilement
|
|
242
|
+
|
|
243
|
+
L'index dans la liste **est** l'ordre d'empilement : le dernier élément d'une fratrie est celui du dessus. `add_nodes` empile au-dessus, et `reorder_nodes` déplace ensuite ce qui existe déjà.
|
|
244
|
+
|
|
245
|
+
| Outil | Effet |
|
|
246
|
+
|---|---|
|
|
247
|
+
| `reorder_nodes` | `position: front / back / forward / backward` sur un ou plusieurs éléments. Un groupe emporte son contenu ; plusieurs éléments gardent leur ordre relatif. |
|
|
248
|
+
|
|
249
|
+
L'empilement est **relatif à la fratrie** : un élément ne passe jamais devant le contenu d'un autre groupe — c'est ce groupe qu'il faut alors réordonner, ou l'élément qu'il faut en sortir via `parentId`. La logique vit dans [`shared/tree.ts`](shared/tree.ts) et sert aussi bien l'outil que le panneau de calques de la GUI : les deux ne peuvent pas diverger.
|
|
250
|
+
|
|
251
|
+
Un lot est intégralement validé avant la moindre écriture : une entrée invalide ne laisse jamais un design à moitié modifié.
|
|
252
|
+
|
|
253
|
+
### Récupérer le travail de l'utilisateur
|
|
254
|
+
|
|
255
|
+
| Outil | Effet |
|
|
256
|
+
|---|---|
|
|
257
|
+
| `get_changes` | Liste tout ce qui a été ajouté, modifié, supprimé ou réordonné depuis la dernière baseline, champ par champ (`x: 40 -> 240`). |
|
|
258
|
+
| `mark_synced` | Fige l'état courant comme nouvelle baseline. |
|
|
259
|
+
| `export_design` | Rend la planche en `html`, `jsx`, `svg`, `json` ou `summary`. `destination_path` + `filename` écrivent un fichier **sur la machine de l'agent** au lieu de renvoyer le contenu. |
|
|
260
|
+
| `screenshot_design` | Rasterise la planche en PNG et le renvoie **en image** à l'agent. `node_id` cadre la capture sur un groupe (ou n'importe quel élément), `scale` zoome, `padding` ajoute de la marge, `destination_path` + `filename` écrivent un `.png` **sur la machine de l'agent** au lieu de renvoyer l'image. |
|
|
261
|
+
|
|
262
|
+
> **Où le fichier est écrit.** `destination_path` est un **dossier absolu** sur la machine de l'agent, `filename` le nom que le fichier y prend. Omis, les deux outils se comportent comme avant : le contenu (ou l'image) revient dans la réponse, sans fichier.
|
|
263
|
+
>
|
|
264
|
+
> En stdio, ce MCP tourne à côté de l'agent et écrit lui-même. Derrière [`@imenam/mcp-http-gateway`](https://www.npmjs.com/package/@imenam/mcp-http-gateway), il tourne sur la machine du gateway et ne peut pas atteindre le disque de l'agent : il déclare alors la paire `destination_path` / `destination_relay`, renvoie les octets, et le relais — qui tourne, lui, sur la machine de l'agent — les y écrit. `destination_relay` est rempli par le relais, jamais par l'agent, et cette voie exige `GATEWAY_INLINE_WRITE_ROOTS` dans le bloc `env` du relais.
|
|
265
|
+
|
|
266
|
+
**`get_changes` n'acquitte jamais ce qu'il montre.** Seul `mark_synced` déplace la baseline, et il ne doit être appelé qu'une fois les modifications réellement reportées dans le code. Un simple appel de lecture ne peut donc pas faire disparaître un diff que l'agent n'a pas encore traité.
|
|
267
|
+
|
|
268
|
+
### Lire les remarques de l'utilisateur
|
|
269
|
+
|
|
270
|
+
| Outil | Effet |
|
|
271
|
+
|---|---|
|
|
272
|
+
| `get_feedback` | Renvoie le plus ancien retour qui a encore des remarques à traiter (le dernier quand tout est traité), ou celui demandé par `round`. Pour chaque remarque : son identifiant, la page, puis la page entière, l'élément visé (id, nom, chemin, boîte) ou la zone et les éléments cités, le texte et son état de traitement. Les captures d'éléments et de zones suivent **en images**, numérotées comme dans le texte ; `captures: false` les omet. La réponse se termine par la liste de tous les retours et de leur avancement. |
|
|
273
|
+
| `mark_feedback_processed` | Déclare traitées des remarques d'un retour (`round`, `remark_ids` — toutes si omis), avec une `note` sur ce qui a été fait. C'est ce que l'utilisateur voit dans la page « Retours ». |
|
|
274
|
+
|
|
275
|
+
Un retour est **figé** au moment où l'utilisateur clique sur « Terminer les retours » : `get_feedback` peut le relire autant de fois que nécessaire, rien ne change. Seuls son nom (renommable dans la GUI) et le suivi du traitement évoluent ; **lire un retour ne le marque jamais traité**, seul `mark_feedback_processed` (ou l'utilisateur) le fait. Le texte est celui que la GUI affiche en aperçu avant l'enregistrement — même fonction de mise en forme, donc même contenu. Tant qu'aucun retour n'existe, l'outil dit combien de remarques attendent encore dans la GUI.
|
|
276
|
+
|
|
277
|
+
Les remarques vivent dans `<MCP_DESIGN_DATA_DIR>/feedback/remarks.json`, les captures PNG dans `<MCP_DESIGN_DATA_DIR>/feedback/captures/` : le même dossier de données que les designs, celui fixé par la configuration du projet.
|
|
278
|
+
|
|
279
|
+
### GUI
|
|
280
|
+
|
|
281
|
+
| Outil | Effet |
|
|
282
|
+
|---|---|
|
|
283
|
+
| `reconnect_gui` | Relance le worker GUI et le réenregistre auprès du proxy. `force: true` redémarre même s'il semble vivant. |
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## Boucle de travail type
|
|
288
|
+
|
|
289
|
+
```
|
|
290
|
+
create_design { name: "Landing" }
|
|
291
|
+
add_nodes { design_id: "landing", nodes: [...] }
|
|
292
|
+
mark_synced { design_id: "landing" } ← point de départ de la comparaison
|
|
293
|
+
|
|
294
|
+
… l'utilisateur ouvre la GUI et retouche la maquette …
|
|
295
|
+
|
|
296
|
+
get_changes { design_id: "landing" } ← ce qu'il a changé, champ par champ
|
|
297
|
+
screenshot_design { design_id: "landing" } ← ce que ça donne réellement, en image
|
|
298
|
+
export_design { design_id: "landing", format: "html" }
|
|
299
|
+
… l'agent reporte les valeurs dans le code de l'application …
|
|
300
|
+
mark_synced { design_id: "landing" } ← acquittement
|
|
301
|
+
|
|
302
|
+
… l'utilisateur passe en mode Remarques, commente, clique « Terminer les retours »,
|
|
303
|
+
puis dit à l'agent qu'il a terminé …
|
|
304
|
+
|
|
305
|
+
get_feedback {} ← retour à traiter : remarques + captures
|
|
306
|
+
… l'agent applique les remarques …
|
|
307
|
+
mark_feedback_processed { round: 1, remark_ids: ["rm_…"], note: "…" } ← suivi visible dans la GUI
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## L'interface
|
|
313
|
+
|
|
314
|
+
Trois colonnes : la liste des planches, le canvas, l'inspecteur et les calques.
|
|
315
|
+
|
|
316
|
+
- **Pages** — la barre de gauche présente l'arbre des groupes : chevron, dossier ouvert ou fermé et nombre de pages par groupe, avec un trait vertical qui relie chaque niveau à son parent. À chaque niveau, les sous-groupes viennent d'abord, puis les pages, chacun par ordre alphabétique ; les pages hors groupe sont regroupées en bas sous « Hors groupe ». À l'ouverture, l'interface rouvre la dernière page consultée dans ce navigateur (mémorisée localement, propre à chaque poste) ; à défaut, la première page de la liste.
|
|
317
|
+
- **Groupe affiché** — le sélecteur en tête montre « Tous les groupes » ou un seul groupe de premier niveau, avec son nombre de pages et de pages à synchroniser. Le choix est mémorisé dans le navigateur.
|
|
318
|
+
- **Recherche et filtres** — le champ de recherche porte sur les noms de pages et de dossiers, à toute profondeur, sans tenir compte des accents ni de la casse (`Échap` le vide) : un dossier dont le nom correspond reste affiché en entier, sinon seules ses pages et sous-dossiers qui correspondent restent. Les filtres « Toutes », « À synchroniser » et « Remarques » (remarques non transmises) portent leur compteur. Une recherche ou un filtre actif déplie tous les groupes.
|
|
319
|
+
- **Tout replier** — le bouton à droite de la recherche replie tous les dossiers du groupe affiché ; quand ils le sont déjà tous, il les déplie. Il est inactif pendant une recherche ou un filtre, qui gardent tout déplié.
|
|
320
|
+
- **Cartes de page** — un aperçu aux proportions de la planche, le nom, le format et la date de dernière modification ; à droite, la bulle violette des remarques non transmises et le point ambre des modifications à synchroniser. Le menu « … », visible au survol, propose **Déplacer vers…** (vide : hors groupe), **Dupliquer** (la copie s'ouvre aussitôt) et **Supprimer**. Une page se range aussi par glisser-déposer sur un groupe.
|
|
321
|
+
- **Nouvelle page** — le bouton en pied ouvre le formulaire de création ; le groupe est prérempli avec le groupe affiché.
|
|
322
|
+
|
|
323
|
+
- **Outils** — sélection, rectangle, ellipse, ligne, texte, icône. Glisser pour dimensionner, cliquer pour poser à la taille par défaut. L'outil icône ouvre d'abord son sélecteur : poser une icône suppose d'avoir choisi laquelle.
|
|
324
|
+
- **Édition** — déplacement au glisser, 8 poignées de redimensionnement (2 extrémités pour une ligne), sélection multiple au rectangle d'encadrement ou au Maj+clic.
|
|
325
|
+
- **Groupes** — `Ctrl+G` groupe la sélection, `Ctrl+Maj+G` la dissout. Un clic dans le canvas sélectionne le **groupe le plus haut** ; `Alt+clic` vise l'élément exact à l'intérieur. Déplacer un groupe déplace tout son contenu.
|
|
326
|
+
- **Texte** — double-clic pour éditer en place, avec la même métrique qu'au rendu final. Une boîte à la taille de son texte le reste : ajouter une ligne l'agrandit d'autant.
|
|
327
|
+
- **Inspecteur** — position, taille, rotation, opacité, couleurs, épaisseur, arrondi, contenu, taille et graisse de police, interligne (en px), alignement, choix de l'icône. Pour un groupe, la taille est affichée en lecture seule puisqu'elle suit son contenu ; pour une icône, un seul champ de taille, puisqu'elle est carrée.
|
|
328
|
+
- **Calques** — arbre repliable, ordre d'empilement, sélection, montée/descente d'un cran.
|
|
329
|
+
|
|
330
|
+
### Naviguer dans la planche
|
|
331
|
+
|
|
332
|
+
- **Zoom** : `Ctrl`/`Cmd` + molette, ancré sous le curseur — le point visé ne bouge pas.
|
|
333
|
+
- **Déplacement** : molette seule, ou **barre espace maintenue + glisser**. Tant que la barre espace est enfoncée, aucun geste ne peut déplacer un élément, même en partant de l'un d'eux. Le clic molette fait la même chose.
|
|
334
|
+
- Le sélecteur de zoom de la barre d'outils reste disponible pour revenir à une valeur ronde.
|
|
335
|
+
|
|
336
|
+
Raccourcis : `V` `R` `O` `L` `T` `I` (outils), `Ctrl+Z` / `Ctrl+Y` (annuler/rétablir), `Ctrl+G` / `Ctrl+Maj+G` (grouper / dissoudre), `Ctrl+D` (dupliquer), `Ctrl+A` (tout sélectionner), `Suppr`, flèches (déplacer d'1 px, 10 px avec `Maj`), `Échap`.
|
|
337
|
+
|
|
338
|
+
La sauvegarde est automatique et différée. Une modification faite par l'agent pendant que la GUI est ouverte y apparaît dans les 3 secondes : la GUI interroge `GET /api/changes` toutes les 3 s ; une modification faite dans la GUI est enregistrée comme venant de l'utilisateur, et c'est ce que `get_changes` restitue.
|
|
339
|
+
|
|
340
|
+
Le point ambre d'une page indique qu'elle porte des modifications que l'agent n'a pas encore acquittées avec `mark_synced` ; leur nombre s'affiche au survol.
|
|
341
|
+
|
|
342
|
+
### Mode Remarques
|
|
343
|
+
|
|
344
|
+
La bascule **Édition / Remarques** de la barre d'outils (raccourci `M`) passe le canvas en lecture : rien ne s'y déplace, et le rail de droite affiche les remarques à la place de l'inspecteur.
|
|
345
|
+
|
|
346
|
+
- **Remarque sur un élément** — un clic vise le **groupe le plus haut**, comme en édition ; un nouveau clic à l'intérieur descend d'un niveau, `Alt+clic` vise l'élément exact. La bulle de saisie affiche le chemin de l'élément : un clic sur un niveau vise ce parent.
|
|
347
|
+
- **Remarque sur une zone** — glisser trace un rectangle. La bulle montre l'aperçu de la zone et les **éléments détectés** : un élément entièrement compris est cité tel quel, un groupe qui déborde est ouvert pour citer ceux de ses éléments que la zone touche, un élément qui déborde est marqué « en partie » et décoché par défaut.
|
|
348
|
+
- **Remarque sur la page** — depuis le panneau, une seule remarque en cours par page.
|
|
349
|
+
- **Captures** — pour un élément ou une zone, « Joindre une capture » (coché par défaut) enregistre un PNG de ce que l'utilisateur commentait, rendu par le serveur au moment de la remarque.
|
|
350
|
+
- **Pastilles** — chaque remarque d'élément ou de zone porte un numéro sur la planche ; survoler une carte du panneau met sa cible en évidence, cliquer une pastille désigne sa carte.
|
|
351
|
+
- **Terminer les retours** — demande un **nom** pour le retour (« Retour n°N » proposé), choisit le périmètre (cette page ou toutes) et les remarques à transmettre, et montre exactement le texte que l'agent recevra. L'enregistrement fige un **retour numéroté et nommé**.
|
|
352
|
+
- **Après l'envoi** — les remarques restent visibles en lecture seule, marquées « Transmise · *nom du retour* », puis « Traitée » une fois traitées : pastille creuse, zone en pointillés. Une case du panneau les masque. Une remarque dont l'élément a disparu depuis le signale.
|
|
353
|
+
|
|
354
|
+
### Page « Retours »
|
|
355
|
+
|
|
356
|
+
Le bouton **Retours** de la barre d'outils (avec le nombre de retours pas encore entièrement traités) ouvre la liste de tous les retours, du plus récent au plus ancien, chacun avec son état — *À traiter*, *En cours* (n/total), *Traité* — et une barre d'avancement. Le détail d'un retour reprend ses remarques page par page, avec leur capture, et pour chacune : *À traiter*, ou *Traitée par l'agent / par vous* avec la date et la note de l'agent. Le nom du retour se modifie en place ; une case coche ou rouvre une remarque à la main, et « Tout marquer comme traité » (ou « Tout rouvrir ») agit sur tout le retour affiché ; le même bouton en haut de la page traite d'un coup tous les retours en attente, après confirmation. Une remarque déjà traitée par l'agent garde son auteur et sa note ; « Ouvrir la page » ramène sur la planche en mode Remarques. `Échap` ferme la page.
|
|
357
|
+
|
|
358
|
+
Les retours créés avant l'arrivée des noms reçoivent « Retour n°N », et aucune de leurs remarques n'est considérée comme traitée.
|
|
359
|
+
|
|
360
|
+
`Ctrl+Entrée` enregistre une remarque, `Échap` la ferme, puis quitte le mode. Le badge violet d'une page dans la liste compte ses remarques non transmises.
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
## Version autonome : un seul fichier HTML
|
|
365
|
+
|
|
366
|
+
La même interface existe en un fichier HTML unique, à ouvrir directement dans le navigateur : ni serveur, ni agent MCP, ni connexion réseau. Les pages naissent d'un assistant conversationnel (ChatGPT ou autre) et s'échangent avec lui par fichiers JSON.
|
|
367
|
+
|
|
368
|
+
```bash
|
|
369
|
+
npm run build:standalone # produit gui/dist-standalone/mcp-design.html
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Le fichier embarque tout : le code, les 1 791 icônes, la police Inter (le canvas, les captures et le récap ont donc le même rendu sur toutes les machines).
|
|
373
|
+
|
|
374
|
+
### Le circuit
|
|
375
|
+
|
|
376
|
+
1. **Projet › Copier le prompt de création**, coller dans ChatGPT, puis décrire ce qu'on veut. Le prompt décrit le format d'une page, les types d'éléments et leurs champs, les règles de structure, les icônes et un exemple complet.
|
|
377
|
+
2. ChatGPT renvoie **une page par réponse**, en JSON. **Projet › Importer une page** : coller la réponse (entière, ou seulement son bloc JSON) ou choisir un fichier `.json`. La page est vérifiée en entier avant tout changement ; à la moindre erreur, rien n'est importé et chaque problème est listé, dans un message que **Copier pour ChatGPT** permet de lui renvoyer tel quel. Une page dont l'`id` existe déjà la remplace ; la boîte de dialogue prévient alors des modifications faites à la main qui seraient perdues.
|
|
378
|
+
3. Retoucher la page à la main, et poser des remarques comme dans la version serveur (mode Remarques, **Terminer les retours**).
|
|
379
|
+
4. **Télécharger le récap** (à la fin de « Terminer les retours », ou dans la page « Retours ») : une archive `recap-retour-N.zip` qui réunit un fichier `recap-retour-N.md` et des images — une vue de chaque page concernée, remarques entourées et numérotées, et les captures des remarques. Le texte reprend les remarques, le JSON courant de chaque page et un rappel du format. **Copier le texte** le met dans le presse-papiers à la place du fichier.
|
|
380
|
+
5. ChatGPT renvoie les pages corrigées, une par réponse, à importer comme à l'étape 2. Chacune liste dans `processed` les remarques traitées, avec une note : elles passent à *Traitée par l'agent* dans la page « Retours ».
|
|
381
|
+
|
|
382
|
+
### Format d'une page
|
|
383
|
+
|
|
384
|
+
```json
|
|
385
|
+
{
|
|
386
|
+
"format": "mcp-design-page",
|
|
387
|
+
"version": 1,
|
|
388
|
+
"page": { "id": "home", "name": "Home", "group": null, "width": 1440, "height": 900, "background": "#ffffff", "nodes": [] },
|
|
389
|
+
"processed": [{ "remark": "rm_xxxxxxxx", "note": "Logo agrandi" }]
|
|
390
|
+
}
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
Les éléments de `nodes` suivent le modèle des outils MCP (liste plate, `parentId`, coordonnées relatives au parent) et passent par la même validation que `add_nodes`. L'import est plus strict : un champ qui ne s'applique pas au type de l'élément est refusé, et les éléments `image` n'existent pas dans cette version (pas de médiathèque). La taille des groupes et des instances, dérivée de leur contenu, n'apparaît pas dans le JSON transmis et n'a pas à être fournie.
|
|
394
|
+
|
|
395
|
+
### Données
|
|
396
|
+
|
|
397
|
+
Le projet (pages, remarques, captures) vit dans le navigateur, qui en garde une copie (IndexedDB) relue à l'ouverture : recharger l'onglet ne perd rien. Cette copie dépend du navigateur et de son profil ; **le fichier exporté fait foi**. **Projet › Exporter le projet** télécharge `mcp-design-projet-AAAA-MM-JJ.json`, **Ouvrir un projet…** le recharge, après confirmation, à la place du projet courant. Le bouton **Projet** porte un point orange tant que des modifications n'ont pas été exportées, rouge si la copie du navigateur ne peut pas être lue ou écrite.
|
|
398
|
+
|
|
399
|
+
Le filtre **À synchroniser** retient les pages modifiées à la main depuis le dernier échange avec ChatGPT : import de la page, ou récap qui la contient.
|
|
400
|
+
|
|
401
|
+
### Ce qui diffère de la version serveur
|
|
402
|
+
|
|
403
|
+
- Pas d'agent MCP : ni outils, ni `get_changes`, ni rafraîchissement périodique. L'assistant ne voit que ce qu'on lui transmet.
|
|
404
|
+
- Pas d'images posées sur la planche.
|
|
405
|
+
- Une seule personne, un seul navigateur : le projet circule par fichier.
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
## Architecture
|
|
410
|
+
|
|
411
|
+
Deux processus, conformément au standard de l'écosystème (`@imenam/mcp-gui-interface`) :
|
|
412
|
+
|
|
413
|
+
- **MCP Master** (`src/index.ts`) — protocole JSON-RPC sur stdio, stockage des designs, cycle de vie du worker via `GuiLauncher`.
|
|
414
|
+
- **GUI Worker** (`src/gui-worker.ts`) — serveur Hono, enregistrement auprès du proxy via `ProxyClient`, API REST, sert la SPA React.
|
|
415
|
+
|
|
416
|
+
Les deux communiquent par IPC. Les évènements que le master pousse au worker (`DESIGNS_UPDATED`, `DESIGN_UPDATED`, `REMARKS_UPDATED`) entrent dans un journal en mémoire, numéroté et limité aux 200 dernières entrées. La GUI le relit toutes les 3 s par `GET /api/changes?since=<seq>&epoch=<epoch>`, qui répond immédiatement : aucune connexion ne reste ouverte, ce qui laisse libres les connexions que le navigateur accorde à l'origine du proxy, partagée par toutes les GUI. La réponse vaut `{ epoch, seq, reset: false, events }` avec les évènements postérieurs à `since`, ou `{ epoch, seq, reset: true }` quand la position du client n'est pas exploitable (premier appel, worker redémarré, entrées déjà sorties du journal) : la GUI recharge alors la liste des pages, les remarques et la page ouverte. Un `since` qui n'est pas un entier positif ou nul est refusé (400). Le worker se termine proprement quand le master disparaît (canal IPC fermé, PID parent absent) et quand une autre instance est déjà enregistrée (HTTP 409), ce qui évite les GUI orphelines.
|
|
417
|
+
|
|
418
|
+
`shared/feedback.ts` porte le modèle des remarques, la détection des éléments d'une zone et la mise en forme d'un retour ; le serveur et la GUI l'importent tous deux, ce qui garantit que l'aperçu affiché est le texte transmis. `shared/render.ts` contient le rendu SVG d'un élément, `shared/tree.ts` la résolution de la hiérarchie en coordonnées document, `shared/instances.ts` l'expansion des instances en groupes, `shared/node-defaults.ts` les valeurs par défaut et `shared/icons/` la bibliothèque d'icônes. Tous sont importés **à la fois** par l'exporteur côté serveur et par le canvas côté navigateur : ce que l'utilisateur voit est exactement ce que `export_design` — et donc `screenshot_design`, qui rasterise ce même SVG via resvg — produit.
|
|
419
|
+
|
|
420
|
+
Les invariants de hiérarchie — parent existant et de type `group`, absence de cycle (y compris dans le graphe composant/instance), taille des groupes et des instances recalculée, ordre préfixe — sont appliqués par `mutatedDesign` (`src/core/design-model.ts`), donc sur **tout** chemin d'écriture : outil MCP, sauvegarde depuis la GUI, import dans la version autonome.
|
|
421
|
+
|
|
422
|
+
`src/core/design-model.ts` et `src/core/feedback-model.ts` portent les règles des pages et des remarques indépendamment de leur stockage : `DesignStore` et `FeedbackStore` les appliquent à des fichiers, la version autonome à son projet en mémoire. `src/core/exchange.ts` porte ce qui est propre à la version autonome et reste testable sans navigateur : prompt, lecture d'une page, récap, export du projet.
|
|
423
|
+
|
|
424
|
+
La version autonome (`gui/vite.standalone.config.ts`) bâtit la même application React depuis `gui/mcp-design.html`, en substituant deux modules : `utils/api.ts` (appels HTTP au worker) par `standalone/api.ts` (projet en mémoire), et `hooks/useServerChanges.ts` par un simple chargement initial. Ce qui distingue les deux hôtes — textes qui nomment le destinataire des retours, menu **Projet**, actions du récap — passe par `gui/src/host.tsx`.
|
|
425
|
+
|
|
426
|
+
**Limite assumée de cette version** : un groupe se déplace, se pivote et change d'opacité, mais ne se redimensionne pas — mettre son contenu à l'échelle demanderait de propager un facteur dans tout le sous-arbre, ce qui dépasse le cadre d'un premier jet sobre.
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
430
|
+
## Développement
|
|
431
|
+
|
|
432
|
+
```bash
|
|
433
|
+
npm install
|
|
434
|
+
npm run build # backend (tsc) + GUI (vite)
|
|
435
|
+
npm run build:standalone # version autonome : gui/dist-standalone/mcp-design.html
|
|
436
|
+
npm test # suite complète
|
|
437
|
+
npm run generate-icons # refige shared/icons/ après une mise à jour de lucide-static
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
La suite comprend :
|
|
441
|
+
|
|
442
|
+
- des tests unitaires du store, de la fabrique d'éléments, du moteur de diff et de l'export ;
|
|
443
|
+
- des tests des outils MCP appelés directement ;
|
|
444
|
+
- des tests des échanges de la version autonome (`test/exchange.test.ts`) : l'exemple du prompt s'importe tel quel, une page exportée se réimporte à l'identique, chaque erreur est signalée sans rien importer, les remarques traitées sont vérifiées puis marquées ;
|
|
445
|
+
- des tests d'exposition réseau (`test/network-exposure.test.ts`) qui lancent le worker GUI en mode `APP_PORT` et vérifient l'écoute sur `127.0.0.1` seul, le refus d'un `Host` étranger et des modifications cross-site, et l'acceptation des requêtes relayées par le proxy ;
|
|
446
|
+
- une suite de bout en bout (`test/e2e.test.ts`) qui lance un vrai serveur MCP sur stdio, attend l'enregistrement de son worker auprès du proxy, puis vérifie l'aller-retour complet agent → GUI → agent.
|
|
447
|
+
|
|
448
|
+
La suite de bout en bout a besoin du proxy (`http://localhost:3000`, ou `E2E_PROXY_URL`). S'il est absent, elle est ignorée plutôt qu'en échec.
|
|
449
|
+
|
|
450
|
+
Pour inspecter le worker sans traverser le proxy, ajoutez `?__direct=1` à son port local : les assets ne sont alors pas préfixés par le chemin de montage.
|
|
451
|
+
|
|
452
|
+
### Méta-configurations du proxy
|
|
453
|
+
|
|
454
|
+
`mcp-design` n'expose pas `GET /api/configurations` : il n'a pas de configuration métier (ni token, ni identifiant de ressource) qui justifierait des profils. Le proxy ignore simplement les MCPs qui n'implémentent pas ces routes.
|