living-documentation 7.41.0 → 7.43.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.
@@ -286,6 +286,57 @@
286
286
  background: #082f49;
287
287
  color: #e0f2fe;
288
288
  }
289
+ #customShapeBar {
290
+ position: absolute;
291
+ left: 0.75rem;
292
+ bottom: 0.75rem;
293
+ z-index: 9;
294
+ display: flex;
295
+ align-items: center;
296
+ max-width: min(42rem, calc(100% - 1.5rem));
297
+ padding: 0.35rem;
298
+ gap: 0.25rem;
299
+ border: 1px solid #e5e7eb;
300
+ border-radius: 0.5rem;
301
+ background: rgba(255, 255, 255, 0.96);
302
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.10);
303
+ }
304
+ .dark #customShapeBar {
305
+ border-color: #374151;
306
+ background: rgba(31, 41, 55, 0.96);
307
+ }
308
+ #customShapeBar.hidden {
309
+ display: none;
310
+ }
311
+ #customShapeBarBody {
312
+ display: flex;
313
+ gap: 0.25rem;
314
+ overflow-x: auto;
315
+ scrollbar-width: thin;
316
+ }
317
+ .custom-shape-btn {
318
+ display: flex;
319
+ align-items: center;
320
+ justify-content: center;
321
+ width: 2.25rem;
322
+ height: 2.25rem;
323
+ border-radius: 0.375rem;
324
+ border: 1px solid transparent;
325
+ background: transparent;
326
+ flex: 0 0 auto;
327
+ }
328
+ .custom-shape-btn:hover {
329
+ background: #f3f4f6;
330
+ }
331
+ .dark .custom-shape-btn:hover {
332
+ background: #111827;
333
+ }
334
+ .custom-shape-btn img {
335
+ max-width: 1.7rem;
336
+ max-height: 1.7rem;
337
+ object-fit: contain;
338
+ pointer-events: none;
339
+ }
289
340
  </style>
290
341
  </head>
291
342
  <body
@@ -445,6 +496,13 @@
445
496
  <path d="M1 9.5 L4 6.5 L6.5 9 L9 7 L13 10.5"/>
446
497
  </svg>
447
498
  </button>
499
+ <a
500
+ href="/shape-editor"
501
+ class="tool-btn"
502
+ data-i18n-title="diagram.toolbar.shape_editor"
503
+ >
504
+
505
+ </a>
448
506
  <div class="w-px h-6 bg-gray-200 dark:bg-gray-700 mx-0.5"></div>
449
507
 
450
508
  <button
@@ -641,6 +699,10 @@
641
699
  <div class="relative flex-1 overflow-hidden bg-gray-50 dark:bg-gray-950">
642
700
  <div id="vis-canvas" class="w-full h-full"></div>
643
701
 
702
+ <div id="customShapeBar" class="hidden">
703
+ <div id="customShapeBarBody"></div>
704
+ </div>
705
+
644
706
  <!-- Debug overlay layer -->
645
707
  <div id="debugLayer"></div>
646
708
 
@@ -1088,6 +1150,13 @@
1088
1150
  >
1089
1151
 
1090
1152
  </button>
1153
+ <button
1154
+ id="edgeBtnFrom"
1155
+ class="tool-btn !w-7 !h-6 text-xs"
1156
+ data-i18n-title="diagram.edge_panel.arrow_from"
1157
+ >
1158
+
1159
+ </button>
1091
1160
  <button
1092
1161
  id="edgeBtnTo"
1093
1162
  class="tool-btn !w-7 !h-6 text-xs"
@@ -1301,6 +1370,7 @@
1301
1370
  import { NODE_COLORS, NODE_L_RATIOS, DEFAULT_NODE_PALETTE, DEFAULT_EDGE_PALETTE, deriveNodeColors } from '/diagram/constants.js';
1302
1371
  import { st } from '/diagram/state.js';
1303
1372
  import { loadDiagramList } from '/diagram/persistence.js';
1373
+ import { loadCustomShapeLibraries, renderCustomShapeBar } from '/diagram/custom-shapes.js';
1304
1374
  (async () => {
1305
1375
  // diagramNodePalette: array of 15 bg hex strings (positional, matching DEFAULT_NODE_PALETTE)
1306
1376
  // diagramEdgePalette: array of hex strings
@@ -1348,6 +1418,8 @@
1348
1418
  btn.title = hex;
1349
1419
  ec.appendChild(btn);
1350
1420
  });
1421
+ await loadCustomShapeLibraries();
1422
+ renderCustomShapeBar();
1351
1423
  window.applyI18n();
1352
1424
  loadDiagramList();
1353
1425
  })();
@@ -409,6 +409,7 @@
409
409
  "diagram.toolbar.postit": "Post-it (P)",
410
410
  "diagram.toolbar.text_free": "Free text (T)",
411
411
  "diagram.toolbar.image": "Image — double-click on canvas to select a file, or paste (⌘V) from clipboard",
412
+ "diagram.toolbar.shape_editor": "Custom shape editor",
412
413
  "diagram.toolbar.arrow": "Arrow (F)",
413
414
  "diagram.toolbar.delete": "Delete (Del)",
414
415
  "diagram.toolbar.align_guides": "Alignment guides — highlights same-type shape alignment",
@@ -465,6 +466,7 @@
465
466
  "diagram.edge_panel.lock": "Lock arrow",
466
467
  "diagram.edge_panel.unlock": "Unlock arrow",
467
468
  "diagram.edge_panel.no_arrow": "Simple line",
469
+ "diagram.edge_panel.arrow_from": "Reverse directional arrow",
468
470
  "diagram.edge_panel.arrow_to": "Directional arrow",
469
471
  "diagram.edge_panel.arrow_both": "Bidirectional",
470
472
  "diagram.edge_panel.solid": "Solid line",
@@ -409,6 +409,7 @@
409
409
  "diagram.toolbar.postit": "Post-it (P)",
410
410
  "diagram.toolbar.text_free": "Texte libre (T)",
411
411
  "diagram.toolbar.image": "Image — double-clic sur le canvas pour choisir un fichier, ou coller (⌘V) depuis le presse-papier",
412
+ "diagram.toolbar.shape_editor": "Éditeur de formes personnalisées",
412
413
  "diagram.toolbar.arrow": "Flèche (F)",
413
414
  "diagram.toolbar.delete": "Supprimer (Suppr)",
414
415
  "diagram.toolbar.align_guides": "Guides d'alignement — indique l'alignement entre formes du même type",
@@ -465,6 +466,7 @@
465
466
  "diagram.edge_panel.lock": "Verrouiller la flèche",
466
467
  "diagram.edge_panel.unlock": "Déverrouiller la flèche",
467
468
  "diagram.edge_panel.no_arrow": "Trait simple",
469
+ "diagram.edge_panel.arrow_from": "Flèche directionnelle inverse",
468
470
  "diagram.edge_panel.arrow_to": "Flèche directionnelle",
469
471
  "diagram.edge_panel.arrow_both": "Bidirectionnelle",
470
472
  "diagram.edge_panel.solid": "Trait plein",
@@ -0,0 +1,465 @@
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>
9
+ tailwind.config = { darkMode: "class", theme: { extend: {} } };
10
+ </script>
11
+ <style>
12
+ .btn {
13
+ display: inline-flex;
14
+ align-items: center;
15
+ justify-content: center;
16
+ height: 2rem;
17
+ padding: 0 0.75rem;
18
+ border-radius: 0.375rem;
19
+ font-size: 0.8125rem;
20
+ font-weight: 600;
21
+ border: 1px solid #d1d5db;
22
+ background: #fff;
23
+ color: #374151;
24
+ }
25
+ .btn:hover { background: #f3f4f6; }
26
+ .btn-primary {
27
+ border-color: #2563eb;
28
+ background: #2563eb;
29
+ color: #fff;
30
+ }
31
+ .btn-primary:hover { background: #1d4ed8; }
32
+ .btn-danger {
33
+ border-color: #fecaca;
34
+ color: #b91c1c;
35
+ }
36
+ .btn-danger:hover { background: #fef2f2; }
37
+ .field {
38
+ height: 2rem;
39
+ border: 1px solid #d1d5db;
40
+ border-radius: 0.375rem;
41
+ padding: 0 0.5rem;
42
+ font-size: 0.875rem;
43
+ background: #fff;
44
+ }
45
+ .shape-item.active {
46
+ background: #eff6ff;
47
+ color: #1d4ed8;
48
+ }
49
+ #previewStage {
50
+ position: relative;
51
+ width: min(34rem, 100%);
52
+ aspect-ratio: 1 / 1;
53
+ border: 1px solid #d1d5db;
54
+ border-radius: 0.5rem;
55
+ background:
56
+ linear-gradient(45deg, #f9fafb 25%, transparent 25%),
57
+ linear-gradient(-45deg, #f9fafb 25%, transparent 25%),
58
+ linear-gradient(45deg, transparent 75%, #f9fafb 75%),
59
+ linear-gradient(-45deg, transparent 75%, #f9fafb 75%);
60
+ background-size: 1.25rem 1.25rem;
61
+ background-position: 0 0, 0 0.625rem, 0.625rem -0.625rem, -0.625rem 0;
62
+ overflow: hidden;
63
+ }
64
+ #previewImage {
65
+ position: absolute;
66
+ inset: 0;
67
+ width: 100%;
68
+ height: 100%;
69
+ object-fit: fill;
70
+ pointer-events: none;
71
+ }
72
+ .anchor-dot {
73
+ position: absolute;
74
+ width: 0.9rem;
75
+ height: 0.9rem;
76
+ transform: translate(-50%, -50%);
77
+ border-radius: 999px;
78
+ border: 2px solid #fff;
79
+ background: #f97316;
80
+ box-shadow: 0 1px 4px rgba(0,0,0,.35);
81
+ cursor: grab;
82
+ }
83
+ .anchor-dot:active { cursor: grabbing; }
84
+ .anchor-dot span {
85
+ position: absolute;
86
+ left: 50%;
87
+ top: calc(100% + 0.2rem);
88
+ transform: translateX(-50%);
89
+ font: 10px/1 system-ui, sans-serif;
90
+ color: #374151;
91
+ background: rgba(255,255,255,.9);
92
+ padding: 1px 3px;
93
+ border-radius: 3px;
94
+ white-space: nowrap;
95
+ }
96
+ </style>
97
+ </head>
98
+ <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">
100
+ <button class="btn" onclick="history.back()">← Back</button>
101
+ <h1 class="text-sm font-semibold">Shape editor</h1>
102
+ <span id="saveState" class="ml-auto text-xs text-gray-500"></span>
103
+ </header>
104
+
105
+ <main class="h-[calc(100vh-3rem)] grid grid-cols-[17rem_1fr] overflow-hidden">
106
+ <aside class="border-r border-gray-200 bg-white flex flex-col min-h-0">
107
+ <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>
109
+ <div class="flex gap-2">
110
+ <select id="librarySelect" class="field flex-1"></select>
111
+ <button id="btnAddLibrary" class="btn">+</button>
112
+ </div>
113
+ <input id="libraryName" class="field w-full" placeholder="Library name" />
114
+ </div>
115
+ <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>
117
+ <button id="btnDeleteShape" class="btn btn-danger">Delete</button>
118
+ </div>
119
+ <div id="shapeList" class="flex-1 overflow-y-auto p-2 space-y-1"></div>
120
+ </aside>
121
+
122
+ <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">
124
+ <div class="space-y-3">
125
+ <div id="previewStage">
126
+ <img id="previewImage" alt="" />
127
+ </div>
128
+ <p class="text-xs text-gray-500">
129
+ Click on the image to add an anchor. Drag anchors to refine their position.
130
+ </p>
131
+ </div>
132
+
133
+ <div class="space-y-4">
134
+ <div class="grid grid-cols-2 gap-3">
135
+ <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" />
138
+ </label>
139
+ <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" />
142
+ </label>
143
+ <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" />
146
+ </label>
147
+ <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" />
150
+ </label>
151
+ <label class="space-y-1 col-span-2">
152
+ <span class="block text-xs font-semibold text-gray-500">Text placement</span>
153
+ <select id="labelPlacement" class="field w-full">
154
+ <option value="below">Below the shape</option>
155
+ <option value="center">Centered in the shape</option>
156
+ </select>
157
+ </label>
158
+ </div>
159
+
160
+ <div class="flex flex-wrap gap-2">
161
+ <button id="btnDefaultAnchors" class="btn">Use 8 default anchors</button>
162
+ <button id="btnClearAnchors" class="btn">Clear anchors</button>
163
+ <button id="btnSaveShape" class="btn btn-primary">Save shape</button>
164
+ </div>
165
+
166
+ <div>
167
+ <div class="flex items-center justify-between mb-2">
168
+ <h2 class="text-xs font-semibold uppercase tracking-wide text-gray-500">Anchors</h2>
169
+ <span id="anchorCount" class="text-xs text-gray-500"></span>
170
+ </div>
171
+ <div id="anchorList" class="space-y-1"></div>
172
+ </div>
173
+ </div>
174
+ </div>
175
+ </section>
176
+ </main>
177
+
178
+ <script type="module">
179
+ import { DEFAULT_CUSTOM_ANCHORS } from '/diagram/custom-shapes.js';
180
+
181
+ 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'),
195
+ };
196
+
197
+ let store = { libraries: [] };
198
+ let activeLibraryId = null;
199
+ let activeShapeId = null;
200
+ let draft = null;
201
+ let draggingAnchorId = null;
202
+
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;
206
+ 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]));
214
+
215
+ function setStatus(text) {
216
+ els.saveState.textContent = text;
217
+ if (text) setTimeout(() => { if (els.saveState.textContent === text) els.saveState.textContent = ''; }, 1800);
218
+ }
219
+
220
+ async function loadStore() {
221
+ const res = await fetch('/api/shape-libraries');
222
+ store = await res.json();
223
+ if (!Array.isArray(store.libraries)) store.libraries = [];
224
+ if (!store.libraries.length) {
225
+ store.libraries.push({ id: uid('lib'), name: 'My shapes', shapes: [] });
226
+ }
227
+ activeLibraryId = store.libraries[0].id;
228
+ activeShapeId = store.libraries[0].shapes[0]?.id || null;
229
+ draft = activeShape() ? structuredClone(activeShape()) : newShapeDraft();
230
+ renderAll();
231
+ }
232
+
233
+ async function saveStore() {
234
+ const res = await fetch('/api/shape-libraries', {
235
+ method: 'PUT',
236
+ headers: { 'Content-Type': 'application/json' },
237
+ body: JSON.stringify(store),
238
+ });
239
+ store = await res.json();
240
+ setStatus('Saved');
241
+ }
242
+
243
+ function newShapeDraft() {
244
+ return {
245
+ id: uid('shape'),
246
+ name: 'New shape',
247
+ imageSrc: '',
248
+ width: 96,
249
+ height: 96,
250
+ labelPlacement: 'below',
251
+ anchors: structuredClone(DEFAULT_CUSTOM_ANCHORS),
252
+ };
253
+ }
254
+
255
+ function renderAll() {
256
+ renderLibraries();
257
+ renderShapeList();
258
+ renderEditor();
259
+ }
260
+
261
+ function renderLibraries() {
262
+ els.librarySelect.innerHTML = '';
263
+ store.libraries.forEach((lib) => {
264
+ const opt = document.createElement('option');
265
+ opt.value = lib.id;
266
+ opt.textContent = lib.name;
267
+ opt.selected = lib.id === activeLibraryId;
268
+ els.librarySelect.appendChild(opt);
269
+ });
270
+ els.libraryName.value = activeLibrary()?.name || '';
271
+ }
272
+
273
+ function renderShapeList() {
274
+ const lib = activeLibrary();
275
+ els.shapeList.innerHTML = '';
276
+ (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');
281
+ 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';
286
+ label.textContent = shape.name;
287
+ btn.append(img, label);
288
+ btn.addEventListener('click', () => {
289
+ activeShapeId = shape.id;
290
+ draft = structuredClone(shape);
291
+ renderAll();
292
+ });
293
+ els.shapeList.appendChild(btn);
294
+ });
295
+ if (!lib?.shapes?.length) {
296
+ els.shapeList.innerHTML = '<p class="text-xs text-gray-400 px-2 py-2">No shape yet.</p>';
297
+ }
298
+ }
299
+
300
+ function renderEditor() {
301
+ 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';
309
+ renderAnchors();
310
+ }
311
+
312
+ function renderAnchors() {
313
+ els.previewStage.querySelectorAll('.anchor-dot').forEach((dot) => dot.remove());
314
+ els.anchorList.innerHTML = '';
315
+ const anchors = draft.anchors || [];
316
+ els.anchorCount.textContent = `${anchors.length} anchor${anchors.length > 1 ? 's' : ''}`;
317
+ anchors.forEach((anchor, index) => {
318
+ const dot = document.createElement('button');
319
+ dot.type = 'button';
320
+ dot.className = 'anchor-dot';
321
+ dot.style.left = `${anchor.x * 100}%`;
322
+ dot.style.top = `${anchor.y * 100}%`;
323
+ dot.dataset.anchorId = anchor.id;
324
+ dot.innerHTML = `<span>${anchor.id}</span>`;
325
+ dot.addEventListener('pointerdown', (event) => {
326
+ event.preventDefault();
327
+ draggingAnchorId = anchor.id;
328
+ dot.setPointerCapture(event.pointerId);
329
+ });
330
+ els.previewStage.appendChild(dot);
331
+
332
+ const row = document.createElement('div');
333
+ row.className = 'grid grid-cols-[4.5rem_1fr_1fr_2rem] gap-2 items-center';
334
+ row.innerHTML = `
335
+ <input class="field !h-7 text-xs" value="${escapeAttr(anchor.id)}">
336
+ <input class="field !h-7 text-xs" type="number" min="0" max="100" step="1" value="${Math.round(anchor.x * 100)}">
337
+ <input class="field !h-7 text-xs" type="number" min="0" max="100" step="1" value="${Math.round(anchor.y * 100)}">
338
+ <button class="btn btn-danger !h-7 !px-0">×</button>
339
+ `;
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', () => {
345
+ draft.anchors = draft.anchors.filter((item) => item !== anchor);
346
+ renderAnchors();
347
+ });
348
+ els.anchorList.appendChild(row);
349
+ });
350
+ }
351
+
352
+ function setAnchorFromEvent(anchor, event) {
353
+ const rect = els.previewStage.getBoundingClientRect();
354
+ anchor.x = clamp01((event.clientX - rect.left) / rect.width);
355
+ anchor.y = clamp01((event.clientY - rect.top) / rect.height);
356
+ }
357
+
358
+ els.previewStage.addEventListener('pointermove', (event) => {
359
+ if (!draggingAnchorId || !draft) return;
360
+ const anchor = draft.anchors.find((item) => item.id === draggingAnchorId);
361
+ if (!anchor) return;
362
+ setAnchorFromEvent(anchor, event);
363
+ renderAnchors();
364
+ });
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 };
369
+ setAnchorFromEvent(anchor, event);
370
+ draft.anchors = [...(draft.anchors || []), anchor];
371
+ renderAnchors();
372
+ });
373
+
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', () => {
384
+ activeLibraryId = els.librarySelect.value;
385
+ activeShapeId = activeLibrary()?.shapes[0]?.id || null;
386
+ draft = activeShape() ? structuredClone(activeShape()) : newShapeDraft();
387
+ renderAll();
388
+ });
389
+ els.libraryName.addEventListener('change', async () => {
390
+ const lib = activeLibrary();
391
+ if (!lib) return;
392
+ lib.name = els.libraryName.value.trim() || lib.name;
393
+ renderLibraries();
394
+ await saveStore();
395
+ });
396
+ document.getElementById('btnNewShape').addEventListener('click', () => {
397
+ activeShapeId = null;
398
+ draft = newShapeDraft();
399
+ renderAll();
400
+ });
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', () => {
434
+ 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';
438
+ });
439
+ });
440
+ els.imageFile.addEventListener('change', async () => {
441
+ const file = els.imageFile.files && els.imageFile.files[0];
442
+ if (!file || !draft) return;
443
+ const data = await new Promise((resolve, reject) => {
444
+ const reader = new FileReader();
445
+ reader.onload = () => resolve(reader.result);
446
+ reader.onerror = reject;
447
+ reader.readAsDataURL(file);
448
+ });
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' },
454
+ body: JSON.stringify({ data, ext, name }),
455
+ });
456
+ const uploaded = await res.json();
457
+ draft.imageSrc = `/images/${uploaded.filename}`;
458
+ if (!els.shapeName.value || els.shapeName.value === 'New shape') draft.name = name || draft.name;
459
+ renderEditor();
460
+ });
461
+
462
+ loadStore();
463
+ </script>
464
+ </body>
465
+ </html>
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../src/mcp/server.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,MAAM,EAAqB,MAAM,SAAS,CAAC;AAo9CpD,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CA4BlD"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../src/mcp/server.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,MAAM,EAAqB,MAAM,SAAS,CAAC;AA++CpD,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CA4BlD"}