living-documentation 3.4.0 → 3.6.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.

Potentially problematic release.


This version of living-documentation might be problematic. Click here for more details.

@@ -5,1179 +5,874 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Diagram — Living Documentation</title>
7
7
  <script src="https://cdn.tailwindcss.com?plugins=typography"></script>
8
- <script>tailwind.config = { darkMode: 'class', theme: { extend: {} } };</script>
8
+ <script>
9
+ tailwind.config = { darkMode: "class", theme: { extend: {} } };
10
+ </script>
9
11
  <script src="https://unpkg.com/vis-network@9.1.9/standalone/umd/vis-network.min.js"></script>
10
12
  <style>
11
- #vis-canvas > div { border: none !important; }
13
+ #vis-canvas > div {
14
+ border: none !important;
15
+ }
12
16
  .tool-btn {
13
- display: flex; align-items: center; justify-content: center;
14
- width: 2rem; height: 2rem; border-radius: 0.5rem;
15
- font-size: 0.875rem; cursor: pointer; flex-shrink: 0;
16
- transition: background-color 0.15s, color 0.15s;
17
- color: #4b5563; background: none; border: none;
17
+ display: flex;
18
+ align-items: center;
19
+ justify-content: center;
20
+ width: 2rem;
21
+ height: 2rem;
22
+ border-radius: 0.5rem;
23
+ font-size: 0.875rem;
24
+ cursor: pointer;
25
+ flex-shrink: 0;
26
+ transition:
27
+ background-color 0.15s,
28
+ color 0.15s;
29
+ color: #4b5563;
30
+ background: none;
31
+ border: none;
32
+ }
33
+ .dark .tool-btn {
34
+ color: #9ca3af;
35
+ }
36
+ .tool-btn:hover {
37
+ background: #f3f4f6;
38
+ }
39
+ .dark .tool-btn:hover {
40
+ background: #1f2937;
41
+ }
42
+ .tool-active {
43
+ background: #fff7ed !important;
44
+ color: #ea580c !important;
45
+ }
46
+ .dark .tool-active {
47
+ background: rgba(124, 45, 18, 0.3) !important;
48
+ color: #fb923c !important;
49
+ }
50
+ #vis-canvas.cursor-crosshair canvas {
51
+ cursor: crosshair !important;
18
52
  }
19
- .dark .tool-btn { color: #9ca3af; }
20
- .tool-btn:hover { background: #f3f4f6; }
21
- .dark .tool-btn:hover { background: #1f2937; }
22
- .tool-active { background: #fff7ed !important; color: #ea580c !important; }
23
- .dark .tool-active { background: rgba(124,45,18,0.3) !important; color: #fb923c !important; }
24
- #vis-canvas.cursor-crosshair canvas { cursor: crosshair !important; }
25
53
 
26
54
  /* Floating panels */
27
55
  .float-panel {
28
- position: absolute; top: 0.75rem; left: 50%; transform: translateX(-50%);
29
- display: flex; align-items: center; gap: 0.25rem; padding: 0.375rem 0.5rem;
30
- background: white; border: 1px solid #e5e7eb; border-radius: 0.5rem;
31
- box-shadow: 0 4px 12px rgba(0,0,0,.10); z-index: 10;
56
+ position: absolute;
57
+ top: 0.75rem;
58
+ left: 50%;
59
+ transform: translateX(-50%);
60
+ display: flex;
61
+ align-items: center;
62
+ gap: 0.25rem;
63
+ padding: 0.375rem 0.5rem;
64
+ background: white;
65
+ border: 1px solid #e5e7eb;
66
+ border-radius: 0.5rem;
67
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
68
+ z-index: 10;
69
+ }
70
+ .dark .float-panel {
71
+ background: #1f2937;
72
+ border-color: #374151;
73
+ }
74
+ .float-panel.hidden {
75
+ display: none !important;
76
+ }
77
+ .panel-sep {
78
+ width: 1px;
79
+ height: 1rem;
80
+ background: #e5e7eb;
81
+ margin: 0 0.125rem;
82
+ flex-shrink: 0;
83
+ }
84
+ .dark .panel-sep {
85
+ background: #374151;
86
+ }
87
+ .edge-btn-active {
88
+ background: #fff7ed !important;
89
+ color: #ea580c !important;
90
+ }
91
+ .dark .edge-btn-active {
92
+ background: rgba(124, 45, 18, 0.3) !important;
93
+ color: #fb923c !important;
32
94
  }
33
- .dark .float-panel { background: #1f2937; border-color: #374151; }
34
- .float-panel.hidden { display: none !important; }
35
- .panel-sep { width:1px; height:1rem; background:#e5e7eb; margin:0 .125rem; flex-shrink:0; }
36
- .dark .panel-sep { background:#374151; }
37
- .edge-btn-active { background:#fff7ed!important; color:#ea580c!important; }
38
- .dark .edge-btn-active { background:rgba(124,45,18,.3)!important; color:#fb923c!important; }
39
95
 
40
96
  /* Floating label textarea */
41
97
  #labelInput {
42
- position: absolute; z-index: 20;
43
- padding: 4px 8px; font-size: 13px;
44
- font-family: system-ui,-apple-system,sans-serif; font-weight: 500;
98
+ position: absolute;
99
+ z-index: 20;
100
+ padding: 4px 8px;
101
+ font-size: 13px;
102
+ font-family:
103
+ system-ui,
104
+ -apple-system,
105
+ sans-serif;
106
+ font-weight: 500;
45
107
  text-align: center;
46
- background: rgba(255,255,255,.95); color: #1f2937;
47
- border: 2px solid #3b82f6; border-radius: .5rem;
48
- box-shadow: 0 4px 16px rgba(0,0,0,.15);
49
- outline: none; resize: none; overflow: hidden;
50
- line-height: 1.4; min-width: 80px; min-height: 28px;
108
+ background: rgba(255, 255, 255, 0.95);
109
+ color: #1f2937;
110
+ border: 2px solid #3b82f6;
111
+ border-radius: 0.5rem;
112
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
113
+ outline: none;
114
+ resize: none;
115
+ overflow: hidden;
116
+ line-height: 1.4;
117
+ min-width: 80px;
118
+ min-height: 28px;
119
+ }
120
+ .dark #labelInput {
121
+ background: rgba(17, 24, 39, 0.95);
122
+ color: #f3f4f6;
123
+ }
124
+ #labelInput.hidden {
125
+ display: none;
126
+ }
127
+
128
+ /* Debug overlay layer */
129
+ #debugLayer {
130
+ position: absolute;
131
+ inset: 0;
132
+ pointer-events: none;
133
+ z-index: 12;
134
+ overflow: hidden;
135
+ }
136
+ .debug-box {
137
+ position: absolute;
138
+ pointer-events: auto;
139
+ font: 10px/1.4 monospace;
140
+ white-space: pre;
141
+ user-select: text;
142
+ cursor: text;
143
+ padding: 3px 6px;
144
+ border: 1px solid #f97316;
145
+ border-radius: 3px;
146
+ color: #c2410c;
147
+ background: rgba(255, 255, 255, 0.9);
148
+ }
149
+ .dark .debug-box {
150
+ color: #fbbf24;
151
+ background: rgba(0, 0, 0, 0.78);
51
152
  }
52
- .dark #labelInput { background: rgba(17,24,39,.95); color: #f3f4f6; }
53
- #labelInput.hidden { display: none; }
54
153
 
55
154
  /* Resize / selection overlay */
56
155
  #selectionOverlay {
57
- position: absolute; display: none;
58
- border: 2px dashed #f97316; border-radius: 3px;
59
- pointer-events: none; z-index: 15; box-sizing: border-box;
156
+ position: absolute;
157
+ display: none;
158
+ border: 2px dashed #f97316;
159
+ border-radius: 3px;
160
+ pointer-events: none;
161
+ z-index: 15;
162
+ box-sizing: border-box;
60
163
  }
61
164
  .resize-handle {
62
- position: absolute; width: 10px; height: 10px;
63
- background: white; border: 2px solid #f97316; border-radius: 2px;
64
- pointer-events: all; z-index: 1;
165
+ position: absolute;
166
+ width: 10px;
167
+ height: 10px;
168
+ background: white;
169
+ border: 2px solid #f97316;
170
+ border-radius: 2px;
171
+ pointer-events: all;
172
+ z-index: 1;
173
+ }
174
+ .dark .resize-handle {
175
+ background: #374151;
65
176
  }
66
- .dark .resize-handle { background: #374151; }
67
177
  </style>
68
178
  </head>
69
- <body class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 flex flex-col h-screen overflow-hidden">
70
-
179
+ <body
180
+ class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 flex flex-col h-screen overflow-hidden"
181
+ >
71
182
  <script>
72
- const _d = localStorage.getItem('ld-dark') === 'true';
73
- document.documentElement.classList.toggle('dark', _d);
183
+ const _d = localStorage.getItem("ld-dark") === "true";
184
+ document.documentElement.classList.toggle("dark", _d);
74
185
  </script>
75
186
 
76
187
  <!-- ── Top bar ── -->
77
- <header class="flex items-center gap-1 px-2 h-12 shrink-0 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 shadow-sm z-10">
78
- <a href="/" class="tool-btn !w-auto px-2 text-xs font-medium text-gray-500 dark:text-gray-400">← Docs</a>
188
+ <header
189
+ class="flex items-center gap-1 px-2 h-12 shrink-0 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 shadow-sm z-10"
190
+ >
191
+ <a
192
+ href="/"
193
+ class="tool-btn !w-auto px-2 text-xs font-medium text-gray-500 dark:text-gray-400"
194
+ >← Docs</a
195
+ >
79
196
  <div class="w-px h-6 bg-gray-200 dark:bg-gray-700 mx-0.5"></div>
80
- <button onclick="toggleSidebar()" class="tool-btn" title="Mes diagrammes">☰</button>
197
+ <button id="btnSidebar" class="tool-btn" title="Mes diagrammes">
198
+
199
+ </button>
81
200
  <div class="w-px h-6 bg-gray-200 dark:bg-gray-700 mx-0.5"></div>
82
201
 
83
- <button id="toolSelect" onclick="setTool('select')" class="tool-btn tool-active" title="Sélectionner (S)">
84
- <svg width="11" height="13" viewBox="0 0 11 13" fill="currentColor" stroke="none"><path d="M1 1 L1 12 L4 9 L6.5 13 L8 12.2 L5.5 8.2 L10 8.2 Z"/></svg>
202
+ <button
203
+ id="toolSelect"
204
+ class="tool-btn tool-active"
205
+ title="Sélectionner (S)"
206
+ >
207
+ <svg
208
+ width="11"
209
+ height="13"
210
+ viewBox="0 0 11 13"
211
+ fill="currentColor"
212
+ stroke="none"
213
+ >
214
+ <path d="M1 1 L1 12 L4 9 L6.5 13 L8 12.2 L5.5 8.2 L10 8.2 Z" />
215
+ </svg>
85
216
  </button>
86
217
  <div class="w-px h-6 bg-gray-200 dark:bg-gray-700 mx-0.5"></div>
87
218
 
88
- <button id="toolBox" onclick="setTool('addNode','box')" class="tool-btn" title="Rectangle (R)">
89
- <svg width="15" height="10" viewBox="0 0 15 10" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="1" y="1" width="13" height="8" rx="1"/></svg>
219
+ <button
220
+ id="toolBox"
221
+ class="tool-btn"
222
+ title="Rectangle (R)"
223
+ >
224
+ <svg
225
+ width="15"
226
+ height="10"
227
+ viewBox="0 0 15 10"
228
+ fill="none"
229
+ stroke="currentColor"
230
+ stroke-width="1.5"
231
+ >
232
+ <rect x="1" y="1" width="13" height="8" rx="1" />
233
+ </svg>
90
234
  </button>
91
- <button id="toolEllipse" onclick="setTool('addNode','ellipse')" class="tool-btn" title="Ellipse (E)">
92
- <svg width="16" height="10" viewBox="0 0 16 10" fill="none" stroke="currentColor" stroke-width="1.5"><ellipse cx="8" cy="5" rx="7" ry="4"/></svg>
235
+ <button
236
+ id="toolEllipse"
237
+ class="tool-btn"
238
+ title="Ellipse (E)"
239
+ >
240
+ <svg
241
+ width="16"
242
+ height="10"
243
+ viewBox="0 0 16 10"
244
+ fill="none"
245
+ stroke="currentColor"
246
+ stroke-width="1.5"
247
+ >
248
+ <ellipse cx="8" cy="5" rx="7" ry="4" />
249
+ </svg>
93
250
  </button>
94
- <button id="toolDatabase" onclick="setTool('addNode','database')" class="tool-btn" title="Base de données (D)">
95
- <svg width="12" height="16" viewBox="0 0 12 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
96
- <ellipse cx="6" cy="3" rx="5" ry="2"/>
97
- <path d="M1 3v10c0 1.1 2.2 2 5 2s5-.9 5-2V3"/>
98
- <path d="M11 7.5c0 1.1-2.2 2-5 2s-5-.9-5-2"/>
251
+ <button
252
+ id="toolDatabase"
253
+ class="tool-btn"
254
+ title="Base de données (D)"
255
+ >
256
+ <svg
257
+ width="12"
258
+ height="16"
259
+ viewBox="0 0 12 16"
260
+ fill="none"
261
+ stroke="currentColor"
262
+ stroke-width="1.5"
263
+ stroke-linecap="round"
264
+ >
265
+ <ellipse cx="6" cy="3" rx="5" ry="2" />
266
+ <path d="M1 3v10c0 1.1 2.2 2 5 2s5-.9 5-2V3" />
267
+ <path d="M11 7.5c0 1.1-2.2 2-5 2s-5-.9-5-2" />
99
268
  </svg>
100
269
  </button>
101
- <button id="toolCircle" onclick="setTool('addNode','circle')" class="tool-btn" title="Cercle (C)">
102
- <svg width="13" height="13" viewBox="0 0 13 13" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="6.5" cy="6.5" r="5.5"/></svg>
270
+ <button
271
+ id="toolCircle"
272
+ class="tool-btn"
273
+ title="Cercle (C)"
274
+ >
275
+ <svg
276
+ width="13"
277
+ height="13"
278
+ viewBox="0 0 13 13"
279
+ fill="none"
280
+ stroke="currentColor"
281
+ stroke-width="1.5"
282
+ >
283
+ <circle cx="6.5" cy="6.5" r="5.5" />
284
+ </svg>
103
285
  </button>
104
- <button id="toolActor" onclick="setTool('addNode','actor')" class="tool-btn" title="Acteur (A)">
105
- <svg width="12" height="17" viewBox="0 0 12 17" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
106
- <circle cx="6" cy="3" r="2.2"/>
107
- <line x1="6" y1="5.2" x2="6" y2="10"/>
108
- <line x1="1.5" y1="7.5" x2="10.5" y2="7.5"/>
109
- <line x1="6" y1="10" x2="2.5" y2="15"/>
110
- <line x1="6" y1="10" x2="9.5" y2="15"/>
286
+ <button
287
+ id="toolActor"
288
+ class="tool-btn"
289
+ title="Acteur (A)"
290
+ >
291
+ <svg
292
+ width="12"
293
+ height="17"
294
+ viewBox="0 0 12 17"
295
+ fill="none"
296
+ stroke="currentColor"
297
+ stroke-width="1.5"
298
+ stroke-linecap="round"
299
+ >
300
+ <circle cx="6" cy="3" r="2.2" />
301
+ <line x1="6" y1="5.2" x2="6" y2="10" />
302
+ <line x1="1.5" y1="7.5" x2="10.5" y2="7.5" />
303
+ <line x1="6" y1="10" x2="2.5" y2="15" />
304
+ <line x1="6" y1="10" x2="9.5" y2="15" />
111
305
  </svg>
112
306
  </button>
113
307
  <div class="w-px h-6 bg-gray-200 dark:bg-gray-700 mx-0.5"></div>
114
308
 
115
- <button id="toolArrow" onclick="setTool('addEdge')" class="tool-btn" title="Flèche (F)">→</button>
309
+ <button
310
+ id="toolArrow"
311
+ class="tool-btn"
312
+ title="Flèche (F)"
313
+ >
314
+
315
+ </button>
116
316
  <div class="w-px h-6 bg-gray-200 dark:bg-gray-700 mx-0.5"></div>
117
317
 
118
- <button onclick="deleteSelected()" class="tool-btn" title="Supprimer (Suppr)">
119
- <svg width="11" height="11" viewBox="0 0 11 11" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round">
120
- <line x1="1" y1="1" x2="10" y2="10"/><line x1="10" y1="1" x2="1" y2="10"/>
318
+ <button
319
+ id="btnDelete"
320
+ class="tool-btn"
321
+ title="Supprimer (Suppr)"
322
+ >
323
+ <svg
324
+ width="11"
325
+ height="11"
326
+ viewBox="0 0 11 11"
327
+ fill="none"
328
+ stroke="currentColor"
329
+ stroke-width="1.8"
330
+ stroke-linecap="round"
331
+ >
332
+ <line x1="1" y1="1" x2="10" y2="10" />
333
+ <line x1="10" y1="1" x2="1" y2="10" />
121
334
  </svg>
122
335
  </button>
123
336
  <div class="w-px h-6 bg-gray-200 dark:bg-gray-700 mx-0.5"></div>
124
337
 
125
- <button id="btnPhysics" onclick="togglePhysics()" class="tool-btn tool-active" title="Auto-espacement actif">
126
- <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.4">
127
- <circle cx="7" cy="7" r="2"/>
128
- <path d="M7 1.5v2M7 10.5v2M1.5 7h2M10.5 7h2M3.4 3.4l1.4 1.4M9.2 9.2l1.4 1.4M10.6 3.4l-1.4 1.4M4.8 9.2l-1.4 1.4"/>
338
+ <button
339
+ id="btnPhysics"
340
+ class="tool-btn"
341
+ title="Auto-espacement actif"
342
+ >
343
+ <svg
344
+ width="14"
345
+ height="14"
346
+ viewBox="0 0 14 14"
347
+ fill="none"
348
+ stroke="currentColor"
349
+ stroke-width="1.4"
350
+ >
351
+ <circle cx="7" cy="7" r="2" />
352
+ <path
353
+ d="M7 1.5v2M7 10.5v2M1.5 7h2M10.5 7h2M3.4 3.4l1.4 1.4M9.2 9.2l1.4 1.4M10.6 3.4l-1.4 1.4M4.8 9.2l-1.4 1.4"
354
+ />
129
355
  </svg>
130
356
  </button>
131
- <button id="btnGrid" onclick="toggleGrid()" class="tool-btn" title="Grille / Snap to grid (G)">
132
- <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.2">
133
- <line x1="5" y1="1" x2="5" y2="13"/><line x1="9" y1="1" x2="9" y2="13"/>
134
- <line x1="1" y1="5" x2="13" y2="5"/><line x1="1" y1="9" x2="13" y2="9"/>
135
- <rect x="1" y="1" width="12" height="12" rx="1"/>
357
+ <button
358
+ id="btnGrid"
359
+ class="tool-btn"
360
+ title="Grille / Snap to grid (G)"
361
+ >
362
+ <svg
363
+ width="14"
364
+ height="14"
365
+ viewBox="0 0 14 14"
366
+ fill="none"
367
+ stroke="currentColor"
368
+ stroke-width="1.2"
369
+ >
370
+ <line x1="5" y1="1" x2="5" y2="13" />
371
+ <line x1="9" y1="1" x2="9" y2="13" />
372
+ <line x1="1" y1="5" x2="13" y2="5" />
373
+ <line x1="1" y1="9" x2="13" y2="9" />
374
+ <rect x="1" y="1" width="12" height="12" rx="1" />
136
375
  </svg>
137
376
  </button>
138
377
  <div class="w-px h-6 bg-gray-200 dark:bg-gray-700 mx-0.5"></div>
139
378
 
140
- <input id="diagramTitle" type="text" placeholder="Titre du diagramme"
141
- class="flex-1 min-w-0 px-2 py-1 text-sm bg-transparent border-0 focus:outline-none focus:ring-1 focus:ring-blue-500 rounded text-gray-700 dark:text-gray-300 placeholder:text-gray-400"/>
379
+ <input
380
+ id="diagramTitle"
381
+ type="text"
382
+ placeholder="Titre du diagramme"
383
+ class="flex-1 min-w-0 px-2 py-1 text-sm bg-transparent border-0 focus:outline-none focus:ring-1 focus:ring-blue-500 rounded text-gray-700 dark:text-gray-300 placeholder:text-gray-400"
384
+ />
142
385
  <div class="w-px h-6 bg-gray-200 dark:bg-gray-700 mx-0.5"></div>
143
386
 
144
- <button onclick="adjustZoom(-0.2)" class="tool-btn" title="Zoom −">−</button>
145
- <span id="zoomLevel" class="text-xs text-gray-500 dark:text-gray-400 w-10 text-center tabular-nums select-none">100%</span>
146
- <button onclick="adjustZoom(0.2)" class="tool-btn" title="Zoom +">+</button>
147
- <button onclick="resetZoom()" class="tool-btn" title="Ajuster la vue">⊡</button>
387
+ <button id="btnZoomOut" class="tool-btn" title="Zoom −">
388
+
389
+ </button>
390
+ <span
391
+ id="zoomLevel"
392
+ class="text-xs text-gray-500 dark:text-gray-400 w-10 text-center tabular-nums select-none"
393
+ >100%</span
394
+ >
395
+ <button id="btnZoomIn" class="tool-btn" title="Zoom +">
396
+ +
397
+ </button>
398
+ <button id="btnZoomReset" class="tool-btn" title="Ajuster la vue">
399
+
400
+ </button>
148
401
  <div class="w-px h-6 bg-gray-200 dark:bg-gray-700 mx-0.5"></div>
149
402
 
150
- <button onclick="toggleDark()" class="tool-btn"><span id="darkIcon">☽</span></button>
403
+ <button id="btnDark" class="tool-btn">
404
+ <span id="darkIcon">☽</span>
405
+ </button>
151
406
  <div class="w-px h-6 bg-gray-200 dark:bg-gray-700 mx-0.5"></div>
152
407
 
153
- <button id="btnSave" onclick="saveDiagram()" disabled
154
- class="px-3 py-1.5 text-xs font-semibold rounded-lg bg-blue-600 hover:bg-blue-700 text-white disabled:opacity-40 disabled:cursor-not-allowed shrink-0 transition-colors">
408
+ <button
409
+ id="btnDebug"
410
+ class="hidden tool-btn text-xs font-mono"
411
+ title="Debug overlay"
412
+ >
413
+ dbg
414
+ </button>
415
+ <div
416
+ id="sepDebug"
417
+ class="hidden w-px h-6 bg-gray-200 dark:bg-gray-700 mx-0.5"
418
+ ></div>
419
+
420
+ <button
421
+ id="btnSave"
422
+ disabled
423
+ class="px-3 py-1.5 text-xs font-semibold rounded-lg bg-blue-600 hover:bg-blue-700 text-white disabled:opacity-40 disabled:cursor-not-allowed shrink-0 transition-colors"
424
+ >
155
425
  Enregistrer
156
426
  </button>
157
427
  </header>
158
428
 
159
429
  <!-- ── Body ── -->
160
430
  <div class="flex flex-1 overflow-hidden">
161
-
162
431
  <!-- Sidebar -->
163
- <div id="sidebar"
432
+ <div
433
+ id="sidebar"
164
434
  class="w-56 shrink-0 border-r border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 flex flex-col overflow-hidden"
165
- style="transition: width 0.2s ease;">
166
- <div class="flex items-center justify-between px-3 py-2 border-b border-gray-200 dark:border-gray-700 shrink-0">
167
- <span class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Diagrammes</span>
168
- <button onclick="newDiagram()" class="text-xs font-medium text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 transition-colors">+ Nouveau</button>
435
+ style="transition: width 0.2s ease"
436
+ >
437
+ <div
438
+ class="flex items-center justify-between px-3 py-2 border-b border-gray-200 dark:border-gray-700 shrink-0"
439
+ >
440
+ <span
441
+ class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider"
442
+ >Diagrammes</span
443
+ >
444
+ <button
445
+ id="btnNewDiagram"
446
+ class="text-xs font-medium text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 transition-colors"
447
+ >
448
+ + Nouveau
449
+ </button>
169
450
  </div>
170
451
  <div id="diagramList" class="flex-1 overflow-y-auto py-1"></div>
171
452
  </div>
172
453
 
173
454
  <!-- Canvas area -->
174
455
  <div class="relative flex-1 overflow-hidden bg-gray-50 dark:bg-gray-950">
175
-
176
456
  <div id="vis-canvas" class="w-full h-full"></div>
177
457
 
458
+ <!-- Debug overlay layer -->
459
+ <div id="debugLayer"></div>
460
+
178
461
  <!-- Selection / resize overlay -->
179
462
  <div id="selectionOverlay">
180
- <div id="rh-tl" class="resize-handle" style="top:-5px;left:-5px; cursor:nw-resize;"></div>
181
- <div id="rh-tr" class="resize-handle" style="top:-5px;right:-5px; cursor:ne-resize;"></div>
182
- <div id="rh-bl" class="resize-handle" style="bottom:-5px;left:-5px; cursor:sw-resize;"></div>
183
- <div id="rh-br" class="resize-handle" style="bottom:-5px;right:-5px; cursor:se-resize;"></div>
463
+ <div
464
+ id="rh-tl"
465
+ class="resize-handle"
466
+ style="top: -5px; left: -5px; cursor: nw-resize"
467
+ ></div>
468
+ <div
469
+ id="rh-tr"
470
+ class="resize-handle"
471
+ style="top: -5px; right: -5px; cursor: ne-resize"
472
+ ></div>
473
+ <div
474
+ id="rh-bl"
475
+ class="resize-handle"
476
+ style="bottom: -5px; left: -5px; cursor: sw-resize"
477
+ ></div>
478
+ <div
479
+ id="rh-br"
480
+ class="resize-handle"
481
+ style="bottom: -5px; right: -5px; cursor: se-resize"
482
+ ></div>
184
483
  </div>
185
484
 
186
485
  <!-- Node panel -->
187
486
  <div id="nodePanel" class="float-panel hidden">
188
- <button onclick="setNodeColor('c-gray')" class="w-5 h-5 rounded-full border-2 border-stone-400 bg-stone-200 hover:scale-125 transition-transform" title="Gris"></button>
189
- <button onclick="setNodeColor('c-blue')" class="w-5 h-5 rounded-full border-2 border-blue-500 bg-blue-200 hover:scale-125 transition-transform" title="Bleu"></button>
190
- <button onclick="setNodeColor('c-green')" class="w-5 h-5 rounded-full border-2 border-green-500 bg-green-200 hover:scale-125 transition-transform" title="Vert"></button>
191
- <button onclick="setNodeColor('c-amber')" class="w-5 h-5 rounded-full border-2 border-amber-500 bg-amber-200 hover:scale-125 transition-transform" title="Ambre"></button>
192
- <button onclick="setNodeColor('c-rose')" class="w-5 h-5 rounded-full border-2 border-rose-500 bg-rose-200 hover:scale-125 transition-transform" title="Rose"></button>
193
- <button onclick="setNodeColor('c-purple')" class="w-5 h-5 rounded-full border-2 border-purple-500 bg-purple-200 hover:scale-125 transition-transform" title="Violet"></button>
194
- <button onclick="setNodeColor('c-teal')" class="w-5 h-5 rounded-full border-2 border-teal-500 bg-teal-200 hover:scale-125 transition-transform" title="Teal"></button>
487
+ <button
488
+ data-color="c-gray"
489
+ class="w-5 h-5 rounded-full border-2 border-stone-400 bg-stone-200 hover:scale-125 transition-transform"
490
+ title="Gris"
491
+ ></button>
492
+ <button
493
+ data-color="c-slate"
494
+ class="w-5 h-5 rounded-full border-2 border-slate-500 bg-slate-100 hover:scale-125 transition-transform"
495
+ title="Ardoise"
496
+ ></button>
497
+ <button
498
+ data-color="c-blue"
499
+ class="w-5 h-5 rounded-full border-2 border-blue-500 bg-blue-200 hover:scale-125 transition-transform"
500
+ title="Bleu"
501
+ ></button>
502
+ <button
503
+ data-color="c-sky"
504
+ class="w-5 h-5 rounded-full border-2 border-sky-500 bg-sky-200 hover:scale-125 transition-transform"
505
+ title="Ciel"
506
+ ></button>
507
+ <button
508
+ data-color="c-cyan"
509
+ class="w-5 h-5 rounded-full border-2 border-cyan-500 bg-cyan-200 hover:scale-125 transition-transform"
510
+ title="Cyan"
511
+ ></button>
512
+ <button
513
+ data-color="c-teal"
514
+ class="w-5 h-5 rounded-full border-2 border-teal-500 bg-teal-200 hover:scale-125 transition-transform"
515
+ title="Teal"
516
+ ></button>
517
+ <button
518
+ data-color="c-green"
519
+ class="w-5 h-5 rounded-full border-2 border-green-500 bg-green-200 hover:scale-125 transition-transform"
520
+ title="Vert"
521
+ ></button>
522
+ <button
523
+ data-color="c-lime"
524
+ class="w-5 h-5 rounded-full border-2 border-lime-500 bg-lime-200 hover:scale-125 transition-transform"
525
+ title="Citron"
526
+ ></button>
527
+ <button
528
+ data-color="c-amber"
529
+ class="w-5 h-5 rounded-full border-2 border-amber-500 bg-amber-200 hover:scale-125 transition-transform"
530
+ title="Ambre"
531
+ ></button>
532
+ <button
533
+ data-color="c-orange"
534
+ class="w-5 h-5 rounded-full border-2 border-orange-500 bg-orange-200 hover:scale-125 transition-transform"
535
+ title="Orange"
536
+ ></button>
537
+ <button
538
+ data-color="c-red"
539
+ class="w-5 h-5 rounded-full border-2 border-red-500 bg-red-200 hover:scale-125 transition-transform"
540
+ title="Rouge"
541
+ ></button>
542
+ <button
543
+ data-color="c-rose"
544
+ class="w-5 h-5 rounded-full border-2 border-rose-500 bg-rose-200 hover:scale-125 transition-transform"
545
+ title="Rose"
546
+ ></button>
547
+ <button
548
+ data-color="c-pink"
549
+ class="w-5 h-5 rounded-full border-2 border-pink-500 bg-pink-200 hover:scale-125 transition-transform"
550
+ title="Fuschia"
551
+ ></button>
552
+ <button
553
+ data-color="c-purple"
554
+ class="w-5 h-5 rounded-full border-2 border-purple-500 bg-purple-200 hover:scale-125 transition-transform"
555
+ title="Violet"
556
+ ></button>
557
+ <button
558
+ data-color="c-indigo"
559
+ class="w-5 h-5 rounded-full border-2 border-indigo-500 bg-indigo-200 hover:scale-125 transition-transform"
560
+ title="Indigo"
561
+ ></button>
195
562
  <div class="panel-sep"></div>
196
- <button onclick="startLabelEdit()" class="tool-btn !w-6 !h-6" title="Modifier le texte (double-clic)">✎</button>
563
+ <button
564
+ id="btnNodeLabelEdit"
565
+ class="tool-btn !w-6 !h-6"
566
+ title="Modifier le texte (double-clic)"
567
+ >
568
+
569
+ </button>
197
570
  <div class="panel-sep"></div>
198
- <button onclick="changeNodeFontSize(-1)" class="tool-btn !w-8 !h-6" style="font-size:10px;" title="Réduire la police">Aa−</button>
199
- <button onclick="changeNodeFontSize( 1)" class="tool-btn !w-8 !h-6" style="font-size:10px;" title="Agrandir la police">Aa+</button>
571
+ <button
572
+ id="btnNodeFontDecrease"
573
+ class="tool-btn !w-8 !h-6"
574
+ style="font-size: 10px"
575
+ title="Réduire la police"
576
+ >
577
+ Aa−
578
+ </button>
579
+ <button
580
+ id="btnNodeFontIncrease"
581
+ class="tool-btn !w-8 !h-6"
582
+ style="font-size: 10px"
583
+ title="Agrandir la police"
584
+ >
585
+ Aa+
586
+ </button>
200
587
  <div class="panel-sep"></div>
201
588
  <!-- Horizontal align -->
202
- <button onclick="setTextAlign('left')" class="tool-btn !w-6 !h-6" title="Aligner à gauche">
203
- <svg width="12" height="10" viewBox="0 0 12 10" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round">
204
- <line x1="1" y1="2" x2="11" y2="2"/><line x1="1" y1="5" x2="7" y2="5"/><line x1="1" y1="8" x2="9" y2="8"/>
589
+ <button
590
+ id="btnAlignLeft"
591
+ class="tool-btn !w-6 !h-6"
592
+ title="Aligner à gauche"
593
+ >
594
+ <svg
595
+ width="12"
596
+ height="10"
597
+ viewBox="0 0 12 10"
598
+ fill="none"
599
+ stroke="currentColor"
600
+ stroke-width="1.4"
601
+ stroke-linecap="round"
602
+ >
603
+ <line x1="1" y1="2" x2="11" y2="2" />
604
+ <line x1="1" y1="5" x2="7" y2="5" />
605
+ <line x1="1" y1="8" x2="9" y2="8" />
205
606
  </svg>
206
607
  </button>
207
- <button onclick="setTextAlign('center')" class="tool-btn !w-6 !h-6" title="Centrer horizontalement">
208
- <svg width="12" height="10" viewBox="0 0 12 10" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round">
209
- <line x1="1" y1="2" x2="11" y2="2"/><line x1="2.5" y1="5" x2="9.5" y2="5"/><line x1="1.5" y1="8" x2="10.5" y2="8"/>
608
+ <button
609
+ id="btnAlignCenter"
610
+ class="tool-btn !w-6 !h-6"
611
+ title="Centrer horizontalement"
612
+ >
613
+ <svg
614
+ width="12"
615
+ height="10"
616
+ viewBox="0 0 12 10"
617
+ fill="none"
618
+ stroke="currentColor"
619
+ stroke-width="1.4"
620
+ stroke-linecap="round"
621
+ >
622
+ <line x1="1" y1="2" x2="11" y2="2" />
623
+ <line x1="2.5" y1="5" x2="9.5" y2="5" />
624
+ <line x1="1.5" y1="8" x2="10.5" y2="8" />
210
625
  </svg>
211
626
  </button>
212
- <button onclick="setTextAlign('right')" class="tool-btn !w-6 !h-6" title="Aligner à droite">
213
- <svg width="12" height="10" viewBox="0 0 12 10" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round">
214
- <line x1="1" y1="2" x2="11" y2="2"/><line x1="5" y1="5" x2="11" y2="5"/><line x1="3" y1="8" x2="11" y2="8"/>
627
+ <button
628
+ id="btnAlignRight"
629
+ class="tool-btn !w-6 !h-6"
630
+ title="Aligner à droite"
631
+ >
632
+ <svg
633
+ width="12"
634
+ height="10"
635
+ viewBox="0 0 12 10"
636
+ fill="none"
637
+ stroke="currentColor"
638
+ stroke-width="1.4"
639
+ stroke-linecap="round"
640
+ >
641
+ <line x1="1" y1="2" x2="11" y2="2" />
642
+ <line x1="5" y1="5" x2="11" y2="5" />
643
+ <line x1="3" y1="8" x2="11" y2="8" />
215
644
  </svg>
216
645
  </button>
217
646
  <div class="panel-sep"></div>
218
647
  <!-- Vertical align -->
219
- <button onclick="setTextValign('top')" class="tool-btn !w-6 !h-6" title="Aligner en haut">
220
- <svg width="10" height="12" viewBox="0 0 10 12" fill="none" stroke="currentColor" stroke-linecap="round">
221
- <line x1="1" y1="1.5" x2="9" y2="1.5" stroke-width="2"/>
222
- <line x1="1" y1="5" x2="8" y2="5" stroke-width="1.3"/>
223
- <line x1="1" y1="8.5" x2="6" y2="8.5" stroke-width="1.3"/>
648
+ <button
649
+ id="btnValignTop"
650
+ class="tool-btn !w-6 !h-6"
651
+ title="Aligner en haut"
652
+ >
653
+ <svg
654
+ width="10"
655
+ height="12"
656
+ viewBox="0 0 10 12"
657
+ fill="none"
658
+ stroke="currentColor"
659
+ stroke-linecap="round"
660
+ >
661
+ <line x1="1" y1="1.5" x2="9" y2="1.5" stroke-width="2" />
662
+ <line x1="1" y1="5" x2="8" y2="5" stroke-width="1.3" />
663
+ <line x1="1" y1="8.5" x2="6" y2="8.5" stroke-width="1.3" />
224
664
  </svg>
225
665
  </button>
226
- <button onclick="setTextValign('middle')" class="tool-btn !w-6 !h-6" title="Centrer verticalement">
227
- <svg width="10" height="12" viewBox="0 0 10 12" fill="none" stroke="currentColor" stroke-linecap="round">
228
- <line x1="1" y1="2.5" x2="8" y2="2.5" stroke-width="1.3"/>
229
- <line x1="1" y1="6" x2="9" y2="6" stroke-width="2"/>
230
- <line x1="1" y1="9.5" x2="6" y2="9.5" stroke-width="1.3"/>
666
+ <button
667
+ id="btnValignMiddle"
668
+ class="tool-btn !w-6 !h-6"
669
+ title="Centrer verticalement"
670
+ >
671
+ <svg
672
+ width="10"
673
+ height="12"
674
+ viewBox="0 0 10 12"
675
+ fill="none"
676
+ stroke="currentColor"
677
+ stroke-linecap="round"
678
+ >
679
+ <line x1="1" y1="2.5" x2="8" y2="2.5" stroke-width="1.3" />
680
+ <line x1="1" y1="6" x2="9" y2="6" stroke-width="2" />
681
+ <line x1="1" y1="9.5" x2="6" y2="9.5" stroke-width="1.3" />
231
682
  </svg>
232
683
  </button>
233
- <button onclick="setTextValign('bottom')" class="tool-btn !w-6 !h-6" title="Aligner en bas">
234
- <svg width="10" height="12" viewBox="0 0 10 12" fill="none" stroke="currentColor" stroke-linecap="round">
235
- <line x1="1" y1="3.5" x2="8" y2="3.5" stroke-width="1.3"/>
236
- <line x1="1" y1="7" x2="6" y2="7" stroke-width="1.3"/>
237
- <line x1="1" y1="10.5" x2="9" y2="10.5" stroke-width="2"/>
684
+ <button
685
+ id="btnValignBottom"
686
+ class="tool-btn !w-6 !h-6"
687
+ title="Aligner en bas"
688
+ >
689
+ <svg
690
+ width="10"
691
+ height="12"
692
+ viewBox="0 0 10 12"
693
+ fill="none"
694
+ stroke="currentColor"
695
+ stroke-linecap="round"
696
+ >
697
+ <line x1="1" y1="3.5" x2="8" y2="3.5" stroke-width="1.3" />
698
+ <line x1="1" y1="7" x2="6" y2="7" stroke-width="1.3" />
699
+ <line x1="1" y1="10.5" x2="9" y2="10.5" stroke-width="2" />
238
700
  </svg>
239
701
  </button>
240
702
  <div class="panel-sep"></div>
241
- <button onclick="changeZOrder(-1)" class="tool-btn !w-7 !h-6" title="Envoyer en arrière">
242
- <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
243
- <rect x="1" y="5" width="8" height="8" rx="1"/>
244
- <rect x="5" y="1" width="8" height="8" rx="1" stroke-opacity="0.35"/>
703
+ <button
704
+ id="btnZOrderBack"
705
+ class="tool-btn !w-7 !h-6"
706
+ title="Envoyer en arrière"
707
+ >
708
+ <svg
709
+ width="14"
710
+ height="14"
711
+ viewBox="0 0 14 14"
712
+ fill="none"
713
+ stroke="currentColor"
714
+ stroke-width="1.4"
715
+ stroke-linecap="round"
716
+ stroke-linejoin="round"
717
+ >
718
+ <rect x="1" y="5" width="8" height="8" rx="1" />
719
+ <rect
720
+ x="5"
721
+ y="1"
722
+ width="8"
723
+ height="8"
724
+ rx="1"
725
+ stroke-opacity="0.35"
726
+ />
245
727
  </svg>
246
728
  </button>
247
- <button onclick="changeZOrder( 1)" class="tool-btn !w-7 !h-6" title="Amener au premier plan">
248
- <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
249
- <rect x="1" y="5" width="8" height="8" rx="1" stroke-opacity="0.35"/>
250
- <rect x="5" y="1" width="8" height="8" rx="1"/>
729
+ <button
730
+ id="btnZOrderFront"
731
+ class="tool-btn !w-7 !h-6"
732
+ title="Amener au premier plan"
733
+ >
734
+ <svg
735
+ width="14"
736
+ height="14"
737
+ viewBox="0 0 14 14"
738
+ fill="none"
739
+ stroke="currentColor"
740
+ stroke-width="1.4"
741
+ stroke-linecap="round"
742
+ stroke-linejoin="round"
743
+ >
744
+ <rect
745
+ x="1"
746
+ y="5"
747
+ width="8"
748
+ height="8"
749
+ rx="1"
750
+ stroke-opacity="0.35"
751
+ />
752
+ <rect x="5" y="1" width="8" height="8" rx="1" />
251
753
  </svg>
252
754
  </button>
253
755
  </div>
254
756
 
255
757
  <!-- Edge panel -->
256
758
  <div id="edgePanel" class="float-panel hidden">
257
- <button id="edgeBtnNone" onclick="setEdgeArrow('none')" class="tool-btn !w-7 !h-6 font-mono text-xs" title="Trait simple">—</button>
258
- <button id="edgeBtnTo" onclick="setEdgeArrow('to')" class="tool-btn !w-7 !h-6 text-xs" title="Flèche directionnelle">→</button>
259
- <button id="edgeBtnBoth" onclick="setEdgeArrow('both')" class="tool-btn !w-8 !h-6 text-xs" title="Bidirectionnelle">←→</button>
759
+ <button
760
+ id="edgeBtnNone"
761
+ class="tool-btn !w-7 !h-6 font-mono text-xs"
762
+ title="Trait simple"
763
+ >
764
+
765
+ </button>
766
+ <button
767
+ id="edgeBtnTo"
768
+ class="tool-btn !w-7 !h-6 text-xs"
769
+ title="Flèche directionnelle"
770
+ >
771
+
772
+ </button>
773
+ <button
774
+ id="edgeBtnBoth"
775
+ class="tool-btn !w-8 !h-6 text-xs"
776
+ title="Bidirectionnelle"
777
+ >
778
+ ←→
779
+ </button>
260
780
  <div class="panel-sep"></div>
261
- <button id="edgeBtnSolid" onclick="setEdgeDashes(false)" class="tool-btn !w-8 !h-6" title="Trait plein">
262
- <svg width="22" height="4" viewBox="0 0 22 4"><line x1="1" y1="2" x2="21" y2="2" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
781
+ <button
782
+ id="edgeBtnSolid"
783
+ class="tool-btn !w-8 !h-6"
784
+ title="Trait plein"
785
+ >
786
+ <svg width="22" height="4" viewBox="0 0 22 4">
787
+ <line
788
+ x1="1"
789
+ y1="2"
790
+ x2="21"
791
+ y2="2"
792
+ stroke="currentColor"
793
+ stroke-width="2"
794
+ stroke-linecap="round"
795
+ />
796
+ </svg>
263
797
  </button>
264
- <button id="edgeBtnDashed" onclick="setEdgeDashes(true)" class="tool-btn !w-8 !h-6" title="Pointillé">
265
- <svg width="22" height="4" viewBox="0 0 22 4"><line x1="1" y1="2" x2="21" y2="2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-dasharray="4,3"/></svg>
798
+ <button
799
+ id="edgeBtnDashed"
800
+ class="tool-btn !w-8 !h-6"
801
+ title="Pointillé"
802
+ >
803
+ <svg width="22" height="4" viewBox="0 0 22 4">
804
+ <line
805
+ x1="1"
806
+ y1="2"
807
+ x2="21"
808
+ y2="2"
809
+ stroke="currentColor"
810
+ stroke-width="2"
811
+ stroke-linecap="round"
812
+ stroke-dasharray="4,3"
813
+ />
814
+ </svg>
266
815
  </button>
267
816
  <div class="panel-sep"></div>
268
- <button onclick="changeEdgeFontSize(-1)" class="tool-btn !w-8 !h-6" style="font-size:10px;" title="Réduire la police">Aa−</button>
269
- <button onclick="changeEdgeFontSize( 1)" class="tool-btn !w-8 !h-6" style="font-size:10px;" title="Agrandir la police">Aa+</button>
817
+ <button
818
+ id="btnEdgeFontDecrease"
819
+ class="tool-btn !w-8 !h-6"
820
+ style="font-size: 10px"
821
+ title="Réduire la police"
822
+ >
823
+ Aa−
824
+ </button>
825
+ <button
826
+ id="btnEdgeFontIncrease"
827
+ class="tool-btn !w-8 !h-6"
828
+ style="font-size: 10px"
829
+ title="Agrandir la police"
830
+ >
831
+ Aa+
832
+ </button>
270
833
  <div class="panel-sep"></div>
271
- <button onclick="startEdgeLabelEdit()" class="tool-btn !w-6 !h-6" title="Modifier le libellé de la flèche">✎</button>
834
+ <button
835
+ id="btnEdgeLabelEdit"
836
+ class="tool-btn !w-6 !h-6"
837
+ title="Modifier le libellé de la flèche"
838
+ >
839
+
840
+ </button>
272
841
  </div>
273
842
 
274
843
  <!-- Floating label textarea -->
275
- <textarea id="labelInput" class="hidden" rows="1" placeholder="Label…"></textarea>
844
+ <textarea
845
+ id="labelInput"
846
+ class="hidden"
847
+ rows="1"
848
+ placeholder="Label…"
849
+ ></textarea>
276
850
 
277
851
  <!-- Empty state -->
278
- <div id="emptyState"
279
- class="absolute inset-0 flex flex-col items-center justify-center text-gray-400 dark:text-gray-600 pointer-events-none select-none">
280
- <svg width="52" height="44" viewBox="0 0 52 44" fill="none" stroke="currentColor" stroke-width="1.5" class="mb-3 opacity-50">
281
- <rect x="2" y="4" width="18" height="12" rx="2"/>
282
- <rect x="32" y="28" width="18" height="12" rx="2"/>
283
- <line x1="20" y1="10" x2="32" y2="34" stroke-dasharray="4,3"/>
284
- <rect x="17" y="16" width="18" height="12" rx="2"/>
285
- <line x1="26" y1="16" x2="26" y2="10" stroke-dasharray="4,3"/>
852
+ <div
853
+ id="emptyState"
854
+ class="absolute inset-0 flex flex-col items-center justify-center text-gray-400 dark:text-gray-600 pointer-events-none select-none"
855
+ >
856
+ <svg
857
+ width="52"
858
+ height="44"
859
+ viewBox="0 0 52 44"
860
+ fill="none"
861
+ stroke="currentColor"
862
+ stroke-width="1.5"
863
+ class="mb-3 opacity-50"
864
+ >
865
+ <rect x="2" y="4" width="18" height="12" rx="2" />
866
+ <rect x="32" y="28" width="18" height="12" rx="2" />
867
+ <line x1="20" y1="10" x2="32" y2="34" stroke-dasharray="4,3" />
868
+ <rect x="17" y="16" width="18" height="12" rx="2" />
869
+ <line x1="26" y1="16" x2="26" y2="10" stroke-dasharray="4,3" />
286
870
  </svg>
287
871
  <p class="text-sm">Sélectionne ou crée un diagramme</p>
288
872
  </div>
289
873
  </div>
290
874
  </div>
291
875
 
292
- <script>
293
- // ── State ─────────────────────────────────────────────────────────────────
294
- let network = null;
295
- let nodes = null;
296
- let edges = null;
297
- let diagrams = [];
298
- let currentDiagramId = null;
299
- let currentTool = 'select';
300
- let pendingShape = 'box';
301
- let selectedNodeIds = [];
302
- let selectedEdgeIds = [];
303
- let physicsEnabled = true;
304
- let gridEnabled = false;
305
- const GRID_SIZE = 40;
306
- let isDirty = false;
307
- let sidebarOpen = true;
308
- let editingNodeId = null;
309
- let editingEdgeId = null;
310
- let resizeDrag = null;
311
- let clipboard = null; // { nodes: [], edges: [] }
312
- let _canonicalOrder = []; // canonical z-order (DataSet-independent, immune to vis.js hover reordering)
313
-
314
- // ── Colors ────────────────────────────────────────────────────────────────
315
- const NODE_COLORS = {
316
- 'c-gray': { bg:'#f5f5f4', border:'#a8a29e', font:'#292524', hbg:'#e7e5e4', hborder:'#78716c' },
317
- 'c-blue': { bg:'#dbeafe', border:'#3b82f6', font:'#1e40af', hbg:'#bfdbfe', hborder:'#2563eb' },
318
- 'c-green': { bg:'#dcfce7', border:'#22c55e', font:'#166534', hbg:'#bbf7d0', hborder:'#16a34a' },
319
- 'c-amber': { bg:'#fef9c3', border:'#f59e0b', font:'#78350f', hbg:'#fef08a', hborder:'#d97706' },
320
- 'c-rose': { bg:'#ffe4e6', border:'#f43f5e', font:'#881337', hbg:'#fecdd3', hborder:'#e11d48' },
321
- 'c-purple': { bg:'#ede9fe', border:'#8b5cf6', font:'#4c1d95', hbg:'#ddd6fe', hborder:'#7c3aed' },
322
- 'c-teal': { bg:'#ccfbf1', border:'#14b8a6', font:'#134e4a', hbg:'#99f6e4', hborder:'#0d9488' },
323
- };
324
-
325
- // ── Actor ctxRenderer ─────────────────────────────────────────────────────
326
- function makeActorRenderer(colorKey) {
327
- return function({ ctx, x, y, state: { selected } }) {
328
- const c = NODE_COLORS[colorKey] || NODE_COLORS['c-gray'];
329
- return {
330
- drawNode() {
331
- ctx.save();
332
- ctx.strokeStyle = selected ? '#f97316' : c.border;
333
- ctx.fillStyle = selected ? c.hbg : c.bg;
334
- ctx.lineWidth = 2; ctx.lineCap = 'round';
335
- ctx.beginPath(); ctx.arc(x, y - 20, 8, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
336
- ctx.beginPath(); ctx.moveTo(x, y - 12); ctx.lineTo(x, y + 8); ctx.stroke();
337
- ctx.beginPath(); ctx.moveTo(x - 13, y - 3); ctx.lineTo(x + 13, y - 3); ctx.stroke();
338
- ctx.beginPath(); ctx.moveTo(x, y + 8); ctx.lineTo(x - 10, y + 24); ctx.stroke();
339
- ctx.beginPath(); ctx.moveTo(x, y + 8); ctx.lineTo(x + 10, y + 24); ctx.stroke();
340
- ctx.restore();
341
- },
342
- nodeDimensions: { width: 30, height: 52 },
343
- };
344
- };
345
- }
346
-
347
- // ── Build vis.js node props ───────────────────────────────────────────────
348
- function computeVadjust(textValign, nodeHeight, fontSize) {
349
- if (!textValign || textValign === 'middle') return 0;
350
- const h = nodeHeight || 50;
351
- const fs = fontSize || 13;
352
- const pad = 8;
353
- if (textValign === 'top') return -(h / 2 - fs / 2 - pad);
354
- if (textValign === 'bottom') return (h / 2 - fs / 2 - pad);
355
- return 0;
356
- }
357
-
358
- function visNodeProps(shapeType, colorKey, nodeWidth, nodeHeight, fontSize, textAlign, textValign) {
359
- const c = NODE_COLORS[colorKey] || NODE_COLORS['c-gray'];
360
- const size = fontSize || 13;
361
- const align = textAlign || 'center';
362
- const vadjust = computeVadjust(textValign, nodeHeight, size);
363
- const colorP = {
364
- color: {
365
- background: c.bg, border: c.border,
366
- highlight: { background: c.hbg, border: c.hborder },
367
- hover: { background: c.hbg, border: c.hborder },
368
- },
369
- font: { color: c.font, size, face: 'system-ui,-apple-system,sans-serif', align, vadjust },
370
- };
371
- const sizeP = {};
372
- if (nodeWidth) sizeP.widthConstraint = { minimum: nodeWidth, maximum: nodeWidth };
373
- if (nodeHeight) sizeP.heightConstraint = { minimum: nodeHeight, maximum: nodeHeight };
374
-
375
- if (shapeType === 'actor') {
376
- return { shape: 'custom', ctxRenderer: makeActorRenderer(colorKey), ...colorP, ...sizeP };
377
- }
378
- return { shape: shapeType, ...colorP, ...sizeP };
379
- }
380
-
381
- // ── Build vis.js edge props ───────────────────────────────────────────────
382
- function visEdgeProps(arrowDir, dashes) {
383
- return {
384
- arrows: {
385
- to: { enabled: arrowDir === 'to' || arrowDir === 'both', scaleFactor: 0.7 },
386
- from: { enabled: arrowDir === 'both', scaleFactor: 0.7 },
387
- },
388
- dashes: dashes === true,
389
- };
390
- }
391
-
392
- // ── Network init ──────────────────────────────────────────────────────────
393
- function initNetwork(savedNodes, savedEdges) {
394
- const container = document.getElementById('vis-canvas');
395
-
396
- nodes = new vis.DataSet(savedNodes.map(n => ({
397
- ...n,
398
- ...visNodeProps(n.shapeType || 'box', n.colorKey || 'c-gray', n.nodeWidth, n.nodeHeight, n.fontSize, n.textAlign, n.textValign),
399
- })));
400
- edges = new vis.DataSet(savedEdges.map(e => ({
401
- ...e,
402
- ...visEdgeProps(e.arrowDir ?? 'to', e.dashes ?? false),
403
- ...(e.fontSize ? { font: { size: e.fontSize, align: 'middle', color: '#6b7280' } } : {}),
404
- })));
405
-
406
- const options = {
407
- physics: {
408
- enabled: physicsEnabled, stabilization: { enabled: false },
409
- barnesHut: { gravitationalConstant: -3000, centralGravity: 0.3, springLength: 150, springConstant: 0.04, damping: 0.09 },
410
- },
411
- interaction: {
412
- hover: true, navigationButtons: false, keyboard: false,
413
- multiselect: true, // Ctrl/Cmd+click for multi-select
414
- },
415
- nodes: {
416
- font: { size: 13, face: 'system-ui,-apple-system,sans-serif' },
417
- borderWidth: 1.5, borderWidthSelected: 2.5, shadow: false,
418
- widthConstraint: { minimum: 60 }, heightConstraint: { minimum: 28 },
419
- },
420
- edges: {
421
- smooth: { type: 'continuous' },
422
- color: { color: '#a8a29e', highlight: '#f97316', hover: '#f97316' },
423
- width: 1.5, selectionWidth: 2.5,
424
- font: { size: 11, align: 'middle', color: '#6b7280' },
425
- },
426
- manipulation: {
427
- enabled: false,
428
- addEdge(data, callback) {
429
- data.id = 'e' + Date.now();
430
- data.arrowDir = 'to';
431
- data.dashes = false;
432
- Object.assign(data, visEdgeProps('to', false));
433
- callback(data);
434
- markDirty();
435
- // Stay in arrow mode
436
- setTimeout(() => network.addEdgeMode(), 0);
437
- },
438
- },
439
- };
440
-
441
- if (network) network.destroy();
442
- network = new vis.Network(container, { nodes, edges }, options);
443
-
444
- // Preserve canonical z-order by patching the renderer instead of nodeIndices.
445
- // vis.js _drawNodes does 3 passes (normal → selected → hovered), always drawing
446
- // hover/selected nodes on top regardless of z-order. We replace it with a single
447
- // pass in _canonicalOrder so the user-defined stacking is always respected.
448
- _canonicalOrder = [...network.body.nodeIndices];
449
- network.renderer._drawNodes = function(ctx, alwaysShow = false) {
450
- const bodyNodes = this.body.nodes;
451
- const margin = 20;
452
- const topLeft = this.canvas.DOMtoCanvas({ x: -margin, y: -margin });
453
- const bottomRight = this.canvas.DOMtoCanvas({
454
- x: this.canvas.frame.canvas.clientWidth + margin,
455
- y: this.canvas.frame.canvas.clientHeight + margin,
456
- });
457
- const viewableArea = {
458
- top: topLeft.y, left: topLeft.x,
459
- bottom: bottomRight.y, right: bottomRight.x,
460
- };
461
- const drawExternalLabelCallbacks = [];
462
- for (const id of _canonicalOrder) {
463
- const node = bodyNodes[id];
464
- if (!node) continue;
465
- if (alwaysShow === true) {
466
- const r = node.draw(ctx);
467
- if (r.drawExternalLabel != null) drawExternalLabelCallbacks.push(r.drawExternalLabel);
468
- } else if (node.isBoundingBoxOverlappingWith(viewableArea) === true) {
469
- const r = node.draw(ctx);
470
- if (r.drawExternalLabel != null) drawExternalLabelCallbacks.push(r.drawExternalLabel);
471
- } else {
472
- node.updateBoundingBox(ctx, node.selected);
473
- }
474
- }
475
- return {
476
- drawExternalLabels() {
477
- for (const draw of drawExternalLabelCallbacks) draw();
478
- },
479
- };
480
- };
481
-
482
- // Keep _canonicalOrder in sync when nodes are added or removed via the DataSet
483
- nodes.on('add', (_, { items }) => {
484
- const existing = new Set(_canonicalOrder);
485
- items.forEach(id => { if (!existing.has(id)) _canonicalOrder.push(id); });
486
- });
487
- nodes.on('remove', (_, { items }) => {
488
- const removed = new Set(items);
489
- _canonicalOrder = _canonicalOrder.filter(id => !removed.has(id));
490
- });
491
-
492
- network.on('doubleClick', onDoubleClick);
493
- network.on('selectNode', onSelectNode);
494
- network.on('deselectNode', onDeselectAll);
495
- network.on('selectEdge', onSelectEdge);
496
- network.on('deselectEdge', onDeselectAll);
497
- network.on('zoom', updateZoomDisplay);
498
- network.on('dragEnd', onDragEnd);
499
- network.on('beforeDrawing', drawGrid);
500
- network.on('afterDrawing', updateSelectionOverlay);
501
-
502
- document.getElementById('emptyState').classList.add('hidden');
503
- updateZoomDisplay();
504
- }
505
-
506
- // ── Network events ────────────────────────────────────────────────────────
507
- function onDoubleClick(params) {
508
- if (params.nodes.length > 0) {
509
- // Edit label of double-clicked node
510
- selectedNodeIds = params.nodes;
511
- network.selectNodes(selectedNodeIds);
512
- showNodePanel();
513
- startLabelEdit();
514
- } else if (params.edges.length > 0 && params.nodes.length === 0) {
515
- // Edit label of double-clicked edge
516
- selectedEdgeIds = [params.edges[0]];
517
- showEdgePanel();
518
- startEdgeLabelEdit();
519
- } else if (currentTool === 'addNode') {
520
- // Add node at double-click position
521
- const id = 'n' + Date.now();
522
- nodes.add({
523
- id, label: 'Node',
524
- shapeType: pendingShape, colorKey: 'c-gray',
525
- nodeWidth: null, nodeHeight: null, fontSize: null,
526
- x: params.pointer.canvas.x, y: params.pointer.canvas.y,
527
- ...visNodeProps(pendingShape, 'c-gray', null, null, null, null, null),
528
- });
529
- markDirty();
530
- setTimeout(() => {
531
- network.selectNodes([id]);
532
- selectedNodeIds = [id];
533
- showNodePanel();
534
- startLabelEdit();
535
- }, 50);
536
- }
537
- }
538
-
539
- function onSelectNode(params) {
540
- selectedNodeIds = params.nodes;
541
- selectedEdgeIds = [];
542
- hideEdgePanel();
543
- showNodePanel();
544
- }
545
-
546
- function onSelectEdge(params) {
547
- if (selectedNodeIds.length > 0) return; // node takes priority
548
- selectedEdgeIds = params.edges;
549
- selectedNodeIds = [];
550
- hideNodePanel();
551
- showEdgePanel();
552
- }
553
-
554
- function onDeselectAll() {
555
- selectedNodeIds = [];
556
- selectedEdgeIds = [];
557
- hideNodePanel();
558
- hideEdgePanel();
559
- commitLabelEdit();
560
- hideLabelInput();
561
- hideSelectionOverlay();
562
- }
563
-
564
- // ── Node panel ────────────────────────────────────────────────────────────
565
- function showNodePanel() { document.getElementById('nodePanel').classList.remove('hidden'); }
566
- function hideNodePanel() { document.getElementById('nodePanel').classList.add('hidden'); }
567
-
568
- function setNodeColor(colorKey) {
569
- if (!selectedNodeIds.length) return;
570
- selectedNodeIds.forEach(id => {
571
- const n = nodes.get(id);
572
- if (!n) return;
573
- nodes.update({ id, colorKey, ...visNodeProps(n.shapeType||'box', colorKey, n.nodeWidth, n.nodeHeight, n.fontSize, n.textAlign, n.textValign) });
574
- });
575
- markDirty();
576
- }
577
-
578
- function changeNodeFontSize(delta) {
579
- if (!selectedNodeIds.length) return;
580
- selectedNodeIds.forEach(id => {
581
- const n = nodes.get(id);
582
- if (!n) return;
583
- const newSize = Math.max(8, Math.min(48, (n.fontSize || 13) + delta));
584
- nodes.update({ id, fontSize: newSize, ...visNodeProps(n.shapeType||'box', n.colorKey||'c-gray', n.nodeWidth, n.nodeHeight, newSize, n.textAlign, n.textValign) });
585
- });
586
- markDirty();
587
- }
588
-
589
- function setTextAlign(align) {
590
- if (!selectedNodeIds.length) return;
591
- selectedNodeIds.forEach(id => {
592
- const n = nodes.get(id); if (!n) return;
593
- nodes.update({ id, textAlign: align, ...visNodeProps(n.shapeType||'box', n.colorKey||'c-gray', n.nodeWidth, n.nodeHeight, n.fontSize, align, n.textValign) });
594
- });
595
- markDirty();
596
- }
597
-
598
- function setTextValign(valign) {
599
- if (!selectedNodeIds.length) return;
600
- selectedNodeIds.forEach(id => {
601
- const n = nodes.get(id); if (!n) return;
602
- nodes.update({ id, textValign: valign, ...visNodeProps(n.shapeType||'box', n.colorKey||'c-gray', n.nodeWidth, n.nodeHeight, n.fontSize, n.textAlign, valign) });
603
- });
604
- markDirty();
605
- }
606
-
607
- function changeZOrder(direction) {
608
- // direction: +1 = bring to front (last in _canonicalOrder = drawn on top)
609
- // -1 = send to back (first in _canonicalOrder = drawn below)
610
- if (!selectedNodeIds.length) return;
611
- selectedNodeIds.forEach(id => {
612
- const idx = _canonicalOrder.indexOf(id);
613
- if (idx === -1) return;
614
- _canonicalOrder.splice(idx, 1);
615
- if (direction > 0) {
616
- _canonicalOrder.push(id); // bring to front
617
- } else {
618
- _canonicalOrder.unshift(id); // send to back
619
- }
620
- });
621
- network.redraw();
622
- network.selectNodes(selectedNodeIds);
623
- markDirty();
624
- }
625
-
626
- // ── Edge panel ────────────────────────────────────────────────────────────
627
- function showEdgePanel() {
628
- if (!selectedEdgeIds.length) return;
629
- const e = edges.get(selectedEdgeIds[0]);
630
- if (!e) return;
631
- const dir = e.arrowDir ?? 'to';
632
- const dashes = e.dashes ?? false;
633
- ['edgeBtnNone','edgeBtnTo','edgeBtnBoth'].forEach(id => document.getElementById(id).classList.remove('edge-btn-active'));
634
- document.getElementById({ none:'edgeBtnNone', to:'edgeBtnTo', both:'edgeBtnBoth' }[dir] || 'edgeBtnTo').classList.add('edge-btn-active');
635
- ['edgeBtnSolid','edgeBtnDashed'].forEach(id => document.getElementById(id).classList.remove('edge-btn-active'));
636
- document.getElementById(dashes ? 'edgeBtnDashed' : 'edgeBtnSolid').classList.add('edge-btn-active');
637
- document.getElementById('edgePanel').classList.remove('hidden');
638
- }
639
- function hideEdgePanel() { document.getElementById('edgePanel').classList.add('hidden'); }
640
-
641
- function setEdgeArrow(dir) {
642
- if (!selectedEdgeIds.length) return;
643
- selectedEdgeIds.forEach(id => {
644
- const e = edges.get(id); if (!e) return;
645
- edges.update({ id, arrowDir: dir, ...visEdgeProps(dir, e.dashes ?? false) });
646
- });
647
- showEdgePanel(); markDirty();
648
- }
649
-
650
- function setEdgeDashes(dashes) {
651
- if (!selectedEdgeIds.length) return;
652
- selectedEdgeIds.forEach(id => {
653
- const e = edges.get(id); if (!e) return;
654
- edges.update({ id, dashes, ...visEdgeProps(e.arrowDir ?? 'to', dashes) });
655
- });
656
- showEdgePanel(); markDirty();
657
- }
658
-
659
- function changeEdgeFontSize(delta) {
660
- if (!selectedEdgeIds.length) return;
661
- selectedEdgeIds.forEach(id => {
662
- const e = edges.get(id); if (!e) return;
663
- const newSize = Math.max(8, Math.min(48, (e.fontSize || 11) + delta));
664
- edges.update({ id, fontSize: newSize, font: { size: newSize, align: 'middle', color: '#6b7280' } });
665
- });
666
- markDirty();
667
- }
668
-
669
- // ── Label editor ──────────────────────────────────────────────────────────
670
- function startLabelEdit() {
671
- if (!selectedNodeIds.length || !network) return;
672
- const nodeId = selectedNodeIds[0];
673
- const n = nodes.get(nodeId);
674
- if (!n) return;
675
- editingNodeId = nodeId; editingEdgeId = null;
676
- const pos = network.getPositions([nodeId])[nodeId] || { x: 0, y: 0 };
677
- const dom = network.canvasToDOM(pos);
678
- const ta = document.getElementById('labelInput');
679
- ta.value = n.label || '';
680
- ta.style.left = (dom.x - 75) + 'px';
681
- ta.style.top = (dom.y - 30) + 'px';
682
- ta.style.width = '150px';
683
- ta.classList.remove('hidden');
684
- autoResizeTextarea(ta);
685
- ta.focus(); ta.select();
686
- }
687
-
688
- function startEdgeLabelEdit() {
689
- if (!selectedEdgeIds.length || !network) return;
690
- const edgeId = selectedEdgeIds[0];
691
- const e = edges.get(edgeId);
692
- if (!e) return;
693
- editingEdgeId = edgeId; editingNodeId = null;
694
-
695
- // Midpoint between the two endpoints in canvas coords
696
- const positions = network.getPositions([e.from, e.to]);
697
- const fp = positions[e.from] || { x: 0, y: 0 };
698
- const tp = positions[e.to] || { x: 0, y: 0 };
699
- const mid = network.canvasToDOM({ x: (fp.x + tp.x) / 2, y: (fp.y + tp.y) / 2 });
700
-
701
- const ta = document.getElementById('labelInput');
702
- ta.value = e.label || '';
703
- ta.style.left = (mid.x - 60) + 'px';
704
- ta.style.top = (mid.y - 14) + 'px';
705
- ta.style.width = '120px';
706
- ta.classList.remove('hidden');
707
- autoResizeTextarea(ta);
708
- ta.focus(); ta.select();
709
- }
710
-
711
- function commitLabelEdit() {
712
- const ta = document.getElementById('labelInput');
713
- if (editingNodeId) { nodes.update({ id: editingNodeId, label: ta.value }); markDirty(); }
714
- else if (editingEdgeId) { edges.update({ id: editingEdgeId, label: ta.value }); markDirty(); }
715
- editingNodeId = null; editingEdgeId = null;
716
- }
717
-
718
- function hideLabelInput() {
719
- document.getElementById('labelInput').classList.add('hidden');
720
- editingNodeId = null; editingEdgeId = null;
721
- }
722
-
723
- function autoResizeTextarea(ta) {
724
- ta.style.height = 'auto';
725
- ta.style.height = ta.scrollHeight + 'px';
726
- }
727
-
728
- document.getElementById('labelInput').addEventListener('keydown', e => {
729
- if (e.key === 'Enter' && !e.shiftKey) { commitLabelEdit(); hideLabelInput(); e.preventDefault(); }
730
- else if (e.key === 'Escape') { hideLabelInput(); }
731
- });
732
- document.getElementById('labelInput').addEventListener('input', e => autoResizeTextarea(e.target));
733
- document.getElementById('labelInput').addEventListener('blur', () => { commitLabelEdit(); hideLabelInput(); });
734
-
735
- // ── Selection / resize overlay ─────────────────────────────────────────────
736
- function updateSelectionOverlay() {
737
- if (!network || !selectedNodeIds.length) { hideSelectionOverlay(); return; }
738
-
739
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
740
- for (const id of selectedNodeIds) {
741
- try {
742
- const bb = network.getBoundingBox(id);
743
- minX = Math.min(minX, bb.left); minY = Math.min(minY, bb.top);
744
- maxX = Math.max(maxX, bb.right); maxY = Math.max(maxY, bb.bottom);
745
- } catch(_) { /* node still being created */ }
746
- }
747
- if (minX === Infinity) { hideSelectionOverlay(); return; }
748
-
749
- const PAD = 10;
750
- const tl = network.canvasToDOM({ x: minX, y: minY });
751
- const br = network.canvasToDOM({ x: maxX, y: maxY });
752
- const ov = document.getElementById('selectionOverlay');
753
- ov.style.display = 'block';
754
- ov.style.left = (tl.x - PAD) + 'px';
755
- ov.style.top = (tl.y - PAD) + 'px';
756
- ov.style.width = (br.x - tl.x + PAD * 2) + 'px';
757
- ov.style.height = (br.y - tl.y + PAD * 2) + 'px';
758
- }
759
-
760
- function hideSelectionOverlay() {
761
- document.getElementById('selectionOverlay').style.display = 'none';
762
- }
763
-
764
- // Wire up corner handles
765
- ['tl','tr','bl','br'].forEach(corner => {
766
- document.getElementById('rh-' + corner).addEventListener('mousedown', e => onResizeStart(e, corner));
767
- });
768
-
769
- function onResizeStart(e, corner) {
770
- if (!selectedNodeIds.length || !network) return;
771
- e.preventDefault(); e.stopPropagation();
772
-
773
- const startBBs = selectedNodeIds.map(id => {
774
- const bb = network.getBoundingBox(id);
775
- const n = nodes.get(id);
776
- return { id, node: n,
777
- initW: n.nodeWidth || Math.round(bb.right - bb.left),
778
- initH: n.nodeHeight || Math.round(bb.bottom - bb.top) };
779
- });
780
-
781
- const allBBs = selectedNodeIds.map(id => network.getBoundingBox(id));
782
- const initBoxW = Math.max(...allBBs.map(b => b.right)) - Math.min(...allBBs.map(b => b.left));
783
- const initBoxH = Math.max(...allBBs.map(b => b.bottom)) - Math.min(...allBBs.map(b => b.top));
784
-
785
- resizeDrag = { corner, startMouse: { x: e.clientX, y: e.clientY }, startBBs, initBoxW, initBoxH };
786
-
787
- // Pin nodes during resize so physics doesn't fight us
788
- selectedNodeIds.forEach(id => nodes.update({ id, fixed: true }));
789
- // Block vis.js from receiving mouse events during drag
790
- document.getElementById('vis-canvas').style.pointerEvents = 'none';
791
-
792
- document.addEventListener('mousemove', onResizeDrag);
793
- document.addEventListener('mouseup', onResizeEnd);
794
- }
795
-
796
- function onResizeDrag(e) {
797
- if (!resizeDrag || !network) return;
798
- const scale = network.getScale();
799
- const cdx = (e.clientX - resizeDrag.startMouse.x) / scale;
800
- const cdy = (e.clientY - resizeDrag.startMouse.y) / scale;
801
- const MIN = 40;
802
- const c = resizeDrag.corner;
803
-
804
- if (resizeDrag.startBBs.length === 1) {
805
- // Single node: direct resize
806
- const { id, node, initW, initH } = resizeDrag.startBBs[0];
807
- let nW = initW, nH = initH;
808
- if (c==='br'){nW=initW+cdx; nH=initH+cdy;}
809
- if (c==='bl'){nW=initW-cdx; nH=initH+cdy;}
810
- if (c==='tr'){nW=initW+cdx; nH=initH-cdy;}
811
- if (c==='tl'){nW=initW-cdx; nH=initH-cdy;}
812
- nW = Math.max(MIN, Math.round(nW));
813
- nH = Math.max(MIN, Math.round(nH));
814
- nodes.update({ id, nodeWidth: nW, nodeHeight: nH,
815
- ...visNodeProps(node.shapeType||'box', node.colorKey||'c-gray', nW, nH, node.fontSize, node.textAlign, node.textValign) });
816
- } else {
817
- // Multi: proportional scale
818
- const { initBoxW, initBoxH } = resizeDrag;
819
- let sx = 1, sy = 1;
820
- if (c==='br'){sx=(initBoxW+cdx)/initBoxW; sy=(initBoxH+cdy)/initBoxH;}
821
- if (c==='bl'){sx=(initBoxW-cdx)/initBoxW; sy=(initBoxH+cdy)/initBoxH;}
822
- if (c==='tr'){sx=(initBoxW+cdx)/initBoxW; sy=(initBoxH-cdy)/initBoxH;}
823
- if (c==='tl'){sx=(initBoxW-cdx)/initBoxW; sy=(initBoxH-cdy)/initBoxH;}
824
- sx = Math.max(0.1, sx); sy = Math.max(0.1, sy);
825
- for (const { id, node, initW, initH } of resizeDrag.startBBs) {
826
- const nW = Math.max(MIN, Math.round(initW * sx));
827
- const nH = Math.max(MIN, Math.round(initH * sy));
828
- nodes.update({ id, nodeWidth: nW, nodeHeight: nH,
829
- ...visNodeProps(node.shapeType||'box', node.colorKey||'c-gray', nW, nH, node.fontSize, node.textAlign, node.textValign) });
830
- }
831
- }
832
- updateSelectionOverlay();
833
- }
834
-
835
- function onResizeEnd() {
836
- if (!resizeDrag) return;
837
- selectedNodeIds.forEach(id => nodes.update({ id, fixed: false }));
838
- document.getElementById('vis-canvas').style.pointerEvents = '';
839
- document.removeEventListener('mousemove', onResizeDrag);
840
- document.removeEventListener('mouseup', onResizeEnd);
841
- resizeDrag = null;
842
- markDirty();
843
- }
844
-
845
- // ── Tools ─────────────────────────────────────────────────────────────────
846
- const TOOL_BTN_MAP = {
847
- 'select': 'toolSelect',
848
- 'addNode:box': 'toolBox',
849
- 'addNode:ellipse': 'toolEllipse',
850
- 'addNode:database': 'toolDatabase',
851
- 'addNode:circle': 'toolCircle',
852
- 'addNode:actor': 'toolActor',
853
- 'addEdge': 'toolArrow',
854
- };
855
-
856
- function setTool(tool, shape) {
857
- currentTool = tool;
858
- if (shape) pendingShape = shape;
859
-
860
- document.querySelectorAll('.tool-btn').forEach(b => b.classList.remove('tool-active'));
861
- if (physicsEnabled) document.getElementById('btnPhysics').classList.add('tool-active');
862
- const key = tool === 'addNode' ? `addNode:${shape || pendingShape}` : tool;
863
- const btn = document.getElementById(TOOL_BTN_MAP[key]);
864
- if (btn) btn.classList.add('tool-active');
865
-
866
- document.getElementById('vis-canvas').classList.toggle('cursor-crosshair', tool === 'addNode' || tool === 'addEdge');
867
-
868
- if (tool === 'addEdge' && network) network.addEdgeMode();
869
- else if (network) network.disableEditMode();
870
- }
871
-
872
- function deleteSelected() {
873
- if (!network) return;
874
- network.deleteSelected();
875
- hideNodePanel(); hideEdgePanel(); hideLabelInput(); hideSelectionOverlay();
876
- editingNodeId = null; editingEdgeId = null;
877
- markDirty();
878
- }
879
-
880
- // ── Physics ───────────────────────────────────────────────────────────────
881
- function togglePhysics() {
882
- physicsEnabled = !physicsEnabled;
883
- if (network) network.setOptions({ physics: { enabled: physicsEnabled } });
884
- const btn = document.getElementById('btnPhysics');
885
- btn.classList.toggle('tool-active', physicsEnabled);
886
- btn.title = physicsEnabled ? 'Auto-espacement actif' : 'Auto-espacement inactif';
887
- }
888
-
889
- // ── Grid ─────────────────────────────────────────────────────────────────
890
- function toggleGrid() {
891
- gridEnabled = !gridEnabled;
892
- const btn = document.getElementById('btnGrid');
893
- btn.classList.toggle('tool-active', gridEnabled);
894
- btn.title = gridEnabled ? 'Grille activée (snap on)' : 'Grille / Snap to grid (G)';
895
- if (network) network.redraw();
896
- }
897
-
898
- function drawGrid(ctx) {
899
- if (!gridEnabled || !network) return;
900
- const isDark = document.documentElement.classList.contains('dark');
901
- const color = isDark ? 'rgba(255,255,255,0.18)' : 'rgba(0,0,0,0.15)';
902
- const scale = network.getScale();
903
- const center = network.getViewPosition();
904
- const canvas = ctx.canvas;
905
- const W = canvas.width, H = canvas.height;
906
-
907
- // Canvas-space grid step
908
- const step = GRID_SIZE * scale;
909
-
910
- // Offset so grid aligns to canvas-space multiples of GRID_SIZE
911
- // Use positive modulo to avoid negative offsets when panning left/up
912
- const offsetX = ((W / 2 - center.x * scale) % step + step) % step;
913
- const offsetY = ((H / 2 - center.y * scale) % step + step) % step;
914
-
915
- ctx.save();
916
- // Reset transform — we work in screen (canvas pixel) space
917
- ctx.setTransform(1, 0, 0, 1, 0, 0);
918
- ctx.strokeStyle = color;
919
- ctx.lineWidth = 1;
920
- ctx.beginPath();
921
- for (let x = offsetX; x < W; x += step) { ctx.moveTo(x, 0); ctx.lineTo(x, H); }
922
- for (let y = offsetY; y < H; y += step) { ctx.moveTo(0, y); ctx.lineTo(W, y); }
923
- ctx.stroke();
924
- ctx.restore();
925
- }
926
-
927
- function snapToGrid(v) {
928
- return Math.round(v / GRID_SIZE) * GRID_SIZE;
929
- }
930
-
931
- function onDragEnd(params) {
932
- if (gridEnabled && params.nodes && params.nodes.length > 0) {
933
- const positions = network.getPositions(params.nodes);
934
- params.nodes.forEach(id => {
935
- const p = positions[id];
936
- if (p) network.moveNode(id, snapToGrid(p.x), snapToGrid(p.y));
937
- });
938
- }
939
- markDirty();
940
- }
941
-
942
- // ── Zoom ──────────────────────────────────────────────────────────────────
943
- function adjustZoom(delta) {
944
- if (!network) return;
945
- network.moveTo({ scale: Math.max(0.1, Math.min(3, network.getScale() + delta)), animation: false });
946
- updateZoomDisplay();
947
- }
948
- function resetZoom() {
949
- if (!network) return;
950
- network.fit({ animation: { duration: 300, easingFunction: 'easeInOutQuad' } });
951
- setTimeout(updateZoomDisplay, 350);
952
- }
953
- function updateZoomDisplay() {
954
- if (!network) return;
955
- document.getElementById('zoomLevel').textContent = Math.round(network.getScale() * 100) + '%';
956
- }
957
-
958
- // ── Sidebar ───────────────────────────────────────────────────────────────
959
- function toggleSidebar() {
960
- sidebarOpen = !sidebarOpen;
961
- const sb = document.getElementById('sidebar');
962
- sb.style.width = sidebarOpen ? '14rem' : '0';
963
- sb.style.overflow = sidebarOpen ? '' : 'hidden';
964
- }
965
-
966
- // ── Dirty state ───────────────────────────────────────────────────────────
967
- function markDirty() {
968
- isDirty = true;
969
- document.getElementById('btnSave').disabled = false;
970
- }
971
-
972
- // ── Diagram list ──────────────────────────────────────────────────────────
973
- function renderDiagramList() {
974
- const list = document.getElementById('diagramList');
975
- list.innerHTML = '';
976
- if (!diagrams.length) {
977
- list.innerHTML = '<p class="text-xs text-gray-400 dark:text-gray-600 px-3 py-3">Aucun diagramme</p>';
978
- return;
979
- }
980
- diagrams.forEach(d => {
981
- const isActive = d.id === currentDiagramId;
982
- const item = document.createElement('div');
983
- item.className = [
984
- 'group flex items-center gap-1 px-2 py-1.5 mx-1 my-0.5 rounded-md cursor-pointer',
985
- 'hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors',
986
- isActive ? 'bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300' : 'text-gray-700 dark:text-gray-300',
987
- ].join(' ');
988
- item.innerHTML = `
989
- <span class="flex-1 text-sm truncate">${escapeHtml(d.title)}</span>
990
- <button onclick="deleteDiagram('${escapeHtml(d.id)}', event)" title="Supprimer"
991
- class="hidden group-hover:flex items-center justify-center w-4 h-4 rounded text-gray-400 hover:text-red-500 shrink-0">✕</button>`;
992
- item.addEventListener('click', e => { if (e.target.tagName === 'BUTTON') return; openDiagram(d.id); });
993
- list.appendChild(item);
994
- });
995
- }
996
-
997
- // ── Persistence ───────────────────────────────────────────────────────────
998
- async function loadDiagramList() {
999
- const res = await fetch('/api/diagrams');
1000
- diagrams = await res.json();
1001
- renderDiagramList();
1002
- if (diagrams.length > 0) openDiagram(diagrams[0].id);
1003
- }
1004
-
1005
- async function openDiagram(id) {
1006
- const res = await fetch(`/api/diagrams/${id}`);
1007
- const diagram = await res.json();
1008
- currentDiagramId = id;
1009
- isDirty = false;
1010
- document.getElementById('btnSave').disabled = true;
1011
- document.getElementById('diagramTitle').value = diagram.title || '';
1012
- initNetwork(diagram.nodes || [], diagram.edges || []);
1013
- renderDiagramList();
1014
- document.getElementById('diagramTitle').oninput = () => markDirty();
1015
- }
1016
-
1017
- async function newDiagram() {
1018
- const id = 'd' + Date.now();
1019
- await fetch(`/api/diagrams/${id}`, {
1020
- method: 'PUT', headers: { 'Content-Type': 'application/json' },
1021
- body: JSON.stringify({ title: 'Nouveau diagramme', nodes: [], edges: [] }),
1022
- });
1023
- const res = await fetch('/api/diagrams');
1024
- diagrams = await res.json();
1025
- renderDiagramList();
1026
- openDiagram(id);
1027
- }
1028
-
1029
- async function deleteDiagram(id, event) {
1030
- event.stopPropagation();
1031
- if (!confirm('Supprimer ce diagramme ?')) return;
1032
- await fetch(`/api/diagrams/${id}`, { method: 'DELETE' });
1033
- const res = await fetch('/api/diagrams');
1034
- diagrams = await res.json();
1035
- if (currentDiagramId === id) {
1036
- currentDiagramId = null;
1037
- if (network) { network.destroy(); network = null; }
1038
- nodes = null; edges = null;
1039
- document.getElementById('diagramTitle').value = '';
1040
- document.getElementById('btnSave').disabled = true;
1041
- hideNodePanel(); hideEdgePanel(); hideLabelInput(); hideSelectionOverlay();
1042
- if (diagrams.length > 0) openDiagram(diagrams[0].id);
1043
- else document.getElementById('emptyState').classList.remove('hidden');
1044
- }
1045
- renderDiagramList();
1046
- }
1047
-
1048
- async function saveDiagram() {
1049
- if (!currentDiagramId || !network) return;
1050
- const positions = network.getPositions();
1051
- // Keep only our custom fields, strip vis.js computed ones.
1052
- // Use _canonicalOrder so the saved array preserves z-order on reload.
1053
- const nodeData = _canonicalOrder.map(id => nodes.get(id)).filter(Boolean).map(n => ({
1054
- id: n.id, label: n.label,
1055
- shapeType: n.shapeType || 'box',
1056
- colorKey: n.colorKey || 'c-gray',
1057
- nodeWidth: n.nodeWidth || null,
1058
- nodeHeight: n.nodeHeight || null,
1059
- fontSize: n.fontSize || null,
1060
- textAlign: n.textAlign || null,
1061
- textValign: n.textValign || null,
1062
- x: positions[n.id]?.x ?? n.x,
1063
- y: positions[n.id]?.y ?? n.y,
1064
- }));
1065
- const edgeData = edges.get().map(e => ({
1066
- id: e.id, from: e.from, to: e.to,
1067
- label: e.label || '',
1068
- arrowDir: e.arrowDir || 'to',
1069
- dashes: e.dashes || false,
1070
- fontSize: e.fontSize || null,
1071
- }));
1072
- const title = document.getElementById('diagramTitle').value || 'Sans titre';
1073
- await fetch(`/api/diagrams/${currentDiagramId}`, {
1074
- method: 'PUT', headers: { 'Content-Type': 'application/json' },
1075
- body: JSON.stringify({ title, nodes: nodeData, edges: edgeData }),
1076
- });
1077
- isDirty = false;
1078
- document.getElementById('btnSave').disabled = true;
1079
- const res = await fetch('/api/diagrams');
1080
- diagrams = await res.json();
1081
- renderDiagramList();
1082
- }
1083
-
1084
- // ── Dark mode ─────────────────────────────────────────────────────────────
1085
- function toggleDark() {
1086
- const dark = document.documentElement.classList.toggle('dark');
1087
- localStorage.setItem('ld-dark', dark);
1088
- document.getElementById('darkIcon').textContent = dark ? '☀' : '☽';
1089
- }
1090
- document.getElementById('darkIcon').textContent =
1091
- document.documentElement.classList.contains('dark') ? '☀' : '☽';
1092
-
1093
- // ── Utils ─────────────────────────────────────────────────────────────────
1094
- function escapeHtml(s) {
1095
- return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
1096
- }
1097
-
1098
- // ── Copy / Paste ──────────────────────────────────────────────────────────
1099
- function copySelected() {
1100
- if (!network || !selectedNodeIds.length) return;
1101
-
1102
- const positions = network.getPositions(selectedNodeIds);
1103
- const copiedNodes = selectedNodeIds.map(id => {
1104
- const n = nodes.get(id);
1105
- const { ctxRenderer, ...rest } = n;
1106
- return { ...rest, x: positions[id]?.x ?? n.x, y: positions[id]?.y ?? n.y };
1107
- });
1108
-
1109
- // Copy edges where BOTH endpoints are in the selection
1110
- const selectedSet = new Set(selectedNodeIds);
1111
- const copiedEdges = edges.get().filter(e =>
1112
- selectedSet.has(e.from) && selectedSet.has(e.to)
1113
- );
1114
-
1115
- clipboard = { nodes: copiedNodes, edges: copiedEdges };
1116
- }
1117
-
1118
- function pasteClipboard() {
1119
- if (!clipboard || !clipboard.nodes.length || !network) return;
1120
-
1121
- const OFFSET = 40;
1122
- // Map old id → new id
1123
- const idMap = {};
1124
- clipboard.nodes.forEach(n => { idMap[n.id] = 'n' + Date.now() + Math.random().toString(36).slice(2); });
1125
-
1126
- const newNodes = clipboard.nodes.map(n => ({
1127
- ...n,
1128
- id: idMap[n.id],
1129
- x: (n.x || 0) + OFFSET,
1130
- y: (n.y || 0) + OFFSET,
1131
- ...visNodeProps(n.shapeType || 'box', n.colorKey || 'c-gray', n.nodeWidth, n.nodeHeight, n.fontSize, n.textAlign, n.textValign),
1132
- }));
1133
-
1134
- const newEdges = clipboard.edges.map(e => ({
1135
- ...e,
1136
- id: 'e' + Date.now() + Math.random().toString(36).slice(2),
1137
- from: idMap[e.from],
1138
- to: idMap[e.to],
1139
- ...visEdgeProps(e.arrowDir ?? 'to', e.dashes ?? false),
1140
- ...(e.fontSize ? { font: { size: e.fontSize, align: 'middle', color: '#6b7280' } } : {}),
1141
- }));
1142
-
1143
- nodes.add(newNodes);
1144
- edges.add(newEdges);
1145
-
1146
- // Select the pasted nodes
1147
- const newIds = newNodes.map(n => n.id);
1148
- network.selectNodes(newIds);
1149
- selectedNodeIds = newIds;
1150
- selectedEdgeIds = [];
1151
- showNodePanel();
1152
- markDirty();
1153
-
1154
- // Offset clipboard for next paste
1155
- clipboard = {
1156
- nodes: clipboard.nodes.map(n => ({ ...n, x: (n.x || 0) + OFFSET, y: (n.y || 0) + OFFSET })),
1157
- edges: clipboard.edges,
1158
- };
1159
- }
1160
-
1161
- // ── Keyboard shortcuts ────────────────────────────────────────────────────
1162
- document.addEventListener('keydown', e => {
1163
- if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
1164
- if (e.key === 'Delete' || e.key === 'Backspace') { deleteSelected(); return; }
1165
- if ((e.metaKey || e.ctrlKey) && e.key === 'c') { e.preventDefault(); copySelected(); return; }
1166
- if ((e.metaKey || e.ctrlKey) && e.key === 'v') { e.preventDefault(); pasteClipboard(); return; }
1167
- if ((e.metaKey || e.ctrlKey) && e.key === 's') { e.preventDefault(); saveDiagram(); return; }
1168
- if (e.key === 'Escape') { setTool('select'); return; }
1169
- if (e.key === 's' || e.key === 'S') { setTool('select'); return; }
1170
- if (e.key === 'r' || e.key === 'R') { setTool('addNode','box'); return; }
1171
- if (e.key === 'e' || e.key === 'E') { setTool('addNode','ellipse'); return; }
1172
- if (e.key === 'd' || e.key === 'D') { setTool('addNode','database');return; }
1173
- if (e.key === 'c' || e.key === 'C') { setTool('addNode','circle'); return; }
1174
- if (e.key === 'a' || e.key === 'A') { setTool('addNode','actor'); return; }
1175
- if (e.key === 'f' || e.key === 'F') { setTool('addEdge'); return; }
1176
- if (e.key === 'g' || e.key === 'G') { toggleGrid(); return; }
1177
- });
1178
-
1179
- // ── Init ──────────────────────────────────────────────────────────────────
1180
- loadDiagramList();
1181
- </script>
876
+ <script type="module" src="/diagram/main.js"></script>
1182
877
  </body>
1183
878
  </html>