living-documentation 7.42.0 → 7.44.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 +104 -0
- package/dist/src/frontend/diagram/main.js +10 -1
- package/dist/src/frontend/diagram/network.js +1042 -531
- package/dist/src/frontend/diagram/node-panel.js +47 -0
- package/dist/src/frontend/diagram/node-rendering.js +190 -0
- package/dist/src/frontend/diagram/persistence.js +2 -0
- package/dist/src/frontend/diagram/ports.js +49 -14
- package/dist/src/frontend/diagram/selection-overlay.js +64 -13
- package/dist/src/frontend/diagram/state.js +2 -0
- package/dist/src/frontend/diagram.html +99 -0
- package/dist/src/frontend/i18n/en.json +15 -1
- package/dist/src/frontend/i18n/fr.json +15 -1
- package/dist/src/frontend/shape-editor.html +685 -0
- package/dist/src/routes/shape-libraries.d.ts +3 -0
- package/dist/src/routes/shape-libraries.d.ts.map +1 -0
- package/dist/src/routes/shape-libraries.js +116 -0
- package/dist/src/routes/shape-libraries.js.map +1 -0
- package/dist/src/server.d.ts.map +1 -1
- package/dist/src/server.js +3 -0
- package/dist/src/server.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="fr">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Shape editor — Living Documentation</title>
|
|
7
|
+
<script src="https://cdn.tailwindcss.com?plugins=typography"></script>
|
|
8
|
+
<script src="/i18n.js"></script>
|
|
9
|
+
<script>
|
|
10
|
+
tailwind.config = { darkMode: "class", theme: { extend: {} } };
|
|
11
|
+
</script>
|
|
12
|
+
<style>
|
|
13
|
+
.btn {
|
|
14
|
+
display: inline-flex;
|
|
15
|
+
align-items: center;
|
|
16
|
+
justify-content: center;
|
|
17
|
+
height: 2rem;
|
|
18
|
+
padding: 0 0.75rem;
|
|
19
|
+
border-radius: 0.375rem;
|
|
20
|
+
font-size: 0.8125rem;
|
|
21
|
+
font-weight: 600;
|
|
22
|
+
border: 1px solid #d1d5db;
|
|
23
|
+
background: #fff;
|
|
24
|
+
color: #374151;
|
|
25
|
+
}
|
|
26
|
+
.btn:hover {
|
|
27
|
+
background: #f3f4f6;
|
|
28
|
+
}
|
|
29
|
+
.btn-primary {
|
|
30
|
+
border-color: #2563eb;
|
|
31
|
+
background: #2563eb;
|
|
32
|
+
color: #fff;
|
|
33
|
+
}
|
|
34
|
+
.btn-primary:hover {
|
|
35
|
+
background: #1d4ed8;
|
|
36
|
+
}
|
|
37
|
+
.btn-danger {
|
|
38
|
+
border-color: #fecaca;
|
|
39
|
+
color: #b91c1c;
|
|
40
|
+
}
|
|
41
|
+
.btn-danger:hover {
|
|
42
|
+
background: #fef2f2;
|
|
43
|
+
}
|
|
44
|
+
.field {
|
|
45
|
+
height: 2rem;
|
|
46
|
+
border: 1px solid #d1d5db;
|
|
47
|
+
border-radius: 0.375rem;
|
|
48
|
+
padding: 0 0.5rem;
|
|
49
|
+
font-size: 0.875rem;
|
|
50
|
+
background: #fff;
|
|
51
|
+
}
|
|
52
|
+
.shape-item.active {
|
|
53
|
+
background: #eff6ff;
|
|
54
|
+
color: #1d4ed8;
|
|
55
|
+
}
|
|
56
|
+
#previewStage {
|
|
57
|
+
position: relative;
|
|
58
|
+
width: min(34rem, 100%);
|
|
59
|
+
aspect-ratio: 1 / 1;
|
|
60
|
+
border: 1px solid #d1d5db;
|
|
61
|
+
border-radius: 0.5rem;
|
|
62
|
+
background:
|
|
63
|
+
linear-gradient(45deg, #f9fafb 25%, transparent 25%),
|
|
64
|
+
linear-gradient(-45deg, #f9fafb 25%, transparent 25%),
|
|
65
|
+
linear-gradient(45deg, transparent 75%, #f9fafb 75%),
|
|
66
|
+
linear-gradient(-45deg, transparent 75%, #f9fafb 75%);
|
|
67
|
+
background-size: 1.25rem 1.25rem;
|
|
68
|
+
background-position:
|
|
69
|
+
0 0,
|
|
70
|
+
0 0.625rem,
|
|
71
|
+
0.625rem -0.625rem,
|
|
72
|
+
-0.625rem 0;
|
|
73
|
+
overflow: hidden;
|
|
74
|
+
}
|
|
75
|
+
#previewImage {
|
|
76
|
+
position: absolute;
|
|
77
|
+
inset: 0;
|
|
78
|
+
width: 100%;
|
|
79
|
+
height: 100%;
|
|
80
|
+
object-fit: fill;
|
|
81
|
+
pointer-events: none;
|
|
82
|
+
}
|
|
83
|
+
.anchor-dot {
|
|
84
|
+
position: absolute;
|
|
85
|
+
width: 0.9rem;
|
|
86
|
+
height: 0.9rem;
|
|
87
|
+
transform: translate(-50%, -50%);
|
|
88
|
+
border-radius: 999px;
|
|
89
|
+
border: 2px solid #fff;
|
|
90
|
+
background: #f97316;
|
|
91
|
+
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
|
|
92
|
+
cursor: grab;
|
|
93
|
+
}
|
|
94
|
+
.anchor-dot:active {
|
|
95
|
+
cursor: grabbing;
|
|
96
|
+
}
|
|
97
|
+
.anchor-dot span {
|
|
98
|
+
position: absolute;
|
|
99
|
+
left: 50%;
|
|
100
|
+
top: calc(100% + 0.2rem);
|
|
101
|
+
transform: translateX(-50%);
|
|
102
|
+
font:
|
|
103
|
+
10px/1 system-ui,
|
|
104
|
+
sans-serif;
|
|
105
|
+
color: #374151;
|
|
106
|
+
background: rgba(255, 255, 255, 0.9);
|
|
107
|
+
padding: 1px 3px;
|
|
108
|
+
border-radius: 3px;
|
|
109
|
+
white-space: nowrap;
|
|
110
|
+
}
|
|
111
|
+
</style>
|
|
112
|
+
</head>
|
|
113
|
+
<body class="h-screen overflow-hidden bg-gray-50 text-gray-900">
|
|
114
|
+
<header
|
|
115
|
+
class="h-12 border-b border-gray-200 bg-white flex items-center gap-2 px-3"
|
|
116
|
+
>
|
|
117
|
+
<button class="btn" onclick="history.back()">← Back</button>
|
|
118
|
+
<h1 class="text-sm font-semibold">Shape editor</h1>
|
|
119
|
+
<span id="saveState" class="ml-auto text-xs text-gray-500"></span>
|
|
120
|
+
</header>
|
|
121
|
+
|
|
122
|
+
<main
|
|
123
|
+
class="h-[calc(100vh-3rem)] grid grid-cols-[17rem_1fr] overflow-hidden"
|
|
124
|
+
>
|
|
125
|
+
<aside class="border-r border-gray-200 bg-white flex flex-col min-h-0">
|
|
126
|
+
<div class="p-3 border-b border-gray-200 space-y-2">
|
|
127
|
+
<label class="block text-xs font-semibold text-gray-500"
|
|
128
|
+
>Library</label
|
|
129
|
+
>
|
|
130
|
+
<div class="flex gap-2">
|
|
131
|
+
<select id="librarySelect" class="field flex-1"></select>
|
|
132
|
+
<button id="btnAddLibrary" class="btn">+</button>
|
|
133
|
+
</div>
|
|
134
|
+
<input
|
|
135
|
+
id="libraryName"
|
|
136
|
+
class="field w-full"
|
|
137
|
+
placeholder="Library name"
|
|
138
|
+
/>
|
|
139
|
+
</div>
|
|
140
|
+
<div class="p-3 border-b border-gray-200 flex gap-2">
|
|
141
|
+
<button id="btnNewShape" class="btn btn-primary flex-1">
|
|
142
|
+
New shape
|
|
143
|
+
</button>
|
|
144
|
+
<button id="btnDeleteShape" class="btn btn-danger">Delete</button>
|
|
145
|
+
</div>
|
|
146
|
+
<div id="shapeList" class="flex-1 overflow-y-auto p-2 space-y-1"></div>
|
|
147
|
+
</aside>
|
|
148
|
+
|
|
149
|
+
<section class="min-w-0 min-h-0 overflow-y-auto p-5">
|
|
150
|
+
<div
|
|
151
|
+
class="grid grid-cols-[minmax(20rem,34rem)_minmax(20rem,1fr)] gap-5 items-start"
|
|
152
|
+
>
|
|
153
|
+
<div class="space-y-3">
|
|
154
|
+
<div id="previewStage">
|
|
155
|
+
<img id="previewImage" alt="" />
|
|
156
|
+
</div>
|
|
157
|
+
<p class="text-xs text-gray-500">
|
|
158
|
+
Click on the image to add an anchor. Drag anchors to refine their
|
|
159
|
+
position.
|
|
160
|
+
</p>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<div class="space-y-4">
|
|
164
|
+
<div class="grid grid-cols-2 gap-3">
|
|
165
|
+
<label class="space-y-1">
|
|
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
|
+
/>
|
|
174
|
+
</label>
|
|
175
|
+
<label class="space-y-1">
|
|
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
|
+
/>
|
|
185
|
+
</label>
|
|
186
|
+
<label class="space-y-1">
|
|
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
|
+
/>
|
|
197
|
+
</label>
|
|
198
|
+
<label class="space-y-1">
|
|
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
|
+
/>
|
|
209
|
+
</label>
|
|
210
|
+
<label class="space-y-1 col-span-2">
|
|
211
|
+
<span class="block text-xs font-semibold text-gray-500"
|
|
212
|
+
>Text placement</span
|
|
213
|
+
>
|
|
214
|
+
<select id="labelPlacement" class="field w-full">
|
|
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>
|
|
245
|
+
</select>
|
|
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>
|
|
269
|
+
</div>
|
|
270
|
+
|
|
271
|
+
<div class="flex flex-wrap gap-2">
|
|
272
|
+
<button id="btnDefaultAnchors" class="btn">
|
|
273
|
+
Use 8 default anchors
|
|
274
|
+
</button>
|
|
275
|
+
<button id="btnClearAnchors" class="btn">Clear anchors</button>
|
|
276
|
+
<button id="btnSaveShape" class="btn btn-primary">
|
|
277
|
+
Save shape
|
|
278
|
+
</button>
|
|
279
|
+
</div>
|
|
280
|
+
|
|
281
|
+
<div>
|
|
282
|
+
<div class="flex items-center justify-between mb-2">
|
|
283
|
+
<h2
|
|
284
|
+
class="text-xs font-semibold uppercase tracking-wide text-gray-500"
|
|
285
|
+
>
|
|
286
|
+
Anchors
|
|
287
|
+
</h2>
|
|
288
|
+
<span id="anchorCount" class="text-xs text-gray-500"></span>
|
|
289
|
+
</div>
|
|
290
|
+
<div id="anchorList" class="space-y-1"></div>
|
|
291
|
+
</div>
|
|
292
|
+
</div>
|
|
293
|
+
</div>
|
|
294
|
+
</section>
|
|
295
|
+
</main>
|
|
296
|
+
|
|
297
|
+
<script type="module">
|
|
298
|
+
import {
|
|
299
|
+
DEFAULT_CUSTOM_ANCHORS,
|
|
300
|
+
CUSTOM_SHAPE_DEFAULT_SIZE,
|
|
301
|
+
} from "/diagram/custom-shapes.js";
|
|
302
|
+
|
|
303
|
+
const els = {
|
|
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"),
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
let store = { libraries: [] };
|
|
321
|
+
let activeLibraryId = null;
|
|
322
|
+
let activeShapeId = null;
|
|
323
|
+
let draft = null;
|
|
324
|
+
let draggingAnchorId = null;
|
|
325
|
+
|
|
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;
|
|
333
|
+
const clamp01 = (n) => Math.max(0, Math.min(1, n));
|
|
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
|
+
);
|
|
350
|
+
|
|
351
|
+
function setStatus(text) {
|
|
352
|
+
els.saveState.textContent = text;
|
|
353
|
+
if (text)
|
|
354
|
+
setTimeout(() => {
|
|
355
|
+
if (els.saveState.textContent === text)
|
|
356
|
+
els.saveState.textContent = "";
|
|
357
|
+
}, 1800);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function loadStore() {
|
|
361
|
+
const res = await fetch("/api/shape-libraries");
|
|
362
|
+
store = await res.json();
|
|
363
|
+
if (!Array.isArray(store.libraries)) store.libraries = [];
|
|
364
|
+
if (!store.libraries.length) {
|
|
365
|
+
store.libraries.push({
|
|
366
|
+
id: uid("lib"),
|
|
367
|
+
name: "My shapes",
|
|
368
|
+
shapes: [],
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
activeLibraryId = store.libraries[0].id;
|
|
372
|
+
activeShapeId = store.libraries[0].shapes[0]?.id || null;
|
|
373
|
+
draft = activeShape()
|
|
374
|
+
? structuredClone(activeShape())
|
|
375
|
+
: newShapeDraft();
|
|
376
|
+
renderAll();
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function saveStore() {
|
|
380
|
+
const res = await fetch("/api/shape-libraries", {
|
|
381
|
+
method: "PUT",
|
|
382
|
+
headers: { "Content-Type": "application/json" },
|
|
383
|
+
body: JSON.stringify(store),
|
|
384
|
+
});
|
|
385
|
+
store = await res.json();
|
|
386
|
+
setStatus("Saved");
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function newShapeDraft() {
|
|
390
|
+
return {
|
|
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,
|
|
398
|
+
anchors: structuredClone(DEFAULT_CUSTOM_ANCHORS),
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function renderAll() {
|
|
403
|
+
renderLibraries();
|
|
404
|
+
renderShapeList();
|
|
405
|
+
renderEditor();
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function renderLibraries() {
|
|
409
|
+
els.librarySelect.innerHTML = "";
|
|
410
|
+
store.libraries.forEach((lib) => {
|
|
411
|
+
const opt = document.createElement("option");
|
|
412
|
+
opt.value = lib.id;
|
|
413
|
+
opt.textContent = lib.name;
|
|
414
|
+
opt.selected = lib.id === activeLibraryId;
|
|
415
|
+
els.librarySelect.appendChild(opt);
|
|
416
|
+
});
|
|
417
|
+
els.libraryName.value = activeLibrary()?.name || "";
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function renderShapeList() {
|
|
421
|
+
const lib = activeLibrary();
|
|
422
|
+
els.shapeList.innerHTML = "";
|
|
423
|
+
(lib?.shapes || []).forEach((shape) => {
|
|
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");
|
|
428
|
+
img.src = shape.imageSrc;
|
|
429
|
+
img.alt = "";
|
|
430
|
+
img.className = "w-6 h-6 object-contain shrink-0";
|
|
431
|
+
const label = document.createElement("span");
|
|
432
|
+
label.className = "truncate";
|
|
433
|
+
label.textContent = shape.name;
|
|
434
|
+
btn.append(img, label);
|
|
435
|
+
btn.addEventListener("click", () => {
|
|
436
|
+
activeShapeId = shape.id;
|
|
437
|
+
draft = structuredClone(shape);
|
|
438
|
+
renderAll();
|
|
439
|
+
});
|
|
440
|
+
els.shapeList.appendChild(btn);
|
|
441
|
+
});
|
|
442
|
+
if (!lib?.shapes?.length) {
|
|
443
|
+
els.shapeList.innerHTML =
|
|
444
|
+
'<p class="text-xs text-gray-400 px-2 py-2">No shape yet.</p>';
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function renderEditor() {
|
|
449
|
+
if (!draft) draft = newShapeDraft();
|
|
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;
|
|
460
|
+
renderAnchors();
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function renderAnchors() {
|
|
464
|
+
els.previewStage
|
|
465
|
+
.querySelectorAll(".anchor-dot")
|
|
466
|
+
.forEach((dot) => dot.remove());
|
|
467
|
+
els.anchorList.innerHTML = "";
|
|
468
|
+
const anchors = draft.anchors || [];
|
|
469
|
+
els.anchorCount.textContent = `${anchors.length} anchor${anchors.length > 1 ? "s" : ""}`;
|
|
470
|
+
anchors.forEach((anchor, index) => {
|
|
471
|
+
const dot = document.createElement("button");
|
|
472
|
+
dot.type = "button";
|
|
473
|
+
dot.className = "anchor-dot";
|
|
474
|
+
dot.style.left = `${anchor.x * 100}%`;
|
|
475
|
+
dot.style.top = `${anchor.y * 100}%`;
|
|
476
|
+
dot.dataset.anchorId = anchor.id;
|
|
477
|
+
dot.innerHTML = `<span>${anchor.id}</span>`;
|
|
478
|
+
dot.addEventListener("pointerdown", (event) => {
|
|
479
|
+
event.preventDefault();
|
|
480
|
+
draggingAnchorId = anchor.id;
|
|
481
|
+
dot.setPointerCapture(event.pointerId);
|
|
482
|
+
});
|
|
483
|
+
els.previewStage.appendChild(dot);
|
|
484
|
+
|
|
485
|
+
const row = document.createElement("div");
|
|
486
|
+
row.className =
|
|
487
|
+
"grid grid-cols-[4.5rem_1fr_1fr_2rem] gap-2 items-center";
|
|
488
|
+
row.innerHTML = `
|
|
489
|
+
<input class="field !h-7 text-xs" value="${escapeAttr(anchor.id)}">
|
|
490
|
+
<input class="field !h-7 text-xs" type="number" min="0" max="100" step="1" value="${Math.round(anchor.x * 100)}">
|
|
491
|
+
<input class="field !h-7 text-xs" type="number" min="0" max="100" step="1" value="${Math.round(anchor.y * 100)}">
|
|
492
|
+
<button class="btn btn-danger !h-7 !px-0">×</button>
|
|
493
|
+
`;
|
|
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", () => {
|
|
508
|
+
draft.anchors = draft.anchors.filter((item) => item !== anchor);
|
|
509
|
+
renderAnchors();
|
|
510
|
+
});
|
|
511
|
+
els.anchorList.appendChild(row);
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function setAnchorFromEvent(anchor, event) {
|
|
516
|
+
const rect = els.previewStage.getBoundingClientRect();
|
|
517
|
+
anchor.x = clamp01((event.clientX - rect.left) / rect.width);
|
|
518
|
+
anchor.y = clamp01((event.clientY - rect.top) / rect.height);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
els.previewStage.addEventListener("pointermove", (event) => {
|
|
522
|
+
if (!draggingAnchorId || !draft) return;
|
|
523
|
+
const anchor = draft.anchors.find(
|
|
524
|
+
(item) => item.id === draggingAnchorId,
|
|
525
|
+
);
|
|
526
|
+
if (!anchor) return;
|
|
527
|
+
setAnchorFromEvent(anchor, event);
|
|
528
|
+
renderAnchors();
|
|
529
|
+
});
|
|
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
|
+
};
|
|
541
|
+
setAnchorFromEvent(anchor, event);
|
|
542
|
+
draft.anchors = [...(draft.anchors || []), anchor];
|
|
543
|
+
renderAnchors();
|
|
544
|
+
});
|
|
545
|
+
|
|
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", () => {
|
|
558
|
+
activeLibraryId = els.librarySelect.value;
|
|
559
|
+
activeShapeId = activeLibrary()?.shapes[0]?.id || null;
|
|
560
|
+
draft = activeShape()
|
|
561
|
+
? structuredClone(activeShape())
|
|
562
|
+
: newShapeDraft();
|
|
563
|
+
renderAll();
|
|
564
|
+
});
|
|
565
|
+
els.libraryName.addEventListener("change", async () => {
|
|
566
|
+
const lib = activeLibrary();
|
|
567
|
+
if (!lib) return;
|
|
568
|
+
lib.name = els.libraryName.value.trim() || lib.name;
|
|
569
|
+
renderLibraries();
|
|
570
|
+
await saveStore();
|
|
571
|
+
});
|
|
572
|
+
document.getElementById("btnNewShape").addEventListener("click", () => {
|
|
573
|
+
activeShapeId = null;
|
|
574
|
+
draft = newShapeDraft();
|
|
575
|
+
renderAll();
|
|
576
|
+
});
|
|
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", () => {
|
|
635
|
+
draft.name = els.shapeName.value;
|
|
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;
|
|
642
|
+
});
|
|
643
|
+
});
|
|
644
|
+
els.imageFile.addEventListener("change", async () => {
|
|
645
|
+
const file = els.imageFile.files && els.imageFile.files[0];
|
|
646
|
+
if (!file || !draft) return;
|
|
647
|
+
const data = await new Promise((resolve, reject) => {
|
|
648
|
+
const reader = new FileReader();
|
|
649
|
+
reader.onload = () => resolve(reader.result);
|
|
650
|
+
reader.onerror = reject;
|
|
651
|
+
reader.readAsDataURL(file);
|
|
652
|
+
});
|
|
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" },
|
|
661
|
+
body: JSON.stringify({ data, ext, name }),
|
|
662
|
+
});
|
|
663
|
+
const uploaded = await res.json();
|
|
664
|
+
draft.imageSrc = `/images/${uploaded.filename}`;
|
|
665
|
+
if (!els.shapeName.value || els.shapeName.value === "New shape")
|
|
666
|
+
draft.name = name || draft.name;
|
|
667
|
+
renderEditor();
|
|
668
|
+
});
|
|
669
|
+
|
|
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();
|
|
683
|
+
</script>
|
|
684
|
+
</body>
|
|
685
|
+
</html>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shape-libraries.d.ts","sourceRoot":"","sources":["../../../src/routes/shape-libraries.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAgJjC,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAc7D"}
|