living-documentation 7.43.0 → 7.45.0
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/dist/src/frontend/diagram/custom-shapes.js +11 -3
- package/dist/src/frontend/diagram/main.js +3 -1
- package/dist/src/frontend/diagram/network.js +1000 -497
- package/dist/src/frontend/diagram/node-panel.js +47 -0
- package/dist/src/frontend/diagram/node-rendering.js +117 -23
- package/dist/src/frontend/diagram/persistence.js +1 -0
- package/dist/src/frontend/diagram/ports.js +7 -5
- package/dist/src/frontend/diagram/selection-overlay.js +64 -13
- package/dist/src/frontend/diagram.html +34 -0
- package/dist/src/frontend/i18n/en.json +14 -1
- package/dist/src/frontend/i18n/fr.json +14 -1
- package/dist/src/frontend/shape-editor.html +385 -165
- package/dist/src/routes/shape-libraries.d.ts +1 -1
- package/dist/src/routes/shape-libraries.d.ts.map +1 -1
- package/dist/src/routes/shape-libraries.js +46 -26
- package/dist/src/routes/shape-libraries.js.map +1 -1
- package/package.json +1 -1
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Shape editor — Living Documentation</title>
|
|
7
7
|
<script src="https://cdn.tailwindcss.com?plugins=typography"></script>
|
|
8
|
+
<script src="/i18n.js"></script>
|
|
8
9
|
<script>
|
|
9
10
|
tailwind.config = { darkMode: "class", theme: { extend: {} } };
|
|
10
11
|
</script>
|
|
@@ -22,18 +23,24 @@
|
|
|
22
23
|
background: #fff;
|
|
23
24
|
color: #374151;
|
|
24
25
|
}
|
|
25
|
-
.btn:hover {
|
|
26
|
+
.btn:hover {
|
|
27
|
+
background: #f3f4f6;
|
|
28
|
+
}
|
|
26
29
|
.btn-primary {
|
|
27
30
|
border-color: #2563eb;
|
|
28
31
|
background: #2563eb;
|
|
29
32
|
color: #fff;
|
|
30
33
|
}
|
|
31
|
-
.btn-primary:hover {
|
|
34
|
+
.btn-primary:hover {
|
|
35
|
+
background: #1d4ed8;
|
|
36
|
+
}
|
|
32
37
|
.btn-danger {
|
|
33
38
|
border-color: #fecaca;
|
|
34
39
|
color: #b91c1c;
|
|
35
40
|
}
|
|
36
|
-
.btn-danger:hover {
|
|
41
|
+
.btn-danger:hover {
|
|
42
|
+
background: #fef2f2;
|
|
43
|
+
}
|
|
37
44
|
.field {
|
|
38
45
|
height: 2rem;
|
|
39
46
|
border: 1px solid #d1d5db;
|
|
@@ -58,7 +65,11 @@
|
|
|
58
65
|
linear-gradient(45deg, transparent 75%, #f9fafb 75%),
|
|
59
66
|
linear-gradient(-45deg, transparent 75%, #f9fafb 75%);
|
|
60
67
|
background-size: 1.25rem 1.25rem;
|
|
61
|
-
background-position:
|
|
68
|
+
background-position:
|
|
69
|
+
0 0,
|
|
70
|
+
0 0.625rem,
|
|
71
|
+
0.625rem -0.625rem,
|
|
72
|
+
-0.625rem 0;
|
|
62
73
|
overflow: hidden;
|
|
63
74
|
}
|
|
64
75
|
#previewImage {
|
|
@@ -77,18 +88,22 @@
|
|
|
77
88
|
border-radius: 999px;
|
|
78
89
|
border: 2px solid #fff;
|
|
79
90
|
background: #f97316;
|
|
80
|
-
box-shadow: 0 1px 4px rgba(0,0,0
|
|
91
|
+
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
|
|
81
92
|
cursor: grab;
|
|
82
93
|
}
|
|
83
|
-
.anchor-dot:active {
|
|
94
|
+
.anchor-dot:active {
|
|
95
|
+
cursor: grabbing;
|
|
96
|
+
}
|
|
84
97
|
.anchor-dot span {
|
|
85
98
|
position: absolute;
|
|
86
99
|
left: 50%;
|
|
87
100
|
top: calc(100% + 0.2rem);
|
|
88
101
|
transform: translateX(-50%);
|
|
89
|
-
font:
|
|
102
|
+
font:
|
|
103
|
+
10px/1 system-ui,
|
|
104
|
+
sans-serif;
|
|
90
105
|
color: #374151;
|
|
91
|
-
background: rgba(255,255,255
|
|
106
|
+
background: rgba(255, 255, 255, 0.9);
|
|
92
107
|
padding: 1px 3px;
|
|
93
108
|
border-radius: 3px;
|
|
94
109
|
white-space: nowrap;
|
|
@@ -96,76 +111,180 @@
|
|
|
96
111
|
</style>
|
|
97
112
|
</head>
|
|
98
113
|
<body class="h-screen overflow-hidden bg-gray-50 text-gray-900">
|
|
99
|
-
<header
|
|
114
|
+
<header
|
|
115
|
+
class="h-12 border-b border-gray-200 bg-white flex items-center gap-2 px-3"
|
|
116
|
+
>
|
|
100
117
|
<button class="btn" onclick="history.back()">← Back</button>
|
|
101
118
|
<h1 class="text-sm font-semibold">Shape editor</h1>
|
|
102
119
|
<span id="saveState" class="ml-auto text-xs text-gray-500"></span>
|
|
103
120
|
</header>
|
|
104
121
|
|
|
105
|
-
<main
|
|
122
|
+
<main
|
|
123
|
+
class="h-[calc(100vh-3rem)] grid grid-cols-[17rem_1fr] overflow-hidden"
|
|
124
|
+
>
|
|
106
125
|
<aside class="border-r border-gray-200 bg-white flex flex-col min-h-0">
|
|
107
126
|
<div class="p-3 border-b border-gray-200 space-y-2">
|
|
108
|
-
<label class="block text-xs font-semibold text-gray-500"
|
|
127
|
+
<label class="block text-xs font-semibold text-gray-500"
|
|
128
|
+
>Library</label
|
|
129
|
+
>
|
|
109
130
|
<div class="flex gap-2">
|
|
110
131
|
<select id="librarySelect" class="field flex-1"></select>
|
|
111
132
|
<button id="btnAddLibrary" class="btn">+</button>
|
|
112
133
|
</div>
|
|
113
|
-
<input
|
|
134
|
+
<input
|
|
135
|
+
id="libraryName"
|
|
136
|
+
class="field w-full"
|
|
137
|
+
placeholder="Library name"
|
|
138
|
+
/>
|
|
114
139
|
</div>
|
|
115
140
|
<div class="p-3 border-b border-gray-200 flex gap-2">
|
|
116
|
-
<button id="btnNewShape" class="btn btn-primary flex-1">
|
|
141
|
+
<button id="btnNewShape" class="btn btn-primary flex-1">
|
|
142
|
+
New shape
|
|
143
|
+
</button>
|
|
117
144
|
<button id="btnDeleteShape" class="btn btn-danger">Delete</button>
|
|
118
145
|
</div>
|
|
119
146
|
<div id="shapeList" class="flex-1 overflow-y-auto p-2 space-y-1"></div>
|
|
120
147
|
</aside>
|
|
121
148
|
|
|
122
149
|
<section class="min-w-0 min-h-0 overflow-y-auto p-5">
|
|
123
|
-
<div
|
|
150
|
+
<div
|
|
151
|
+
class="grid grid-cols-[minmax(20rem,34rem)_minmax(20rem,1fr)] gap-5 items-start"
|
|
152
|
+
>
|
|
124
153
|
<div class="space-y-3">
|
|
125
154
|
<div id="previewStage">
|
|
126
155
|
<img id="previewImage" alt="" />
|
|
127
156
|
</div>
|
|
128
157
|
<p class="text-xs text-gray-500">
|
|
129
|
-
Click on the image to add an anchor. Drag anchors to refine their
|
|
158
|
+
Click on the image to add an anchor. Drag anchors to refine their
|
|
159
|
+
position.
|
|
130
160
|
</p>
|
|
131
161
|
</div>
|
|
132
162
|
|
|
133
163
|
<div class="space-y-4">
|
|
134
164
|
<div class="grid grid-cols-2 gap-3">
|
|
135
165
|
<label class="space-y-1">
|
|
136
|
-
<span class="block text-xs font-semibold text-gray-500"
|
|
137
|
-
|
|
166
|
+
<span class="block text-xs font-semibold text-gray-500"
|
|
167
|
+
>Shape name</span
|
|
168
|
+
>
|
|
169
|
+
<input
|
|
170
|
+
id="shapeName"
|
|
171
|
+
class="field w-full"
|
|
172
|
+
placeholder="Shape name"
|
|
173
|
+
/>
|
|
138
174
|
</label>
|
|
139
175
|
<label class="space-y-1">
|
|
140
|
-
<span class="block text-xs font-semibold text-gray-500"
|
|
141
|
-
|
|
176
|
+
<span class="block text-xs font-semibold text-gray-500"
|
|
177
|
+
>Image / SVG</span
|
|
178
|
+
>
|
|
179
|
+
<input
|
|
180
|
+
id="imageFile"
|
|
181
|
+
type="file"
|
|
182
|
+
accept="image/*,.svg"
|
|
183
|
+
class="block w-full text-sm"
|
|
184
|
+
/>
|
|
142
185
|
</label>
|
|
143
186
|
<label class="space-y-1">
|
|
144
|
-
<span class="block text-xs font-semibold text-gray-500"
|
|
145
|
-
|
|
187
|
+
<span class="block text-xs font-semibold text-gray-500"
|
|
188
|
+
>Width</span
|
|
189
|
+
>
|
|
190
|
+
<input
|
|
191
|
+
id="shapeWidth"
|
|
192
|
+
type="number"
|
|
193
|
+
min="16"
|
|
194
|
+
max="1200"
|
|
195
|
+
class="field w-full"
|
|
196
|
+
/>
|
|
146
197
|
</label>
|
|
147
198
|
<label class="space-y-1">
|
|
148
|
-
<span class="block text-xs font-semibold text-gray-500"
|
|
149
|
-
|
|
199
|
+
<span class="block text-xs font-semibold text-gray-500"
|
|
200
|
+
>Height</span
|
|
201
|
+
>
|
|
202
|
+
<input
|
|
203
|
+
id="shapeHeight"
|
|
204
|
+
type="number"
|
|
205
|
+
min="16"
|
|
206
|
+
max="1200"
|
|
207
|
+
class="field w-full"
|
|
208
|
+
/>
|
|
150
209
|
</label>
|
|
151
210
|
<label class="space-y-1 col-span-2">
|
|
152
|
-
<span class="block text-xs font-semibold text-gray-500"
|
|
211
|
+
<span class="block text-xs font-semibold text-gray-500"
|
|
212
|
+
>Text placement</span
|
|
213
|
+
>
|
|
153
214
|
<select id="labelPlacement" class="field w-full">
|
|
154
|
-
<option
|
|
155
|
-
|
|
215
|
+
<option
|
|
216
|
+
value="below"
|
|
217
|
+
data-i18n="shape_editor.label_placement.below"
|
|
218
|
+
>
|
|
219
|
+
Below the shape
|
|
220
|
+
</option>
|
|
221
|
+
<option
|
|
222
|
+
value="above"
|
|
223
|
+
data-i18n="shape_editor.label_placement.above"
|
|
224
|
+
>
|
|
225
|
+
Above the shape
|
|
226
|
+
</option>
|
|
227
|
+
<option
|
|
228
|
+
value="right"
|
|
229
|
+
data-i18n="shape_editor.label_placement.right"
|
|
230
|
+
>
|
|
231
|
+
To the right
|
|
232
|
+
</option>
|
|
233
|
+
<option
|
|
234
|
+
value="left"
|
|
235
|
+
data-i18n="shape_editor.label_placement.left"
|
|
236
|
+
>
|
|
237
|
+
To the left
|
|
238
|
+
</option>
|
|
239
|
+
<option
|
|
240
|
+
value="center"
|
|
241
|
+
data-i18n="shape_editor.label_placement.center"
|
|
242
|
+
>
|
|
243
|
+
Centered in the shape
|
|
244
|
+
</option>
|
|
156
245
|
</select>
|
|
157
246
|
</label>
|
|
247
|
+
<label
|
|
248
|
+
class="col-span-2 flex items-start gap-2 rounded-md border border-gray-200 bg-white px-2 py-2 text-sm"
|
|
249
|
+
>
|
|
250
|
+
<input
|
|
251
|
+
id="shapeShowInDiagram"
|
|
252
|
+
type="checkbox"
|
|
253
|
+
class="mt-0.5 h-4 w-4 rounded border-gray-300 text-blue-600"
|
|
254
|
+
/>
|
|
255
|
+
<span class="space-y-0.5">
|
|
256
|
+
<span
|
|
257
|
+
class="block font-semibold text-gray-700"
|
|
258
|
+
data-i18n="shape_editor.show_in_diagram_label"
|
|
259
|
+
>Show in diagram palette</span
|
|
260
|
+
>
|
|
261
|
+
<span
|
|
262
|
+
class="block text-xs text-gray-500"
|
|
263
|
+
data-i18n="shape_editor.show_in_diagram_hint"
|
|
264
|
+
>When disabled, existing diagram nodes still render but this
|
|
265
|
+
shape is hidden from the bottom palette.</span
|
|
266
|
+
>
|
|
267
|
+
</span>
|
|
268
|
+
</label>
|
|
158
269
|
</div>
|
|
159
270
|
|
|
160
271
|
<div class="flex flex-wrap gap-2">
|
|
161
|
-
<button id="btnDefaultAnchors" class="btn">
|
|
272
|
+
<button id="btnDefaultAnchors" class="btn">
|
|
273
|
+
Use 8 default anchors
|
|
274
|
+
</button>
|
|
162
275
|
<button id="btnClearAnchors" class="btn">Clear anchors</button>
|
|
163
|
-
<button id="btnSaveShape" class="btn btn-primary">
|
|
276
|
+
<button id="btnSaveShape" class="btn btn-primary">
|
|
277
|
+
Save shape
|
|
278
|
+
</button>
|
|
164
279
|
</div>
|
|
165
280
|
|
|
166
281
|
<div>
|
|
167
282
|
<div class="flex items-center justify-between mb-2">
|
|
168
|
-
<h2
|
|
283
|
+
<h2
|
|
284
|
+
class="text-xs font-semibold uppercase tracking-wide text-gray-500"
|
|
285
|
+
>
|
|
286
|
+
Anchors
|
|
287
|
+
</h2>
|
|
169
288
|
<span id="anchorCount" class="text-xs text-gray-500"></span>
|
|
170
289
|
</div>
|
|
171
290
|
<div id="anchorList" class="space-y-1"></div>
|
|
@@ -176,22 +295,26 @@
|
|
|
176
295
|
</main>
|
|
177
296
|
|
|
178
297
|
<script type="module">
|
|
179
|
-
import {
|
|
298
|
+
import {
|
|
299
|
+
DEFAULT_CUSTOM_ANCHORS,
|
|
300
|
+
CUSTOM_SHAPE_DEFAULT_SIZE,
|
|
301
|
+
} from "/diagram/custom-shapes.js";
|
|
180
302
|
|
|
181
303
|
const els = {
|
|
182
|
-
saveState: document.getElementById(
|
|
183
|
-
librarySelect: document.getElementById(
|
|
184
|
-
libraryName: document.getElementById(
|
|
185
|
-
shapeList: document.getElementById(
|
|
186
|
-
previewStage: document.getElementById(
|
|
187
|
-
previewImage: document.getElementById(
|
|
188
|
-
shapeName: document.getElementById(
|
|
189
|
-
imageFile: document.getElementById(
|
|
190
|
-
shapeWidth: document.getElementById(
|
|
191
|
-
shapeHeight: document.getElementById(
|
|
192
|
-
labelPlacement: document.getElementById(
|
|
193
|
-
|
|
194
|
-
|
|
304
|
+
saveState: document.getElementById("saveState"),
|
|
305
|
+
librarySelect: document.getElementById("librarySelect"),
|
|
306
|
+
libraryName: document.getElementById("libraryName"),
|
|
307
|
+
shapeList: document.getElementById("shapeList"),
|
|
308
|
+
previewStage: document.getElementById("previewStage"),
|
|
309
|
+
previewImage: document.getElementById("previewImage"),
|
|
310
|
+
shapeName: document.getElementById("shapeName"),
|
|
311
|
+
imageFile: document.getElementById("imageFile"),
|
|
312
|
+
shapeWidth: document.getElementById("shapeWidth"),
|
|
313
|
+
shapeHeight: document.getElementById("shapeHeight"),
|
|
314
|
+
labelPlacement: document.getElementById("labelPlacement"),
|
|
315
|
+
shapeShowInDiagram: document.getElementById("shapeShowInDiagram"),
|
|
316
|
+
anchorList: document.getElementById("anchorList"),
|
|
317
|
+
anchorCount: document.getElementById("anchorCount"),
|
|
195
318
|
};
|
|
196
319
|
|
|
197
320
|
let store = { libraries: [] };
|
|
@@ -200,54 +323,78 @@
|
|
|
200
323
|
let draft = null;
|
|
201
324
|
let draggingAnchorId = null;
|
|
202
325
|
|
|
203
|
-
const uid = (prefix) =>
|
|
204
|
-
|
|
205
|
-
const
|
|
326
|
+
const uid = (prefix) =>
|
|
327
|
+
`${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
328
|
+
const activeLibrary = () =>
|
|
329
|
+
store.libraries.find((lib) => lib.id === activeLibraryId) || null;
|
|
330
|
+
const activeShape = () =>
|
|
331
|
+
activeLibrary()?.shapes.find((shape) => shape.id === activeShapeId) ||
|
|
332
|
+
null;
|
|
206
333
|
const clamp01 = (n) => Math.max(0, Math.min(1, n));
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
"
|
|
213
|
-
|
|
334
|
+
const normalizeLabelPlacement = (value) =>
|
|
335
|
+
["below", "above", "right", "left", "center"].includes(value)
|
|
336
|
+
? value
|
|
337
|
+
: "below";
|
|
338
|
+
const escapeAttr = (value) =>
|
|
339
|
+
String(value ?? "").replace(
|
|
340
|
+
/[&<>"']/g,
|
|
341
|
+
(char) =>
|
|
342
|
+
({
|
|
343
|
+
"&": "&",
|
|
344
|
+
"<": "<",
|
|
345
|
+
">": ">",
|
|
346
|
+
'"': """,
|
|
347
|
+
"'": "'",
|
|
348
|
+
})[char],
|
|
349
|
+
);
|
|
214
350
|
|
|
215
351
|
function setStatus(text) {
|
|
216
352
|
els.saveState.textContent = text;
|
|
217
|
-
if (text)
|
|
353
|
+
if (text)
|
|
354
|
+
setTimeout(() => {
|
|
355
|
+
if (els.saveState.textContent === text)
|
|
356
|
+
els.saveState.textContent = "";
|
|
357
|
+
}, 1800);
|
|
218
358
|
}
|
|
219
359
|
|
|
220
360
|
async function loadStore() {
|
|
221
|
-
const res = await fetch(
|
|
361
|
+
const res = await fetch("/api/shape-libraries");
|
|
222
362
|
store = await res.json();
|
|
223
363
|
if (!Array.isArray(store.libraries)) store.libraries = [];
|
|
224
364
|
if (!store.libraries.length) {
|
|
225
|
-
store.libraries.push({
|
|
365
|
+
store.libraries.push({
|
|
366
|
+
id: uid("lib"),
|
|
367
|
+
name: "My shapes",
|
|
368
|
+
shapes: [],
|
|
369
|
+
});
|
|
226
370
|
}
|
|
227
371
|
activeLibraryId = store.libraries[0].id;
|
|
228
372
|
activeShapeId = store.libraries[0].shapes[0]?.id || null;
|
|
229
|
-
draft = activeShape()
|
|
373
|
+
draft = activeShape()
|
|
374
|
+
? structuredClone(activeShape())
|
|
375
|
+
: newShapeDraft();
|
|
230
376
|
renderAll();
|
|
231
377
|
}
|
|
232
378
|
|
|
233
379
|
async function saveStore() {
|
|
234
|
-
const res = await fetch(
|
|
235
|
-
method:
|
|
236
|
-
headers: {
|
|
380
|
+
const res = await fetch("/api/shape-libraries", {
|
|
381
|
+
method: "PUT",
|
|
382
|
+
headers: { "Content-Type": "application/json" },
|
|
237
383
|
body: JSON.stringify(store),
|
|
238
384
|
});
|
|
239
385
|
store = await res.json();
|
|
240
|
-
setStatus(
|
|
386
|
+
setStatus("Saved");
|
|
241
387
|
}
|
|
242
388
|
|
|
243
389
|
function newShapeDraft() {
|
|
244
390
|
return {
|
|
245
|
-
id: uid(
|
|
246
|
-
name:
|
|
247
|
-
imageSrc:
|
|
248
|
-
width:
|
|
249
|
-
height:
|
|
250
|
-
labelPlacement:
|
|
391
|
+
id: uid("shape"),
|
|
392
|
+
name: "New shape",
|
|
393
|
+
imageSrc: "",
|
|
394
|
+
width: CUSTOM_SHAPE_DEFAULT_SIZE,
|
|
395
|
+
height: CUSTOM_SHAPE_DEFAULT_SIZE,
|
|
396
|
+
labelPlacement: "below",
|
|
397
|
+
showInDiagram: true,
|
|
251
398
|
anchors: structuredClone(DEFAULT_CUSTOM_ANCHORS),
|
|
252
399
|
};
|
|
253
400
|
}
|
|
@@ -259,33 +406,33 @@
|
|
|
259
406
|
}
|
|
260
407
|
|
|
261
408
|
function renderLibraries() {
|
|
262
|
-
els.librarySelect.innerHTML =
|
|
409
|
+
els.librarySelect.innerHTML = "";
|
|
263
410
|
store.libraries.forEach((lib) => {
|
|
264
|
-
const opt = document.createElement(
|
|
411
|
+
const opt = document.createElement("option");
|
|
265
412
|
opt.value = lib.id;
|
|
266
413
|
opt.textContent = lib.name;
|
|
267
414
|
opt.selected = lib.id === activeLibraryId;
|
|
268
415
|
els.librarySelect.appendChild(opt);
|
|
269
416
|
});
|
|
270
|
-
els.libraryName.value = activeLibrary()?.name ||
|
|
417
|
+
els.libraryName.value = activeLibrary()?.name || "";
|
|
271
418
|
}
|
|
272
419
|
|
|
273
420
|
function renderShapeList() {
|
|
274
421
|
const lib = activeLibrary();
|
|
275
|
-
els.shapeList.innerHTML =
|
|
422
|
+
els.shapeList.innerHTML = "";
|
|
276
423
|
(lib?.shapes || []).forEach((shape) => {
|
|
277
|
-
const btn = document.createElement(
|
|
278
|
-
btn.type =
|
|
279
|
-
btn.className = `shape-item w-full flex items-center gap-2 px-2 py-1.5 rounded text-sm text-left hover:bg-gray-100 ${shape.id === activeShapeId ?
|
|
280
|
-
const img = document.createElement(
|
|
424
|
+
const btn = document.createElement("button");
|
|
425
|
+
btn.type = "button";
|
|
426
|
+
btn.className = `shape-item w-full flex items-center gap-2 px-2 py-1.5 rounded text-sm text-left hover:bg-gray-100 ${shape.id === activeShapeId ? "active" : ""}`;
|
|
427
|
+
const img = document.createElement("img");
|
|
281
428
|
img.src = shape.imageSrc;
|
|
282
|
-
img.alt =
|
|
283
|
-
img.className =
|
|
284
|
-
const label = document.createElement(
|
|
285
|
-
label.className =
|
|
429
|
+
img.alt = "";
|
|
430
|
+
img.className = "w-6 h-6 object-contain shrink-0";
|
|
431
|
+
const label = document.createElement("span");
|
|
432
|
+
label.className = "truncate";
|
|
286
433
|
label.textContent = shape.name;
|
|
287
434
|
btn.append(img, label);
|
|
288
|
-
btn.addEventListener(
|
|
435
|
+
btn.addEventListener("click", () => {
|
|
289
436
|
activeShapeId = shape.id;
|
|
290
437
|
draft = structuredClone(shape);
|
|
291
438
|
renderAll();
|
|
@@ -293,55 +440,71 @@
|
|
|
293
440
|
els.shapeList.appendChild(btn);
|
|
294
441
|
});
|
|
295
442
|
if (!lib?.shapes?.length) {
|
|
296
|
-
els.shapeList.innerHTML =
|
|
443
|
+
els.shapeList.innerHTML =
|
|
444
|
+
'<p class="text-xs text-gray-400 px-2 py-2">No shape yet.</p>';
|
|
297
445
|
}
|
|
298
446
|
}
|
|
299
447
|
|
|
300
448
|
function renderEditor() {
|
|
301
449
|
if (!draft) draft = newShapeDraft();
|
|
302
|
-
els.previewStage.style.aspectRatio = `${Math.max(16, Number(draft.width) ||
|
|
303
|
-
els.previewImage.src = draft.imageSrc ||
|
|
304
|
-
els.previewImage.style.display = draft.imageSrc ?
|
|
305
|
-
els.shapeName.value = draft.name ||
|
|
306
|
-
els.shapeWidth.value = draft.width ||
|
|
307
|
-
els.shapeHeight.value = draft.height ||
|
|
308
|
-
els.labelPlacement.value =
|
|
450
|
+
els.previewStage.style.aspectRatio = `${Math.max(16, Number(draft.width) || CUSTOM_SHAPE_DEFAULT_SIZE)} / ${Math.max(16, Number(draft.height) || CUSTOM_SHAPE_DEFAULT_SIZE)}`;
|
|
451
|
+
els.previewImage.src = draft.imageSrc || "";
|
|
452
|
+
els.previewImage.style.display = draft.imageSrc ? "block" : "none";
|
|
453
|
+
els.shapeName.value = draft.name || "";
|
|
454
|
+
els.shapeWidth.value = draft.width || CUSTOM_SHAPE_DEFAULT_SIZE;
|
|
455
|
+
els.shapeHeight.value = draft.height || CUSTOM_SHAPE_DEFAULT_SIZE;
|
|
456
|
+
els.labelPlacement.value = normalizeLabelPlacement(
|
|
457
|
+
draft.labelPlacement,
|
|
458
|
+
);
|
|
459
|
+
els.shapeShowInDiagram.checked = draft.showInDiagram !== false;
|
|
309
460
|
renderAnchors();
|
|
310
461
|
}
|
|
311
462
|
|
|
312
463
|
function renderAnchors() {
|
|
313
|
-
els.previewStage
|
|
314
|
-
|
|
464
|
+
els.previewStage
|
|
465
|
+
.querySelectorAll(".anchor-dot")
|
|
466
|
+
.forEach((dot) => dot.remove());
|
|
467
|
+
els.anchorList.innerHTML = "";
|
|
315
468
|
const anchors = draft.anchors || [];
|
|
316
|
-
els.anchorCount.textContent = `${anchors.length} anchor${anchors.length > 1 ?
|
|
469
|
+
els.anchorCount.textContent = `${anchors.length} anchor${anchors.length > 1 ? "s" : ""}`;
|
|
317
470
|
anchors.forEach((anchor, index) => {
|
|
318
|
-
const dot = document.createElement(
|
|
319
|
-
dot.type =
|
|
320
|
-
dot.className =
|
|
471
|
+
const dot = document.createElement("button");
|
|
472
|
+
dot.type = "button";
|
|
473
|
+
dot.className = "anchor-dot";
|
|
321
474
|
dot.style.left = `${anchor.x * 100}%`;
|
|
322
475
|
dot.style.top = `${anchor.y * 100}%`;
|
|
323
476
|
dot.dataset.anchorId = anchor.id;
|
|
324
477
|
dot.innerHTML = `<span>${anchor.id}</span>`;
|
|
325
|
-
dot.addEventListener(
|
|
478
|
+
dot.addEventListener("pointerdown", (event) => {
|
|
326
479
|
event.preventDefault();
|
|
327
480
|
draggingAnchorId = anchor.id;
|
|
328
481
|
dot.setPointerCapture(event.pointerId);
|
|
329
482
|
});
|
|
330
483
|
els.previewStage.appendChild(dot);
|
|
331
484
|
|
|
332
|
-
const row = document.createElement(
|
|
333
|
-
row.className =
|
|
485
|
+
const row = document.createElement("div");
|
|
486
|
+
row.className =
|
|
487
|
+
"grid grid-cols-[4.5rem_1fr_1fr_2rem] gap-2 items-center";
|
|
334
488
|
row.innerHTML = `
|
|
335
489
|
<input class="field !h-7 text-xs" value="${escapeAttr(anchor.id)}">
|
|
336
490
|
<input class="field !h-7 text-xs" type="number" min="0" max="100" step="1" value="${Math.round(anchor.x * 100)}">
|
|
337
491
|
<input class="field !h-7 text-xs" type="number" min="0" max="100" step="1" value="${Math.round(anchor.y * 100)}">
|
|
338
492
|
<button class="btn btn-danger !h-7 !px-0">×</button>
|
|
339
493
|
`;
|
|
340
|
-
const [idInput, xInput, yInput] = row.querySelectorAll(
|
|
341
|
-
idInput.addEventListener(
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
494
|
+
const [idInput, xInput, yInput] = row.querySelectorAll("input");
|
|
495
|
+
idInput.addEventListener("input", () => {
|
|
496
|
+
anchor.id = idInput.value.trim() || `p${index + 1}`;
|
|
497
|
+
renderAnchors();
|
|
498
|
+
});
|
|
499
|
+
xInput.addEventListener("input", () => {
|
|
500
|
+
anchor.x = clamp01(Number(xInput.value) / 100);
|
|
501
|
+
renderAnchors();
|
|
502
|
+
});
|
|
503
|
+
yInput.addEventListener("input", () => {
|
|
504
|
+
anchor.y = clamp01(Number(yInput.value) / 100);
|
|
505
|
+
renderAnchors();
|
|
506
|
+
});
|
|
507
|
+
row.querySelector("button").addEventListener("click", () => {
|
|
345
508
|
draft.anchors = draft.anchors.filter((item) => item !== anchor);
|
|
346
509
|
renderAnchors();
|
|
347
510
|
});
|
|
@@ -355,89 +518,130 @@
|
|
|
355
518
|
anchor.y = clamp01((event.clientY - rect.top) / rect.height);
|
|
356
519
|
}
|
|
357
520
|
|
|
358
|
-
els.previewStage.addEventListener(
|
|
521
|
+
els.previewStage.addEventListener("pointermove", (event) => {
|
|
359
522
|
if (!draggingAnchorId || !draft) return;
|
|
360
|
-
const anchor = draft.anchors.find(
|
|
523
|
+
const anchor = draft.anchors.find(
|
|
524
|
+
(item) => item.id === draggingAnchorId,
|
|
525
|
+
);
|
|
361
526
|
if (!anchor) return;
|
|
362
527
|
setAnchorFromEvent(anchor, event);
|
|
363
528
|
renderAnchors();
|
|
364
529
|
});
|
|
365
|
-
window.addEventListener(
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
530
|
+
window.addEventListener("pointerup", () => {
|
|
531
|
+
draggingAnchorId = null;
|
|
532
|
+
});
|
|
533
|
+
els.previewStage.addEventListener("click", (event) => {
|
|
534
|
+
if (event.target.closest(".anchor-dot") || draggingAnchorId || !draft)
|
|
535
|
+
return;
|
|
536
|
+
const anchor = {
|
|
537
|
+
id: `p${(draft.anchors || []).length + 1}`,
|
|
538
|
+
x: 0.5,
|
|
539
|
+
y: 0.5,
|
|
540
|
+
};
|
|
369
541
|
setAnchorFromEvent(anchor, event);
|
|
370
542
|
draft.anchors = [...(draft.anchors || []), anchor];
|
|
371
543
|
renderAnchors();
|
|
372
544
|
});
|
|
373
545
|
|
|
374
|
-
document
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
546
|
+
document
|
|
547
|
+
.getElementById("btnAddLibrary")
|
|
548
|
+
.addEventListener("click", async () => {
|
|
549
|
+
const lib = { id: uid("lib"), name: "New library", shapes: [] };
|
|
550
|
+
store.libraries.push(lib);
|
|
551
|
+
activeLibraryId = lib.id;
|
|
552
|
+
activeShapeId = null;
|
|
553
|
+
draft = newShapeDraft();
|
|
554
|
+
renderAll();
|
|
555
|
+
await saveStore();
|
|
556
|
+
});
|
|
557
|
+
els.librarySelect.addEventListener("change", () => {
|
|
384
558
|
activeLibraryId = els.librarySelect.value;
|
|
385
559
|
activeShapeId = activeLibrary()?.shapes[0]?.id || null;
|
|
386
|
-
draft = activeShape()
|
|
560
|
+
draft = activeShape()
|
|
561
|
+
? structuredClone(activeShape())
|
|
562
|
+
: newShapeDraft();
|
|
387
563
|
renderAll();
|
|
388
564
|
});
|
|
389
|
-
els.libraryName.addEventListener(
|
|
565
|
+
els.libraryName.addEventListener("change", async () => {
|
|
390
566
|
const lib = activeLibrary();
|
|
391
567
|
if (!lib) return;
|
|
392
568
|
lib.name = els.libraryName.value.trim() || lib.name;
|
|
393
569
|
renderLibraries();
|
|
394
570
|
await saveStore();
|
|
395
571
|
});
|
|
396
|
-
document.getElementById(
|
|
572
|
+
document.getElementById("btnNewShape").addEventListener("click", () => {
|
|
397
573
|
activeShapeId = null;
|
|
398
574
|
draft = newShapeDraft();
|
|
399
575
|
renderAll();
|
|
400
576
|
});
|
|
401
|
-
document
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
document
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
577
|
+
document
|
|
578
|
+
.getElementById("btnDeleteShape")
|
|
579
|
+
.addEventListener("click", async () => {
|
|
580
|
+
const lib = activeLibrary();
|
|
581
|
+
if (!lib || !activeShapeId) return;
|
|
582
|
+
lib.shapes = lib.shapes.filter((shape) => shape.id !== activeShapeId);
|
|
583
|
+
activeShapeId = lib.shapes[0]?.id || null;
|
|
584
|
+
draft = activeShape()
|
|
585
|
+
? structuredClone(activeShape())
|
|
586
|
+
: newShapeDraft();
|
|
587
|
+
renderAll();
|
|
588
|
+
await saveStore();
|
|
589
|
+
});
|
|
590
|
+
document
|
|
591
|
+
.getElementById("btnDefaultAnchors")
|
|
592
|
+
.addEventListener("click", () => {
|
|
593
|
+
draft.anchors = structuredClone(DEFAULT_CUSTOM_ANCHORS);
|
|
594
|
+
renderAnchors();
|
|
595
|
+
});
|
|
596
|
+
document
|
|
597
|
+
.getElementById("btnClearAnchors")
|
|
598
|
+
.addEventListener("click", () => {
|
|
599
|
+
draft.anchors = [];
|
|
600
|
+
renderAnchors();
|
|
601
|
+
});
|
|
602
|
+
document
|
|
603
|
+
.getElementById("btnSaveShape")
|
|
604
|
+
.addEventListener("click", async () => {
|
|
605
|
+
const lib = activeLibrary();
|
|
606
|
+
if (!lib || !draft || !draft.imageSrc) return;
|
|
607
|
+
draft.name = els.shapeName.value.trim() || "Untitled shape";
|
|
608
|
+
draft.width = Math.max(
|
|
609
|
+
16,
|
|
610
|
+
Math.min(1200, Math.round(Number(els.shapeWidth.value) || CUSTOM_SHAPE_DEFAULT_SIZE)),
|
|
611
|
+
);
|
|
612
|
+
draft.height = Math.max(
|
|
613
|
+
16,
|
|
614
|
+
Math.min(1200, Math.round(Number(els.shapeHeight.value) || CUSTOM_SHAPE_DEFAULT_SIZE)),
|
|
615
|
+
);
|
|
616
|
+
draft.labelPlacement = normalizeLabelPlacement(
|
|
617
|
+
els.labelPlacement.value,
|
|
618
|
+
);
|
|
619
|
+
draft.showInDiagram = els.shapeShowInDiagram.checked;
|
|
620
|
+
const idx = lib.shapes.findIndex((shape) => shape.id === draft.id);
|
|
621
|
+
if (idx >= 0) lib.shapes[idx] = structuredClone(draft);
|
|
622
|
+
else lib.shapes.push(structuredClone(draft));
|
|
623
|
+
activeShapeId = draft.id;
|
|
624
|
+
renderAll();
|
|
625
|
+
await saveStore();
|
|
626
|
+
});
|
|
627
|
+
[
|
|
628
|
+
"shapeName",
|
|
629
|
+
"shapeWidth",
|
|
630
|
+
"shapeHeight",
|
|
631
|
+
"labelPlacement",
|
|
632
|
+
"shapeShowInDiagram",
|
|
633
|
+
].forEach((id) => {
|
|
634
|
+
document.getElementById(id).addEventListener("input", () => {
|
|
434
635
|
draft.name = els.shapeName.value;
|
|
435
|
-
draft.width = Number(els.shapeWidth.value) ||
|
|
436
|
-
draft.height = Number(els.shapeHeight.value) ||
|
|
437
|
-
draft.labelPlacement =
|
|
636
|
+
draft.width = Number(els.shapeWidth.value) || CUSTOM_SHAPE_DEFAULT_SIZE;
|
|
637
|
+
draft.height = Number(els.shapeHeight.value) || CUSTOM_SHAPE_DEFAULT_SIZE;
|
|
638
|
+
draft.labelPlacement = normalizeLabelPlacement(
|
|
639
|
+
els.labelPlacement.value,
|
|
640
|
+
);
|
|
641
|
+
draft.showInDiagram = els.shapeShowInDiagram.checked;
|
|
438
642
|
});
|
|
439
643
|
});
|
|
440
|
-
els.imageFile.addEventListener(
|
|
644
|
+
els.imageFile.addEventListener("change", async () => {
|
|
441
645
|
const file = els.imageFile.files && els.imageFile.files[0];
|
|
442
646
|
if (!file || !draft) return;
|
|
443
647
|
const data = await new Promise((resolve, reject) => {
|
|
@@ -446,20 +650,36 @@
|
|
|
446
650
|
reader.onerror = reject;
|
|
447
651
|
reader.readAsDataURL(file);
|
|
448
652
|
});
|
|
449
|
-
const ext =
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
653
|
+
const ext =
|
|
654
|
+
(file.name.split(".").pop() || "png")
|
|
655
|
+
.toLowerCase()
|
|
656
|
+
.replace(/[^a-z0-9]/g, "") || "png";
|
|
657
|
+
const name = file.name.replace(/\.[^.]+$/, "").slice(0, 60);
|
|
658
|
+
const res = await fetch("/api/images/upload", {
|
|
659
|
+
method: "POST",
|
|
660
|
+
headers: { "Content-Type": "application/json" },
|
|
454
661
|
body: JSON.stringify({ data, ext, name }),
|
|
455
662
|
});
|
|
456
663
|
const uploaded = await res.json();
|
|
457
664
|
draft.imageSrc = `/images/${uploaded.filename}`;
|
|
458
|
-
if (!els.shapeName.value || els.shapeName.value ===
|
|
665
|
+
if (!els.shapeName.value || els.shapeName.value === "New shape")
|
|
666
|
+
draft.name = name || draft.name;
|
|
459
667
|
renderEditor();
|
|
460
668
|
});
|
|
461
669
|
|
|
462
|
-
|
|
670
|
+
async function initShapeEditorI18n() {
|
|
671
|
+
try {
|
|
672
|
+
const res = await fetch("/api/config");
|
|
673
|
+
const cfg = res.ok ? await res.json() : {};
|
|
674
|
+
await window.initI18n(cfg.language || "en");
|
|
675
|
+
} catch {
|
|
676
|
+
await window.initI18n("en");
|
|
677
|
+
}
|
|
678
|
+
window.applyI18n();
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
await initShapeEditorI18n();
|
|
682
|
+
await loadStore();
|
|
463
683
|
</script>
|
|
464
684
|
</body>
|
|
465
685
|
</html>
|