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.
@@ -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 { background: #f3f4f6; }
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 { background: #1d4ed8; }
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 { background: #fef2f2; }
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: 0 0, 0 0.625rem, 0.625rem -0.625rem, -0.625rem 0;
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,.35);
91
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
81
92
  cursor: grab;
82
93
  }
83
- .anchor-dot:active { cursor: grabbing; }
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: 10px/1 system-ui, sans-serif;
102
+ font:
103
+ 10px/1 system-ui,
104
+ sans-serif;
90
105
  color: #374151;
91
- background: rgba(255,255,255,.9);
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 class="h-12 border-b border-gray-200 bg-white flex items-center gap-2 px-3">
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 class="h-[calc(100vh-3rem)] grid grid-cols-[17rem_1fr] overflow-hidden">
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">Library</label>
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 id="libraryName" class="field w-full" placeholder="Library name" />
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">New shape</button>
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 class="grid grid-cols-[minmax(20rem,34rem)_minmax(20rem,1fr)] gap-5 items-start">
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 position.
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">Shape name</span>
137
- <input id="shapeName" class="field w-full" placeholder="Shape name" />
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">Image / SVG</span>
141
- <input id="imageFile" type="file" accept="image/*,.svg" class="block w-full text-sm" />
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">Width</span>
145
- <input id="shapeWidth" type="number" min="16" max="1200" class="field w-full" />
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">Height</span>
149
- <input id="shapeHeight" type="number" min="16" max="1200" class="field w-full" />
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">Text placement</span>
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 value="below">Below the shape</option>
155
- <option value="center">Centered in the shape</option>
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">Use 8 default anchors</button>
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">Save shape</button>
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 class="text-xs font-semibold uppercase tracking-wide text-gray-500">Anchors</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 { DEFAULT_CUSTOM_ANCHORS } from '/diagram/custom-shapes.js';
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('saveState'),
183
- librarySelect: document.getElementById('librarySelect'),
184
- libraryName: document.getElementById('libraryName'),
185
- shapeList: document.getElementById('shapeList'),
186
- previewStage: document.getElementById('previewStage'),
187
- previewImage: document.getElementById('previewImage'),
188
- shapeName: document.getElementById('shapeName'),
189
- imageFile: document.getElementById('imageFile'),
190
- shapeWidth: document.getElementById('shapeWidth'),
191
- shapeHeight: document.getElementById('shapeHeight'),
192
- labelPlacement: document.getElementById('labelPlacement'),
193
- anchorList: document.getElementById('anchorList'),
194
- anchorCount: document.getElementById('anchorCount'),
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) => `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
204
- const activeLibrary = () => store.libraries.find((lib) => lib.id === activeLibraryId) || null;
205
- const activeShape = () => activeLibrary()?.shapes.find((shape) => shape.id === activeShapeId) || null;
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 escapeAttr = (value) => String(value ?? '').replace(/[&<>"']/g, (char) => ({
208
- '&': '&amp;',
209
- '<': '&lt;',
210
- '>': '&gt;',
211
- '"': '&quot;',
212
- "'": '&#39;',
213
- }[char]));
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
+ "&": "&amp;",
344
+ "<": "&lt;",
345
+ ">": "&gt;",
346
+ '"': "&quot;",
347
+ "'": "&#39;",
348
+ })[char],
349
+ );
214
350
 
215
351
  function setStatus(text) {
216
352
  els.saveState.textContent = text;
217
- if (text) setTimeout(() => { if (els.saveState.textContent === text) els.saveState.textContent = ''; }, 1800);
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('/api/shape-libraries');
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({ id: uid('lib'), name: 'My shapes', shapes: [] });
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() ? structuredClone(activeShape()) : newShapeDraft();
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('/api/shape-libraries', {
235
- method: 'PUT',
236
- headers: { 'Content-Type': 'application/json' },
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('Saved');
386
+ setStatus("Saved");
241
387
  }
242
388
 
243
389
  function newShapeDraft() {
244
390
  return {
245
- id: uid('shape'),
246
- name: 'New shape',
247
- imageSrc: '',
248
- width: 96,
249
- height: 96,
250
- labelPlacement: 'below',
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('option');
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('button');
278
- btn.type = 'button';
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 ? 'active' : ''}`;
280
- const img = document.createElement('img');
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 = 'w-6 h-6 object-contain shrink-0';
284
- const label = document.createElement('span');
285
- label.className = 'truncate';
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('click', () => {
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 = '<p class="text-xs text-gray-400 px-2 py-2">No shape yet.</p>';
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) || 96)} / ${Math.max(16, Number(draft.height) || 96)}`;
303
- els.previewImage.src = draft.imageSrc || '';
304
- els.previewImage.style.display = draft.imageSrc ? 'block' : 'none';
305
- els.shapeName.value = draft.name || '';
306
- els.shapeWidth.value = draft.width || 96;
307
- els.shapeHeight.value = draft.height || 96;
308
- els.labelPlacement.value = draft.labelPlacement === 'center' ? 'center' : 'below';
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.querySelectorAll('.anchor-dot').forEach((dot) => dot.remove());
314
- els.anchorList.innerHTML = '';
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 ? 's' : ''}`;
469
+ els.anchorCount.textContent = `${anchors.length} anchor${anchors.length > 1 ? "s" : ""}`;
317
470
  anchors.forEach((anchor, index) => {
318
- const dot = document.createElement('button');
319
- dot.type = 'button';
320
- dot.className = 'anchor-dot';
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('pointerdown', (event) => {
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('div');
333
- row.className = 'grid grid-cols-[4.5rem_1fr_1fr_2rem] gap-2 items-center';
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('input');
341
- idInput.addEventListener('input', () => { anchor.id = idInput.value.trim() || `p${index + 1}`; renderAnchors(); });
342
- xInput.addEventListener('input', () => { anchor.x = clamp01(Number(xInput.value) / 100); renderAnchors(); });
343
- yInput.addEventListener('input', () => { anchor.y = clamp01(Number(yInput.value) / 100); renderAnchors(); });
344
- row.querySelector('button').addEventListener('click', () => {
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('pointermove', (event) => {
521
+ els.previewStage.addEventListener("pointermove", (event) => {
359
522
  if (!draggingAnchorId || !draft) return;
360
- const anchor = draft.anchors.find((item) => item.id === draggingAnchorId);
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('pointerup', () => { draggingAnchorId = null; });
366
- els.previewStage.addEventListener('click', (event) => {
367
- if (event.target.closest('.anchor-dot') || draggingAnchorId || !draft) return;
368
- const anchor = { id: `p${(draft.anchors || []).length + 1}`, x: 0.5, y: 0.5 };
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.getElementById('btnAddLibrary').addEventListener('click', async () => {
375
- const lib = { id: uid('lib'), name: 'New library', shapes: [] };
376
- store.libraries.push(lib);
377
- activeLibraryId = lib.id;
378
- activeShapeId = null;
379
- draft = newShapeDraft();
380
- renderAll();
381
- await saveStore();
382
- });
383
- els.librarySelect.addEventListener('change', () => {
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() ? structuredClone(activeShape()) : newShapeDraft();
560
+ draft = activeShape()
561
+ ? structuredClone(activeShape())
562
+ : newShapeDraft();
387
563
  renderAll();
388
564
  });
389
- els.libraryName.addEventListener('change', async () => {
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('btnNewShape').addEventListener('click', () => {
572
+ document.getElementById("btnNewShape").addEventListener("click", () => {
397
573
  activeShapeId = null;
398
574
  draft = newShapeDraft();
399
575
  renderAll();
400
576
  });
401
- document.getElementById('btnDeleteShape').addEventListener('click', async () => {
402
- const lib = activeLibrary();
403
- if (!lib || !activeShapeId) return;
404
- lib.shapes = lib.shapes.filter((shape) => shape.id !== activeShapeId);
405
- activeShapeId = lib.shapes[0]?.id || null;
406
- draft = activeShape() ? structuredClone(activeShape()) : newShapeDraft();
407
- renderAll();
408
- await saveStore();
409
- });
410
- document.getElementById('btnDefaultAnchors').addEventListener('click', () => {
411
- draft.anchors = structuredClone(DEFAULT_CUSTOM_ANCHORS);
412
- renderAnchors();
413
- });
414
- document.getElementById('btnClearAnchors').addEventListener('click', () => {
415
- draft.anchors = [];
416
- renderAnchors();
417
- });
418
- document.getElementById('btnSaveShape').addEventListener('click', async () => {
419
- const lib = activeLibrary();
420
- if (!lib || !draft || !draft.imageSrc) return;
421
- draft.name = els.shapeName.value.trim() || 'Untitled shape';
422
- draft.width = Math.max(16, Math.min(1200, Math.round(Number(els.shapeWidth.value) || 96)));
423
- draft.height = Math.max(16, Math.min(1200, Math.round(Number(els.shapeHeight.value) || 96)));
424
- draft.labelPlacement = els.labelPlacement.value === 'below' ? 'below' : 'center';
425
- const idx = lib.shapes.findIndex((shape) => shape.id === draft.id);
426
- if (idx >= 0) lib.shapes[idx] = structuredClone(draft);
427
- else lib.shapes.push(structuredClone(draft));
428
- activeShapeId = draft.id;
429
- renderAll();
430
- await saveStore();
431
- });
432
- ['shapeName', 'shapeWidth', 'shapeHeight', 'labelPlacement'].forEach((id) => {
433
- document.getElementById(id).addEventListener('input', () => {
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) || 96;
436
- draft.height = Number(els.shapeHeight.value) || 96;
437
- draft.labelPlacement = els.labelPlacement.value === 'below' ? 'below' : 'center';
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('change', async () => {
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 = (file.name.split('.').pop() || 'png').toLowerCase().replace(/[^a-z0-9]/g, '') || 'png';
450
- const name = file.name.replace(/\.[^.]+$/, '').slice(0, 60);
451
- const res = await fetch('/api/images/upload', {
452
- method: 'POST',
453
- headers: { 'Content-Type': 'application/json' },
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 === 'New shape') draft.name = name || draft.name;
665
+ if (!els.shapeName.value || els.shapeName.value === "New shape")
666
+ draft.name = name || draft.name;
459
667
  renderEditor();
460
668
  });
461
669
 
462
- loadStore();
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>