jekyll-theme-zer0 0.21.2 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +54 -0
  3. data/README.md +86 -46
  4. data/_data/authors.yml +12 -3
  5. data/_data/features.yml +1 -1
  6. data/_data/glossary.yml +101 -0
  7. data/_data/navigation/docs.yml +12 -0
  8. data/_data/navigation/home.yml +2 -2
  9. data/_data/navigation/main.yml +2 -8
  10. data/_data/prompts.yml +184 -0
  11. data/_includes/components/author-eeat.html +133 -0
  12. data/_includes/components/cookie-consent.html +9 -9
  13. data/_includes/components/dev-shortcuts.html +36 -27
  14. data/_includes/components/env-detect.html +14 -0
  15. data/_includes/components/env-switcher.html +38 -16
  16. data/_includes/components/halfmoon.html +31 -20
  17. data/_includes/components/info-section.html +4 -3
  18. data/_includes/components/js-cdn.html +8 -15
  19. data/_includes/components/mermaid.html +13 -9
  20. data/_includes/components/powered-by.html +5 -3
  21. data/_includes/content/intro.html +64 -4
  22. data/_includes/content/jsonld-faq.html +47 -0
  23. data/_includes/content/jsonld-software.html +121 -0
  24. data/_includes/content/sitemap.html +2 -2
  25. data/_includes/core/branding.html +9 -7
  26. data/_includes/core/footer.html +12 -9
  27. data/_includes/core/head.html +17 -14
  28. data/_includes/core/header.html +33 -21
  29. data/_includes/navigation/navbar.html +130 -124
  30. data/_includes/navigation/sidebar-left.html +3 -3
  31. data/_includes/navigation/sidebar-right.html +4 -8
  32. data/_includes/search-data.json +1 -2
  33. data/_layouts/landing.html +8 -3
  34. data/_layouts/root.html +4 -4
  35. data/_layouts/sitemap-collection.html +20 -10
  36. data/_sass/core/_docs-layout.scss +756 -0
  37. data/_sass/core/_navbar.scss +522 -69
  38. data/_sass/core/_offcanvas-panels.scss +48 -0
  39. data/_sass/core/_syntax.scss +1 -51
  40. data/_sass/core/_theme.scss +2 -249
  41. data/_sass/core/_variables.scss +1 -54
  42. data/_sass/core/code-copy.scss +6 -6
  43. data/_sass/custom.scss +119 -133
  44. data/_sass/theme/_color-modes.scss +3 -0
  45. data/_sass/theme/_css-variables.scss +29 -0
  46. data/_sass/theme/_wizard-mode.scss +31 -0
  47. data/assets/css/custom.css +5 -120
  48. data/assets/css/main.scss +6 -2
  49. data/assets/css/stats.css +3 -0
  50. data/assets/css/theme-npm-entry.scss +6 -0
  51. data/assets/css/vendor/.gitkeep +0 -0
  52. data/assets/images/authors/bamr87.png +0 -0
  53. data/assets/js/auto-hide-nav.js +71 -20
  54. data/assets/js/color-modes.js +8 -2
  55. data/assets/js/halfmoon.js +8 -2
  56. data/assets/js/myScript.js +4 -12
  57. data/assets/js/navigation.js +174 -19
  58. data/assets/js/search-modal.js +50 -7
  59. data/assets/vendor/bootstrap/css/bootstrap.min.css +5 -0
  60. data/assets/vendor/bootstrap/js/bootstrap.bundle.min.js +6 -0
  61. data/assets/vendor/bootstrap-icons/font/bootstrap-icons.css +2018 -0
  62. data/assets/vendor/bootstrap-icons/font/fonts/bootstrap-icons.woff +0 -0
  63. data/assets/vendor/bootstrap-icons/font/fonts/bootstrap-icons.woff2 +0 -0
  64. data/assets/vendor/font-awesome/css/all.min.css +9 -0
  65. data/assets/vendor/font-awesome/webfonts/fa-brands-400.ttf +0 -0
  66. data/assets/vendor/font-awesome/webfonts/fa-brands-400.woff2 +0 -0
  67. data/assets/vendor/font-awesome/webfonts/fa-regular-400.ttf +0 -0
  68. data/assets/vendor/font-awesome/webfonts/fa-regular-400.woff2 +0 -0
  69. data/assets/vendor/font-awesome/webfonts/fa-solid-900.ttf +0 -0
  70. data/assets/vendor/font-awesome/webfonts/fa-solid-900.woff2 +0 -0
  71. data/assets/vendor/font-awesome/webfonts/fa-v4compatibility.ttf +0 -0
  72. data/assets/vendor/font-awesome/webfonts/fa-v4compatibility.woff2 +0 -0
  73. data/assets/vendor/github-calendar/github-calendar-responsive.css +231 -0
  74. data/assets/vendor/github-calendar/github-calendar.min.js +240 -0
  75. data/assets/vendor/jquery/jquery-3.7.1.min.js +2 -0
  76. data/assets/vendor/mathjax/es5/adaptors/liteDOM.js +1 -0
  77. data/assets/vendor/mathjax/es5/core.js +1 -0
  78. data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_AMS-Regular.woff +0 -0
  79. data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Calligraphic-Bold.woff +0 -0
  80. data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Calligraphic-Regular.woff +0 -0
  81. data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Fraktur-Bold.woff +0 -0
  82. data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Fraktur-Regular.woff +0 -0
  83. data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Main-Bold.woff +0 -0
  84. data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Main-Italic.woff +0 -0
  85. data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Main-Regular.woff +0 -0
  86. data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Math-BoldItalic.woff +0 -0
  87. data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Math-Italic.woff +0 -0
  88. data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Math-Regular.woff +0 -0
  89. data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_SansSerif-Bold.woff +0 -0
  90. data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_SansSerif-Italic.woff +0 -0
  91. data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_SansSerif-Regular.woff +0 -0
  92. data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Script-Regular.woff +0 -0
  93. data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Size1-Regular.woff +0 -0
  94. data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Size2-Regular.woff +0 -0
  95. data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Size3-Regular.woff +0 -0
  96. data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Size4-Regular.woff +0 -0
  97. data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Typewriter-Regular.woff +0 -0
  98. data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Vector-Bold.woff +0 -0
  99. data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Vector-Regular.woff +0 -0
  100. data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Zero.woff +0 -0
  101. data/assets/vendor/mathjax/es5/startup.js +1 -0
  102. data/assets/vendor/mathjax/es5/tex-mml-chtml.js +1 -0
  103. data/assets/vendor/mermaid/mermaid.min.js +2029 -0
  104. data/scripts/bin/build +12 -2
  105. data/scripts/lib/version.sh +41 -0
  106. data/scripts/test/integration/mermaid +1 -1
  107. data/scripts/vendor-install.sh +196 -0
  108. metadata +62 -3
  109. data/_sass/core/_docs.scss +0 -3219
@@ -1,19 +1,22 @@
1
1
  /**
2
- * Auto-hide navbar on scroll
2
+ * Auto-hide navbar on scroll with enhanced UX
3
3
  *
4
4
  * Behavior:
5
5
  * - Navbar is fixed at top and visible by default
6
- * - Hides when scrolling DOWN past a threshold (100px)
6
+ * - Hides when scrolling DOWN past a threshold (80px)
7
7
  * - Reappears when scrolling UP
8
+ * - Shows immediately when near top of page
8
9
  * - Respects prefers-reduced-motion accessibility setting
9
10
  * - Adds body padding to prevent content jump
11
+ * - Smooth transitions for better visual experience
10
12
  */
11
13
  (function() {
12
14
  'use strict';
13
15
 
14
16
  // Configuration
15
- const SCROLL_THRESHOLD = 100; // Pixels before hide/show triggers
16
- const SCROLL_DELTA = 5; // Minimum scroll distance to trigger change
17
+ const SCROLL_THRESHOLD = 80; // Reduced from 100px for quicker response
18
+ const SCROLL_DELTA = 3; // Reduced from 5px for smoother detection
19
+ const SHOW_ON_TOP_OFFSET = 50; // Show navbar when within 50px of top
17
20
 
18
21
  document.addEventListener('DOMContentLoaded', function() {
19
22
  const navbar = document.getElementById('navbar');
@@ -21,6 +24,7 @@
21
24
 
22
25
  let lastScrollTop = 0;
23
26
  let ticking = false;
27
+ let isNavbarHidden = false;
24
28
 
25
29
  // Check for reduced motion preference
26
30
  const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
@@ -34,10 +38,14 @@
34
38
  // Initial padding setup
35
39
  updateBodyPadding();
36
40
 
37
- // Update padding on window resize
38
- window.addEventListener('resize', updateBodyPadding, { passive: true });
41
+ // Update padding on window resize with debounce
42
+ let resizeTimeout;
43
+ window.addEventListener('resize', function() {
44
+ clearTimeout(resizeTimeout);
45
+ resizeTimeout = setTimeout(updateBodyPadding, 150);
46
+ }, { passive: true });
39
47
 
40
- // Scroll handler
48
+ // Enhanced scroll handler with better logic
41
49
  function handleScroll() {
42
50
  const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
43
51
  const scrollDelta = scrollTop - lastScrollTop;
@@ -48,17 +56,30 @@
48
56
  return;
49
57
  }
50
58
 
51
- if (scrollDelta > 0 && scrollTop > SCROLL_THRESHOLD) {
52
- // Scrolling DOWN past threshold - hide navbar
53
- navbar.classList.add('navbar-hidden');
54
- } else if (scrollDelta < 0) {
55
- // Scrolling UP - show navbar
56
- navbar.classList.remove('navbar-hidden');
59
+ // Always show navbar when near top of page
60
+ if (scrollTop <= SHOW_ON_TOP_OFFSET) {
61
+ if (isNavbarHidden) {
62
+ navbar.classList.remove('navbar-hidden');
63
+ isNavbarHidden = false;
64
+ }
65
+ lastScrollTop = scrollTop;
66
+ ticking = false;
67
+ return;
57
68
  }
58
69
 
59
- // Always show navbar when at top of page
60
- if (scrollTop <= 0) {
61
- navbar.classList.remove('navbar-hidden');
70
+ // Hide navbar when scrolling down past threshold
71
+ if (scrollDelta > 0 && scrollTop > SCROLL_THRESHOLD) {
72
+ if (!isNavbarHidden) {
73
+ navbar.classList.add('navbar-hidden');
74
+ isNavbarHidden = true;
75
+ }
76
+ }
77
+ // Show navbar when scrolling up
78
+ else if (scrollDelta < 0) {
79
+ if (isNavbarHidden) {
80
+ navbar.classList.remove('navbar-hidden');
81
+ isNavbarHidden = false;
82
+ }
62
83
  }
63
84
 
64
85
  lastScrollTop = Math.max(0, scrollTop);
@@ -66,16 +87,46 @@
66
87
  }
67
88
 
68
89
  // Optimized scroll listener using requestAnimationFrame
69
- window.addEventListener('scroll', function() {
90
+ function onScroll() {
70
91
  if (!ticking) {
71
92
  window.requestAnimationFrame(handleScroll);
72
93
  ticking = true;
73
94
  }
74
- }, { passive: true });
95
+ }
96
+ window.addEventListener('scroll', onScroll, { passive: true });
75
97
 
76
- // Disable animations if user prefers reduced motion
77
- if (prefersReducedMotion) {
98
+ // Apply smooth transition (unless user prefers reduced motion)
99
+ if (!prefersReducedMotion) {
100
+ navbar.style.transition = 'transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s ease';
101
+ } else {
78
102
  navbar.style.transition = 'none';
79
103
  }
104
+
105
+ // Add CSS for the hidden state if not already present
106
+ if (!document.getElementById('navbar-autohide-styles')) {
107
+ const style = document.createElement('style');
108
+ style.id = 'navbar-autohide-styles';
109
+ style.textContent = `
110
+ #navbar.navbar-hidden {
111
+ transform: translateY(-100%);
112
+ box-shadow: none;
113
+ }
114
+ `;
115
+ document.head.appendChild(style);
116
+ }
117
+
118
+ // Pause auto-hide when offcanvas is open so fixed positioning works
119
+ const offcanvasEl = document.getElementById('bdNavbar');
120
+ if (offcanvasEl) {
121
+ offcanvasEl.addEventListener('show.bs.offcanvas', function() {
122
+ navbar.classList.remove('navbar-hidden');
123
+ isNavbarHidden = false;
124
+ window.removeEventListener('scroll', onScroll);
125
+ });
126
+ offcanvasEl.addEventListener('hidden.bs.offcanvas', function() {
127
+ lastScrollTop = window.pageYOffset || document.documentElement.scrollTop;
128
+ window.addEventListener('scroll', onScroll, { passive: true });
129
+ });
130
+ }
80
131
  });
81
132
  })();
@@ -39,7 +39,12 @@
39
39
  const themeSwitcherText = document.querySelector('#bd-theme-text')
40
40
  const activeThemeIcon = document.querySelector('.theme-icon-active use')
41
41
  const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`)
42
- const svgOfActiveBtn = btnToActive.querySelector('svg use').getAttribute('href')
42
+ const themeIconUse = btnToActive?.querySelector('svg use')
43
+ if (!btnToActive || !themeIconUse || !activeThemeIcon) {
44
+ return
45
+ }
46
+
47
+ const svgOfActiveBtn = themeIconUse.getAttribute('href')
43
48
 
44
49
  document.querySelectorAll('[data-bs-theme-value]').forEach(element => {
45
50
  element.classList.remove('active')
@@ -49,7 +54,8 @@
49
54
  btnToActive.classList.add('active')
50
55
  btnToActive.setAttribute('aria-pressed', 'true')
51
56
  activeThemeIcon.setAttribute('href', svgOfActiveBtn)
52
- const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.dataset.bsThemeValue})`
57
+ const baseLabel = themeSwitcherText?.textContent?.trim() || 'Toggle theme'
58
+ const themeSwitcherLabel = `${baseLabel} (${btnToActive.dataset.bsThemeValue})`
53
59
  themeSwitcher.setAttribute('aria-label', themeSwitcherLabel)
54
60
 
55
61
  if (focus) {
@@ -39,7 +39,12 @@
39
39
  const themeSwitcherText = document.querySelector('#bd-theme-text')
40
40
  const activeThemeIcon = document.querySelector('.theme-icon-active use')
41
41
  const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`)
42
- const svgOfActiveBtn = btnToActive.querySelector('svg use').getAttribute('href')
42
+ const themeIconUse = btnToActive?.querySelector('svg use')
43
+ if (!btnToActive || !themeIconUse || !activeThemeIcon) {
44
+ return
45
+ }
46
+
47
+ const svgOfActiveBtn = themeIconUse.getAttribute('href')
43
48
 
44
49
  document.querySelectorAll('[data-bs-theme-value]').forEach(element => {
45
50
  element.classList.remove('active')
@@ -49,7 +54,8 @@
49
54
  btnToActive.classList.add('active')
50
55
  btnToActive.setAttribute('aria-pressed', 'true')
51
56
  activeThemeIcon.setAttribute('href', svgOfActiveBtn)
52
- const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.dataset.bsThemeValue})`
57
+ const baseLabel = themeSwitcherText?.textContent?.trim() || 'Toggle theme'
58
+ const themeSwitcherLabel = `${baseLabel} (${btnToActive.dataset.bsThemeValue})`
53
59
  themeSwitcher.setAttribute('aria-label', themeSwitcherLabel)
54
60
 
55
61
  if (focus) {
@@ -1,12 +1,4 @@
1
- function myFunction() {
2
- myVar = 1;
3
- myVar = myVar + 1;
4
- document.getElementById("demo").innerHTML = myVar;
5
- }
6
-
7
- // Updates each img tag with the class img-fluid
8
-
9
- var imgs = document.getElementsByTagName('img');
10
- for (var i = 0; i < imgs.length; i++) {
11
- imgs[i].classList.add('img-fluid');
12
- }
1
+ /**
2
+ * Legacy asset path — not used by the current theme (see _includes/core/head.html).
3
+ * Kept so older builds, cached HTML, or bookmarks that still reference this URL do not 404.
4
+ */
@@ -3,8 +3,8 @@
3
3
  * NAVIGATION SCRIPTS - Zer0-Mistakes Theme
4
4
  * ==============================================================================
5
5
  *
6
- * Handles offcanvas navigation, dropdowns, and mobile interactions
7
- * Extracted from navbar.html inline scripts
6
+ * Handles offcanvas navigation, dropdowns, mobile interactions, and accessibility
7
+ * Enhanced for better UX across all device sizes
8
8
  * ==============================================================================
9
9
  */
10
10
 
@@ -12,11 +12,16 @@
12
12
  'use strict';
13
13
 
14
14
  const MOBILE_BREAKPOINT = 992;
15
+ const TOOLTIP_DELAY = { show: 400, hide: 100 }; // Increased show delay for better UX
15
16
 
16
17
  function isMobile() {
17
18
  return window.innerWidth < MOBILE_BREAKPOINT;
18
19
  }
19
20
 
21
+ function isCompactDesktop() {
22
+ return window.innerWidth >= 992 && window.innerWidth < 1200;
23
+ }
24
+
20
25
  /**
21
26
  * Initialize navigation when DOM is ready
22
27
  */
@@ -26,6 +31,9 @@
26
31
  setupMobileDropdowns();
27
32
  setupOutsideClickClose();
28
33
  setupOffcanvasReset();
34
+ setupNavTooltips();
35
+ setupDropdownHoverDelay();
36
+ setupFocusTrap();
29
37
  }
30
38
 
31
39
  /**
@@ -52,7 +60,7 @@
52
60
  }
53
61
 
54
62
  /**
55
- * Desktop keyboard accessibility for hover dropdowns
63
+ * Enhanced keyboard accessibility for hover dropdowns
56
64
  */
57
65
  function setupKeyboardAccessibility() {
58
66
  const dropdowns = document.querySelectorAll('.nav-hover-dropdown');
@@ -67,6 +75,7 @@
67
75
  toggle.addEventListener('focus', () => {
68
76
  if (!isMobile()) {
69
77
  menu.classList.add('show');
78
+ toggle.setAttribute('aria-expanded', 'true');
70
79
  }
71
80
  });
72
81
 
@@ -74,33 +83,58 @@
74
83
  dropdown.addEventListener('focusout', (e) => {
75
84
  if (!dropdown.contains(e.relatedTarget)) {
76
85
  menu.classList.remove('show');
86
+ toggle.setAttribute('aria-expanded', 'false');
77
87
  }
78
88
  });
79
89
 
80
- // Arrow key navigation
90
+ // Enhanced arrow key navigation
81
91
  dropdown.addEventListener('keydown', (e) => {
82
- if (!menu.classList.contains('show')) return;
92
+ if (!menu.classList.contains('show')) {
93
+ // Open dropdown with Enter or Space
94
+ if ((e.key === 'Enter' || e.key === ' ') && e.target === toggle) {
95
+ e.preventDefault();
96
+ menu.classList.add('show');
97
+ toggle.setAttribute('aria-expanded', 'true');
98
+ // Focus first item
99
+ const firstItem = menu.querySelector('.dropdown-item:not(:disabled)');
100
+ if (firstItem) firstItem.focus();
101
+ }
102
+ return;
103
+ }
83
104
 
84
105
  const items = menu.querySelectorAll('.dropdown-item:not(:disabled)');
85
106
  const currentIndex = Array.from(items).indexOf(document.activeElement);
86
107
 
87
108
  if (e.key === 'ArrowDown') {
88
109
  e.preventDefault();
89
- items[(currentIndex + 1) % items.length]?.focus();
110
+ const nextIndex = (currentIndex + 1) % items.length;
111
+ items[nextIndex]?.focus();
90
112
  } else if (e.key === 'ArrowUp') {
91
113
  e.preventDefault();
92
- items[(currentIndex - 1 + items.length) % items.length]?.focus();
114
+ const prevIndex = (currentIndex - 1 + items.length) % items.length;
115
+ items[prevIndex]?.focus();
116
+ } else if (e.key === 'Home') {
117
+ e.preventDefault();
118
+ items[0]?.focus();
119
+ } else if (e.key === 'End') {
120
+ e.preventDefault();
121
+ items[items.length - 1]?.focus();
93
122
  } else if (e.key === 'Escape') {
94
123
  e.preventDefault();
95
124
  menu.classList.remove('show');
125
+ toggle.setAttribute('aria-expanded', 'false');
96
126
  toggle.focus();
127
+ } else if (e.key === 'Tab') {
128
+ // Close dropdown on Tab
129
+ menu.classList.remove('show');
130
+ toggle.setAttribute('aria-expanded', 'false');
97
131
  }
98
132
  });
99
133
  });
100
134
  }
101
135
 
102
136
  /**
103
- * Mobile dropdown toggle handling
137
+ * Mobile dropdown toggle handling with smooth animations
104
138
  */
105
139
  function setupMobileDropdowns() {
106
140
  const dropdowns = document.querySelectorAll('.nav-hover-dropdown');
@@ -119,6 +153,18 @@
119
153
 
120
154
  const isOpen = menu.classList.contains('show');
121
155
 
156
+ // Close all other dropdowns first
157
+ document.querySelectorAll('.nav-hover-dropdown .dropdown-menu.show').forEach(otherMenu => {
158
+ if (otherMenu !== menu) {
159
+ otherMenu.classList.remove('show');
160
+ const otherToggle = otherMenu.closest('.nav-hover-dropdown')?.querySelector('.dropdown-toggle-split');
161
+ if (otherToggle) {
162
+ otherToggle.classList.remove('show');
163
+ otherToggle.setAttribute('aria-expanded', 'false');
164
+ }
165
+ }
166
+ });
167
+
122
168
  if (isOpen) {
123
169
  menu.classList.remove('show');
124
170
  toggle.classList.remove('show');
@@ -127,6 +173,11 @@
127
173
  menu.classList.add('show');
128
174
  toggle.classList.add('show');
129
175
  toggle.setAttribute('aria-expanded', 'true');
176
+
177
+ // Smooth scroll to show the opened menu
178
+ setTimeout(() => {
179
+ toggle.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
180
+ }, 100);
130
181
  }
131
182
  });
132
183
  });
@@ -179,28 +230,132 @@
179
230
  * Shows link title on hover when labels are hidden (992px-1199px)
180
231
  */
181
232
  function setupNavTooltips() {
233
+ if (typeof bootstrap === 'undefined' || !bootstrap.Tooltip) return;
234
+
182
235
  const navLinks = document.querySelectorAll('#bdNavbar .nav-link[title]');
236
+ const tooltips = [];
237
+
183
238
  navLinks.forEach(link => {
184
- // Only initialize tooltip if Bootstrap is available
185
- if (typeof bootstrap !== 'undefined' && bootstrap.Tooltip) {
186
- new bootstrap.Tooltip(link, {
187
- trigger: 'hover',
188
- placement: 'bottom',
189
- delay: { show: 300, hide: 0 }
239
+ const tooltip = new bootstrap.Tooltip(link, {
240
+ // Only trigger tooltip in compact desktop (992-1199px) where text labels are hidden
241
+ trigger: 'manual',
242
+ placement: 'bottom',
243
+ delay: TOOLTIP_DELAY,
244
+ boundary: 'window',
245
+ fallbackPlacements: ['top', 'bottom'],
246
+ customClass: 'nav-tooltip'
247
+ });
248
+ tooltips.push(tooltip);
249
+
250
+ // Manually show/hide based on viewport
251
+ link.addEventListener('mouseenter', () => {
252
+ if (isCompactDesktop()) tooltip.show();
253
+ });
254
+ link.addEventListener('mouseleave', () => tooltip.hide());
255
+ link.addEventListener('focus', () => {
256
+ if (isCompactDesktop()) tooltip.show();
257
+ });
258
+ link.addEventListener('blur', () => tooltip.hide());
259
+ });
260
+
261
+ // Update tooltip state on window resize
262
+ let resizeTimeout;
263
+ window.addEventListener('resize', () => {
264
+ clearTimeout(resizeTimeout);
265
+ resizeTimeout = setTimeout(() => {
266
+ tooltips.forEach(tooltip => {
267
+ // Hide tooltips if not in compact desktop view
268
+ if (!isCompactDesktop()) {
269
+ tooltip.hide();
270
+ }
190
271
  });
272
+ }, 150);
273
+ });
274
+ }
275
+
276
+ /**
277
+ * Add slight delay to dropdown hover on desktop
278
+ * Prevents accidental opening when moving cursor across menu
279
+ */
280
+ function setupDropdownHoverDelay() {
281
+ if (isMobile()) return;
282
+
283
+ const dropdowns = document.querySelectorAll('.nav-hover-dropdown');
284
+ const hoverDelay = 150; // ms
285
+
286
+ dropdowns.forEach(dropdown => {
287
+ let hoverTimeout;
288
+
289
+ dropdown.addEventListener('mouseenter', () => {
290
+ hoverTimeout = setTimeout(() => {
291
+ const menu = dropdown.querySelector('.dropdown-menu');
292
+ if (menu && !isMobile()) {
293
+ menu.classList.add('show');
294
+ }
295
+ }, hoverDelay);
296
+ });
297
+
298
+ dropdown.addEventListener('mouseleave', () => {
299
+ clearTimeout(hoverTimeout);
300
+ const menu = dropdown.querySelector('.dropdown-menu');
301
+ if (menu && !isMobile()) {
302
+ menu.classList.remove('show');
303
+ }
304
+ });
305
+ });
306
+ }
307
+
308
+ /**
309
+ * Focus trap for offcanvas on mobile
310
+ * Keeps focus within the menu for better accessibility
311
+ */
312
+ function setupFocusTrap() {
313
+ const offcanvasEl = document.getElementById('bdNavbar');
314
+ if (!offcanvasEl) return;
315
+
316
+ offcanvasEl.addEventListener('shown.bs.offcanvas', () => {
317
+ // Focus first focusable element
318
+ const firstFocusable = offcanvasEl.querySelector(
319
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
320
+ );
321
+ if (firstFocusable) {
322
+ firstFocusable.focus();
191
323
  }
192
324
  });
193
325
  }
194
326
 
195
327
  // Initialize
196
328
  if (document.readyState === 'loading') {
197
- document.addEventListener('DOMContentLoaded', () => {
198
- initNavigation();
199
- setupNavTooltips();
200
- });
329
+ document.addEventListener('DOMContentLoaded', initNavigation);
201
330
  } else {
202
331
  initNavigation();
203
- setupNavTooltips();
204
332
  }
205
333
 
334
+ // Re-initialize on window resize for responsive behavior
335
+ let resizeTimer;
336
+ window.addEventListener('resize', () => {
337
+ clearTimeout(resizeTimer);
338
+ resizeTimer = setTimeout(() => {
339
+ // Update mobile dropdown behavior
340
+ const dropdowns = document.querySelectorAll('.nav-hover-dropdown .dropdown-menu');
341
+ dropdowns.forEach(menu => {
342
+ if (!isMobile()) {
343
+ // Close the dropdown menu
344
+ menu.classList.remove('show');
345
+
346
+ // Also reset the corresponding toggle state for accessibility
347
+ const dropdown = menu.closest('.dropdown');
348
+ const toggle = dropdown
349
+ ? dropdown.querySelector('[data-bs-toggle="dropdown"], .dropdown-toggle')
350
+ : null;
351
+
352
+ if (toggle) {
353
+ toggle.classList.remove('show');
354
+ toggle.setAttribute('aria-expanded', 'false');
355
+ }
356
+ }
357
+ });
358
+ }, 250);
359
+ });
360
+
206
361
  })();
@@ -2,10 +2,33 @@
2
2
  * Search Modal Controller
3
3
  * - Opens modal on navigation:searchRequest event ("/" shortcut)
4
4
  * - Focuses search input on open
5
+ * - Mutually exclusive with Settings (#info-section) and cookie settings modal so Bootstrap
6
+ * does not stack multiple .modal-backdrop layers (search vs About/Settings conflict).
5
7
  */
6
8
  (function() {
7
9
  'use strict';
8
10
 
11
+ /**
12
+ * If modal is visible, hide it and run next() on hidden.bs.modal; else run next() now.
13
+ */
14
+ function afterModalClosed(modalEl, next) {
15
+ if (!modalEl || typeof bootstrap === 'undefined') {
16
+ next();
17
+ return;
18
+ }
19
+ if (!modalEl.classList.contains('show')) {
20
+ next();
21
+ return;
22
+ }
23
+ const inst = bootstrap.Modal.getInstance(modalEl);
24
+ if (!inst) {
25
+ next();
26
+ return;
27
+ }
28
+ modalEl.addEventListener('hidden.bs.modal', next, { once: true });
29
+ inst.hide();
30
+ }
31
+
9
32
  function initSearchModal() {
10
33
  const modalEl = document.getElementById('siteSearchModal');
11
34
  if (!modalEl) return;
@@ -19,16 +42,36 @@
19
42
  let searchIndexPromise = null;
20
43
  let searchTimeout = null;
21
44
 
22
- const openModal = () => {
23
- const modalInstance = typeof bootstrap !== 'undefined'
24
- ? bootstrap.Modal.getOrCreateInstance(modalEl)
25
- : null;
45
+ const showSearchModal = () => {
46
+ if (typeof bootstrap === 'undefined') return;
47
+ bootstrap.Modal.getOrCreateInstance(modalEl).show();
48
+ };
26
49
 
27
- if (modalInstance) {
28
- modalInstance.show();
29
- }
50
+ const openModal = () => {
51
+ if (typeof bootstrap === 'undefined') return;
52
+ const cookieEl = document.getElementById('cookieSettingsModal');
53
+ const infoEl = document.getElementById('info-section');
54
+ afterModalClosed(cookieEl, () => {
55
+ afterModalClosed(infoEl, showSearchModal);
56
+ });
30
57
  };
31
58
 
59
+ const infoSectionEl = document.getElementById('info-section');
60
+ if (infoSectionEl) {
61
+ infoSectionEl.addEventListener(
62
+ 'show.bs.modal',
63
+ (e) => {
64
+ if (!modalEl.classList.contains('show')) return;
65
+ e.preventDefault();
66
+ e.stopImmediatePropagation();
67
+ afterModalClosed(modalEl, () => {
68
+ bootstrap.Modal.getOrCreateInstance(infoSectionEl).show();
69
+ });
70
+ },
71
+ true,
72
+ );
73
+ }
74
+
32
75
  // Open modal when keyboard shortcut requests search
33
76
  document.addEventListener('navigation:searchRequest', openModal);
34
77