rails_accessibility_testing 1.5.3 → 1.5.5

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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +376 -1
  3. data/CHANGELOG.md +63 -1
  4. data/GUIDES/getting_started.md +40 -5
  5. data/GUIDES/system_specs_for_accessibility.md +12 -4
  6. data/README.md +52 -8
  7. data/docs_site/Gemfile.lock +89 -0
  8. data/docs_site/_config.yml +9 -0
  9. data/docs_site/_includes/header.html +1 -0
  10. data/docs_site/_layouts/default.html +754 -15
  11. data/docs_site/architecture.md +533 -0
  12. data/docs_site/index.md +2 -1
  13. data/exe/a11y_live_scanner +10 -39
  14. data/exe/a11y_static_scanner +333 -0
  15. data/lib/generators/rails_a11y/install/install_generator.rb +19 -30
  16. data/lib/generators/rails_a11y/install/templates/accessibility.yml.erb +39 -0
  17. data/lib/generators/rails_a11y/install/templates/all_pages_accessibility_spec.rb.erb +132 -45
  18. data/lib/rails_accessibility_testing/accessibility_helper.rb +131 -126
  19. data/lib/rails_accessibility_testing/checks/base_check.rb +14 -5
  20. data/lib/rails_accessibility_testing/checks/form_errors_check.rb +1 -1
  21. data/lib/rails_accessibility_testing/checks/form_labels_check.rb +6 -4
  22. data/lib/rails_accessibility_testing/checks/heading_check.rb +7 -15
  23. data/lib/rails_accessibility_testing/checks/image_alt_text_check.rb +1 -1
  24. data/lib/rails_accessibility_testing/checks/interactive_elements_check.rb +12 -8
  25. data/lib/rails_accessibility_testing/config/yaml_loader.rb +20 -0
  26. data/lib/rails_accessibility_testing/erb_extractor.rb +141 -0
  27. data/lib/rails_accessibility_testing/error_message_builder.rb +11 -6
  28. data/lib/rails_accessibility_testing/file_change_tracker.rb +95 -0
  29. data/lib/rails_accessibility_testing/line_number_finder.rb +61 -0
  30. data/lib/rails_accessibility_testing/rspec_integration.rb +74 -33
  31. data/lib/rails_accessibility_testing/shared_examples.rb +2 -0
  32. data/lib/rails_accessibility_testing/static_file_scanner.rb +80 -0
  33. data/lib/rails_accessibility_testing/static_page_adapter.rb +116 -0
  34. data/lib/rails_accessibility_testing/static_scanning.rb +61 -0
  35. data/lib/rails_accessibility_testing/version.rb +3 -1
  36. data/lib/rails_accessibility_testing/violation_converter.rb +80 -0
  37. data/lib/rails_accessibility_testing.rb +9 -1
  38. metadata +26 -2
@@ -1,32 +1,53 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="en">
3
+
3
4
  <head>
4
5
  <meta charset="UTF-8">
5
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
7
  <title>{% if page.title %}{{ page.title }} - {% endif %}{{ site.title }}</title>
7
8
  <meta name="description" content="{{ site.description }}">
8
9
  <style>
9
- * { margin: 0; padding: 0; box-sizing: border-box; }
10
+ * {
11
+ margin: 0;
12
+ padding: 0;
13
+ box-sizing: border-box;
14
+ }
15
+
10
16
  body {
11
17
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
12
18
  line-height: 1.6;
13
19
  color: #333;
14
20
  background: #fff;
15
21
  }
22
+
16
23
  header {
17
24
  background: #2c3e50;
18
25
  color: white;
19
26
  padding: 2rem 0;
20
27
  text-align: center;
21
28
  }
22
- header h1 { font-size: 2.5rem; margin-bottom: 0.5rem; }
23
- header h1 a { color: white; text-decoration: none; }
24
- header p { font-size: 1.2rem; opacity: 0.9; }
29
+
30
+ header h1 {
31
+ font-size: 2.5rem;
32
+ margin-bottom: 0.5rem;
33
+ }
34
+
35
+ header h1 a {
36
+ color: white;
37
+ text-decoration: none;
38
+ }
39
+
40
+ header p {
41
+ font-size: 1.2rem;
42
+ opacity: 0.9;
43
+ }
44
+
25
45
  nav {
26
46
  background: #34495e;
27
47
  padding: 1rem 0;
28
48
  text-align: center;
29
49
  }
50
+
30
51
  nav a {
31
52
  color: white;
32
53
  text-decoration: none;
@@ -34,22 +55,42 @@
34
55
  padding: 0.5rem 1rem;
35
56
  display: inline-block;
36
57
  }
37
- nav a:hover { background: #2c3e50; }
58
+
59
+ nav a:hover {
60
+ background: #2c3e50;
61
+ }
62
+
38
63
  main {
39
64
  max-width: 1200px;
40
65
  margin: 2rem auto;
41
66
  padding: 0 2rem;
42
67
  }
68
+
43
69
  section {
44
70
  margin: 3rem 0;
45
71
  }
46
- h1, h2 {
72
+
73
+ h1,
74
+ h2 {
47
75
  color: #2c3e50;
48
76
  margin-bottom: 1rem;
49
77
  }
50
- h1 { font-size: 2.5rem; }
51
- h2 { font-size: 2rem; }
52
- h3 { font-size: 1.5rem; color: #34495e; margin-top: 2rem; margin-bottom: 1rem; }
78
+
79
+ h1 {
80
+ font-size: 2.5rem;
81
+ }
82
+
83
+ h2 {
84
+ font-size: 2rem;
85
+ }
86
+
87
+ h3 {
88
+ font-size: 1.5rem;
89
+ color: #34495e;
90
+ margin-top: 2rem;
91
+ margin-bottom: 1rem;
92
+ }
93
+
53
94
  code {
54
95
  background: #f4f4f4;
55
96
  padding: 0.2rem 0.4rem;
@@ -57,6 +98,7 @@
57
98
  font-family: 'Courier New', monospace;
58
99
  font-size: 0.9em;
59
100
  }
101
+
60
102
  pre {
61
103
  background: #f4f4f4;
62
104
  padding: 1rem;
@@ -64,10 +106,12 @@
64
106
  overflow-x: auto;
65
107
  margin: 1rem 0;
66
108
  }
109
+
67
110
  pre code {
68
111
  background: none;
69
112
  padding: 0;
70
113
  }
114
+
71
115
  footer {
72
116
  background: #2c3e50;
73
117
  color: white;
@@ -75,6 +119,7 @@
75
119
  padding: 2rem 0;
76
120
  margin-top: 4rem;
77
121
  }
122
+
78
123
  .cta {
79
124
  background: #3498db;
80
125
  color: white;
@@ -84,47 +129,741 @@
84
129
  display: inline-block;
85
130
  margin: 1rem 0;
86
131
  }
87
- .cta:hover { background: #2980b9; }
88
- ul, ol {
132
+
133
+ .cta:hover {
134
+ background: #2980b9;
135
+ }
136
+
137
+ ul,
138
+ ol {
89
139
  margin-left: 2rem;
90
140
  margin-bottom: 1rem;
91
141
  }
142
+
92
143
  li {
93
144
  margin-bottom: 0.5rem;
94
145
  }
146
+
95
147
  blockquote {
96
148
  border-left: 4px solid #3498db;
97
149
  padding-left: 1rem;
98
150
  margin: 1rem 0;
99
151
  color: #666;
100
152
  }
153
+
101
154
  table {
102
155
  width: 100%;
103
156
  border-collapse: collapse;
104
157
  margin: 1rem 0;
105
158
  }
106
- th, td {
159
+
160
+ th,
161
+ td {
107
162
  border: 1px solid #ddd;
108
163
  padding: 0.5rem;
109
164
  text-align: left;
110
165
  }
166
+
111
167
  th {
112
168
  background: #f4f4f4;
113
169
  font-weight: 600;
114
170
  }
171
+
172
+ /* Mermaid diagram container styles */
173
+ .mermaid-container {
174
+ position: relative;
175
+ margin: 2rem 0;
176
+ border: 1px solid #ddd;
177
+ border-radius: 8px;
178
+ padding: 1.5rem;
179
+ background: #fafafa;
180
+ }
181
+
182
+ .mermaid-container .mermaid {
183
+ margin: 0;
184
+ text-align: center;
185
+ }
186
+
187
+ /* Fullscreen button */
188
+ .fullscreen-btn {
189
+ position: absolute;
190
+ top: 10px;
191
+ right: 10px;
192
+ background: #3498db;
193
+ color: white;
194
+ border: none;
195
+ padding: 8px 12px;
196
+ border-radius: 5px;
197
+ cursor: pointer;
198
+ font-size: 14px;
199
+ display: flex;
200
+ align-items: center;
201
+ gap: 5px;
202
+ transition: background 0.3s, transform 0.2s;
203
+ z-index: 10;
204
+ }
205
+
206
+ .fullscreen-btn:hover {
207
+ background: #2980b9;
208
+ transform: scale(1.05);
209
+ }
210
+
211
+ .fullscreen-btn:focus {
212
+ outline: 2px solid #2c3e50;
213
+ outline-offset: 2px;
214
+ }
215
+
216
+ .fullscreen-btn svg {
217
+ width: 16px;
218
+ height: 16px;
219
+ }
220
+
221
+ /* Fullscreen overlay */
222
+ .diagram-fullscreen-overlay {
223
+ display: none;
224
+ position: fixed;
225
+ top: 0;
226
+ left: 0;
227
+ width: 100%;
228
+ height: 100%;
229
+ background: rgba(0, 0, 0, 0.95);
230
+ z-index: 9999;
231
+ overflow: auto;
232
+ animation: fadeIn 0.3s ease;
233
+ }
234
+
235
+ .diagram-fullscreen-overlay.active {
236
+ display: flex;
237
+ align-items: center;
238
+ justify-content: center;
239
+ padding: 2rem;
240
+ }
241
+
242
+ .diagram-fullscreen-content {
243
+ background: white;
244
+ border-radius: 8px;
245
+ padding: 2rem;
246
+ width: 98vw;
247
+ height: 98vh;
248
+ overflow: auto;
249
+ position: relative;
250
+ animation: slideIn 0.3s ease;
251
+ display: flex;
252
+ align-items: center;
253
+ justify-content: center;
254
+ }
255
+
256
+ #diagram-zoom-wrapper {
257
+ display: flex;
258
+ align-items: center;
259
+ justify-content: center;
260
+ min-width: 100%;
261
+ min-height: 100%;
262
+ }
263
+
264
+ .diagram-fullscreen-content .mermaid {
265
+ margin: 0;
266
+ display: block;
267
+ width: auto;
268
+ height: auto;
269
+ }
270
+
271
+ .diagram-fullscreen-content .mermaid svg {
272
+ max-width: none !important;
273
+ max-height: none !important;
274
+ width: auto !important;
275
+ height: auto !important;
276
+ display: block;
277
+ margin: 0 auto;
278
+ }
279
+
280
+ /* Close button */
281
+ .close-fullscreen-btn {
282
+ position: fixed;
283
+ top: 20px;
284
+ right: 20px;
285
+ background: #e74c3c;
286
+ color: white;
287
+ border: none;
288
+ padding: 10px 20px;
289
+ border-radius: 5px;
290
+ cursor: pointer;
291
+ font-size: 16px;
292
+ font-weight: bold;
293
+ z-index: 10000;
294
+ transition: background 0.3s, transform 0.2s;
295
+ }
296
+
297
+ .close-fullscreen-btn:hover {
298
+ background: #c0392b;
299
+ transform: scale(1.05);
300
+ }
301
+
302
+ .close-fullscreen-btn:focus {
303
+ outline: 2px solid white;
304
+ outline-offset: 2px;
305
+ }
306
+
307
+ /* Zoom controls */
308
+ .zoom-controls {
309
+ position: fixed;
310
+ top: 20px;
311
+ left: 20px;
312
+ display: flex;
313
+ gap: 10px;
314
+ z-index: 10000;
315
+ }
316
+
317
+ .zoom-btn {
318
+ background: #3498db;
319
+ color: white;
320
+ border: none;
321
+ padding: 10px 15px;
322
+ border-radius: 5px;
323
+ cursor: pointer;
324
+ font-size: 16px;
325
+ font-weight: bold;
326
+ transition: background 0.3s, transform 0.2s;
327
+ display: flex;
328
+ align-items: center;
329
+ gap: 5px;
330
+ }
331
+
332
+ .zoom-btn:hover {
333
+ background: #2980b9;
334
+ transform: scale(1.05);
335
+ }
336
+
337
+ .zoom-btn:focus {
338
+ outline: 2px solid white;
339
+ outline-offset: 2px;
340
+ }
341
+
342
+ .zoom-btn:disabled {
343
+ background: #95a5a6;
344
+ cursor: not-allowed;
345
+ transform: none;
346
+ }
347
+
348
+ .zoom-level {
349
+ background: rgba(255, 255, 255, 0.9);
350
+ color: #2c3e50;
351
+ padding: 10px 15px;
352
+ border-radius: 5px;
353
+ font-size: 14px;
354
+ font-weight: bold;
355
+ display: flex;
356
+ align-items: center;
357
+ min-width: 80px;
358
+ justify-content: center;
359
+ }
360
+
361
+ /* Animations */
362
+ @keyframes fadeIn {
363
+ from {
364
+ opacity: 0;
365
+ }
366
+
367
+ to {
368
+ opacity: 1;
369
+ }
370
+ }
371
+
372
+ @keyframes slideIn {
373
+ from {
374
+ opacity: 0;
375
+ transform: scale(0.9) translateY(-20px);
376
+ }
377
+
378
+ to {
379
+ opacity: 1;
380
+ transform: scale(1) translateY(0);
381
+ }
382
+ }
383
+
384
+ /* Responsive adjustments */
385
+ @media (max-width: 768px) {
386
+ .diagram-fullscreen-content {
387
+ padding: 1rem;
388
+ }
389
+
390
+ .fullscreen-btn {
391
+ font-size: 12px;
392
+ padding: 6px 10px;
393
+ }
394
+ }
115
395
  </style>
116
396
  </head>
397
+
117
398
  <body>
118
399
  {% include header.html %}
119
-
400
+
120
401
  <main>
121
402
  {{ content }}
122
403
  </main>
123
-
404
+
124
405
  <footer>
125
406
  <p>&copy; {{ 'now' | date: "%Y" }} Rails Accessibility Testing. MIT License.</p>
126
407
  <p><a href="https://github.com/rayraycodes/rails-accessibility-testing" style="color: white;">GitHub</a></p>
127
408
  </footer>
409
+
410
+ <!-- Mermaid.js for diagram rendering - Load at end of body for better reliability -->
411
+ <script type="module">
412
+ import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
413
+
414
+ // Initialize Mermaid
415
+ mermaid.initialize({
416
+ startOnLoad: true,
417
+ theme: 'default',
418
+ securityLevel: 'loose',
419
+ flowchart: { useMaxWidth: true },
420
+ sequence: { useMaxWidth: true },
421
+ gantt: { useMaxWidth: true }
422
+ });
423
+
424
+ // Function to create fullscreen overlay
425
+ function createFullscreenOverlay() {
426
+ if (document.getElementById('diagram-fullscreen-overlay')) {
427
+ return; // Already exists
428
+ }
429
+
430
+ const overlay = document.createElement('div');
431
+ overlay.id = 'diagram-fullscreen-overlay';
432
+ overlay.className = 'diagram-fullscreen-overlay';
433
+ overlay.setAttribute('role', 'dialog');
434
+ overlay.setAttribute('aria-modal', 'true');
435
+ overlay.setAttribute('aria-label', 'Fullscreen diagram viewer');
436
+
437
+ // Zoom controls
438
+ const zoomControls = document.createElement('div');
439
+ zoomControls.className = 'zoom-controls';
440
+
441
+ const zoomOutBtn = document.createElement('button');
442
+ zoomOutBtn.className = 'zoom-btn';
443
+ zoomOutBtn.textContent = '−';
444
+ zoomOutBtn.setAttribute('aria-label', 'Zoom out');
445
+ zoomOutBtn.onclick = zoomOut;
446
+
447
+ const zoomLevel = document.createElement('div');
448
+ zoomLevel.className = 'zoom-level';
449
+ zoomLevel.id = 'zoom-level-display';
450
+ zoomLevel.textContent = '100%';
451
+
452
+ const zoomInBtn = document.createElement('button');
453
+ zoomInBtn.className = 'zoom-btn';
454
+ zoomInBtn.textContent = '+';
455
+ zoomInBtn.setAttribute('aria-label', 'Zoom in');
456
+ zoomInBtn.onclick = zoomIn;
457
+
458
+ const resetBtn = document.createElement('button');
459
+ resetBtn.className = 'zoom-btn';
460
+ resetBtn.textContent = 'Reset';
461
+ resetBtn.setAttribute('aria-label', 'Reset zoom to 100%');
462
+ resetBtn.onclick = resetZoom;
463
+
464
+ zoomControls.appendChild(zoomOutBtn);
465
+ zoomControls.appendChild(zoomLevel);
466
+ zoomControls.appendChild(zoomInBtn);
467
+ zoomControls.appendChild(resetBtn);
468
+
469
+ const closeBtn = document.createElement('button');
470
+ closeBtn.className = 'close-fullscreen-btn';
471
+ closeBtn.textContent = '✕ Close';
472
+ closeBtn.setAttribute('aria-label', 'Close fullscreen diagram');
473
+ closeBtn.onclick = closeFullscreen;
474
+
475
+ const content = document.createElement('div');
476
+ content.className = 'diagram-fullscreen-content';
477
+
478
+ overlay.appendChild(zoomControls);
479
+ overlay.appendChild(closeBtn);
480
+ overlay.appendChild(content);
481
+ document.body.appendChild(overlay);
482
+
483
+ // Close on overlay click (but not content click)
484
+ overlay.addEventListener('click', (e) => {
485
+ if (e.target === overlay) {
486
+ closeFullscreen();
487
+ }
488
+ });
489
+
490
+ // Close on Escape key
491
+ document.addEventListener('keydown', (e) => {
492
+ if (e.key === 'Escape' && overlay.classList.contains('active')) {
493
+ closeFullscreen();
494
+ }
495
+ });
496
+
497
+ // Keyboard shortcuts for zoom
498
+ document.addEventListener('keydown', (e) => {
499
+ if (overlay.classList.contains('active')) {
500
+ if (e.key === '+' || e.key === '=') {
501
+ e.preventDefault();
502
+ zoomIn();
503
+ } else if (e.key === '-') {
504
+ e.preventDefault();
505
+ zoomOut();
506
+ } else if (e.key === '0') {
507
+ e.preventDefault();
508
+ resetZoom();
509
+ }
510
+ }
511
+ });
512
+ }
513
+
514
+ // Drag/pan functionality
515
+ let isDragging = false;
516
+ let startX = 0;
517
+ let startY = 0;
518
+ let currentX = 0;
519
+ let currentY = 0;
520
+
521
+ function initDrag(wrapper) {
522
+ wrapper.style.cursor = 'grab';
523
+ wrapper.style.userSelect = 'none';
524
+
525
+ wrapper.addEventListener('mousedown', (e) => {
526
+ if (e.button === 0) { // Left mouse button only
527
+ isDragging = true;
528
+ startX = e.clientX - currentX;
529
+ startY = e.clientY - currentY;
530
+ wrapper.style.cursor = 'grabbing';
531
+ }
532
+ });
533
+
534
+ document.addEventListener('mousemove', (e) => {
535
+ if (isDragging) {
536
+ e.preventDefault();
537
+ currentX = e.clientX - startX;
538
+ currentY = e.clientY - startY;
539
+ wrapper.style.transform = `scale(${currentZoom / 100}) translate(${currentX}px, ${currentY}px)`;
540
+ }
541
+ });
542
+
543
+ document.addEventListener('mouseup', () => {
544
+ if (isDragging) {
545
+ isDragging = false;
546
+ wrapper.style.cursor = 'grab';
547
+ }
548
+ });
549
+
550
+ // Touch support for mobile
551
+ let touchStartX = 0;
552
+ let touchStartY = 0;
553
+
554
+ wrapper.addEventListener('touchstart', (e) => {
555
+ if (e.touches.length === 1) {
556
+ isDragging = true;
557
+ touchStartX = e.touches[0].clientX - currentX;
558
+ touchStartY = e.touches[0].clientY - currentY;
559
+ }
560
+ });
561
+
562
+ document.addEventListener('touchmove', (e) => {
563
+ if (isDragging && e.touches.length === 1) {
564
+ e.preventDefault();
565
+ currentX = e.touches[0].clientX - touchStartX;
566
+ currentY = e.touches[0].clientY - touchStartY;
567
+ wrapper.style.transform = `scale(${currentZoom / 100}) translate(${currentX}px, ${currentY}px)`;
568
+ }
569
+ });
570
+
571
+ document.addEventListener('touchend', () => {
572
+ isDragging = false;
573
+ });
574
+ }
575
+
576
+ // Function to open diagram in fullscreen
577
+ function openFullscreen(diagramElement) {
578
+ const overlay = document.getElementById('diagram-fullscreen-overlay');
579
+ const content = overlay.querySelector('.diagram-fullscreen-content');
580
+
581
+ // Get the original Mermaid code from the source
582
+ let mermaidCode = null;
583
+ const originalPre = diagramElement.closest('pre');
584
+ if (originalPre) {
585
+ const codeBlock = originalPre.querySelector('code.language-mermaid');
586
+ if (codeBlock) {
587
+ mermaidCode = codeBlock.textContent || codeBlock.innerText;
588
+ }
589
+ }
590
+
591
+ // If we can't find the code, try to find it from all code blocks
592
+ if (!mermaidCode) {
593
+ const allCodeBlocks = document.querySelectorAll('code.language-mermaid');
594
+ for (const codeBlock of allCodeBlocks) {
595
+ const parentPre = codeBlock.closest('pre');
596
+ if (parentPre && parentPre.querySelector('.mermaid') === diagramElement) {
597
+ mermaidCode = codeBlock.textContent || codeBlock.innerText;
598
+ break;
599
+ }
600
+ }
601
+ }
602
+
603
+ content.innerHTML = '';
604
+
605
+ // Create wrapper for zoom and drag functionality
606
+ const wrapper = document.createElement('div');
607
+ wrapper.id = 'diagram-zoom-wrapper';
608
+ wrapper.style.display = 'flex';
609
+ wrapper.style.alignItems = 'center';
610
+ wrapper.style.justifyContent = 'center';
611
+ wrapper.style.position = 'relative';
612
+ wrapper.style.width = 'fit-content';
613
+ wrapper.style.height = 'fit-content';
614
+ wrapper.style.minWidth = '100%';
615
+ wrapper.style.minHeight = '100%';
616
+
617
+ // If we have the Mermaid code, create a new div and let Mermaid render it
618
+ if (mermaidCode) {
619
+ const mermaidDiv = document.createElement('div');
620
+ mermaidDiv.className = 'mermaid';
621
+ mermaidDiv.textContent = mermaidCode;
622
+ mermaidDiv.style.display = 'block';
623
+ mermaidDiv.style.visibility = 'visible';
624
+ mermaidDiv.style.opacity = '1';
625
+ wrapper.appendChild(mermaidDiv);
626
+
627
+ // Re-render with Mermaid
628
+ mermaid.run({ nodes: [mermaidDiv] }).then(() => {
629
+ const svg = mermaidDiv.querySelector('svg');
630
+ if (svg) {
631
+ svg.style.display = 'block';
632
+ svg.style.visibility = 'visible';
633
+ }
634
+ });
635
+ } else {
636
+ // Fallback: clone the existing diagram
637
+ const clone = diagramElement.cloneNode(true);
638
+ clone.style.display = 'block';
639
+ clone.style.visibility = 'visible';
640
+ clone.style.opacity = '1';
641
+ wrapper.appendChild(clone);
642
+ }
643
+
644
+ content.appendChild(wrapper);
645
+
646
+ // Ensure content can scroll to show entire diagram
647
+ content.style.overflow = 'auto';
648
+ content.style.display = 'flex';
649
+ content.style.alignItems = 'center';
650
+ content.style.justifyContent = 'center';
651
+
652
+ // Reset zoom and position
653
+ currentZoom = 100;
654
+ currentX = 0;
655
+ currentY = 0;
656
+ wrapper.style.transform = 'scale(1) translate(0px, 0px)';
657
+ wrapper.style.transformOrigin = 'center center';
658
+ updateZoomLevel(100);
659
+
660
+ // Initialize drag functionality
661
+ initDrag(wrapper);
662
+
663
+ // Show overlay
664
+ overlay.classList.add('active');
665
+ document.body.style.overflow = 'hidden';
666
+
667
+ // Ensure visibility and center after rendering
668
+ setTimeout(() => {
669
+ wrapper.style.display = 'flex';
670
+ wrapper.style.visibility = 'visible';
671
+ wrapper.style.opacity = '1';
672
+
673
+ const mermaidEl = wrapper.querySelector('.mermaid');
674
+ if (mermaidEl) {
675
+ mermaidEl.style.display = 'block';
676
+ mermaidEl.style.visibility = 'visible';
677
+ mermaidEl.style.opacity = '1';
678
+ }
679
+
680
+ const svg = wrapper.querySelector('svg');
681
+ if (svg) {
682
+ svg.style.display = 'block';
683
+ svg.style.visibility = 'visible';
684
+ svg.style.opacity = '1';
685
+ }
686
+
687
+ // Center scroll position
688
+ setTimeout(() => {
689
+ if (content.scrollHeight > content.clientHeight) {
690
+ content.scrollTop = (content.scrollHeight - content.clientHeight) / 2;
691
+ }
692
+ if (content.scrollWidth > content.clientWidth) {
693
+ content.scrollLeft = (content.scrollWidth - content.clientWidth) / 2;
694
+ }
695
+ }, 200);
696
+ }, 300);
697
+
698
+ // Focus on close button for accessibility
699
+ const closeBtn = overlay.querySelector('.close-fullscreen-btn');
700
+ closeBtn.focus();
701
+ }
702
+
703
+ // Zoom functionality
704
+ let currentZoom = 100;
705
+ const minZoom = 25;
706
+ const maxZoom = 500;
707
+ const zoomStep = 25;
708
+
709
+ function zoomIn() {
710
+ if (currentZoom < maxZoom) {
711
+ currentZoom = Math.min(currentZoom + zoomStep, maxZoom);
712
+ applyZoom();
713
+ }
714
+ }
715
+
716
+ function zoomOut() {
717
+ if (currentZoom > minZoom) {
718
+ currentZoom = Math.max(currentZoom - zoomStep, minZoom);
719
+ applyZoom();
720
+ }
721
+ }
722
+
723
+ function resetZoom() {
724
+ currentZoom = 100;
725
+ currentX = 0;
726
+ currentY = 0;
727
+ const wrapper = document.getElementById('diagram-zoom-wrapper');
728
+ if (wrapper) {
729
+ wrapper.style.transform = 'scale(1) translate(0px, 0px)';
730
+ updateZoomLevel(100);
731
+ }
732
+ }
733
+
734
+ function applyZoom() {
735
+ const wrapper = document.getElementById('diagram-zoom-wrapper');
736
+ if (wrapper) {
737
+ // Preserve current position when zooming
738
+ wrapper.style.transform = `scale(${currentZoom / 100}) translate(${currentX}px, ${currentY}px)`;
739
+ updateZoomLevel(currentZoom);
740
+ }
741
+ }
742
+
743
+ function updateZoomLevel(level) {
744
+ const zoomLevelEl = document.getElementById('zoom-level-display');
745
+ if (zoomLevelEl) {
746
+ zoomLevelEl.textContent = `${level}%`;
747
+ }
748
+ }
749
+
750
+ // Function to close fullscreen
751
+ function closeFullscreen() {
752
+ const overlay = document.getElementById('diagram-fullscreen-overlay');
753
+ overlay.classList.remove('active');
754
+ document.body.style.overflow = '';
755
+ // Reset zoom for next time
756
+ currentZoom = 100;
757
+ }
758
+
759
+ // Make functions global for button onclick
760
+ window.closeFullscreen = closeFullscreen;
761
+ window.zoomIn = zoomIn;
762
+ window.zoomOut = zoomOut;
763
+ window.resetZoom = resetZoom;
764
+
765
+ // Function to wrap diagrams in containers with fullscreen buttons
766
+ function wrapDiagramsWithButtons() {
767
+ const diagrams = document.querySelectorAll('.mermaid');
768
+
769
+ diagrams.forEach((diagram, index) => {
770
+ // Skip if already wrapped
771
+ if (diagram.parentElement.classList.contains('mermaid-container')) {
772
+ return;
773
+ }
774
+
775
+ // Create container
776
+ const container = document.createElement('div');
777
+ container.className = 'mermaid-container';
778
+
779
+ // Create fullscreen button
780
+ const btn = document.createElement('button');
781
+ btn.className = 'fullscreen-btn';
782
+ btn.setAttribute('aria-label', `View diagram ${index + 1} in fullscreen`);
783
+ btn.innerHTML = `
784
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
785
+ <path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"></path>
786
+ </svg>
787
+ <span>Fullscreen</span>
788
+ `;
789
+
790
+ btn.onclick = () => openFullscreen(diagram);
791
+
792
+ // Wrap diagram
793
+ diagram.parentNode.insertBefore(container, diagram);
794
+ container.appendChild(diagram);
795
+ container.appendChild(btn);
796
+ });
797
+ }
798
+
799
+ // Function to process Mermaid diagrams
800
+ function processMermaidDiagrams() {
801
+ // Find all code blocks with mermaid language
802
+ const mermaidBlocks = document.querySelectorAll('code.language-mermaid, pre code.language-mermaid');
803
+
804
+ if (mermaidBlocks.length > 0) {
805
+ console.log('Found', mermaidBlocks.length, 'Mermaid diagram blocks');
806
+
807
+ // Process each block
808
+ mermaidBlocks.forEach((block, index) => {
809
+ // Get the parent pre element if it exists
810
+ const preElement = block.parentElement;
811
+ if (preElement && preElement.tagName === 'PRE') {
812
+ // Get the mermaid code
813
+ const mermaidCode = block.textContent || block.innerText;
814
+
815
+ // Create a div for Mermaid to render into
816
+ const mermaidDiv = document.createElement('div');
817
+ mermaidDiv.className = 'mermaid';
818
+ mermaidDiv.textContent = mermaidCode;
819
+
820
+ // Replace the pre element with the mermaid div
821
+ preElement.parentNode.replaceChild(mermaidDiv, preElement);
822
+ }
823
+ });
824
+
825
+ // Run Mermaid to render all diagrams
826
+ mermaid.run().then(() => {
827
+ // After rendering, wrap diagrams with fullscreen buttons
828
+ createFullscreenOverlay();
829
+ wrapDiagramsWithButtons();
830
+ });
831
+ } else {
832
+ // Fallback: look for pre > code blocks and check if they contain mermaid syntax
833
+ const allCodeBlocks = document.querySelectorAll('pre code');
834
+ allCodeBlocks.forEach((codeBlock) => {
835
+ const code = codeBlock.textContent || codeBlock.innerText;
836
+ // Check if it looks like mermaid code (starts with graph, sequenceDiagram, etc.)
837
+ if (code.trim().match(/^(graph|sequenceDiagram|flowchart|classDiagram|stateDiagram|erDiagram|gantt|pie|gitgraph|journey)/)) {
838
+ const preElement = codeBlock.parentElement;
839
+ if (preElement && preElement.tagName === 'PRE') {
840
+ const mermaidDiv = document.createElement('div');
841
+ mermaidDiv.className = 'mermaid';
842
+ mermaidDiv.textContent = code;
843
+ preElement.parentNode.replaceChild(mermaidDiv, preElement);
844
+ }
845
+ }
846
+ });
847
+
848
+ // Run Mermaid after processing
849
+ if (document.querySelectorAll('.mermaid').length > 0) {
850
+ mermaid.run().then(() => {
851
+ // After rendering, wrap diagrams with fullscreen buttons
852
+ createFullscreenOverlay();
853
+ wrapDiagramsWithButtons();
854
+ });
855
+ }
856
+ }
857
+ }
858
+
859
+ // Process diagrams when DOM is ready
860
+ if (document.readyState === 'loading') {
861
+ document.addEventListener('DOMContentLoaded', processMermaidDiagrams);
862
+ } else {
863
+ // DOM already loaded, process immediately
864
+ processMermaidDiagrams();
865
+ }
866
+ </script>
128
867
  </body>
129
- </html>
130
868
 
869
+ </html>