@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.
- package/README.md +67 -5
- package/dist/shared/feedback.d.ts +128 -0
- package/dist/shared/feedback.js +165 -0
- package/dist/shared/node-defaults.js +6 -1
- package/dist/shared/node-fields.d.ts +2 -0
- package/dist/shared/node-fields.js +5 -1
- package/dist/shared/render.d.ts +30 -5
- package/dist/shared/render.js +58 -11
- package/dist/shared/schemas/design.schema.d.ts +36 -1
- package/dist/shared/schemas/design.schema.js +10 -1
- package/dist/shared/schemas/feedback.schema.d.ts +191 -0
- package/dist/shared/schemas/feedback.schema.js +87 -0
- package/dist/shared/tree.d.ts +2 -0
- package/dist/shared/tree.js +4 -0
- package/dist/src/core/design-store.d.ts +4 -5
- package/dist/src/core/design-store.js +7 -56
- package/dist/src/core/export.d.ts +2 -1
- package/dist/src/core/export.js +35 -7
- package/dist/src/core/feedback-store.d.ts +73 -0
- package/dist/src/core/feedback-store.js +308 -0
- package/dist/src/core/file-lock.d.ts +8 -0
- package/dist/src/core/file-lock.js +61 -0
- package/dist/src/core/image-store.d.ts +45 -0
- package/dist/src/core/image-store.js +175 -0
- package/dist/src/core/node-factory.js +16 -0
- package/dist/src/core/screenshot.d.ts +6 -0
- package/dist/src/core/screenshot.js +32 -3
- package/dist/src/gui-worker.js +113 -1
- package/dist/src/index.js +231 -3
- package/dist/src/tools/add-image.d.ts +3 -0
- package/dist/src/tools/add-image.js +65 -0
- package/dist/src/tools/add-nodes.js +2 -0
- package/dist/src/tools/delete-design.d.ts +3 -1
- package/dist/src/tools/delete-design.js +4 -1
- package/dist/src/tools/export-design.js +1 -1
- package/dist/src/tools/get-feedback.d.ts +3 -0
- package/dist/src/tools/get-feedback.js +62 -0
- package/dist/src/tools/mark-feedback-processed.d.ts +8 -0
- package/dist/src/tools/mark-feedback-processed.js +29 -0
- package/dist/src/tools/screenshot-design.js +1 -0
- package/dist/src/tools/update-nodes.js +4 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +16 -2
- package/gui/dist/assets/index-DNUHDrGi.css +1 -0
- package/gui/dist/assets/index-uk5sDkVA.js +15 -0
- package/gui/dist/assets/{search-CSwiX0lv.js → search-jSqY2fPS.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +2 -3
- package/gui/dist/assets/index-DBZNXgF_.js +0 -10
- package/gui/dist/assets/index-PQqxUQLx.css +0 -1
- package/scripts/generate-version.js +0 -20
package/dist/shared/render.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/shared/tree.d.ts
CHANGED
|
@@ -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"];
|
package/dist/shared/tree.js
CHANGED
|
@@ -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;
|