@knowcode/doc-builder 1.4.7 → 1.4.9

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.
@@ -1,1331 +0,0 @@
1
- // Documentation Builder - Main JavaScript
2
-
3
- // Preview Banner Management
4
- // Set up banner state immediately to prevent flash
5
- const bannerDismissed = localStorage.getItem('banner-dismissed') === 'true';
6
-
7
- // Apply styles immediately if banner should be visible
8
- if (!bannerDismissed) {
9
- document.documentElement.style.setProperty('--banner-offset', '3.5rem');
10
- } else {
11
- document.documentElement.style.setProperty('--banner-offset', '0rem');
12
- }
13
-
14
- document.addEventListener('DOMContentLoaded', function() {
15
- const banner = document.getElementById('preview-banner');
16
- const dismissButton = document.getElementById('dismiss-banner');
17
- const mainWrapper = document.querySelector('.main-wrapper');
18
- const sidebar = document.querySelector('.sidebar');
19
- const breadcrumbs = document.querySelector('.breadcrumbs');
20
-
21
- if (bannerDismissed) {
22
- banner.classList.add('hidden');
23
- } else {
24
- // Show banner and adjust layout
25
- banner.classList.add('visible');
26
- mainWrapper.classList.add('banner-visible');
27
- sidebar.classList.add('banner-visible');
28
- breadcrumbs?.classList.add('banner-visible');
29
- }
30
-
31
- // Handle banner dismissal
32
- if (dismissButton) {
33
- dismissButton.addEventListener('click', function() {
34
- banner.classList.remove('visible');
35
- banner.classList.add('hidden');
36
- mainWrapper.classList.remove('banner-visible');
37
- sidebar.classList.remove('banner-visible');
38
- breadcrumbs?.classList.remove('banner-visible');
39
- document.documentElement.style.setProperty('--banner-offset', '0rem');
40
-
41
- // Remember that the banner was dismissed
42
- localStorage.setItem('banner-dismissed', 'true');
43
- });
44
- }
45
-
46
- // Handle Escape key to dismiss banner
47
- document.addEventListener('keydown', function(e) {
48
- if (e.key === 'Escape' && banner.classList.contains('visible')) {
49
- dismissButton.click();
50
- }
51
- });
52
-
53
- // Initialize Mermaid Full Screen Functionality
54
- initializeMermaidFullScreen();
55
- });
56
-
57
- // Mermaid Full Screen Viewer
58
- function initializeMermaidFullScreen() {
59
- // Wait for Mermaid to initialize
60
- if (typeof mermaid === 'undefined') {
61
- setTimeout(initializeMermaidFullScreen, 100);
62
- return;
63
- }
64
-
65
- // Find all Mermaid diagrams and wrap them with full-screen controls
66
- const mermaidDivs = document.querySelectorAll('.mermaid');
67
-
68
- mermaidDivs.forEach((mermaidDiv, index) => {
69
- // Skip if already processed
70
- if (mermaidDiv.closest('.mermaid-container')) {
71
- return;
72
- }
73
-
74
- // Create container
75
- const container = document.createElement('div');
76
- container.className = 'mermaid-container';
77
-
78
- // Create toolbar
79
- const toolbar = document.createElement('div');
80
- toolbar.className = 'mermaid-toolbar';
81
-
82
- const title = document.createElement('div');
83
- title.textContent = 'Mermaid Diagram';
84
-
85
- const actions = document.createElement('div');
86
- actions.className = 'mermaid-actions';
87
-
88
- // Full screen button
89
- const fullScreenBtn = document.createElement('button');
90
- fullScreenBtn.className = 'mermaid-btn';
91
- fullScreenBtn.innerHTML = '<i class="fas fa-expand"></i> Full Screen';
92
- fullScreenBtn.addEventListener('click', () => openMermaidFullScreen(mermaidDiv, index));
93
-
94
- // Copy SVG button
95
- const copyBtn = document.createElement('button');
96
- copyBtn.className = 'mermaid-btn';
97
- copyBtn.innerHTML = '<i class="fas fa-copy"></i> Copy SVG';
98
- copyBtn.addEventListener('click', () => copyMermaidSVG(mermaidDiv));
99
-
100
- // Copy Mermaid source button
101
- const copyMermaidBtn = document.createElement('button');
102
- copyMermaidBtn.className = 'mermaid-btn';
103
- copyMermaidBtn.innerHTML = '<i class="fas fa-code"></i> Copy Mermaid';
104
- copyMermaidBtn.addEventListener('click', () => copyMermaidSource(mermaidDiv));
105
-
106
- actions.appendChild(fullScreenBtn);
107
- actions.appendChild(copyBtn);
108
- actions.appendChild(copyMermaidBtn);
109
-
110
- toolbar.appendChild(title);
111
- toolbar.appendChild(actions);
112
-
113
- // Create wrapper for the diagram
114
- const wrapper = document.createElement('div');
115
- wrapper.className = 'mermaid-wrapper';
116
-
117
- // Insert container before mermaid div
118
- mermaidDiv.parentNode.insertBefore(container, mermaidDiv);
119
-
120
- // Move mermaid div into wrapper
121
- wrapper.appendChild(mermaidDiv);
122
-
123
- // Assemble container
124
- container.appendChild(toolbar);
125
- container.appendChild(wrapper);
126
- });
127
-
128
- // Create fullscreen modal (only once)
129
- if (!document.getElementById('mermaid-fullscreen-modal')) {
130
- createMermaidFullScreenModal();
131
- }
132
- }
133
-
134
- function createMermaidFullScreenModal() {
135
- const modal = document.createElement('div');
136
- modal.id = 'mermaid-fullscreen-modal';
137
- modal.className = 'mermaid-fullscreen';
138
-
139
- modal.innerHTML = `
140
- <div class="mermaid-fullscreen-toolbar">
141
- <div class="mermaid-fullscreen-title">Mermaid Diagram - Full Screen View</div>
142
- <div class="mermaid-fullscreen-controls">
143
- <div class="mermaid-zoom-controls">
144
- <button class="mermaid-zoom-btn" id="zoom-out">
145
- <i class="fas fa-minus"></i>
146
- </button>
147
- <div class="mermaid-zoom-level" id="zoom-level">100%</div>
148
- <button class="mermaid-zoom-btn" id="zoom-in">
149
- <i class="fas fa-plus"></i>
150
- </button>
151
- <button class="mermaid-zoom-btn" id="zoom-reset">
152
- <i class="fas fa-expand-arrows-alt"></i>
153
- </button>
154
- </div>
155
- <button class="mermaid-close-btn" id="close-fullscreen">
156
- <i class="fas fa-times"></i> Close
157
- </button>
158
- </div>
159
- </div>
160
- <div class="mermaid-fullscreen-content">
161
- <div class="mermaid-fullscreen-wrapper" id="fullscreen-wrapper">
162
- <div class="mermaid-fullscreen-diagram" id="fullscreen-diagram">
163
- <!-- Diagram will be inserted here -->
164
- </div>
165
- </div>
166
- </div>
167
- `;
168
-
169
- document.body.appendChild(modal);
170
-
171
- // Set up event listeners - store zoom in modal element
172
- modal.currentZoom = 1;
173
- const wrapper = document.getElementById('fullscreen-wrapper');
174
- const zoomLevel = document.getElementById('zoom-level');
175
-
176
- function updateZoom() {
177
- const currentZoom = modal.currentZoom || 1;
178
- wrapper.style.transform = `scale(${currentZoom})`;
179
- zoomLevel.textContent = `${Math.round(currentZoom * 100)}%`;
180
-
181
- if (currentZoom > 1) {
182
- wrapper.classList.add('zoomed');
183
- } else {
184
- wrapper.classList.remove('zoomed');
185
- }
186
- }
187
-
188
- // Zoom controls
189
- document.getElementById('zoom-in').addEventListener('click', () => {
190
- modal.currentZoom = Math.min((modal.currentZoom || 1) + 0.25, 3);
191
- updateZoom();
192
- });
193
-
194
- document.getElementById('zoom-out').addEventListener('click', () => {
195
- modal.currentZoom = Math.max((modal.currentZoom || 1) - 0.25, 0.25);
196
- updateZoom();
197
- });
198
-
199
- document.getElementById('zoom-reset').addEventListener('click', () => {
200
- modal.currentZoom = 1;
201
- updateZoom();
202
- });
203
-
204
- // Close functionality
205
- document.getElementById('close-fullscreen').addEventListener('click', closeMermaidFullScreen);
206
-
207
- // Close on backdrop click
208
- modal.addEventListener('click', (e) => {
209
- if (e.target === modal) {
210
- closeMermaidFullScreen();
211
- }
212
- });
213
-
214
- // Close on Escape key
215
- document.addEventListener('keydown', (e) => {
216
- if (e.key === 'Escape' && modal.classList.contains('active')) {
217
- closeMermaidFullScreen();
218
- }
219
- });
220
- }
221
-
222
- function openMermaidFullScreen(mermaidDiv, index) {
223
- const modal = document.getElementById('mermaid-fullscreen-modal');
224
- const diagramContainer = document.getElementById('fullscreen-diagram');
225
- const wrapper = document.getElementById('fullscreen-wrapper');
226
- const zoomLevel = document.getElementById('zoom-level');
227
-
228
- // Reset zoom to 100% when opening new diagram
229
- modal.currentZoom = 1;
230
- wrapper.style.transform = `scale(${modal.currentZoom})`;
231
- zoomLevel.textContent = `${Math.round(modal.currentZoom * 100)}%`;
232
- wrapper.classList.remove('zoomed');
233
-
234
- // Clone the mermaid diagram
235
- const clonedDiagram = mermaidDiv.cloneNode(true);
236
-
237
- // Reset all styles that might interfere
238
- clonedDiagram.style.cssText = '';
239
-
240
- // Find the SVG and make it scale properly
241
- const svg = clonedDiagram.querySelector('svg');
242
- if (svg) {
243
- // Store original dimensions for reference
244
- const originalWidth = svg.getAttribute('width');
245
- const originalHeight = svg.getAttribute('height');
246
- const originalViewBox = svg.getAttribute('viewBox');
247
-
248
- // Reset SVG styles
249
- svg.style.cssText = '';
250
-
251
- // Ensure we have a proper viewBox for scaling
252
- if (!originalViewBox && originalWidth && originalHeight) {
253
- svg.setAttribute('viewBox', `0 0 ${originalWidth} ${originalHeight}`);
254
- }
255
-
256
- // Remove fixed dimensions to enable responsive scaling
257
- svg.removeAttribute('width');
258
- svg.removeAttribute('height');
259
-
260
- // Set responsive attributes
261
- svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
262
-
263
- // Apply CSS for proper scaling
264
- svg.style.width = '100%';
265
- svg.style.height = '100%';
266
- svg.style.maxWidth = '100%';
267
- svg.style.maxHeight = '100%';
268
- svg.style.display = 'block';
269
- }
270
-
271
- // Apply proper styles to the cloned diagram
272
- clonedDiagram.style.width = '100%';
273
- clonedDiagram.style.height = '100%';
274
- clonedDiagram.style.display = 'flex';
275
- clonedDiagram.style.justifyContent = 'center';
276
- clonedDiagram.style.alignItems = 'center';
277
-
278
- // Clear previous content and add new diagram
279
- diagramContainer.innerHTML = '';
280
- diagramContainer.appendChild(clonedDiagram);
281
-
282
- // Show modal
283
- modal.classList.add('active');
284
- document.body.style.overflow = 'hidden';
285
-
286
- // Update title
287
- const title = document.querySelector('.mermaid-fullscreen-title');
288
- const container = mermaidDiv.closest('.mermaid-container');
289
- const originalTitle = container ? container.querySelector('.mermaid-toolbar div').textContent : 'Mermaid Diagram';
290
- title.textContent = `${originalTitle} - Full Screen View`;
291
-
292
- // Debug logging
293
- console.log('Fullscreen opened with diagram:', clonedDiagram);
294
- console.log('SVG found:', svg);
295
- if (svg) {
296
- console.log('SVG viewBox:', svg.getAttribute('viewBox'));
297
- console.log('SVG dimensions:', svg.getBoundingClientRect());
298
- }
299
- }
300
-
301
- function closeMermaidFullScreen() {
302
- const modal = document.getElementById('mermaid-fullscreen-modal');
303
- modal.classList.remove('active');
304
- document.body.style.overflow = '';
305
-
306
- // Clear diagram content
307
- setTimeout(() => {
308
- const diagramContainer = document.getElementById('fullscreen-diagram');
309
- diagramContainer.innerHTML = '';
310
- }, 300);
311
- }
312
-
313
- function copyMermaidSVG(mermaidDiv) {
314
- try {
315
- const svg = mermaidDiv.querySelector('svg');
316
- if (svg) {
317
- const svgString = new XMLSerializer().serializeToString(svg);
318
-
319
- // Try to use the modern clipboard API
320
- if (navigator.clipboard && navigator.clipboard.writeText) {
321
- navigator.clipboard.writeText(svgString).then(() => {
322
- showCopySuccess('SVG copied to clipboard!');
323
- }).catch(() => {
324
- fallbackCopy(svgString, 'SVG');
325
- });
326
- } else {
327
- fallbackCopy(svgString, 'SVG');
328
- }
329
- }
330
- } catch (error) {
331
- console.error('Error copying SVG:', error);
332
- showCopyError();
333
- }
334
- }
335
-
336
- function copyMermaidSource(mermaidDiv) {
337
- try {
338
- // Find the original Mermaid source code
339
- let mermaidSource = '';
340
-
341
- // Try to get from data attribute first
342
- if (mermaidDiv.dataset.mermaidSource) {
343
- mermaidSource = mermaidDiv.dataset.mermaidSource;
344
- } else {
345
- // Try to find in the page content - look for the nearest pre code block
346
- const container = mermaidDiv.closest('.content');
347
- if (container) {
348
- const codeBlocks = container.querySelectorAll('pre code');
349
- for (const block of codeBlocks) {
350
- if (block.textContent.includes('flowchart') || block.textContent.includes('graph')) {
351
- mermaidSource = block.textContent;
352
- break;
353
- }
354
- }
355
- }
356
-
357
- // Fallback: extract from the SVG if available
358
- if (!mermaidSource) {
359
- const svg = mermaidDiv.querySelector('svg');
360
- if (svg) {
361
- // Try to reconstruct basic Mermaid from SVG elements
362
- mermaidSource = reconstructMermaidFromSVG(svg);
363
- }
364
- }
365
- }
366
-
367
- if (mermaidSource) {
368
- // Try to use the modern clipboard API
369
- if (navigator.clipboard && navigator.clipboard.writeText) {
370
- navigator.clipboard.writeText(mermaidSource).then(() => {
371
- showCopySuccess('Mermaid source copied to clipboard!');
372
- }).catch(() => {
373
- fallbackCopy(mermaidSource, 'Mermaid');
374
- });
375
- } else {
376
- fallbackCopy(mermaidSource, 'Mermaid');
377
- }
378
- } else {
379
- showCopyError('Could not find Mermaid source');
380
- }
381
- } catch (error) {
382
- console.error('Error copying Mermaid source:', error);
383
- showCopyError();
384
- }
385
- }
386
-
387
- function reconstructMermaidFromSVG(svg) {
388
- // Basic reconstruction - this is a fallback method
389
- let mermaidCode = 'flowchart TD\n';
390
-
391
- // Try to extract node information from SVG
392
- const nodes = svg.querySelectorAll('g.node');
393
- const edges = svg.querySelectorAll('g.edgePath');
394
-
395
- // Add nodes
396
- nodes.forEach((node, index) => {
397
- const label = node.querySelector('span, text, foreignObject');
398
- if (label) {
399
- const nodeText = label.textContent.trim();
400
- const nodeId = `N${index + 1}`;
401
-
402
- if (nodeText.includes('?')) {
403
- mermaidCode += ` ${nodeId}{{"${nodeText}"}}\n`;
404
- } else {
405
- mermaidCode += ` ${nodeId}["${nodeText}"]\n`;
406
- }
407
- }
408
- });
409
-
410
- mermaidCode += '\n %% Note: This is a reconstructed version - original source may differ\n';
411
-
412
- return mermaidCode;
413
- }
414
-
415
- function fallbackCopy(text, type = 'content') {
416
- // Fallback for older browsers
417
- const textArea = document.createElement('textarea');
418
- textArea.value = text;
419
- textArea.style.position = 'fixed';
420
- textArea.style.opacity = '0';
421
- document.body.appendChild(textArea);
422
- textArea.select();
423
-
424
- try {
425
- document.execCommand('copy');
426
- showCopySuccess(`${type} copied to clipboard!`);
427
- } catch (error) {
428
- showCopyError(`Failed to copy ${type.toLowerCase()}`);
429
- }
430
-
431
- document.body.removeChild(textArea);
432
- }
433
-
434
- function showCopySuccess(message = 'Content copied to clipboard!') {
435
- // Create temporary success message
436
- const messageDiv = document.createElement('div');
437
- messageDiv.textContent = message;
438
- messageDiv.style.cssText = `
439
- position: fixed;
440
- top: 50%;
441
- left: 50%;
442
- transform: translate(-50%, -50%);
443
- background: var(--success);
444
- color: white;
445
- padding: 1rem 2rem;
446
- border-radius: 0.5rem;
447
- z-index: 10001;
448
- font-size: 0.875rem;
449
- box-shadow: var(--shadow-lg);
450
- max-width: 300px;
451
- text-align: center;
452
- `;
453
-
454
- document.body.appendChild(messageDiv);
455
-
456
- setTimeout(() => {
457
- messageDiv.remove();
458
- }, 2000);
459
- }
460
-
461
- function showCopyError(message = 'Failed to copy content') {
462
- // Create temporary error message
463
- const messageDiv = document.createElement('div');
464
- messageDiv.textContent = message;
465
- messageDiv.style.cssText = `
466
- position: fixed;
467
- top: 50%;
468
- left: 50%;
469
- transform: translate(-50%, -50%);
470
- background: var(--danger);
471
- color: white;
472
- padding: 1rem 2rem;
473
- border-radius: 0.5rem;
474
- z-index: 10001;
475
- font-size: 0.875rem;
476
- box-shadow: var(--shadow-lg);
477
- max-width: 300px;
478
- text-align: center;
479
- `;
480
-
481
- document.body.appendChild(messageDiv);
482
-
483
- setTimeout(() => {
484
- messageDiv.remove();
485
- }, 2000);
486
- }
487
-
488
- // Theme Management
489
- const themeToggle = document.getElementById('theme-toggle');
490
- const html = document.documentElement;
491
-
492
- // Check for saved theme preference or default to 'light'
493
- const currentTheme = localStorage.getItem('theme') || 'light';
494
- html.setAttribute('data-theme', currentTheme);
495
- updateThemeIcon(currentTheme);
496
-
497
- themeToggle.addEventListener('click', () => {
498
- const newTheme = html.getAttribute('data-theme') === 'light' ? 'dark' : 'light';
499
- html.setAttribute('data-theme', newTheme);
500
- localStorage.setItem('theme', newTheme);
501
- updateThemeIcon(newTheme);
502
- });
503
-
504
- function updateThemeIcon(theme) {
505
- const icon = themeToggle.querySelector('i');
506
- if (icon) {
507
- icon.className = theme === 'light' ? 'fas fa-moon' : 'fas fa-sun';
508
- }
509
- }
510
-
511
- // Mobile Menu Toggle
512
- const menuToggle = document.getElementById('menu-toggle');
513
- const sidebar = document.querySelector('.sidebar');
514
-
515
- if (menuToggle) {
516
- menuToggle.addEventListener('click', () => {
517
- sidebar.classList.toggle('open');
518
- });
519
- }
520
-
521
- // Prevent sidebar from closing when clicking nav items
522
- // Only close when clicking outside the sidebar or the close button
523
- document.addEventListener('click', (e) => {
524
- // Check if we're on mobile
525
- if (window.innerWidth <= 768) {
526
- const isClickInsideSidebar = sidebar && sidebar.contains(e.target);
527
- const isMenuToggle = e.target.closest('#menu-toggle');
528
- const isNavItem = e.target.closest('.nav-item, .nav-title');
529
-
530
- // Close sidebar only if clicking outside AND not on menu toggle AND not on nav items
531
- if (!isClickInsideSidebar && !isMenuToggle && !isNavItem && sidebar?.classList.contains('open')) {
532
- sidebar.classList.remove('open');
533
- }
534
- }
535
- });
536
-
537
- // Smooth Scrolling for Anchor Links
538
- document.querySelectorAll('a[href^="#"]').forEach(anchor => {
539
- anchor.addEventListener('click', function (e) {
540
- e.preventDefault();
541
- const target = document.querySelector(this.getAttribute('href'));
542
- if (target) {
543
- target.scrollIntoView({
544
- behavior: 'smooth',
545
- block: 'start'
546
- });
547
- }
548
- });
549
- });
550
-
551
- // Active Navigation Highlighting
552
- const sections = document.querySelectorAll('section[id]');
553
- const navItems = document.querySelectorAll('.nav-item');
554
-
555
- function highlightNavigation() {
556
- const scrollY = window.pageYOffset;
557
-
558
- sections.forEach(section => {
559
- const sectionHeight = section.offsetHeight;
560
- const sectionTop = section.offsetTop - 100;
561
- const sectionId = section.getAttribute('id');
562
-
563
- if (scrollY > sectionTop && scrollY <= sectionTop + sectionHeight) {
564
- navItems.forEach(item => {
565
- item.classList.remove('active');
566
- if (item.getAttribute('href') === `#${sectionId}`) {
567
- item.classList.add('active');
568
- }
569
- });
570
- }
571
- });
572
- }
573
-
574
- window.addEventListener('scroll', highlightNavigation);
575
-
576
- // Search Functionality (Basic Implementation)
577
- const searchInput = document.getElementById('search-input');
578
- const searchResults = document.getElementById('search-results');
579
-
580
- if (searchInput) {
581
- searchInput.addEventListener('input', (e) => {
582
- const query = e.target.value.toLowerCase();
583
-
584
- if (query.length < 2) {
585
- searchResults.style.display = 'none';
586
- return;
587
- }
588
-
589
- // This would be replaced with actual search logic
590
- performSearch(query);
591
- });
592
-
593
- // Close search results when clicking outside
594
- document.addEventListener('click', (e) => {
595
- if (!e.target.closest('.search-box')) {
596
- searchResults.style.display = 'none';
597
- }
598
- });
599
- }
600
-
601
- function performSearch(query) {
602
- // Placeholder for search functionality
603
- // In a real implementation, this would search through all content
604
- searchResults.innerHTML = `
605
- <div class="search-result-item">
606
- <strong>Search results for "${query}"</strong>
607
- <p>Search functionality will be implemented here...</p>
608
- </div>
609
- `;
610
- searchResults.style.display = 'block';
611
- }
612
-
613
- // Copy Code Blocks
614
- document.querySelectorAll('pre').forEach(block => {
615
- const wrapper = document.createElement('div');
616
- wrapper.className = 'code-block-wrapper';
617
- block.parentNode.insertBefore(wrapper, block);
618
- wrapper.appendChild(block);
619
-
620
- const button = document.createElement('button');
621
- button.className = 'copy-button';
622
- button.textContent = 'Copy';
623
- wrapper.appendChild(button);
624
-
625
- button.addEventListener('click', () => {
626
- const code = block.textContent;
627
- navigator.clipboard.writeText(code).then(() => {
628
- button.textContent = 'Copied!';
629
- setTimeout(() => {
630
- button.textContent = 'Copy';
631
- }, 2000);
632
- });
633
- });
634
- });
635
-
636
- // Table of Contents Generation
637
- function generateTableOfContents() {
638
- const toc = document.getElementById('table-of-contents');
639
- if (!toc) return;
640
-
641
- const headings = document.querySelectorAll('.content h2, .content h3');
642
- const tocList = document.createElement('ul');
643
- tocList.className = 'toc-list';
644
-
645
- headings.forEach(heading => {
646
- const li = document.createElement('li');
647
- const a = document.createElement('a');
648
- a.href = `#${heading.id}`;
649
- a.textContent = heading.textContent;
650
- a.className = heading.tagName.toLowerCase() === 'h3' ? 'toc-h3' : 'toc-h2';
651
- li.appendChild(a);
652
- tocList.appendChild(li);
653
- });
654
-
655
- toc.appendChild(tocList);
656
- }
657
-
658
- // Keyboard Shortcuts
659
- document.addEventListener('keydown', (e) => {
660
- // Cmd/Ctrl + K for search
661
- if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
662
- e.preventDefault();
663
- searchInput?.focus();
664
- }
665
-
666
- // Escape to close mobile menu
667
- if (e.key === 'Escape') {
668
- sidebar?.classList.remove('open');
669
- }
670
- });
671
-
672
- // Fade-in Animation on Scroll
673
- const observerOptions = {
674
- threshold: 0.1,
675
- rootMargin: '0px 0px -100px 0px'
676
- };
677
-
678
- const observer = new IntersectionObserver((entries) => {
679
- entries.forEach(entry => {
680
- if (entry.isIntersecting) {
681
- entry.target.classList.add('fade-in');
682
- observer.unobserve(entry.target);
683
- }
684
- });
685
- }, observerOptions);
686
-
687
- document.querySelectorAll('.feature-card, .timeline-item, .metric-card').forEach(el => {
688
- observer.observe(el);
689
- });
690
-
691
- // Sidebar Resizing
692
- function initSidebarResize() {
693
- const sidebar = document.querySelector('.sidebar');
694
- const resizeHandle = document.querySelector('.resize-handle');
695
- const content = document.querySelector('.content');
696
-
697
- if (!sidebar || !resizeHandle || !content) return;
698
-
699
- let isResizing = false;
700
- let startX = 0;
701
- let startWidth = 0;
702
-
703
- // Restore saved width on load
704
- const savedWidth = localStorage.getItem('sidebarWidth');
705
- if (savedWidth && savedWidth >= 200 && savedWidth <= 500) {
706
- sidebar.style.width = `${savedWidth}px`;
707
- // Don't set margin-left - flexbox handles the layout
708
- }
709
-
710
- // Mouse down on resize handle
711
- resizeHandle.addEventListener('mousedown', (e) => {
712
- isResizing = true;
713
- startX = e.clientX;
714
- startWidth = parseInt(document.defaultView.getComputedStyle(sidebar).width, 10);
715
-
716
- // Add global event listeners
717
- document.addEventListener('mousemove', handleMouseMove);
718
- document.addEventListener('mouseup', handleMouseUp);
719
-
720
- // Prevent text selection during resize
721
- document.body.style.userSelect = 'none';
722
- document.body.style.cursor = 'col-resize';
723
-
724
- e.preventDefault();
725
- });
726
-
727
- function handleMouseMove(e) {
728
- if (!isResizing) return;
729
-
730
- const width = startWidth + e.clientX - startX;
731
-
732
- // Constrain width between 200px and 500px
733
- const constrainedWidth = Math.max(200, Math.min(500, width));
734
-
735
- sidebar.style.width = `${constrainedWidth}px`;
736
- // Don't set margin-left - flexbox handles the layout
737
-
738
- e.preventDefault();
739
- }
740
-
741
- function handleMouseUp() {
742
- if (!isResizing) return;
743
-
744
- isResizing = false;
745
-
746
- // Remove global event listeners
747
- document.removeEventListener('mousemove', handleMouseMove);
748
- document.removeEventListener('mouseup', handleMouseUp);
749
-
750
- // Restore normal cursor and text selection
751
- document.body.style.userSelect = '';
752
- document.body.style.cursor = '';
753
-
754
- // Save the current width
755
- const currentWidth = parseInt(document.defaultView.getComputedStyle(sidebar).width, 10);
756
- localStorage.setItem('sidebarWidth', currentWidth);
757
- }
758
-
759
- // Touch events for mobile support
760
- resizeHandle.addEventListener('touchstart', (e) => {
761
- isResizing = true;
762
- startX = e.touches[0].clientX;
763
- startWidth = parseInt(document.defaultView.getComputedStyle(sidebar).width, 10);
764
-
765
- document.addEventListener('touchmove', handleTouchMove);
766
- document.addEventListener('touchend', handleTouchEnd);
767
-
768
- e.preventDefault();
769
- });
770
-
771
- function handleTouchMove(e) {
772
- if (!isResizing) return;
773
-
774
- const width = startWidth + e.touches[0].clientX - startX;
775
- const constrainedWidth = Math.max(200, Math.min(500, width));
776
-
777
- sidebar.style.width = `${constrainedWidth}px`;
778
- // Don't set margin-left - flexbox handles the layout
779
-
780
- e.preventDefault();
781
- }
782
-
783
- function handleTouchEnd() {
784
- if (!isResizing) return;
785
-
786
- isResizing = false;
787
-
788
- document.removeEventListener('touchmove', handleTouchMove);
789
- document.removeEventListener('touchend', handleTouchEnd);
790
-
791
- // Save the current width
792
- const currentWidth = parseInt(document.defaultView.getComputedStyle(sidebar).width, 10);
793
- localStorage.setItem('sidebarWidth', currentWidth);
794
- }
795
- }
796
-
797
- // Collapsible Navigation
798
- function initCollapsibleNavigation() {
799
- // Debug: Log initial state
800
- console.log('[Navigation] Initializing collapsible navigation');
801
- const allNavSections = document.querySelectorAll('.nav-section');
802
- console.log(`[Navigation] Found ${allNavSections.length} nav sections`);
803
-
804
- // First, ensure sections with active items are expanded on page load
805
- expandActiveNavSections();
806
-
807
- // Also run it again after a short delay to handle any timing issues
808
- setTimeout(() => {
809
- console.log('[Navigation] Running delayed expandActiveNavSections');
810
- expandActiveNavSections();
811
- }, 100);
812
-
813
- // Additional fallback: if no active items found, try to expand based on current URL
814
- setTimeout(() => {
815
- const activeItems = document.querySelectorAll('.nav-item.active');
816
- if (activeItems.length === 0) {
817
- console.log('[Navigation] No active items found, trying URL-based expansion');
818
- expandSectionByCurrentURL();
819
- }
820
- }, 200);
821
-
822
- const collapsibleTitles = document.querySelectorAll('.nav-title.collapsible');
823
-
824
- collapsibleTitles.forEach(title => {
825
- title.addEventListener('click', (e) => {
826
- // Prevent default link behavior for collapsible titles
827
- e.preventDefault();
828
-
829
- // Get the target content to toggle
830
- const targetId = title.getAttribute('data-target');
831
- const content = document.getElementById(targetId);
832
-
833
- if (content) {
834
- const isExpanded = title.classList.contains('expanded');
835
-
836
- if (isExpanded) {
837
- // Collapse this section
838
- title.classList.remove('expanded');
839
- content.classList.add('collapsed');
840
-
841
- // Also collapse all child sections within this content
842
- const childSections = content.querySelectorAll('.nav-title.collapsible');
843
- childSections.forEach(childTitle => {
844
- const childTargetId = childTitle.getAttribute('data-target');
845
- const childContent = document.getElementById(childTargetId);
846
- if (childContent) {
847
- childTitle.classList.remove('expanded');
848
- childContent.classList.add('collapsed');
849
- }
850
- });
851
- } else {
852
- // Expand this section
853
- title.classList.add('expanded');
854
- content.classList.remove('collapsed');
855
- }
856
- }
857
- });
858
- });
859
-
860
- // Prevent nav items from triggering collapse and maintain parent expansion
861
- const navItems = document.querySelectorAll('.nav-item');
862
- navItems.forEach(item => {
863
- item.addEventListener('click', (e) => {
864
- // Only stop propagation to prevent collapse, but allow normal link navigation
865
- e.stopPropagation(); // Prevent event from bubbling up to the nav-title
866
-
867
- // Ensure ALL parent sections stay expanded when navigating within them
868
- let currentElement = item;
869
- while (currentElement) {
870
- const parentContent = currentElement.closest('.nav-content');
871
- if (!parentContent) break;
872
-
873
- const parentTitle = parentContent.parentElement?.querySelector('.nav-title.collapsible');
874
-
875
- if (parentTitle && parentContent) {
876
- parentTitle.classList.add('expanded');
877
- parentContent.classList.remove('collapsed');
878
- }
879
-
880
- // Move up to check for nested sections
881
- currentElement = parentContent.parentElement;
882
- }
883
-
884
- // Allow normal link navigation - no preventDefault or manual navigation needed
885
- });
886
- });
887
- }
888
-
889
- // Expand navigation section based on current URL
890
- function expandSectionByCurrentURL() {
891
- try {
892
- const currentPath = window.location.pathname;
893
- console.log('[Navigation] Current path:', currentPath);
894
-
895
- // Find all nav items and check if any match the current URL
896
- const navItems = document.querySelectorAll('.nav-item');
897
- let foundMatch = false;
898
-
899
- navItems.forEach(item => {
900
- const href = item.getAttribute('href');
901
- if (href) {
902
- // Normalize paths for comparison
903
- const itemPath = new URL(href, window.location.href).pathname;
904
- if (itemPath === currentPath) {
905
- console.log('[Navigation] Found matching nav item by URL:', item.textContent.trim());
906
- item.classList.add('active');
907
- foundMatch = true;
908
-
909
- // Expand parent sections
910
- let currentElement = item;
911
- while (currentElement && currentElement !== document.body) {
912
- if (currentElement.classList && currentElement.classList.contains('nav-content')) {
913
- const parentSection = currentElement.closest('.nav-section');
914
- if (parentSection) {
915
- const parentTitle = parentSection.querySelector('.nav-title.collapsible');
916
- if (parentTitle && currentElement.classList.contains('collapsed')) {
917
- parentTitle.classList.add('expanded');
918
- currentElement.classList.remove('collapsed');
919
- console.log('[Navigation] Expanded section by URL:', parentTitle.textContent.trim());
920
- }
921
- }
922
- }
923
- currentElement = currentElement.parentElement;
924
- }
925
- }
926
- }
927
- });
928
-
929
- if (!foundMatch) {
930
- console.warn('[Navigation] No matching nav item found for current URL');
931
- }
932
- } catch (error) {
933
- console.error('[Navigation] Error in expandSectionByCurrentURL:', error);
934
- }
935
- }
936
-
937
- // Ensure sections containing active nav items stay expanded
938
- function expandActiveNavSections() {
939
- try {
940
- const activeNavItems = document.querySelectorAll('.nav-item.active');
941
-
942
- console.log(`[Navigation] Found ${activeNavItems.length} active nav items`);
943
-
944
- if (activeNavItems.length === 0) {
945
- console.warn('[Navigation] No active navigation items found!');
946
- return;
947
- }
948
-
949
- activeNavItems.forEach(activeItem => {
950
- console.log(`[Navigation] Expanding sections for: ${activeItem.textContent.trim()}`);
951
-
952
- // Start from the active item and work up the DOM tree
953
- let currentElement = activeItem;
954
- let sectionsExpanded = 0;
955
-
956
- while (currentElement && currentElement !== document.body) {
957
- // Check if we're inside a nav-content element
958
- if (currentElement.classList && currentElement.classList.contains('nav-content')) {
959
- console.log('[Navigation] Found nav-content element with id:', currentElement.id);
960
-
961
- // Find the corresponding nav-title in the parent nav-section
962
- const parentSection = currentElement.closest('.nav-section');
963
- if (parentSection) {
964
- const parentTitle = parentSection.querySelector('.nav-title.collapsible');
965
-
966
- if (parentTitle && currentElement.classList.contains('collapsed')) {
967
- // Expand this section
968
- parentTitle.classList.add('expanded');
969
- currentElement.classList.remove('collapsed');
970
- sectionsExpanded++;
971
- console.log(`[Navigation] Expanded section: ${parentTitle.textContent.trim()}`);
972
- } else if (parentTitle && !currentElement.classList.contains('collapsed')) {
973
- console.log(`[Navigation] Section already expanded: ${parentTitle.textContent.trim()}`);
974
- }
975
- }
976
- }
977
-
978
- // Move up to the parent element
979
- currentElement = currentElement.parentElement;
980
- }
981
-
982
- if (sectionsExpanded === 0) {
983
- console.warn('[Navigation] No sections were expanded for active item:', activeItem.textContent.trim());
984
- } else {
985
- console.log(`[Navigation] Successfully expanded ${sectionsExpanded} sections`);
986
- }
987
- });
988
- } catch (error) {
989
- console.error('[Navigation] Error in expandActiveNavSections:', error);
990
- }
991
- }
992
-
993
- // Navigation Filter
994
- function initNavigationFilter() {
995
- const filterInput = document.getElementById('nav-filter');
996
- if (!filterInput) return;
997
-
998
- filterInput.addEventListener('input', (e) => {
999
- const query = e.target.value.toLowerCase().trim();
1000
- const navItems = document.querySelectorAll('.nav-item');
1001
- const navSections = document.querySelectorAll('.nav-section');
1002
-
1003
- if (query === '') {
1004
- // Show all items and restore original state
1005
- navItems.forEach(item => {
1006
- item.style.display = 'flex';
1007
- });
1008
- navSections.forEach(section => {
1009
- section.style.display = 'block';
1010
- });
1011
- } else {
1012
- // Filter items
1013
- navItems.forEach(item => {
1014
- const text = item.textContent.toLowerCase();
1015
- const shouldShow = text.includes(query);
1016
- item.style.display = shouldShow ? 'flex' : 'none';
1017
- });
1018
-
1019
- // Show/hide sections based on whether they have visible items
1020
- navSections.forEach(section => {
1021
- const visibleItems = section.querySelectorAll('.nav-item[style*="flex"]');
1022
- const hasVisibleItems = Array.from(section.querySelectorAll('.nav-item')).some(item =>
1023
- item.style.display !== 'none'
1024
- );
1025
- section.style.display = hasVisibleItems ? 'block' : 'none';
1026
-
1027
- // Expand sections with matches
1028
- if (hasVisibleItems && query !== '') {
1029
- const navContent = section.querySelector('.nav-content');
1030
- const navTitle = section.querySelector('.nav-title.collapsible');
1031
- if (navContent && navTitle) {
1032
- navContent.classList.remove('collapsed');
1033
- navTitle.classList.add('expanded');
1034
- }
1035
- }
1036
- });
1037
- }
1038
- });
1039
- }
1040
-
1041
- // PDF Export functionality
1042
- function exportToPDF() {
1043
- // Hide UI elements for printing
1044
- const elementsToHide = [
1045
- '.sidebar',
1046
- '.header',
1047
- '.preview-banner',
1048
- '.resize-handle',
1049
- '.copy-button'
1050
- ];
1051
-
1052
- elementsToHide.forEach(selector => {
1053
- const elements = document.querySelectorAll(selector);
1054
- elements.forEach(el => el.style.display = 'none');
1055
- });
1056
-
1057
- // Adjust content for printing
1058
- const content = document.querySelector('.content');
1059
- const mainWrapper = document.querySelector('.main-wrapper');
1060
-
1061
- if (content) {
1062
- content.style.padding = '20px';
1063
- content.style.maxWidth = 'none';
1064
- }
1065
-
1066
- if (mainWrapper) {
1067
- mainWrapper.style.paddingTop = '0';
1068
- }
1069
-
1070
- // Add print-specific styles
1071
- const printStyles = document.createElement('style');
1072
- printStyles.id = 'print-styles';
1073
- printStyles.textContent = `
1074
- @media print {
1075
- body {
1076
- font-size: 12pt;
1077
- line-height: 1.4;
1078
- color: black !important;
1079
- }
1080
- .content {
1081
- margin: 0 !important;
1082
- padding: 0 !important;
1083
- max-width: none !important;
1084
- }
1085
- .main-wrapper {
1086
- padding-top: 0 !important;
1087
- }
1088
- h1, h2, h3, h4, h5, h6 {
1089
- color: black !important;
1090
- page-break-after: avoid;
1091
- }
1092
- .hero {
1093
- background: none !important;
1094
- color: black !important;
1095
- padding: 20px 0 !important;
1096
- margin: 0 !important;
1097
- }
1098
- .hero h1 {
1099
- color: black !important;
1100
- text-shadow: none !important;
1101
- font-size: 24pt !important;
1102
- }
1103
- .hero-subtitle {
1104
- color: black !important;
1105
- text-shadow: none !important;
1106
- }
1107
- .feature-grid, .metrics-grid {
1108
- display: block !important;
1109
- }
1110
- .feature-card, .metric-card {
1111
- break-inside: avoid;
1112
- margin-bottom: 10px;
1113
- border: 1px solid #ccc;
1114
- padding: 10px;
1115
- }
1116
- .mermaid {
1117
- break-inside: avoid;
1118
- background: white !important;
1119
- border: 1px solid #ccc;
1120
- }
1121
- pre, code {
1122
- background: #f5f5f5 !important;
1123
- border: 1px solid #ddd;
1124
- font-size: 10pt;
1125
- }
1126
- table {
1127
- break-inside: avoid;
1128
- }
1129
- .timeline {
1130
- display: block !important;
1131
- }
1132
- .timeline-item {
1133
- break-inside: avoid;
1134
- margin-bottom: 15px;
1135
- padding-left: 0 !important;
1136
- }
1137
- .timeline::before {
1138
- display: none;
1139
- }
1140
- .timeline-item::before {
1141
- display: none;
1142
- }
1143
- a {
1144
- color: black !important;
1145
- text-decoration: underline;
1146
- }
1147
- .gradient-text {
1148
- color: black !important;
1149
- background: none !important;
1150
- -webkit-text-fill-color: black !important;
1151
- }
1152
- }
1153
- `;
1154
- document.head.appendChild(printStyles);
1155
-
1156
- // Trigger print dialog
1157
- setTimeout(() => {
1158
- window.print();
1159
-
1160
- // Restore UI after print dialog
1161
- setTimeout(() => {
1162
- // Remove print styles
1163
- const printStylesEl = document.getElementById('print-styles');
1164
- if (printStylesEl) {
1165
- printStylesEl.remove();
1166
- }
1167
-
1168
- // Restore hidden elements
1169
- elementsToHide.forEach(selector => {
1170
- const elements = document.querySelectorAll(selector);
1171
- elements.forEach(el => el.style.display = '');
1172
- });
1173
-
1174
- // Restore content styles
1175
- if (content) {
1176
- content.style.padding = '';
1177
- content.style.maxWidth = '';
1178
- }
1179
-
1180
- if (mainWrapper) {
1181
- mainWrapper.style.paddingTop = '';
1182
- }
1183
- }, 500);
1184
- }, 100);
1185
- }
1186
-
1187
- // Add PDF export button functionality
1188
- function addPDFExportButton() {
1189
- const headerActions = document.querySelector('.header-actions');
1190
- if (headerActions) {
1191
- const pdfButton = document.createElement('button');
1192
- pdfButton.innerHTML = '<i class="fas fa-file-pdf"></i>';
1193
- pdfButton.className = 'theme-toggle';
1194
- pdfButton.title = 'Export to PDF';
1195
- pdfButton.setAttribute('aria-label', 'Export to PDF');
1196
- pdfButton.addEventListener('click', exportToPDF);
1197
-
1198
- // Insert before theme toggle
1199
- const themeToggle = document.getElementById('theme-toggle');
1200
- headerActions.insertBefore(pdfButton, themeToggle);
1201
- }
1202
- }
1203
-
1204
- // Breadcrumb Generation
1205
- function generateBreadcrumbs() {
1206
- const breadcrumbContainer = document.getElementById('breadcrumbs');
1207
- if (!breadcrumbContainer) return;
1208
-
1209
- // Decode the URL to handle special characters and spaces
1210
- const currentPath = decodeURIComponent(window.location.pathname);
1211
- let pathSegments = currentPath.split('/').filter(segment => segment !== '');
1212
-
1213
- // Find the index of 'html' directory and slice from there
1214
- const htmlIndex = pathSegments.findIndex(segment => segment === 'html');
1215
- if (htmlIndex !== -1) {
1216
- // Remove everything before and including 'html'
1217
- pathSegments = pathSegments.slice(htmlIndex + 1);
1218
- }
1219
-
1220
- // Remove .html extension from the last segment
1221
- if (pathSegments.length > 0) {
1222
- const lastSegment = pathSegments[pathSegments.length - 1];
1223
- if (lastSegment.endsWith('.html')) {
1224
- pathSegments[pathSegments.length - 1] = lastSegment.slice(0, -5);
1225
- }
1226
- }
1227
-
1228
- const breadcrumbs = [];
1229
-
1230
- // Calculate relative path to root for proper navigation
1231
- const depth = pathSegments.length;
1232
- const relativeRoot = depth > 0 ? '../'.repeat(depth) : './';
1233
-
1234
- // Always start with Home (relative to current page)
1235
- breadcrumbs.push({
1236
- text: 'Home',
1237
- href: relativeRoot + 'index.html',
1238
- icon: 'fas fa-home'
1239
- });
1240
-
1241
- // Build breadcrumb path
1242
- let currentUrl = '';
1243
- pathSegments.forEach((segment, index) => {
1244
- currentUrl += '/' + segment;
1245
-
1246
- // Calculate relative path for this breadcrumb level
1247
- const remainingDepth = pathSegments.length - index - 1;
1248
- const relativePath = remainingDepth > 0 ? '../'.repeat(remainingDepth) : './';
1249
-
1250
- // For the last segment, don't add .html back if it's not index
1251
- const href = index === pathSegments.length - 1 && segment !== 'index'
1252
- ? '#' // Current page, no navigation needed
1253
- : relativePath + segment + '.html';
1254
-
1255
- // Prettify segment names
1256
- const text = segment
1257
- .replace(/[-_]/g, ' ')
1258
- .split(' ')
1259
- .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
1260
- .join(' ')
1261
- .replace(/\b(Api|Html|Css|Js|Pdf|Qa|Ai)\b/g, (match) => match.toUpperCase())
1262
- .replace(/\bReadme\b/g, 'Overview');
1263
-
1264
- // Get appropriate icon based on segment
1265
- let icon = 'fas fa-folder';
1266
- if (segment.includes('bubble')) icon = 'fas fa-circle';
1267
- else if (segment.includes('system')) icon = 'fas fa-sitemap';
1268
- else if (segment.includes('middleware')) icon = 'fas fa-layer-group';
1269
- else if (segment.includes('quickbase')) icon = 'fas fa-database';
1270
- else if (segment.includes('product-roadmap')) icon = 'fas fa-road';
1271
- else if (segment.includes('team')) icon = 'fas fa-users';
1272
- else if (segment.includes('testing')) icon = 'fas fa-vial';
1273
- else if (segment.includes('paths')) icon = 'fas fa-route';
1274
- else if (segment.includes('diagrams')) icon = 'fas fa-project-diagram';
1275
- else if (segment.includes('technical')) icon = 'fas fa-cogs';
1276
- else if (segment.includes('application')) icon = 'fas fa-desktop';
1277
- else if (index === pathSegments.length - 1) icon = 'fas fa-file-alt';
1278
-
1279
- breadcrumbs.push({
1280
- text,
1281
- href,
1282
- icon,
1283
- isLast: index === pathSegments.length - 1
1284
- });
1285
- });
1286
-
1287
- // Generate breadcrumb HTML
1288
- const breadcrumbHTML = breadcrumbs.map((crumb, index) => {
1289
- if (crumb.isLast) {
1290
- return `<span class="breadcrumb-item current">
1291
- <i class="${crumb.icon}"></i>
1292
- <span>${crumb.text}</span>
1293
- </span>`;
1294
- } else {
1295
- return `<a href="${crumb.href}" class="breadcrumb-item">
1296
- <i class="${crumb.icon}"></i>
1297
- <span>${crumb.text}</span>
1298
- </a>`;
1299
- }
1300
- }).join('<i class="fas fa-chevron-right breadcrumb-separator"></i>');
1301
-
1302
- breadcrumbContainer.innerHTML = breadcrumbHTML;
1303
- }
1304
-
1305
- // Initialize tooltip positioning for navigation items
1306
- function initTooltips() {
1307
- const tooltipElements = document.querySelectorAll('[data-tooltip]');
1308
-
1309
- tooltipElements.forEach(element => {
1310
- element.addEventListener('mouseenter', function(e) {
1311
- const rect = element.getBoundingClientRect();
1312
- const tooltip = window.getComputedStyle(element, '::after');
1313
-
1314
- // Position the tooltip using CSS variables
1315
- element.style.setProperty('--tooltip-left', `${rect.right + 10}px`);
1316
- element.style.setProperty('--tooltip-top', `${rect.top + rect.height / 2}px`);
1317
- });
1318
- });
1319
- }
1320
-
1321
- // Initialize on DOM Load
1322
- document.addEventListener('DOMContentLoaded', () => {
1323
- highlightNavigation();
1324
- generateTableOfContents();
1325
- initSidebarResize();
1326
- initCollapsibleNavigation();
1327
- initNavigationFilter();
1328
- addPDFExportButton();
1329
- generateBreadcrumbs();
1330
- initTooltips();
1331
- });