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