canvasframework 0.3.21 → 0.3.23

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.
@@ -0,0 +1,1247 @@
1
+ /**
2
+ * Outils de développement pour CanvasFramework
3
+ * @class
4
+ */
5
+ class DevTools {
6
+ /**
7
+ * Crée une instance de DevTools
8
+ * @param {CanvasFramework} framework - Instance du framework
9
+ */
10
+ constructor(framework) {
11
+ this.framework = framework;
12
+ this.isOpen = false;
13
+ this.currentTab = 'components';
14
+ this.panels = {};
15
+ this.hoveredComponent = null;
16
+ this.selectedComponent = null;
17
+ this.performanceStats = {
18
+ fps: [],
19
+ memory: [],
20
+ drawCalls: []
21
+ };
22
+ this.setupUI();
23
+ }
24
+
25
+ /**
26
+ * Configure l'interface du DevTools
27
+ */
28
+ setupUI() {
29
+ // Créer le conteneur principal
30
+ this.container = document.createElement('div');
31
+ this.container.style.cssText = `
32
+ position: fixed;
33
+ top: 0;
34
+ right: 0;
35
+ width: 400px;
36
+ height: 100vh;
37
+ background: #1e1e1e;
38
+ color: #fff;
39
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
40
+ font-size: 12px;
41
+ z-index: 9999;
42
+ transform: translateX(100%);
43
+ transition: transform 0.3s ease;
44
+ display: flex;
45
+ flex-direction: column;
46
+ box-shadow: -2px 0 10px rgba(0,0,0,0.5);
47
+ `;
48
+
49
+ // Header
50
+ this.header = document.createElement('div');
51
+ this.header.style.cssText = `
52
+ background: #252526;
53
+ padding: 10px;
54
+ display: flex;
55
+ justify-content: space-between;
56
+ align-items: center;
57
+ border-bottom: 1px solid #333;
58
+ user-select: none;
59
+ `;
60
+
61
+ this.title = document.createElement('div');
62
+ this.title.textContent = 'Canvas Framework DevTools';
63
+ this.title.style.fontWeight = 'bold';
64
+
65
+ this.closeBtn = document.createElement('button');
66
+ this.closeBtn.textContent = '×';
67
+ this.closeBtn.style.cssText = `
68
+ background: transparent;
69
+ border: none;
70
+ color: #fff;
71
+ font-size: 20px;
72
+ cursor: pointer;
73
+ padding: 0 8px;
74
+ `;
75
+ this.closeBtn.onclick = () => this.toggle();
76
+
77
+ this.header.appendChild(this.title);
78
+ this.header.appendChild(this.closeBtn);
79
+
80
+ // Tabs
81
+ this.tabs = document.createElement('div');
82
+ this.tabs.style.cssText = `
83
+ display: flex;
84
+ background: #2d2d2d;
85
+ border-bottom: 1px solid #333;
86
+ `;
87
+
88
+ const tabConfigs = [
89
+ { id: 'components', label: 'Composants' },
90
+ { id: 'performance', label: 'Performance' },
91
+ { id: 'hierarchy', label: 'Hiérarchie' },
92
+ { id: 'properties', label: 'Propriétés' },
93
+ { id: 'events', label: 'Événements' },
94
+ { id: 'routing', label: 'Routing' }
95
+ ];
96
+
97
+ tabConfigs.forEach(config => {
98
+ const tab = document.createElement('button');
99
+ tab.textContent = config.label;
100
+ tab.dataset.tab = config.id;
101
+ tab.style.cssText = `
102
+ flex: 1;
103
+ padding: 8px;
104
+ background: transparent;
105
+ border: none;
106
+ color: #ccc;
107
+ cursor: pointer;
108
+ border-bottom: 2px solid transparent;
109
+ `;
110
+ tab.onclick = () => this.switchTab(config.id);
111
+ this.tabs.appendChild(tab);
112
+ });
113
+
114
+ // Content area
115
+ this.content = document.createElement('div');
116
+ this.content.style.cssText = `
117
+ flex: 1;
118
+ overflow: auto;
119
+ padding: 10px;
120
+ `;
121
+
122
+ // Create panels
123
+ this.createComponentsPanel();
124
+ this.createPerformancePanel();
125
+ this.createHierarchyPanel();
126
+ this.createPropertiesPanel();
127
+ this.createEventsPanel();
128
+ this.createRoutingPanel();
129
+
130
+ // Assembler le conteneur
131
+ this.container.appendChild(this.header);
132
+ this.container.appendChild(this.tabs);
133
+ this.container.appendChild(this.content);
134
+
135
+ document.body.appendChild(this.container);
136
+
137
+ // Bouton toggle flottant
138
+ this.toggleBtn = document.createElement('button');
139
+ this.toggleBtn.textContent = 'DevTools';
140
+ this.toggleBtn.style.cssText = `
141
+ position: fixed;
142
+ bottom: 20px;
143
+ right: 20px;
144
+ background: #007acc;
145
+ color: white;
146
+ border: none;
147
+ border-radius: 4px;
148
+ padding: 8px 12px;
149
+ font-size: 12px;
150
+ cursor: pointer;
151
+ z-index: 10000;
152
+ box-shadow: 0 2px 5px rgba(0,0,0,0.2);
153
+ `;
154
+ this.toggleBtn.onclick = () => this.toggle();
155
+ document.body.appendChild(this.toggleBtn);
156
+
157
+ // Setup hotkeys
158
+ this.setupHotkeys();
159
+ }
160
+
161
+ /**
162
+ * Crée le panneau des composants
163
+ */
164
+ createComponentsPanel() {
165
+ const panel = document.createElement('div');
166
+ panel.id = 'components-panel';
167
+ panel.style.display = 'none';
168
+
169
+ // Stats
170
+ const stats = document.createElement('div');
171
+ stats.style.cssText = `
172
+ background: #252526;
173
+ padding: 8px;
174
+ border-radius: 4px;
175
+ margin-bottom: 10px;
176
+ `;
177
+ stats.innerHTML = `
178
+ <div style="display: flex; justify-content: space-between;">
179
+ <span>Total: <span id="component-count">0</span></span>
180
+ <span>Visibles: <span id="visible-count">0</span></span>
181
+ <span>Dirty: <span id="dirty-count">0</span></span>
182
+ </div>
183
+ `;
184
+
185
+ // Filter
186
+ const filter = document.createElement('input');
187
+ filter.type = 'text';
188
+ filter.placeholder = 'Filtrer les composants...';
189
+ filter.style.cssText = `
190
+ width: 100%;
191
+ padding: 6px;
192
+ margin-bottom: 10px;
193
+ background: #333;
194
+ border: 1px solid #555;
195
+ color: #fff;
196
+ border-radius: 4px;
197
+ `;
198
+
199
+ // Component list
200
+ const list = document.createElement('div');
201
+ list.id = 'component-list';
202
+ list.style.cssText = `
203
+ max-height: 400px;
204
+ overflow-y: auto;
205
+ border: 1px solid #333;
206
+ border-radius: 4px;
207
+ `;
208
+
209
+ // Controls
210
+ const controls = document.createElement('div');
211
+ controls.style.cssText = `
212
+ display: flex;
213
+ gap: 8px;
214
+ margin-top: 10px;
215
+ `;
216
+
217
+ const toggleAllBtn = document.createElement('button');
218
+ toggleAllBtn.textContent = 'Toggle All';
219
+ toggleAllBtn.style.cssText = `
220
+ flex: 1;
221
+ padding: 6px;
222
+ background: #555;
223
+ color: #fff;
224
+ border: none;
225
+ border-radius: 4px;
226
+ cursor: pointer;
227
+ `;
228
+ toggleAllBtn.onclick = () => this.toggleAllComponents();
229
+
230
+ const refreshBtn = document.createElement('button');
231
+ refreshBtn.textContent = 'Refresh';
232
+ refreshBtn.style.cssText = toggleAllBtn.style.cssText;
233
+ refreshBtn.onclick = () => this.refreshComponents();
234
+
235
+ controls.appendChild(toggleAllBtn);
236
+ controls.appendChild(refreshBtn);
237
+
238
+ panel.appendChild(stats);
239
+ panel.appendChild(filter);
240
+ panel.appendChild(list);
241
+ panel.appendChild(controls);
242
+
243
+ this.content.appendChild(panel);
244
+ this.panels.components = panel;
245
+ }
246
+
247
+ /**
248
+ * Crée le panneau de performance
249
+ */
250
+ createPerformancePanel() {
251
+ const panel = document.createElement('div');
252
+ panel.id = 'performance-panel';
253
+ panel.style.display = 'none';
254
+
255
+ // FPS Chart
256
+ const fpsSection = document.createElement('div');
257
+ fpsSection.style.marginBottom = '20px';
258
+
259
+ const fpsTitle = document.createElement('h3');
260
+ fpsTitle.textContent = 'FPS';
261
+ fpsTitle.style.marginBottom = '8px';
262
+
263
+ const fpsCanvas = document.createElement('canvas');
264
+ fpsCanvas.width = 380;
265
+ fpsCanvas.height = 100;
266
+ fpsCanvas.style.cssText = `
267
+ width: 100%;
268
+ height: 100px;
269
+ background: #252526;
270
+ border-radius: 4px;
271
+ `;
272
+
273
+ fpsSection.appendChild(fpsTitle);
274
+ fpsSection.appendChild(fpsCanvas);
275
+
276
+ // Memory Stats
277
+ const memorySection = document.createElement('div');
278
+ memorySection.style.marginBottom = '20px';
279
+
280
+ const memoryTitle = document.createElement('h3');
281
+ memoryTitle.textContent = 'Mémoire';
282
+ memoryTitle.style.marginBottom = '8px';
283
+
284
+ const memoryInfo = document.createElement('div');
285
+ memoryInfo.id = 'memory-info';
286
+ memoryInfo.style.cssText = `
287
+ background: #252526;
288
+ padding: 8px;
289
+ border-radius: 4px;
290
+ font-family: monospace;
291
+ `;
292
+
293
+ memorySection.appendChild(memoryTitle);
294
+ memorySection.appendChild(memoryInfo);
295
+
296
+ // Performance Tips
297
+ const tipsSection = document.createElement('div');
298
+
299
+ const tipsTitle = document.createElement('h3');
300
+ tipsTitle.textContent = 'Conseils d\'optimisation';
301
+ tipsTitle.style.marginBottom = '8px';
302
+
303
+ const tipsList = document.createElement('ul');
304
+ tipsList.style.cssText = `
305
+ padding-left: 20px;
306
+ color: #aaa;
307
+ `;
308
+ tipsList.innerHTML = `
309
+ <li>Activer l'optimizationEnabled</li>
310
+ <li>Réduire le nombre de composants visibles</li>
311
+ <li>Utiliser VirtualList pour les longues listes</li>
312
+ <li>Éviter les animations sur trop de composants</li>
313
+ `;
314
+
315
+ tipsSection.appendChild(tipsTitle);
316
+ tipsSection.appendChild(tipsList);
317
+
318
+ panel.appendChild(fpsSection);
319
+ panel.appendChild(memorySection);
320
+ panel.appendChild(tipsSection);
321
+
322
+ this.content.appendChild(panel);
323
+ this.panels.performance = panel;
324
+ this.fpsCanvas = fpsCanvas;
325
+ }
326
+
327
+ /**
328
+ * Crée le panneau hiérarchique
329
+ */
330
+ createHierarchyPanel() {
331
+ const panel = document.createElement('div');
332
+ panel.id = 'hierarchy-panel';
333
+ panel.style.display = 'none';
334
+
335
+ const tree = document.createElement('div');
336
+ tree.id = 'component-tree';
337
+ tree.style.cssText = `
338
+ font-family: monospace;
339
+ line-height: 1.4;
340
+ `;
341
+
342
+ panel.appendChild(tree);
343
+ this.content.appendChild(panel);
344
+ this.panels.hierarchy = panel;
345
+ }
346
+
347
+ /**
348
+ * Crée le panneau des propriétés
349
+ */
350
+ createPropertiesPanel() {
351
+ const panel = document.createElement('div');
352
+ panel.id = 'properties-panel';
353
+ panel.style.display = 'none';
354
+
355
+ const noSelection = document.createElement('div');
356
+ noSelection.id = 'no-selection';
357
+ noSelection.textContent = 'Sélectionnez un composant pour voir ses propriétés';
358
+ noSelection.style.color = '#888';
359
+
360
+ const propertiesTable = document.createElement('div');
361
+ propertiesTable.id = 'properties-table';
362
+ propertiesTable.style.display = 'none';
363
+
364
+ panel.appendChild(noSelection);
365
+ panel.appendChild(propertiesTable);
366
+ this.content.appendChild(panel);
367
+ this.panels.properties = panel;
368
+ }
369
+
370
+ /**
371
+ * Crée le panneau des événements
372
+ */
373
+ createEventsPanel() {
374
+ const panel = document.createElement('div');
375
+ panel.id = 'events-panel';
376
+ panel.style.display = 'none';
377
+
378
+ const eventsList = document.createElement('div');
379
+ eventsList.id = 'events-list';
380
+ eventsList.style.cssText = `
381
+ max-height: 400px;
382
+ overflow-y: auto;
383
+ border: 1px solid #333;
384
+ border-radius: 4px;
385
+ padding: 8px;
386
+ `;
387
+
388
+ const clearBtn = document.createElement('button');
389
+ clearBtn.textContent = 'Clear Events';
390
+ clearBtn.style.cssText = `
391
+ margin-top: 10px;
392
+ padding: 6px 12px;
393
+ background: #555;
394
+ color: #fff;
395
+ border: none;
396
+ border-radius: 4px;
397
+ cursor: pointer;
398
+ `;
399
+ clearBtn.onclick = () => this.clearEvents();
400
+
401
+ panel.appendChild(eventsList);
402
+ panel.appendChild(clearBtn);
403
+ this.content.appendChild(panel);
404
+ this.panels.events = panel;
405
+ this.eventsList = eventsList;
406
+ }
407
+
408
+ /**
409
+ * Crée le panneau de routing
410
+ */
411
+ createRoutingPanel() {
412
+ const panel = document.createElement('div');
413
+ panel.id = 'routing-panel';
414
+ panel.style.display = 'none';
415
+
416
+ const routeInfo = document.createElement('div');
417
+ routeInfo.id = 'route-info';
418
+ routeInfo.style.cssText = `
419
+ background: #252526;
420
+ padding: 10px;
421
+ border-radius: 4px;
422
+ margin-bottom: 10px;
423
+ `;
424
+
425
+ const historySection = document.createElement('div');
426
+
427
+ const historyTitle = document.createElement('h3');
428
+ historyTitle.textContent = 'Historique de navigation';
429
+ historyTitle.style.marginBottom = '8px';
430
+
431
+ const historyList = document.createElement('div');
432
+ historyList.id = 'history-list';
433
+ historyList.style.cssText = `
434
+ max-height: 200px;
435
+ overflow-y: auto;
436
+ border: 1px solid #333;
437
+ border-radius: 4px;
438
+ padding: 8px;
439
+ `;
440
+
441
+ historySection.appendChild(historyTitle);
442
+ historySection.appendChild(historyList);
443
+
444
+ panel.appendChild(routeInfo);
445
+ panel.appendChild(historySection);
446
+
447
+ this.content.appendChild(panel);
448
+ this.panels.routing = panel;
449
+ }
450
+
451
+ /**
452
+ * Configure les raccourcis clavier
453
+ */
454
+ setupHotkeys() {
455
+ document.addEventListener('keydown', (e) => {
456
+ if (e.ctrlKey && e.shiftKey && e.key === 'D') {
457
+ e.preventDefault();
458
+ this.toggle();
459
+ }
460
+
461
+ // Permet d'inspecter les composants avec Ctrl+Click
462
+ this.framework.canvas.addEventListener('click', (e) => {
463
+ if (e.ctrlKey && this.isOpen) {
464
+ e.preventDefault();
465
+ const rect = this.framework.canvas.getBoundingClientRect();
466
+ const x = e.clientX - rect.left;
467
+ const y = e.clientY - rect.top;
468
+ this.inspectComponentAt(x, y);
469
+ }
470
+ });
471
+ });
472
+ }
473
+
474
+ /**
475
+ * Bascule l'affichage du DevTools
476
+ */
477
+ toggle() {
478
+ this.isOpen = !this.isOpen;
479
+ if (this.isOpen) {
480
+ this.container.style.transform = 'translateX(0)';
481
+ this.refreshAll();
482
+ this.startMonitoring();
483
+ } else {
484
+ this.container.style.transform = 'translateX(100%)';
485
+ this.stopMonitoring();
486
+ }
487
+ }
488
+
489
+ /**
490
+ * Change l'onglet actif
491
+ */
492
+ switchTab(tabId) {
493
+ this.currentTab = tabId;
494
+
495
+ // Mettre à jour les styles des tabs
496
+ this.tabs.querySelectorAll('button').forEach(btn => {
497
+ btn.style.borderBottomColor = btn.dataset.tab === tabId ? '#007acc' : 'transparent';
498
+ btn.style.color = btn.dataset.tab === tabId ? '#fff' : '#ccc';
499
+ });
500
+
501
+ // Afficher/masquer les panels
502
+ Object.keys(this.panels).forEach(panelId => {
503
+ this.panels[panelId].style.display = panelId === tabId ? 'block' : 'none';
504
+ });
505
+
506
+ if (tabId === 'performance') {
507
+ this.updatePerformancePanel();
508
+ } else if (tabId === 'hierarchy') {
509
+ this.updateHierarchyPanel();
510
+ } else if (tabId === 'components') {
511
+ this.updateComponentsPanel();
512
+ } else if (tabId === 'routing') {
513
+ this.updateRoutingPanel();
514
+ }
515
+ }
516
+
517
+ /**
518
+ * Rafraîchit toutes les données
519
+ */
520
+ refreshAll() {
521
+ this.updateComponentsPanel();
522
+ this.updateHierarchyPanel();
523
+ this.updateRoutingPanel();
524
+ }
525
+
526
+ /**
527
+ * Met à jour le panneau des composants
528
+ */
529
+ updateComponentsPanel() {
530
+ if (!this.isOpen || this.currentTab !== 'components') return;
531
+
532
+ const components = this.framework.components;
533
+ const visibleCount = components.filter(c => c.visible).length;
534
+ const dirtyCount = components.filter(c => c._dirty).length;
535
+
536
+ document.getElementById('component-count').textContent = components.length;
537
+ document.getElementById('visible-count').textContent = visibleCount;
538
+ document.getElementById('dirty-count').textContent = dirtyCount;
539
+
540
+ const list = document.getElementById('component-list');
541
+ list.innerHTML = '';
542
+
543
+ components.forEach((comp, index) => {
544
+ const item = document.createElement('div');
545
+ item.style.cssText = `
546
+ padding: 6px 8px;
547
+ border-bottom: 1px solid #333;
548
+ cursor: pointer;
549
+ display: flex;
550
+ align-items: center;
551
+ background: ${comp === this.selectedComponent ? '#007acc20' : 'transparent'};
552
+ `;
553
+
554
+ const type = comp.constructor.name;
555
+ const bounds = `[${Math.round(comp.x)},${Math.round(comp.y)} ${Math.round(comp.width)}x${Math.round(comp.height)}]`;
556
+
557
+ item.innerHTML = `
558
+ <div style="display: flex; align-items: center; flex: 1;">
559
+ <input type="checkbox" ${comp.visible ? 'checked' : ''}
560
+ style="margin-right: 8px;"
561
+ onchange="event.stopPropagation(); devTools.toggleComponentVisibility(${index}, this.checked)">
562
+ <span style="color: #4ec9b0; min-width: 120px;">${type}</span>
563
+ <span style="color: #ccc; margin-left: 8px;">${bounds}</span>
564
+ ${comp._dirty ? '<span style="color: orange; margin-left: 8px;">●</span>' : ''}
565
+ </div>
566
+ <button onclick="event.stopPropagation(); devTools.highlightComponent(${index})"
567
+ style="background: transparent; border: 1px solid #555; color: #ccc; padding: 2px 6px; border-radius: 3px; font-size: 10px;">
568
+
569
+ </button>
570
+ `;
571
+
572
+ item.onclick = () => this.selectComponent(comp);
573
+ list.appendChild(item);
574
+ });
575
+ }
576
+
577
+ /**
578
+ * Met à jour le panneau de performance
579
+ */
580
+ updatePerformancePanel() {
581
+ if (!this.isOpen || this.currentTab !== 'performance') return;
582
+
583
+ // Mettre à jour les stats FPS
584
+ this.performanceStats.fps.push(this.framework.fps);
585
+ if (this.performanceStats.fps.length > 100) {
586
+ this.performanceStats.fps.shift();
587
+ }
588
+
589
+ // Mettre à jour le graphique FPS
590
+ this.drawFPSChart();
591
+
592
+ // Mettre à jour les infos mémoire
593
+ if (performance.memory) {
594
+ const memory = performance.memory;
595
+ document.getElementById('memory-info').innerHTML = `
596
+ <div>Used: ${Math.round(memory.usedJSHeapSize / 1024 / 1024)} MB</div>
597
+ <div>Total: ${Math.round(memory.totalJSHeapSize / 1024 / 1024)} MB</div>
598
+ <div>Limit: ${Math.round(memory.jsHeapSizeLimit / 1024 / 1024)} MB</div>
599
+ `;
600
+ }
601
+ }
602
+
603
+ /**
604
+ * Dessine le graphique FPS
605
+ */
606
+ drawFPSChart() {
607
+ if (!this.fpsCanvas) return;
608
+
609
+ const ctx = this.fpsCanvas.getContext('2d');
610
+ const width = this.fpsCanvas.width;
611
+ const height = this.fpsCanvas.height;
612
+
613
+ // Clear
614
+ ctx.fillStyle = '#252526';
615
+ ctx.fillRect(0, 0, width, height);
616
+
617
+ // Draw grid
618
+ ctx.strokeStyle = '#333';
619
+ ctx.lineWidth = 1;
620
+
621
+ // Horizontal lines (FPS markers)
622
+ for (let i = 0; i <= 60; i += 15) {
623
+ const y = height - (i / 60 * height);
624
+ ctx.beginPath();
625
+ ctx.moveTo(0, y);
626
+ ctx.lineTo(width, y);
627
+ ctx.stroke();
628
+
629
+ // Labels
630
+ ctx.fillStyle = '#888';
631
+ ctx.font = '10px monospace';
632
+ ctx.fillText(`${i}`, 5, y - 2);
633
+ }
634
+
635
+ // Draw FPS line
636
+ if (this.performanceStats.fps.length > 1) {
637
+ ctx.strokeStyle = '#4ec9b0';
638
+ ctx.lineWidth = 2;
639
+ ctx.beginPath();
640
+
641
+ const step = width / (this.performanceStats.fps.length - 1);
642
+ this.performanceStats.fps.forEach((fps, i) => {
643
+ const x = i * step;
644
+ const y = height - (Math.min(fps, 60) / 60 * height);
645
+
646
+ if (i === 0) {
647
+ ctx.moveTo(x, y);
648
+ } else {
649
+ ctx.lineTo(x, y);
650
+ }
651
+ });
652
+
653
+ ctx.stroke();
654
+
655
+ // Current FPS
656
+ const currentFps = this.performanceStats.fps[this.performanceStats.fps.length - 1] || 0;
657
+ ctx.fillStyle = currentFps < 30 ? '#ff5555' : '#4ec9b0';
658
+ ctx.font = 'bold 12px monospace';
659
+ ctx.textAlign = 'right';
660
+ ctx.fillText(`${currentFps} FPS`, width - 5, 15);
661
+ }
662
+ }
663
+
664
+ /**
665
+ * Met à jour le panneau hiérarchique
666
+ */
667
+ updateHierarchyPanel() {
668
+ if (!this.isOpen || this.currentTab !== 'hierarchy') return;
669
+
670
+ const tree = document.getElementById('component-tree');
671
+ tree.innerHTML = '';
672
+
673
+ const renderComponent = (comp, depth = 0) => {
674
+ const item = document.createElement('div');
675
+ item.style.cssText = `
676
+ padding-left: ${depth * 20}px;
677
+ margin: 2px 0;
678
+ cursor: pointer;
679
+ background: ${comp === this.selectedComponent ? '#007acc20' : 'transparent'};
680
+ `;
681
+
682
+ const type = comp.constructor.name;
683
+ const hasChildren = comp.children && comp.children.length > 0;
684
+
685
+ item.innerHTML = `
686
+ <span style="color: ${comp.visible ? '#4ec9b0' : '#555'}">
687
+ ${hasChildren ? '▼' : '○'} ${type}
688
+ <span style="color: #888; font-size: 10px;">
689
+ (${comp.width}x${comp.height})
690
+ </span>
691
+ </span>
692
+ `;
693
+
694
+ item.onclick = () => this.selectComponent(comp);
695
+ tree.appendChild(item);
696
+
697
+ // Rendre les enfants si c'est une Card ou autre conteneur
698
+ if (hasChildren && comp.children) {
699
+ // Vérifier si c'est un itérable
700
+ const childArray = Array.isArray(comp.children) ? comp.children : [];
701
+
702
+ childArray.forEach(child => {
703
+ // Vérifier si c'est un objet avec les propriétés d'un composant
704
+ if (child && typeof child === 'object' &&
705
+ 'x' in child && 'y' in child && 'width' in child && 'height' in child) {
706
+ renderComponent(child, depth + 1);
707
+ }
708
+ });
709
+ }
710
+ };
711
+
712
+ this.framework.components.forEach(comp => {
713
+ renderComponent(comp);
714
+ });
715
+ }
716
+
717
+ /**
718
+ * Met à jour le panneau des propriétés
719
+ */
720
+ updatePropertiesPanel() {
721
+ if (!this.selectedComponent) return;
722
+
723
+ const table = document.getElementById('properties-table');
724
+ const noSelection = document.getElementById('no-selection');
725
+
726
+ noSelection.style.display = 'none';
727
+ table.style.display = 'block';
728
+ table.innerHTML = '';
729
+
730
+ const comp = this.selectedComponent;
731
+ const props = {};
732
+
733
+ // Collecter toutes les propriétés
734
+ for (let key in comp) {
735
+ if (key.startsWith('_') || key === 'framework') continue; // <-- EXCLURE 'framework'
736
+
737
+ try {
738
+ const value = comp[key];
739
+ if (typeof value === 'function') continue;
740
+
741
+ // Éviter les références circulaires
742
+ if (key === 'parent' || key === 'children' || key === 'framework') {
743
+ props[key] = `[${value?.constructor?.name || 'Object'}]`;
744
+ continue;
745
+ }
746
+
747
+ // Tenter de stringifier, mais avec un repli sûr
748
+ props[key] = value;
749
+ } catch (e) {
750
+ props[key] = '<error>';
751
+ }
752
+ }
753
+
754
+ // Créer le tableau
755
+ Object.entries(props).forEach(([key, value]) => {
756
+ const row = document.createElement('div');
757
+ row.style.cssText = `
758
+ display: flex;
759
+ padding: 4px 0;
760
+ border-bottom: 1px solid #333;
761
+ `;
762
+
763
+ const keyCell = document.createElement('div');
764
+ keyCell.textContent = key;
765
+ keyCell.style.cssText = `
766
+ color: #9cdcfe;
767
+ min-width: 150px;
768
+ font-family: monospace;
769
+ `;
770
+
771
+ const valueCell = document.createElement('div');
772
+
773
+ try {
774
+ // Fonction pour stringifier en évitant les cycles
775
+ const safeStringify = (obj, replacer, space) => {
776
+ const seen = new WeakSet();
777
+ return JSON.stringify(obj, (key, value) => {
778
+ if (typeof value === 'object' && value !== null) {
779
+ if (seen.has(value)) {
780
+ return '[Circular Reference]';
781
+ }
782
+ seen.add(value);
783
+ }
784
+ return replacer ? replacer(key, value) : value;
785
+ }, space);
786
+ };
787
+
788
+ if (typeof value === 'object' && value !== null) {
789
+ // Exclure certaines propriétés problématiques
790
+ if (value === comp || value === this.framework) {
791
+ valueCell.textContent = '[Circular Reference]';
792
+ } else if (value instanceof HTMLElement) {
793
+ valueCell.textContent = `[HTMLElement: ${value.tagName}]`;
794
+ } else if (value instanceof Event) {
795
+ valueCell.textContent = `[Event: ${value.type}]`;
796
+ } else if (value instanceof NodeList || value instanceof HTMLCollection) {
797
+ valueCell.textContent = `[Collection: ${value.length} items]`;
798
+ } else {
799
+ try {
800
+ valueCell.textContent = safeStringify(value, null, 2);
801
+ } catch (e) {
802
+ valueCell.textContent = `[Object: ${value.constructor?.name || 'Unknown'}]`;
803
+ }
804
+ }
805
+ valueCell.style.color = '#ce9178';
806
+ } else if (typeof value === 'boolean') {
807
+ valueCell.textContent = value.toString();
808
+ valueCell.style.color = '#569cd6';
809
+ } else if (typeof value === 'number') {
810
+ valueCell.textContent = value;
811
+ valueCell.style.color = '#b5cea8';
812
+ } else {
813
+ valueCell.textContent = String(value);
814
+ valueCell.style.color = '#d4d4d4';
815
+ }
816
+ } catch (stringifyError) {
817
+ valueCell.textContent = '[Unserializable]';
818
+ valueCell.style.color = '#ff5555';
819
+ }
820
+
821
+ valueCell.style.cssText += `
822
+ font-family: monospace;
823
+ white-space: pre-wrap;
824
+ word-break: break-all;
825
+ flex: 1;
826
+ `;
827
+
828
+ row.appendChild(keyCell);
829
+ row.appendChild(valueCell);
830
+ table.appendChild(row);
831
+ });
832
+
833
+ // Ajouter des contrôles d'édition pour les propriétés importantes
834
+ const controls = document.createElement('div');
835
+ controls.style.marginTop = '20px';
836
+
837
+ const editTitle = document.createElement('h4');
838
+ editTitle.textContent = 'Modifier';
839
+ editTitle.style.marginBottom = '8px';
840
+
841
+ // Éditeur de position
842
+ const posEditor = document.createElement('div');
843
+ posEditor.style.cssText = `
844
+ display: grid;
845
+ grid-template-columns: 1fr 1fr;
846
+ gap: 8px;
847
+ margin-bottom: 10px;
848
+ `;
849
+
850
+ const createNumberInput = (label, value, onChange) => {
851
+ const container = document.createElement('div');
852
+ container.style.cssText = `
853
+ display: flex;
854
+ align-items: center;
855
+ gap: 4px;
856
+ `;
857
+
858
+ const labelEl = document.createElement('span');
859
+ labelEl.textContent = label;
860
+ labelEl.style.minWidth = '40px';
861
+
862
+ const input = document.createElement('input');
863
+ input.type = 'number';
864
+ input.value = value;
865
+ input.style.cssText = `
866
+ width: 60px;
867
+ padding: 2px 4px;
868
+ background: #333;
869
+ border: 1px solid #555;
870
+ color: #fff;
871
+ border-radius: 3px;
872
+ `;
873
+ input.onchange = (e) => onChange(parseFloat(e.target.value));
874
+
875
+ container.appendChild(labelEl);
876
+ container.appendChild(input);
877
+ return container;
878
+ };
879
+
880
+ posEditor.appendChild(
881
+ createNumberInput('X:', comp.x, (val) => {
882
+ comp.x = val;
883
+ comp.markDirty();
884
+ })
885
+ );
886
+
887
+ posEditor.appendChild(
888
+ createNumberInput('Y:', comp.y, (val) => {
889
+ comp.y = val;
890
+ comp.markDirty();
891
+ })
892
+ );
893
+
894
+ posEditor.appendChild(
895
+ createNumberInput('W:', comp.width, (val) => {
896
+ comp.width = val;
897
+ comp.markDirty();
898
+ })
899
+ );
900
+
901
+ posEditor.appendChild(
902
+ createNumberInput('H:', comp.height, (val) => {
903
+ comp.height = val;
904
+ comp.markDirty();
905
+ })
906
+ );
907
+
908
+ // Toggle pour visible
909
+ const visibleControl = document.createElement('div');
910
+ visibleControl.style.cssText = `
911
+ display: flex;
912
+ align-items: center;
913
+ gap: 8px;
914
+ margin-top: 10px;
915
+ `;
916
+
917
+ const visibleLabel = document.createElement('span');
918
+ visibleLabel.textContent = 'Visible:';
919
+
920
+ const visibleCheckbox = document.createElement('input');
921
+ visibleCheckbox.type = 'checkbox';
922
+ visibleCheckbox.checked = comp.visible;
923
+ visibleCheckbox.onchange = () => {
924
+ comp.visible = visibleCheckbox.checked;
925
+ comp.markDirty();
926
+ };
927
+
928
+ visibleControl.appendChild(visibleLabel);
929
+ visibleControl.appendChild(visibleCheckbox);
930
+
931
+ controls.appendChild(editTitle);
932
+ controls.appendChild(posEditor);
933
+ controls.appendChild(visibleControl);
934
+ table.appendChild(controls);
935
+ }
936
+
937
+ /**
938
+ * Met à jour le panneau de routing
939
+ */
940
+ updateRoutingPanel() {
941
+ if (!this.isOpen || this.currentTab !== 'routing') return;
942
+
943
+ const routeInfo = document.getElementById('route-info');
944
+ const historyList = document.getElementById('history-list');
945
+
946
+ // Current route
947
+ routeInfo.innerHTML = `
948
+ <div><strong>Route actuelle:</strong> ${this.framework.currentRoute}</div>
949
+ <div><strong>Paramètres:</strong> ${JSON.stringify(this.framework.currentParams)}</div>
950
+ <div><strong>Query:</strong> ${JSON.stringify(this.framework.currentQuery)}</div>
951
+ `;
952
+
953
+ // History
954
+ historyList.innerHTML = '';
955
+ this.framework.history.forEach((entry, index) => {
956
+ const item = document.createElement('div');
957
+ item.style.cssText = `
958
+ padding: 4px 8px;
959
+ margin: 2px 0;
960
+ background: ${index === this.framework.historyIndex ? '#007acc20' : 'transparent'};
961
+ border-radius: 3px;
962
+ cursor: pointer;
963
+ font-family: monospace;
964
+ font-size: 11px;
965
+ `;
966
+ item.textContent = `${index}: ${entry.path}`;
967
+ item.onclick = () => {
968
+ this.framework.navigate(entry.path, { replace: true });
969
+ };
970
+ historyList.appendChild(item);
971
+ });
972
+ }
973
+
974
+ /**
975
+ * Ajoute un événement au panneau
976
+ */
977
+ logEvent(type, data) {
978
+ if (!this.isOpen || this.currentTab !== 'events') return;
979
+
980
+ const eventItem = document.createElement('div');
981
+ eventItem.style.cssText = `
982
+ padding: 4px 8px;
983
+ margin: 2px 0;
984
+ background: #252526;
985
+ border-left: 3px solid #007acc;
986
+ font-family: monospace;
987
+ font-size: 11px;
988
+ `;
989
+
990
+ const time = new Date().toLocaleTimeString();
991
+ eventItem.innerHTML = `
992
+ <div style="color: #569cd6;">${time} - ${type}</div>
993
+ <div style="color: #888; font-size: 10px;">${JSON.stringify(data)}</div>
994
+ `;
995
+
996
+ this.eventsList.appendChild(eventItem);
997
+ this.eventsList.scrollTop = this.eventsList.scrollHeight;
998
+ }
999
+
1000
+ /**
1001
+ * Nettoie la liste des événements
1002
+ */
1003
+ clearEvents() {
1004
+ this.eventsList.innerHTML = '';
1005
+ }
1006
+
1007
+ /**
1008
+ * Sélectionne un composant
1009
+ */
1010
+ selectComponent(component) {
1011
+ this.selectedComponent = component;
1012
+ this.updatePropertiesPanel();
1013
+ this.updateComponentsPanel();
1014
+ this.updateHierarchyPanel();
1015
+ this.highlightComponentDirectly(component);
1016
+ }
1017
+
1018
+ /**
1019
+ * Inspecte un composant à une position donnée
1020
+ */
1021
+ inspectComponentAt(x, y) {
1022
+ const adjustedY = y - this.framework.scrollOffset;
1023
+
1024
+ for (let i = this.framework.components.length - 1; i >= 0; i--) {
1025
+ const comp = this.framework.components[i];
1026
+
1027
+ if (comp.visible && comp.isPointInside(x, adjustedY)) {
1028
+ this.selectComponent(comp);
1029
+ this.switchTab('properties');
1030
+ return comp;
1031
+ }
1032
+ }
1033
+ return null;
1034
+ }
1035
+
1036
+ /**
1037
+ * Met en surbrillance un composant
1038
+ */
1039
+ highlightComponent(index) {
1040
+ if (index >= 0 && index < this.framework.components.length) {
1041
+ const comp = this.framework.components[index];
1042
+ this.highlightComponentDirectly(comp);
1043
+
1044
+ // Animation de flash
1045
+ const originalOpacity = comp.opacity;
1046
+ let flashCount = 0;
1047
+ const flashInterval = setInterval(() => {
1048
+ comp.opacity = flashCount % 2 === 0 ? 0.5 : (originalOpacity || 1);
1049
+ comp.markDirty();
1050
+ flashCount++;
1051
+
1052
+ if (flashCount > 5) {
1053
+ clearInterval(flashInterval);
1054
+ comp.opacity = originalOpacity || 1;
1055
+ comp.markDirty();
1056
+ }
1057
+ }, 200);
1058
+ }
1059
+ }
1060
+
1061
+ /**
1062
+ * Met en surbrillance directe d'un composant
1063
+ */
1064
+ highlightComponentDirectly(comp) {
1065
+ // Dessiner une surbrillance autour du composant
1066
+ const originalDraw = comp.draw;
1067
+ comp.draw = function(ctx) {
1068
+ originalDraw.call(this, ctx);
1069
+
1070
+ // Dessiner un cadre de surbrillance
1071
+ ctx.save();
1072
+ ctx.strokeStyle = '#ff5555';
1073
+ ctx.lineWidth = 2;
1074
+ ctx.setLineDash([5, 3]);
1075
+ ctx.strokeRect(this.x - 2, this.y - 2, this.width + 4, this.height + 4);
1076
+
1077
+ // Ajouter une étiquette
1078
+ ctx.fillStyle = '#ff5555';
1079
+ ctx.font = '10px Arial';
1080
+ ctx.fillText(`${this.constructor.name}`, this.x, this.y - 5);
1081
+ ctx.restore();
1082
+ };
1083
+
1084
+ comp.markDirty();
1085
+
1086
+ // Restaurer après 2 secondes
1087
+ setTimeout(() => {
1088
+ comp.draw = originalDraw;
1089
+ comp.markDirty();
1090
+ }, 2000);
1091
+ }
1092
+
1093
+ /**
1094
+ * Bascule la visibilité de tous les composants
1095
+ */
1096
+ toggleAllComponents() {
1097
+ const allVisible = this.framework.components.every(c => c.visible);
1098
+ this.framework.components.forEach(comp => {
1099
+ comp.visible = !allVisible;
1100
+ comp.markDirty();
1101
+ });
1102
+ this.updateComponentsPanel();
1103
+ }
1104
+
1105
+ /**
1106
+ * Bascule la visibilité d'un composant
1107
+ */
1108
+ toggleComponentVisibility(index, visible) {
1109
+ if (index >= 0 && index < this.framework.components.length) {
1110
+ const comp = this.framework.components[index];
1111
+ comp.visible = visible;
1112
+ comp.markDirty();
1113
+ this.updateComponentsPanel();
1114
+ }
1115
+ }
1116
+
1117
+ /**
1118
+ * Rafraîchit la liste des composants
1119
+ */
1120
+ refreshComponents() {
1121
+ this.updateComponentsPanel();
1122
+ }
1123
+
1124
+ /**
1125
+ * Démarre la surveillance des performances
1126
+ */
1127
+ startMonitoring() {
1128
+ this.monitorInterval = setInterval(() => {
1129
+ this.updatePerformancePanel();
1130
+ }, 1000);
1131
+ }
1132
+
1133
+ /**
1134
+ * Arrête la surveillance des performances
1135
+ */
1136
+ stopMonitoring() {
1137
+ if (this.monitorInterval) {
1138
+ clearInterval(this.monitorInterval);
1139
+ }
1140
+ }
1141
+
1142
+ /**
1143
+ * Nettoie les références et restaure le framework
1144
+ */
1145
+ cleanup() {
1146
+ // Restaurer les méthodes originales si elles existent
1147
+ if (this.framework) {
1148
+ if (this.originalNavigate) {
1149
+ this.framework.navigate = this.originalNavigate;
1150
+ this.originalNavigate = null;
1151
+ }
1152
+ if (this.originalAdd) {
1153
+ this.framework.add = this.originalAdd;
1154
+ this.originalAdd = null;
1155
+ }
1156
+ }
1157
+
1158
+ // Supprimer la référence globale
1159
+ if (window.devTools === this) {
1160
+ delete window.devTools;
1161
+ }
1162
+
1163
+ // Masquer le DevTools
1164
+ if (this.isOpen) {
1165
+ this.toggle();
1166
+ }
1167
+
1168
+ // Masquer le bouton flottant
1169
+ if (this.toggleBtn) {
1170
+ this.toggleBtn.style.display = 'none';
1171
+ }
1172
+
1173
+ // Nettoyer les écouteurs d'événements
1174
+ if (this.closeBtn) {
1175
+ this.closeBtn.onclick = null;
1176
+ }
1177
+
1178
+ if (this.toggleBtn) {
1179
+ this.toggleBtn.onclick = null;
1180
+ }
1181
+
1182
+ // Nettoyer les tabs
1183
+ this.tabs.querySelectorAll('button').forEach(tab => {
1184
+ tab.onclick = null;
1185
+ });
1186
+ }
1187
+
1188
+ /**
1189
+ * Enregistre le DevTools dans le framework
1190
+ */
1191
+ attachToFramework() {
1192
+ window.devTools = this;
1193
+
1194
+ // Sauvegarder les références originales
1195
+ this.originalNavigate = this.framework.navigate;
1196
+ this.originalAdd = this.framework.add;
1197
+
1198
+ // Intercepter les événements du framework
1199
+ this.framework.navigate = (...args) => {
1200
+ if (window.devTools) {
1201
+ window.devTools.logEvent('NAVIGATE', { path: args[0] });
1202
+ }
1203
+ return this.originalNavigate.apply(this.framework, args);
1204
+ };
1205
+
1206
+ // Intercepter les ajouts de composants
1207
+ this.framework.add = (...args) => {
1208
+ if (window.devTools) {
1209
+ window.devTools.logEvent('ADD_COMPONENT', {
1210
+ type: args[0]?.constructor?.name
1211
+ });
1212
+ }
1213
+ return this.originalAdd.apply(this.framework, args);
1214
+ };
1215
+ }
1216
+
1217
+ /**
1218
+ * Détache le DevTools du framework
1219
+ */
1220
+ detachFromFramework() {
1221
+ // Restaurer les méthodes originales
1222
+ if (this.originalNavigate && this.framework) {
1223
+ this.framework.navigate = this.originalNavigate;
1224
+ }
1225
+
1226
+ if (this.originalAdd && this.framework) {
1227
+ this.framework.add = this.originalAdd;
1228
+ }
1229
+
1230
+ // Supprimer la référence globale
1231
+ if (window.devTools === this) {
1232
+ delete window.devTools;
1233
+ }
1234
+
1235
+ // Masquer le DevTools s'il est ouvert
1236
+ if (this.isOpen) {
1237
+ this.toggle();
1238
+ }
1239
+
1240
+ // Masquer le bouton flottant
1241
+ if (this.toggleBtn) {
1242
+ this.toggleBtn.style.display = 'none';
1243
+ }
1244
+ }
1245
+ }
1246
+
1247
+ export default DevTools;