@knowcode/doc-builder 1.9.1 → 1.9.2

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 (43) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +1 -0
  3. package/html/README.html +150 -33
  4. package/html/css/notion-style.css +394 -13
  5. package/html/documentation-index.html +128 -10
  6. package/html/guides/authentication-default-change.html +131 -7
  7. package/html/guides/authentication-guide.html +142 -19
  8. package/html/guides/claude-workflow-guide.html +138 -22
  9. package/html/guides/documentation-standards.html +134 -11
  10. package/html/guides/html-embedding-guide.html +450 -0
  11. package/html/guides/image-modal-guide.html +133 -10
  12. package/html/guides/phosphor-icons-guide.html +151 -27
  13. package/html/guides/private-directory-authentication.html +131 -7
  14. package/html/guides/public-site-deployment.html +131 -7
  15. package/html/guides/search-engine-verification-guide.html +131 -7
  16. package/html/guides/seo-guide.html +131 -7
  17. package/html/guides/seo-optimization-guide.html +160 -38
  18. package/html/guides/troubleshooting-guide.html +142 -19
  19. package/html/guides/windows-setup-guide.html +162 -38
  20. package/html/image-modal-test.html +126 -8
  21. package/html/index.html +150 -33
  22. package/html/js/auth.js +118 -39
  23. package/html/js/main.js +259 -25
  24. package/html/private/cache-control-anti-pattern.html +136 -12
  25. package/html/private/launch/README.html +145 -15
  26. package/html/private/launch/auth-cleanup-summary.html +154 -24
  27. package/html/private/launch/bubble-plugin-specification.html +137 -7
  28. package/html/private/launch/go-to-market-strategy.html +139 -9
  29. package/html/private/launch/launch-announcements.html +153 -24
  30. package/html/private/launch/vercel-deployment-auth-setup.html +139 -9
  31. package/html/private/next-steps-walkthrough.html +138 -14
  32. package/html/private/supabase-auth-implementation-completed.html +144 -21
  33. package/html/private/supabase-auth-implementation-plan.html +137 -14
  34. package/html/private/supabase-auth-integration-plan.html +143 -21
  35. package/html/private/supabase-auth-setup-guide.html +140 -16
  36. package/html/private/test-private-doc.html +131 -7
  37. package/html/private/user-management-tooling.html +131 -7
  38. package/html/prompts/markdown-document-standards.html +135 -12
  39. package/html/sitemap.xml +58 -46
  40. package/html/vercel-cli-setup-guide.html +129 -11
  41. package/html/vercel-first-time-setup-guide.html +126 -8
  42. package/lib/config.js +2 -0
  43. package/package.json +1 -1
package/html/js/auth.js CHANGED
@@ -1,10 +1,7 @@
1
+
1
2
  /**
2
- * Simple Client-Side Authentication for Documentation
3
- * This runs on every page load to check authentication
4
- *
5
- * IMPORTANT: This is a basic authentication system suitable for
6
- * protecting documentation from casual access. For production
7
- * use with sensitive data, implement server-side authentication.
3
+ * Supabase Authentication for Documentation Site
4
+ * Generated by @knowcode/doc-builder
8
5
  */
9
6
 
10
7
  (function() {
@@ -12,56 +9,138 @@
12
9
 
13
10
  // Skip auth check on login and logout pages
14
11
  const currentPage = window.location.pathname;
15
- if (currentPage === '/login.html' || currentPage === '/logout.html' || currentPage.includes('login') || currentPage.includes('logout')) {
12
+ if (currentPage === '/login.html' || currentPage === '/logout.html' ||
13
+ currentPage.includes('login') || currentPage.includes('logout')) {
16
14
  return;
17
15
  }
18
16
 
19
- // Check if user is authenticated
20
- function isAuthenticated() {
21
- const authToken = getCookie('doc-auth');
22
- if (!authToken) return false;
23
-
17
+ // Initialize Supabase client
18
+ const { createClient } = supabase;
19
+ const supabaseClient = createClient('https://xcihhnfcitjrwbynxmka.supabase.co', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InhjaWhobmZjaXRqcndieW54bWthIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTM0Mzc2MzcsImV4cCI6MjA2OTAxMzYzN30.zvWp3JFIR8fBIiwuFF5gqOR_Kxb42baZS5fsBz60XOY', {
20
+ auth: {
21
+ persistSession: true,
22
+ autoRefreshToken: true,
23
+ detectSessionInUrl: true
24
+ }
25
+ });
26
+
27
+ // Check authentication and site access
28
+ async function checkAuth() {
24
29
  try {
25
- // Simple token validation - just check if it exists and has expected format
26
- // The actual validation happens server-side (or in login page for static sites)
27
- const decoded = atob(authToken);
28
- return decoded && decoded.includes(':');
30
+ // Check if current page is in private directory
31
+ const currentPath = window.location.pathname;
32
+ const isPrivatePage = currentPath.includes('/private/');
33
+
34
+ // Get current user session
35
+ const { data: { user }, error: userError } = await supabaseClient.auth.getUser();
36
+
37
+ if (userError || !user) {
38
+ // Only redirect if we're on a private page
39
+ if (isPrivatePage) {
40
+ redirectToLogin();
41
+ } else {
42
+ // Public page, just show it
43
+ document.body.classList.add('authenticated'); // Use same class to show body
44
+ updateAuthButton(false);
45
+ }
46
+ return;
47
+ }
48
+
49
+ // Check if user has access to this site (using domain)
50
+ const { data: access, error: accessError } = await supabaseClient
51
+ .from('docbuilder_access')
52
+ .select('*')
53
+ .eq('user_id', user.id)
54
+ .eq('domain', window.location.host)
55
+ .single();
56
+
57
+ if (accessError || !access) {
58
+ if (isPrivatePage) {
59
+ showAccessDenied();
60
+ } else {
61
+ // Public page, just show it
62
+ document.body.classList.add('authenticated');
63
+ updateAuthButton(false);
64
+ }
65
+ return;
66
+ }
67
+
68
+ // User is authenticated and has access
69
+ console.log('User authenticated and authorized');
70
+ document.body.classList.add('authenticated');
71
+ updateAuthButton(true);
72
+
29
73
  } catch (error) {
30
- return false;
74
+ console.error('Auth check failed:', error);
75
+ if (window.location.pathname.includes('/private/')) {
76
+ redirectToLogin();
77
+ } else {
78
+ // Public page, show it anyway
79
+ document.body.classList.add('authenticated');
80
+ updateAuthButton(false);
81
+ }
31
82
  }
32
83
  }
33
84
 
34
- // Get cookie value
35
- function getCookie(name) {
36
- const value = `; ${document.cookie}`;
37
- const parts = value.split(`; ${name}=`);
38
- if (parts.length === 2) return parts.pop().split(';').shift();
39
- return null;
40
- }
41
-
42
- // Redirect to login if not authenticated
85
+ // Redirect to login page
43
86
  function redirectToLogin() {
44
87
  const currentUrl = window.location.pathname + window.location.search;
45
88
  const loginUrl = '/login.html' + (currentUrl !== '/' ? '?redirect=' + encodeURIComponent(currentUrl) : '');
46
89
  window.location.href = loginUrl;
47
90
  }
48
91
 
49
- // Check authentication on page load
50
- if (!isAuthenticated()) {
51
- redirectToLogin();
92
+ // Show access denied message
93
+ function showAccessDenied() {
94
+ document.body.classList.add('authenticated'); // Show the body
95
+ document.body.innerHTML = `
96
+ <div style="display: flex; justify-content: center; align-items: center; height: 100vh; font-family: Inter, sans-serif;">
97
+ <div style="text-align: center; max-width: 400px;">
98
+ <h1 style="color: #ef4444; margin-bottom: 1rem;">Access Denied</h1>
99
+ <p style="color: #6b7280; margin-bottom: 2rem;">You don't have permission to view this documentation site.</p>
100
+ <a href="/login.html" style="background: #3b82f6; color: white; padding: 0.75rem 1.5rem; border-radius: 0.5rem; text-decoration: none;">Try Different Account</a>
101
+ </div>
102
+ </div>
103
+ `;
52
104
  }
53
105
 
54
- // Add logout functionality to logout buttons
106
+ // Function to update auth button
107
+ function updateAuthButton(isAuthenticated) {
108
+ const authBtn = document.querySelector('.auth-btn');
109
+ if (authBtn) {
110
+ const icon = authBtn.querySelector('i');
111
+ if (icon) {
112
+ if (isAuthenticated) {
113
+ icon.className = 'fas fa-sign-out-alt';
114
+ authBtn.title = 'Logout';
115
+ authBtn.href = '/logout.html';
116
+ } else {
117
+ icon.className = 'fas fa-sign-in-alt';
118
+ authBtn.title = 'Login';
119
+ authBtn.href = '/login.html';
120
+ }
121
+ }
122
+ }
123
+ }
124
+
125
+ // Add auth button functionality
55
126
  document.addEventListener('DOMContentLoaded', function() {
56
- const logoutLinks = document.querySelectorAll('a[href*="logout"]');
57
- logoutLinks.forEach(link => {
58
- link.addEventListener('click', function(e) {
59
- e.preventDefault();
60
- // Clear auth cookie
61
- document.cookie = 'doc-auth=; path=/; expires=Thu, 01 Jan 1970 00:00:01 GMT;';
62
- window.location.href = '/logout.html';
127
+ const authBtn = document.querySelector('.auth-btn');
128
+ if (authBtn) {
129
+ authBtn.addEventListener('click', async function(e) {
130
+ // Check if we're logged in
131
+ const { data: { user } } = await supabaseClient.auth.getUser();
132
+ if (user) {
133
+ // Logged in - sign out
134
+ e.preventDefault();
135
+ await supabaseClient.auth.signOut();
136
+ window.location.href = '/logout.html';
137
+ }
138
+ // If not logged in, normal navigation to login page will occur
63
139
  });
64
- });
140
+ }
65
141
  });
66
142
 
67
- })();
143
+ // Run auth check
144
+ checkAuth();
145
+
146
+ })();
package/html/js/main.js CHANGED
@@ -79,9 +79,6 @@ function initializeMermaidFullScreen() {
79
79
  const toolbar = document.createElement('div');
80
80
  toolbar.className = 'mermaid-toolbar';
81
81
 
82
- const title = document.createElement('div');
83
- title.textContent = 'Mermaid Diagram';
84
-
85
82
  const actions = document.createElement('div');
86
83
  actions.className = 'mermaid-actions';
87
84
 
@@ -91,23 +88,8 @@ function initializeMermaidFullScreen() {
91
88
  fullScreenBtn.innerHTML = '<i class="fas fa-expand"></i> Full Screen';
92
89
  fullScreenBtn.addEventListener('click', () => openMermaidFullScreen(mermaidDiv, index));
93
90
 
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
91
  actions.appendChild(fullScreenBtn);
107
- actions.appendChild(copyBtn);
108
- actions.appendChild(copyMermaidBtn);
109
92
 
110
- toolbar.appendChild(title);
111
93
  toolbar.appendChild(actions);
112
94
 
113
95
  // Create wrapper for the diagram
@@ -512,12 +494,147 @@ function updateThemeIcon(theme) {
512
494
  const menuToggle = document.getElementById('menu-toggle');
513
495
  const sidebar = document.querySelector('.sidebar');
514
496
 
497
+ // Set initial menu state based on configuration
498
+ const menuDefaultOpen = window.docBuilderConfig?.features?.menuDefaultOpen !== false;
499
+ if (sidebar && window.innerWidth > 768) {
500
+ if (!menuDefaultOpen) {
501
+ sidebar.classList.add('closed');
502
+ // Add class to body to show menu toggle on desktop when menu starts closed
503
+ document.body.classList.add('menu-starts-closed');
504
+ }
505
+ }
506
+
507
+ // Create overlay element for mobile
508
+ let overlay = document.querySelector('.sidebar-overlay');
509
+ if (!overlay && window.innerWidth <= 768) {
510
+ overlay = document.createElement('div');
511
+ overlay.className = 'sidebar-overlay';
512
+ document.body.appendChild(overlay);
513
+ }
514
+
515
515
  if (menuToggle) {
516
516
  menuToggle.addEventListener('click', () => {
517
+ if (window.innerWidth <= 768) {
518
+ // Mobile: toggle 'open' class
519
+ sidebar.classList.toggle('open');
520
+ } else {
521
+ // Desktop: toggle 'closed' class
522
+ sidebar.classList.toggle('closed');
523
+ // Update visibility of menu toggle based on sidebar state
524
+ updateMenuToggleVisibility();
525
+ }
526
+ if (overlay) {
527
+ overlay.classList.toggle('active');
528
+ }
529
+ });
530
+ }
531
+
532
+ // Function to update menu toggle visibility
533
+ function updateMenuToggleVisibility() {
534
+ if (window.innerWidth > 768) {
535
+ if (!menuDefaultOpen || sidebar.classList.contains('closed')) {
536
+ document.body.classList.add('show-menu-toggle');
537
+ } else {
538
+ document.body.classList.remove('show-menu-toggle');
539
+ }
540
+ }
541
+ }
542
+
543
+ // Initial check
544
+ updateMenuToggleVisibility();
545
+
546
+ // Update on window resize
547
+ window.addEventListener('resize', updateMenuToggleVisibility);
548
+
549
+ // Close menu when clicking overlay
550
+ if (overlay) {
551
+ overlay.addEventListener('click', () => {
552
+ sidebar.classList.remove('open');
553
+ overlay.classList.remove('active');
554
+ });
555
+ }
556
+
557
+ // Floating Menu Button for Mobile
558
+ function initFloatingMenuButton() {
559
+ // Only initialize on mobile
560
+ if (window.innerWidth > 768) return;
561
+
562
+ // Check if button already exists
563
+ if (document.getElementById('floating-menu-toggle')) return;
564
+
565
+ // Create floating button
566
+ const floatingButton = document.createElement('button');
567
+ floatingButton.id = 'floating-menu-toggle';
568
+ floatingButton.className = 'floating-menu-toggle';
569
+ floatingButton.setAttribute('aria-label', 'Toggle menu');
570
+ floatingButton.innerHTML = '<i class="fas fa-bars"></i>';
571
+ floatingButton.style.display = 'flex'; // Always visible on mobile
572
+ floatingButton.classList.add('visible'); // Start visible
573
+
574
+ // Add to body
575
+ document.body.appendChild(floatingButton);
576
+
577
+ // Toggle sidebar on click
578
+ floatingButton.addEventListener('click', () => {
517
579
  sidebar.classList.toggle('open');
580
+
581
+ // Handle overlay
582
+ let overlay = document.querySelector('.sidebar-overlay');
583
+ if (!overlay) {
584
+ overlay = document.createElement('div');
585
+ overlay.className = 'sidebar-overlay';
586
+ document.body.appendChild(overlay);
587
+
588
+ // Add overlay click handler
589
+ overlay.addEventListener('click', () => {
590
+ sidebar.classList.remove('open');
591
+ overlay.classList.remove('active');
592
+ floatingButton.querySelector('i').className = 'fas fa-bars';
593
+ });
594
+ }
595
+
596
+ if (overlay) {
597
+ overlay.classList.toggle('active');
598
+ }
599
+
600
+ // Update icon based on state
601
+ const icon = floatingButton.querySelector('i');
602
+ if (sidebar.classList.contains('open')) {
603
+ icon.className = 'fas fa-times';
604
+ } else {
605
+ icon.className = 'fas fa-bars';
606
+ }
607
+ });
608
+
609
+ // Remove scroll-based visibility - button is always visible on mobile
610
+
611
+ // Update icon when sidebar state changes from other sources
612
+ const observer = new MutationObserver(() => {
613
+ const icon = floatingButton.querySelector('i');
614
+ if (sidebar.classList.contains('open')) {
615
+ icon.className = 'fas fa-times';
616
+ } else {
617
+ icon.className = 'fas fa-bars';
618
+ }
619
+ });
620
+
621
+ observer.observe(sidebar, {
622
+ attributes: true,
623
+ attributeFilter: ['class']
518
624
  });
519
625
  }
520
626
 
627
+ // Initialize floating button on load and resize
628
+ document.addEventListener('DOMContentLoaded', initFloatingMenuButton);
629
+ window.addEventListener('resize', () => {
630
+ const existingButton = document.getElementById('floating-menu-toggle');
631
+ if (window.innerWidth > 768 && existingButton) {
632
+ existingButton.remove();
633
+ } else if (window.innerWidth <= 768 && !existingButton) {
634
+ initFloatingMenuButton();
635
+ }
636
+ });
637
+
521
638
  // Prevent sidebar from closing when clicking nav items
522
639
  // Only close when clicking outside the sidebar or the close button
523
640
  document.addEventListener('click', (e) => {
@@ -525,11 +642,21 @@ document.addEventListener('click', (e) => {
525
642
  if (window.innerWidth <= 768) {
526
643
  const isClickInsideSidebar = sidebar && sidebar.contains(e.target);
527
644
  const isMenuToggle = e.target.closest('#menu-toggle');
645
+ const isFloatingButton = e.target.closest('#floating-menu-toggle');
528
646
  const isNavItem = e.target.closest('.nav-item, .nav-title');
647
+ const overlay = document.querySelector('.sidebar-overlay');
529
648
 
530
649
  // 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')) {
650
+ if (!isClickInsideSidebar && !isMenuToggle && !isFloatingButton && !isNavItem && sidebar?.classList.contains('open')) {
532
651
  sidebar.classList.remove('open');
652
+ if (overlay) {
653
+ overlay.classList.remove('active');
654
+ }
655
+ // Update floating button icon if it exists
656
+ const floatingBtn = document.getElementById('floating-menu-toggle');
657
+ if (floatingBtn) {
658
+ floatingBtn.querySelector('i').className = 'fas fa-bars';
659
+ }
533
660
  }
534
661
  }
535
662
  });
@@ -538,12 +665,16 @@ document.addEventListener('click', (e) => {
538
665
  document.querySelectorAll('a[href^="#"]').forEach(anchor => {
539
666
  anchor.addEventListener('click', function (e) {
540
667
  e.preventDefault();
541
- const target = document.querySelector(this.getAttribute('href'));
542
- if (target) {
543
- target.scrollIntoView({
544
- behavior: 'smooth',
545
- block: 'start'
546
- });
668
+ const href = this.getAttribute('href');
669
+ // Skip if href is just '#' (prevents querySelector error)
670
+ if (href && href !== '#') {
671
+ const target = document.querySelector(href);
672
+ if (target) {
673
+ target.scrollIntoView({
674
+ behavior: 'smooth',
675
+ block: 'start'
676
+ });
677
+ }
547
678
  }
548
679
  });
549
680
  });
@@ -1186,6 +1317,10 @@ function exportToPDF() {
1186
1317
 
1187
1318
  // Add PDF export button functionality
1188
1319
  function addPDFExportButton() {
1320
+ // Check configuration - default to true if not set
1321
+ const showPdfDownload = window.docBuilderConfig?.features?.showPdfDownload !== false;
1322
+ if (!showPdfDownload) return;
1323
+
1189
1324
  const headerActions = document.querySelector('.header-actions');
1190
1325
  if (headerActions) {
1191
1326
  const pdfButton = document.createElement('button');
@@ -1318,8 +1453,106 @@ function initTooltips() {
1318
1453
  });
1319
1454
  }
1320
1455
 
1456
+ // Handle .md link redirects
1457
+ function initMarkdownLinkRedirects() {
1458
+ // Check if current URL ends with .md and redirect
1459
+ if (window.location.pathname.endsWith('.md')) {
1460
+ const htmlPath = window.location.pathname.replace(/\.md$/, '.html');
1461
+ console.log(`Redirecting from .md to .html: ${htmlPath}`);
1462
+ window.location.replace(htmlPath);
1463
+ return; // Stop execution as we're redirecting
1464
+ }
1465
+
1466
+ // Intercept clicks on .md links
1467
+ document.addEventListener('click', function(e) {
1468
+ const link = e.target.closest('a');
1469
+ if (link && link.href && link.href.endsWith('.md')) {
1470
+ e.preventDefault();
1471
+ const htmlUrl = link.href.replace(/\.md$/, '.html');
1472
+ console.log(`Converting .md link to .html: ${htmlUrl}`);
1473
+ window.location.href = htmlUrl;
1474
+ }
1475
+ });
1476
+ }
1477
+
1478
+ // Image Modal System
1479
+ function initImageModal() {
1480
+ // Create modal HTML structure
1481
+ const modalHTML = `
1482
+ <div class="image-modal" id="imageModal">
1483
+ <div class="image-modal-content">
1484
+ <div class="image-modal-close" id="imageModalClose">&times;</div>
1485
+ <img class="image-modal-img" id="imageModalImg" src="" alt="">
1486
+ <div class="image-modal-caption" id="imageModalCaption"></div>
1487
+ </div>
1488
+ </div>
1489
+ `;
1490
+
1491
+ // Add modal to document body
1492
+ document.body.insertAdjacentHTML('beforeend', modalHTML);
1493
+
1494
+ const modal = document.getElementById('imageModal');
1495
+ const modalImg = document.getElementById('imageModalImg');
1496
+ const modalCaption = document.getElementById('imageModalCaption');
1497
+ const closeBtn = document.getElementById('imageModalClose');
1498
+
1499
+ // Add click handlers to all content images
1500
+ const contentImages = document.querySelectorAll('.content img');
1501
+ contentImages.forEach(img => {
1502
+ img.addEventListener('click', function() {
1503
+ modal.classList.add('active');
1504
+ modalImg.src = this.src;
1505
+ modalImg.alt = this.alt;
1506
+ modalCaption.textContent = this.alt;
1507
+
1508
+ // Hide caption if no alt text
1509
+ if (!this.alt) {
1510
+ modalCaption.style.display = 'none';
1511
+ } else {
1512
+ modalCaption.style.display = 'block';
1513
+ }
1514
+
1515
+ // Prevent body scrolling
1516
+ document.body.style.overflow = 'hidden';
1517
+ });
1518
+ });
1519
+
1520
+ // Close modal functions
1521
+ function closeModal() {
1522
+ modal.classList.remove('active');
1523
+ document.body.style.overflow = '';
1524
+ }
1525
+
1526
+ // Close on X button click
1527
+ closeBtn.addEventListener('click', closeModal);
1528
+
1529
+ // Close on overlay click (but not on image click)
1530
+ modal.addEventListener('click', function(e) {
1531
+ if (e.target === modal) {
1532
+ closeModal();
1533
+ }
1534
+ });
1535
+
1536
+ // Close on Escape key
1537
+ document.addEventListener('keydown', function(e) {
1538
+ if (e.key === 'Escape' && modal.classList.contains('active')) {
1539
+ closeModal();
1540
+ }
1541
+ });
1542
+
1543
+ // Prevent modal content from closing modal when clicked
1544
+ modalImg.addEventListener('click', function(e) {
1545
+ e.stopPropagation();
1546
+ });
1547
+
1548
+ document.querySelector('.image-modal-content').addEventListener('click', function(e) {
1549
+ e.stopPropagation();
1550
+ });
1551
+ }
1552
+
1321
1553
  // Initialize on DOM Load
1322
1554
  document.addEventListener('DOMContentLoaded', () => {
1555
+ initMarkdownLinkRedirects();
1323
1556
  highlightNavigation();
1324
1557
  generateTableOfContents();
1325
1558
  initSidebarResize();
@@ -1327,5 +1560,6 @@ document.addEventListener('DOMContentLoaded', () => {
1327
1560
  initNavigationFilter();
1328
1561
  addPDFExportButton();
1329
1562
  generateBreadcrumbs();
1563
+ initImageModal();
1330
1564
  initTooltips();
1331
1565
  });