@imenam/mcp-design 1.0.10 → 1.0.12

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 (51) hide show
  1. package/README.md +67 -5
  2. package/dist/shared/feedback.d.ts +128 -0
  3. package/dist/shared/feedback.js +165 -0
  4. package/dist/shared/node-defaults.js +6 -1
  5. package/dist/shared/node-fields.d.ts +2 -0
  6. package/dist/shared/node-fields.js +5 -1
  7. package/dist/shared/render.d.ts +30 -5
  8. package/dist/shared/render.js +58 -11
  9. package/dist/shared/schemas/design.schema.d.ts +36 -1
  10. package/dist/shared/schemas/design.schema.js +10 -1
  11. package/dist/shared/schemas/feedback.schema.d.ts +191 -0
  12. package/dist/shared/schemas/feedback.schema.js +87 -0
  13. package/dist/shared/tree.d.ts +2 -0
  14. package/dist/shared/tree.js +4 -0
  15. package/dist/src/core/design-store.d.ts +4 -5
  16. package/dist/src/core/design-store.js +7 -56
  17. package/dist/src/core/export.d.ts +2 -1
  18. package/dist/src/core/export.js +35 -7
  19. package/dist/src/core/feedback-store.d.ts +73 -0
  20. package/dist/src/core/feedback-store.js +308 -0
  21. package/dist/src/core/file-lock.d.ts +8 -0
  22. package/dist/src/core/file-lock.js +61 -0
  23. package/dist/src/core/image-store.d.ts +45 -0
  24. package/dist/src/core/image-store.js +175 -0
  25. package/dist/src/core/node-factory.js +16 -0
  26. package/dist/src/core/screenshot.d.ts +6 -0
  27. package/dist/src/core/screenshot.js +32 -3
  28. package/dist/src/gui-worker.js +113 -1
  29. package/dist/src/index.js +231 -3
  30. package/dist/src/tools/add-image.d.ts +3 -0
  31. package/dist/src/tools/add-image.js +65 -0
  32. package/dist/src/tools/add-nodes.js +2 -0
  33. package/dist/src/tools/delete-design.d.ts +3 -1
  34. package/dist/src/tools/delete-design.js +4 -1
  35. package/dist/src/tools/export-design.js +1 -1
  36. package/dist/src/tools/get-feedback.d.ts +3 -0
  37. package/dist/src/tools/get-feedback.js +62 -0
  38. package/dist/src/tools/mark-feedback-processed.d.ts +8 -0
  39. package/dist/src/tools/mark-feedback-processed.js +29 -0
  40. package/dist/src/tools/screenshot-design.js +1 -0
  41. package/dist/src/tools/update-nodes.js +4 -1
  42. package/dist/src/version.d.ts +1 -1
  43. package/dist/src/version.js +16 -2
  44. package/gui/dist/assets/index-DNUHDrGi.css +1 -0
  45. package/gui/dist/assets/index-uk5sDkVA.js +15 -0
  46. package/gui/dist/assets/{search-CSwiX0lv.js → search-jSqY2fPS.js} +1 -1
  47. package/gui/dist/index.html +2 -2
  48. package/package.json +2 -3
  49. package/gui/dist/assets/index-DBZNXgF_.js +0 -10
  50. package/gui/dist/assets/index-PQqxUQLx.css +0 -1
  51. package/scripts/generate-version.js +0 -20
@@ -6,7 +6,7 @@
6
6
  * Ce module ne dépend volontairement d'aucune librairie (pas de zod) pour rester
7
7
  * importable depuis le bundle GUI.
8
8
  */
9
- import { boundsOf, childrenByParent, computeLayout, normalizedBox, spaceOriginOf } from "./tree.js";
9
+ import { boundsOf, boxesIntersect, childrenByParent, computeLayout, normalizedBox, spaceOriginOf, } from "./tree.js";
10
10
  import { expandInstances } from "./instances.js";
11
11
  import { DEFAULT_ICON_STROKE_WIDTH, ICON_GRID, UNKNOWN_ICON_ELEMENTS, iconElements, } from "./icons/index.js";
12
12
  // La géométrie de base vit dans tree.ts (module bas-niveau, sans dépendance vers
@@ -175,6 +175,22 @@ export function iconStyle(node) {
175
175
  "stroke-linejoin": "round",
176
176
  };
177
177
  }
178
+ const IMAGE_ASPECT = {
179
+ cover: "xMidYMid slice",
180
+ contain: "xMidYMid meet",
181
+ fill: "none",
182
+ };
183
+ /** Valeur de `preserveAspectRatio` correspondant au `fit` du noeud. */
184
+ export function imageAspect(node) {
185
+ return IMAGE_ASPECT[node.fit ?? "cover"];
186
+ }
187
+ /** Adresse de l'image du noeud ; echoue si l'appelant n'a pas dit comment la resoudre. */
188
+ export function imageSource(node, options) {
189
+ if (!options.imageHref) {
190
+ throw new Error(`Node "${node.id}" is an image, but this render was given no way to resolve image files.`);
191
+ }
192
+ return options.imageHref(node.image ?? "");
193
+ }
178
194
  function attributesToSvg(attributes) {
179
195
  return Object.entries(attributes)
180
196
  .map(([key, value]) => `${key}="${escapeXml(value)}"`)
@@ -188,7 +204,7 @@ export function iconElementToSvg([tag, attributes]) {
188
204
  * Un groupe ne produit rien par lui-meme : c'est un conteneur, rendu par
189
205
  * `documentToSvg` (serveur) ou par le canvas (navigateur).
190
206
  */
191
- export function nodeToSvg(node) {
207
+ export function nodeToSvg(node, options = {}) {
192
208
  // Un groupe est un conteneur ; une instance n'atteint jamais le rendu, elle
193
209
  // est remplacee par un groupe en amont par `expandInstances`.
194
210
  if (node.type === "group" || node.type === "instance")
@@ -221,6 +237,11 @@ export function nodeToSvg(node) {
221
237
  const body = iconElementsOf(node).map(iconElementToSvg).join("");
222
238
  return `<g transform="${placement}" ${attributesToSvg(iconStyle(node))}${opacityAttr}>${body}</g>`;
223
239
  }
240
+ case "image": {
241
+ return (`<image x="${round(box.x)}" y="${round(box.y)}" width="${round(box.width)}" height="${round(box.height)}"` +
242
+ ` href="${escapeXml(imageSource(node, options))}" preserveAspectRatio="${imageAspect(node)}"` +
243
+ `${opacityAttr}${transformAttr} />`);
244
+ }
224
245
  case "text": {
225
246
  const m = textMetrics(node);
226
247
  const lines = textLines(node);
@@ -238,24 +259,31 @@ export function nodeToSvg(node) {
238
259
  /**
239
260
  * Rend une liste de noeuds frères, en descendant récursivement dans les groupes.
240
261
  * Partagé par le rendu plein document et le rendu cadré sur un seul élément.
262
+ *
263
+ * Avec `frame`, un élément semi-transparent qui ne recoupe pas le cadre est
264
+ * omis : il n'y serait pas visible, et resvg panique — en emportant le
265
+ * processus — sur un élément semi-transparent dessiné entièrement hors du cadre.
241
266
  */
242
- function renderSiblings(nodes, children, layout, depth) {
267
+ function renderSiblings(nodes, children, layout, depth, options, frame) {
243
268
  const pad = " ".repeat(depth + 1);
244
269
  return nodes.flatMap((node) => {
245
- if (node.type !== "group")
246
- return [`${pad}${nodeToSvg(node)}`];
247
270
  const bounds = boundsOf(layout, node.id);
271
+ if (frame && node.opacity < 1 && !boxesIntersect(bounds, frame))
272
+ return [];
273
+ if (node.type !== "group")
274
+ return [`${pad}${nodeToSvg(node, options)}`];
248
275
  const origin = layout.origin.get(node.id) ?? { x: 0, y: 0 };
249
276
  const center = { cx: bounds.x + bounds.width / 2 - origin.x, cy: bounds.y + bounds.height / 2 - origin.y };
250
277
  const opacity = node.opacity >= 1 ? "" : ` opacity="${round(node.opacity)}"`;
251
278
  return [
252
279
  `${pad}<g transform="${groupTransform(node, center)}"${opacity}>`,
253
- ...renderSiblings(children.get(node.id) ?? [], children, layout, depth + 1),
280
+ ...renderSiblings(children.get(node.id) ?? [], children, layout, depth + 1, options, frame),
254
281
  `${pad}</g>`,
255
282
  ];
256
283
  });
257
284
  }
258
- export function documentToSvg(doc) {
285
+ /** Contenu SVG de la planche, en coordonnées document, limité à `frame` s'il est donné. */
286
+ function documentBody(doc, options, frame) {
259
287
  // Les instances sont resolues ici, une fois pour toutes : en dessous, il n'y
260
288
  // a plus que des groupes ordinaires.
261
289
  const nodes = expandInstances(doc.nodes);
@@ -263,13 +291,32 @@ export function documentToSvg(doc) {
263
291
  const children = childrenByParent(nodes);
264
292
  // Un groupe devient un <g> : ses enfants gardent leurs coordonnées locales,
265
293
  // c'est la transformation du conteneur qui les place.
266
- const body = renderSiblings(children.get(null) ?? [], children, layout, 0).join("\n");
294
+ return renderSiblings(children.get(null) ?? [], children, layout, 0, options, frame).join("\n");
295
+ }
296
+ export function documentToSvg(doc, options = {}) {
297
+ const body = documentBody(doc, options);
267
298
  return (`<svg xmlns="http://www.w3.org/2000/svg" width="${doc.width}" height="${doc.height}" ` +
268
299
  `viewBox="0 0 ${doc.width} ${doc.height}">\n` +
269
300
  ` <rect x="0" y="0" width="${doc.width}" height="${doc.height}" fill="${escapeXml(doc.background)}" />\n` +
270
301
  (body ? `${body}\n` : "") +
271
302
  `</svg>`);
272
303
  }
304
+ /**
305
+ * SVG cadré sur une zone rectangulaire de la planche, en coordonnées document.
306
+ * Tout ce qui recoupe la zone y figure, coupé à ses bords ; les éléments
307
+ * semi-transparents qui ne la recoupent pas sont omis.
308
+ */
309
+ export function regionToSvg(doc, region, options = {}) {
310
+ const body = documentBody(doc, options, region);
311
+ const width = round(Math.max(1, region.width));
312
+ const height = round(Math.max(1, region.height));
313
+ return (`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" ` +
314
+ `viewBox="${round(region.x)} ${round(region.y)} ${width} ${height}">\n` +
315
+ ` <rect x="${round(region.x)}" y="${round(region.y)}" width="${width}" height="${height}" ` +
316
+ `fill="${escapeXml(doc.background)}" />\n` +
317
+ (body ? `${body}\n` : "") +
318
+ `</svg>`);
319
+ }
273
320
  /**
274
321
  * SVG cadré sur un seul élément — l'élément et son contenu, rien d'autre.
275
322
  *
@@ -277,7 +324,7 @@ export function documentToSvg(doc) {
277
324
  * position document : le viewBox peut alors être exprimé dans le même repère
278
325
  * que `computeLayout`, donc directement sur la boîte englobante de l'élément.
279
326
  */
280
- export function subtreeToSvg(doc, nodeId, padding = 0) {
327
+ export function subtreeToSvg(doc, nodeId, padding = 0, options = {}) {
281
328
  const nodes = expandInstances(doc.nodes);
282
329
  const node = nodes.find((candidate) => candidate.id === nodeId);
283
330
  if (!node)
@@ -297,8 +344,8 @@ export function subtreeToSvg(doc, nodeId, padding = 0) {
297
344
  // noeud est rendu tel quel, dans le repère de son parent.
298
345
  const origin = node.type === "group" ? (layout.origin.get(nodeId) ?? { x: 0, y: 0 }) : spaceOriginOf(layout, node.parentId);
299
346
  const inner = node.type === "group"
300
- ? renderSiblings([{ ...node, x: 0, y: 0 }], children, layout, 1).join("\n")
301
- : ` ${nodeToSvg(node)}`;
347
+ ? renderSiblings([{ ...node, x: 0, y: 0 }], children, layout, 1, options).join("\n")
348
+ : ` ${nodeToSvg(node, options)}`;
302
349
  return (`<svg xmlns="http://www.w3.org/2000/svg" width="${round(region.width)}" height="${round(region.height)}" ` +
303
350
  `viewBox="${round(region.x)} ${round(region.y)} ${round(region.width)} ${round(region.height)}">\n` +
304
351
  ` <rect x="${round(region.x)}" y="${round(region.y)}" width="${round(region.width)}" ` +
@@ -14,11 +14,12 @@ import { z } from "zod";
14
14
  * stockage reste plat, donc diffable et lisible, tandis que les coordonnees
15
15
  * d'un noeud sont exprimees dans l'espace de son parent.
16
16
  */
17
- export declare const NODE_TYPES: readonly ["rect", "ellipse", "line", "text", "icon", "group", "instance"];
17
+ export declare const NODE_TYPES: readonly ["rect", "ellipse", "line", "text", "icon", "image", "group", "instance"];
18
18
  export declare const NodeTypeSchema: z.ZodEnum<{
19
19
  ellipse: "ellipse";
20
20
  group: "group";
21
21
  icon: "icon";
22
+ image: "image";
22
23
  instance: "instance";
23
24
  line: "line";
24
25
  rect: "rect";
@@ -31,6 +32,12 @@ export declare const TextAlignSchema: z.ZodEnum<{
31
32
  left: "left";
32
33
  right: "right";
33
34
  }>;
35
+ export declare const IMAGE_FITS: readonly ["cover", "contain", "fill"];
36
+ export declare const ImageFitSchema: z.ZodEnum<{
37
+ contain: "contain";
38
+ cover: "cover";
39
+ fill: "fill";
40
+ }>;
34
41
  /**
35
42
  * Un seul schéma pour tous les types de noeuds : les champs non pertinents pour
36
43
  * un type donné sont simplement absents. Un schéma discriminé obligerait l'agent
@@ -42,6 +49,7 @@ export declare const DesignNodeSchema: z.ZodObject<{
42
49
  ellipse: "ellipse";
43
50
  group: "group";
44
51
  icon: "icon";
52
+ image: "image";
45
53
  instance: "instance";
46
54
  line: "line";
47
55
  rect: "rect";
@@ -61,6 +69,12 @@ export declare const DesignNodeSchema: z.ZodObject<{
61
69
  strokeWidth: z.ZodOptional<z.ZodNumber>;
62
70
  radius: z.ZodOptional<z.ZodNumber>;
63
71
  icon: z.ZodOptional<z.ZodString>;
72
+ image: z.ZodOptional<z.ZodString>;
73
+ fit: z.ZodOptional<z.ZodEnum<{
74
+ contain: "contain";
75
+ cover: "cover";
76
+ fill: "fill";
77
+ }>>;
64
78
  text: z.ZodOptional<z.ZodString>;
65
79
  color: z.ZodOptional<z.ZodString>;
66
80
  fontSize: z.ZodOptional<z.ZodNumber>;
@@ -88,6 +102,7 @@ export declare const BaselineSchema: z.ZodObject<{
88
102
  ellipse: "ellipse";
89
103
  group: "group";
90
104
  icon: "icon";
105
+ image: "image";
91
106
  instance: "instance";
92
107
  line: "line";
93
108
  rect: "rect";
@@ -107,6 +122,12 @@ export declare const BaselineSchema: z.ZodObject<{
107
122
  strokeWidth: z.ZodOptional<z.ZodNumber>;
108
123
  radius: z.ZodOptional<z.ZodNumber>;
109
124
  icon: z.ZodOptional<z.ZodString>;
125
+ image: z.ZodOptional<z.ZodString>;
126
+ fit: z.ZodOptional<z.ZodEnum<{
127
+ contain: "contain";
128
+ cover: "cover";
129
+ fill: "fill";
130
+ }>>;
110
131
  text: z.ZodOptional<z.ZodString>;
111
132
  color: z.ZodOptional<z.ZodString>;
112
133
  fontSize: z.ZodOptional<z.ZodNumber>;
@@ -148,6 +169,7 @@ export declare const DesignSchema: z.ZodObject<{
148
169
  ellipse: "ellipse";
149
170
  group: "group";
150
171
  icon: "icon";
172
+ image: "image";
151
173
  instance: "instance";
152
174
  line: "line";
153
175
  rect: "rect";
@@ -167,6 +189,12 @@ export declare const DesignSchema: z.ZodObject<{
167
189
  strokeWidth: z.ZodOptional<z.ZodNumber>;
168
190
  radius: z.ZodOptional<z.ZodNumber>;
169
191
  icon: z.ZodOptional<z.ZodString>;
192
+ image: z.ZodOptional<z.ZodString>;
193
+ fit: z.ZodOptional<z.ZodEnum<{
194
+ contain: "contain";
195
+ cover: "cover";
196
+ fill: "fill";
197
+ }>>;
170
198
  text: z.ZodOptional<z.ZodString>;
171
199
  color: z.ZodOptional<z.ZodString>;
172
200
  fontSize: z.ZodOptional<z.ZodNumber>;
@@ -199,6 +227,7 @@ export declare const DesignSchema: z.ZodObject<{
199
227
  ellipse: "ellipse";
200
228
  group: "group";
201
229
  icon: "icon";
230
+ image: "image";
202
231
  instance: "instance";
203
232
  line: "line";
204
233
  rect: "rect";
@@ -218,6 +247,12 @@ export declare const DesignSchema: z.ZodObject<{
218
247
  strokeWidth: z.ZodOptional<z.ZodNumber>;
219
248
  radius: z.ZodOptional<z.ZodNumber>;
220
249
  icon: z.ZodOptional<z.ZodString>;
250
+ image: z.ZodOptional<z.ZodString>;
251
+ fit: z.ZodOptional<z.ZodEnum<{
252
+ contain: "contain";
253
+ cover: "cover";
254
+ fill: "fill";
255
+ }>>;
221
256
  text: z.ZodOptional<z.ZodString>;
222
257
  color: z.ZodOptional<z.ZodString>;
223
258
  fontSize: z.ZodOptional<z.ZodNumber>;
@@ -16,10 +16,12 @@ import { NODE_FIELD_DOCS } from "../node-fields.js";
16
16
  * stockage reste plat, donc diffable et lisible, tandis que les coordonnees
17
17
  * d'un noeud sont exprimees dans l'espace de son parent.
18
18
  */
19
- export const NODE_TYPES = ["rect", "ellipse", "line", "text", "icon", "group", "instance"];
19
+ export const NODE_TYPES = ["rect", "ellipse", "line", "text", "icon", "image", "group", "instance"];
20
20
  export const NodeTypeSchema = z.enum(NODE_TYPES);
21
21
  export const TEXT_ALIGNS = ["left", "center", "right"];
22
22
  export const TextAlignSchema = z.enum(TEXT_ALIGNS);
23
+ export const IMAGE_FITS = ["cover", "contain", "fill"];
24
+ export const ImageFitSchema = z.enum(IMAGE_FITS);
23
25
  /**
24
26
  * Un seul schéma pour tous les types de noeuds : les champs non pertinents pour
25
27
  * un type donné sont simplement absents. Un schéma discriminé obligerait l'agent
@@ -57,6 +59,13 @@ export const DesignNodeSchema = z.object({
57
59
  * se redessiner, et un agent qui relit le design sait ce qu'elle represente.
58
60
  */
59
61
  icon: z.string().describe(NODE_FIELD_DOCS.icon).optional(),
62
+ /**
63
+ * Image — le nom du fichier dans la mediatheque du dossier de donnees, jamais
64
+ * ses octets : le design reste un JSON lisible, et une meme image posee
65
+ * plusieurs fois n'est stockee qu'une fois.
66
+ */
67
+ image: z.string().describe(NODE_FIELD_DOCS.image).optional(),
68
+ fit: ImageFitSchema.describe(NODE_FIELD_DOCS.fit).optional(),
60
69
  // Texte — fontSize et lineHeight sont tous deux en PIXELS.
61
70
  text: z.string().describe(NODE_FIELD_DOCS.text).optional(),
62
71
  color: z.string().describe(NODE_FIELD_DOCS.color).optional(),
@@ -0,0 +1,191 @@
1
+ import { z } from "zod";
2
+ export declare const RemarkTargetSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
3
+ kind: z.ZodLiteral<"page">;
4
+ }, z.core.$strip>, z.ZodObject<{
5
+ kind: z.ZodLiteral<"node">;
6
+ element: z.ZodObject<{
7
+ id: z.ZodString;
8
+ name: z.ZodString;
9
+ type: z.ZodEnum<{
10
+ ellipse: "ellipse";
11
+ group: "group";
12
+ icon: "icon";
13
+ image: "image";
14
+ instance: "instance";
15
+ line: "line";
16
+ rect: "rect";
17
+ text: "text";
18
+ }>;
19
+ }, z.core.$strip>;
20
+ path: z.ZodArray<z.ZodString>;
21
+ box: z.ZodObject<{
22
+ x: z.ZodNumber;
23
+ y: z.ZodNumber;
24
+ width: z.ZodNumber;
25
+ height: z.ZodNumber;
26
+ }, z.core.$strip>;
27
+ }, z.core.$strip>, z.ZodObject<{
28
+ kind: z.ZodLiteral<"area">;
29
+ box: z.ZodObject<{
30
+ x: z.ZodNumber;
31
+ y: z.ZodNumber;
32
+ width: z.ZodNumber;
33
+ height: z.ZodNumber;
34
+ }, z.core.$strip>;
35
+ elements: z.ZodArray<z.ZodObject<{
36
+ id: z.ZodString;
37
+ name: z.ZodString;
38
+ type: z.ZodEnum<{
39
+ ellipse: "ellipse";
40
+ group: "group";
41
+ icon: "icon";
42
+ image: "image";
43
+ instance: "instance";
44
+ line: "line";
45
+ rect: "rect";
46
+ text: "text";
47
+ }>;
48
+ }, z.core.$strip>>;
49
+ }, z.core.$strip>], "kind">;
50
+ export declare const FeedbackFileSchema: z.ZodObject<{
51
+ version: z.ZodLiteral<2>;
52
+ remarks: z.ZodArray<z.ZodObject<{
53
+ id: z.ZodString;
54
+ designId: z.ZodString;
55
+ number: z.ZodNullable<z.ZodNumber>;
56
+ target: z.ZodDiscriminatedUnion<[z.ZodObject<{
57
+ kind: z.ZodLiteral<"page">;
58
+ }, z.core.$strip>, z.ZodObject<{
59
+ kind: z.ZodLiteral<"node">;
60
+ element: z.ZodObject<{
61
+ id: z.ZodString;
62
+ name: z.ZodString;
63
+ type: z.ZodEnum<{
64
+ ellipse: "ellipse";
65
+ group: "group";
66
+ icon: "icon";
67
+ image: "image";
68
+ instance: "instance";
69
+ line: "line";
70
+ rect: "rect";
71
+ text: "text";
72
+ }>;
73
+ }, z.core.$strip>;
74
+ path: z.ZodArray<z.ZodString>;
75
+ box: z.ZodObject<{
76
+ x: z.ZodNumber;
77
+ y: z.ZodNumber;
78
+ width: z.ZodNumber;
79
+ height: z.ZodNumber;
80
+ }, z.core.$strip>;
81
+ }, z.core.$strip>, z.ZodObject<{
82
+ kind: z.ZodLiteral<"area">;
83
+ box: z.ZodObject<{
84
+ x: z.ZodNumber;
85
+ y: z.ZodNumber;
86
+ width: z.ZodNumber;
87
+ height: z.ZodNumber;
88
+ }, z.core.$strip>;
89
+ elements: z.ZodArray<z.ZodObject<{
90
+ id: z.ZodString;
91
+ name: z.ZodString;
92
+ type: z.ZodEnum<{
93
+ ellipse: "ellipse";
94
+ group: "group";
95
+ icon: "icon";
96
+ image: "image";
97
+ instance: "instance";
98
+ line: "line";
99
+ rect: "rect";
100
+ text: "text";
101
+ }>;
102
+ }, z.core.$strip>>;
103
+ }, z.core.$strip>], "kind">;
104
+ text: z.ZodString;
105
+ capture: z.ZodNullable<z.ZodString>;
106
+ status: z.ZodEnum<{
107
+ draft: "draft";
108
+ sent: "sent";
109
+ }>;
110
+ round: z.ZodNullable<z.ZodNumber>;
111
+ createdAt: z.ZodString;
112
+ updatedAt: z.ZodString;
113
+ }, z.core.$strip>>;
114
+ rounds: z.ZodArray<z.ZodObject<{
115
+ number: z.ZodNumber;
116
+ title: z.ZodString;
117
+ submittedAt: z.ZodString;
118
+ remarks: z.ZodArray<z.ZodObject<{
119
+ id: z.ZodString;
120
+ designId: z.ZodString;
121
+ designName: z.ZodString;
122
+ number: z.ZodNullable<z.ZodNumber>;
123
+ target: z.ZodDiscriminatedUnion<[z.ZodObject<{
124
+ kind: z.ZodLiteral<"page">;
125
+ }, z.core.$strip>, z.ZodObject<{
126
+ kind: z.ZodLiteral<"node">;
127
+ element: z.ZodObject<{
128
+ id: z.ZodString;
129
+ name: z.ZodString;
130
+ type: z.ZodEnum<{
131
+ ellipse: "ellipse";
132
+ group: "group";
133
+ icon: "icon";
134
+ image: "image";
135
+ instance: "instance";
136
+ line: "line";
137
+ rect: "rect";
138
+ text: "text";
139
+ }>;
140
+ }, z.core.$strip>;
141
+ path: z.ZodArray<z.ZodString>;
142
+ box: z.ZodObject<{
143
+ x: z.ZodNumber;
144
+ y: z.ZodNumber;
145
+ width: z.ZodNumber;
146
+ height: z.ZodNumber;
147
+ }, z.core.$strip>;
148
+ }, z.core.$strip>, z.ZodObject<{
149
+ kind: z.ZodLiteral<"area">;
150
+ box: z.ZodObject<{
151
+ x: z.ZodNumber;
152
+ y: z.ZodNumber;
153
+ width: z.ZodNumber;
154
+ height: z.ZodNumber;
155
+ }, z.core.$strip>;
156
+ elements: z.ZodArray<z.ZodObject<{
157
+ id: z.ZodString;
158
+ name: z.ZodString;
159
+ type: z.ZodEnum<{
160
+ ellipse: "ellipse";
161
+ group: "group";
162
+ icon: "icon";
163
+ image: "image";
164
+ instance: "instance";
165
+ line: "line";
166
+ rect: "rect";
167
+ text: "text";
168
+ }>;
169
+ }, z.core.$strip>>;
170
+ }, z.core.$strip>], "kind">;
171
+ text: z.ZodString;
172
+ capture: z.ZodNullable<z.ZodString>;
173
+ processed: z.ZodNullable<z.ZodObject<{
174
+ at: z.ZodString;
175
+ by: z.ZodEnum<{
176
+ agent: "agent";
177
+ user: "user";
178
+ }>;
179
+ note: z.ZodNullable<z.ZodString>;
180
+ }, z.core.$strip>>;
181
+ }, z.core.$strip>>;
182
+ }, z.core.$strip>>;
183
+ lastNumbers: z.ZodRecord<z.ZodString, z.ZodNumber>;
184
+ }, z.core.$strip>;
185
+ export type FeedbackFile = z.infer<typeof FeedbackFileSchema>;
186
+ /**
187
+ * Met un fichier de version 1 au format courant : chaque retour reçoit le nom
188
+ * proposé par défaut, et aucune remarque n'est encore traitée. Renvoie null
189
+ * pour un fichier qui n'est pas en version 1.
190
+ */
191
+ export declare function migrateFeedbackFile(raw: unknown, defaultTitle: (number: number) => string): unknown | null;
@@ -0,0 +1,87 @@
1
+ import { z } from "zod";
2
+ import { NODE_TYPES } from "./design.schema.js";
3
+ /**
4
+ * Fichier des remarques : `<dataDir>/feedback/remarks.json`.
5
+ *
6
+ * Il tient les remarques en cours (brouillons et transmises) et la liste des
7
+ * retours transmis, chacun figé au moment de l'envoi, hormis le suivi de son
8
+ * traitement. Les types TypeScript vivent dans `shared/feedback.ts`,
9
+ * importable par la GUI sans zod.
10
+ */
11
+ const BoxSchema = z.object({ x: z.number(), y: z.number(), width: z.number(), height: z.number() });
12
+ const RemarkElementSchema = z.object({ id: z.string(), name: z.string(), type: z.enum(NODE_TYPES) });
13
+ export const RemarkTargetSchema = z.discriminatedUnion("kind", [
14
+ z.object({ kind: z.literal("page") }),
15
+ z.object({ kind: z.literal("node"), element: RemarkElementSchema, path: z.array(z.string()), box: BoxSchema }),
16
+ z.object({ kind: z.literal("area"), box: BoxSchema, elements: z.array(RemarkElementSchema) }),
17
+ ]);
18
+ const RemarkSchema = z.object({
19
+ id: z.string(),
20
+ designId: z.string(),
21
+ number: z.number().int().positive().nullable(),
22
+ target: RemarkTargetSchema,
23
+ text: z.string().min(1),
24
+ capture: z.string().nullable(),
25
+ status: z.enum(["draft", "sent"]),
26
+ round: z.number().int().positive().nullable(),
27
+ createdAt: z.string(),
28
+ updatedAt: z.string(),
29
+ });
30
+ const RemarkProcessingSchema = z.object({
31
+ at: z.string(),
32
+ by: z.enum(["agent", "user"]),
33
+ note: z.string().nullable(),
34
+ });
35
+ const RoundRemarkSchema = z.object({
36
+ id: z.string(),
37
+ designId: z.string(),
38
+ designName: z.string(),
39
+ number: z.number().int().positive().nullable(),
40
+ target: RemarkTargetSchema,
41
+ text: z.string().min(1),
42
+ capture: z.string().nullable(),
43
+ processed: RemarkProcessingSchema.nullable(),
44
+ });
45
+ const FeedbackRoundSchema = z.object({
46
+ number: z.number().int().positive(),
47
+ title: z.string().min(1),
48
+ submittedAt: z.string(),
49
+ remarks: z.array(RoundRemarkSchema),
50
+ });
51
+ export const FeedbackFileSchema = z.object({
52
+ version: z.literal(2),
53
+ remarks: z.array(RemarkSchema),
54
+ rounds: z.array(FeedbackRoundSchema),
55
+ /** Dernier numéro de pastille attribué par design : un numéro supprimé n'est jamais réutilisé. */
56
+ lastNumbers: z.record(z.string(), z.number().int().nonnegative()),
57
+ });
58
+ /** Version 1 : des retours sans nom, et des remarques sans suivi de traitement. */
59
+ const FeedbackFileV1Schema = z.object({
60
+ version: z.literal(1),
61
+ remarks: z.array(RemarkSchema),
62
+ rounds: z.array(z.object({
63
+ number: z.number().int().positive(),
64
+ submittedAt: z.string(),
65
+ remarks: z.array(RoundRemarkSchema.omit({ processed: true })),
66
+ })),
67
+ lastNumbers: z.record(z.string(), z.number().int().nonnegative()),
68
+ });
69
+ /**
70
+ * Met un fichier de version 1 au format courant : chaque retour reçoit le nom
71
+ * proposé par défaut, et aucune remarque n'est encore traitée. Renvoie null
72
+ * pour un fichier qui n'est pas en version 1.
73
+ */
74
+ export function migrateFeedbackFile(raw, defaultTitle) {
75
+ const v1 = FeedbackFileV1Schema.safeParse(raw);
76
+ if (!v1.success)
77
+ return null;
78
+ return {
79
+ ...v1.data,
80
+ version: 2,
81
+ rounds: v1.data.rounds.map((round) => ({
82
+ ...round,
83
+ title: defaultTitle(round.number),
84
+ remarks: round.remarks.map((remark) => ({ ...remark, processed: null })),
85
+ })),
86
+ };
87
+ }
@@ -66,6 +66,8 @@ export declare function computeLayout(nodes: RenderNode[]): Layout;
66
66
  /** Origine de l'espace de coordonnées dans lequel vit un élément. */
67
67
  export declare function spaceOriginOf(layout: Layout, parentId: string | null): Point;
68
68
  export declare function boundsOf(layout: Layout, id: string): Box;
69
+ /** Vrai si les deux boîtes ont une surface commune ; un simple contact ne compte pas. */
70
+ export declare function boxesIntersect(a: Box, b: Box): boolean;
69
71
  /** Boîte englobante commune à plusieurs éléments, en coordonnées document. */
70
72
  export declare function unionBoundsOf(layout: Layout, ids: string[]): Box | null;
71
73
  export declare const REORDER_DIRECTIONS: readonly ["front", "back", "forward", "backward"];
@@ -140,6 +140,10 @@ export function spaceOriginOf(layout, parentId) {
140
140
  export function boundsOf(layout, id) {
141
141
  return layout.bounds.get(id) ?? { x: 0, y: 0, width: 0, height: 0 };
142
142
  }
143
+ /** Vrai si les deux boîtes ont une surface commune ; un simple contact ne compte pas. */
144
+ export function boxesIntersect(a, b) {
145
+ return a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height;
146
+ }
143
147
  /** Boîte englobante commune à plusieurs éléments, en coordonnées document. */
144
148
  export function unionBoundsOf(layout, ids) {
145
149
  return unionBox(ids.map((id) => layout.bounds.get(id)).filter((box) => box !== undefined));
@@ -1,4 +1,5 @@
1
1
  import { type Design, type DesignSummary } from "../../shared/schemas/design.schema.js";
2
+ import { ImageStore } from "./image-store.js";
2
3
  export interface CreateDesignInput {
3
4
  name: string;
4
5
  width?: number;
@@ -24,6 +25,8 @@ export interface MutateOptions {
24
25
  export declare class DesignStore {
25
26
  private readonly dataDir;
26
27
  private readonly designsDir;
28
+ /** Images posées sur les planches, partagées par tous les designs du dossier de données. */
29
+ readonly images: ImageStore;
27
30
  /**
28
31
  * Cache mémoire : la GUI sauvegarde à chaque geste, relire le disque à chaque
29
32
  * fois serait inutile. Chaque entrée retient la signature du fichier au moment
@@ -38,11 +41,7 @@ export declare class DesignStore {
38
41
  private stampOf;
39
42
  private remember;
40
43
  private write;
41
- /**
42
- * Sérialise le cycle lire-modifier-écrire entre processus. `wx` échoue si le
43
- * fichier existe déjà : c'est l'opération atomique qui départage deux
44
- * écrivains concurrents. Non réentrant — ne jamais appeler depuis `fn`.
45
- */
44
+ /** Verrou inter-processus du fichier d'un design. Non réentrant — ne jamais appeler depuis `fn`. */
46
45
  private withLock;
47
46
  /** Identifiant lisible dérivé du nom, suffixé seulement en cas de collision. */
48
47
  private allocateId;