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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +54 -0
- data/README.md +86 -46
- data/_data/authors.yml +12 -3
- data/_data/features.yml +1 -1
- data/_data/glossary.yml +101 -0
- data/_data/navigation/docs.yml +12 -0
- data/_data/navigation/home.yml +2 -2
- data/_data/navigation/main.yml +2 -8
- data/_data/prompts.yml +184 -0
- data/_includes/components/author-eeat.html +133 -0
- data/_includes/components/cookie-consent.html +9 -9
- data/_includes/components/dev-shortcuts.html +36 -27
- data/_includes/components/env-detect.html +14 -0
- data/_includes/components/env-switcher.html +38 -16
- data/_includes/components/halfmoon.html +31 -20
- data/_includes/components/info-section.html +4 -3
- data/_includes/components/js-cdn.html +8 -15
- data/_includes/components/mermaid.html +13 -9
- data/_includes/components/powered-by.html +5 -3
- data/_includes/content/intro.html +64 -4
- data/_includes/content/jsonld-faq.html +47 -0
- data/_includes/content/jsonld-software.html +121 -0
- data/_includes/content/sitemap.html +2 -2
- data/_includes/core/branding.html +9 -7
- data/_includes/core/footer.html +12 -9
- data/_includes/core/head.html +17 -14
- data/_includes/core/header.html +33 -21
- data/_includes/navigation/navbar.html +130 -124
- data/_includes/navigation/sidebar-left.html +3 -3
- data/_includes/navigation/sidebar-right.html +4 -8
- data/_includes/search-data.json +1 -2
- data/_layouts/landing.html +8 -3
- data/_layouts/root.html +4 -4
- data/_layouts/sitemap-collection.html +20 -10
- data/_sass/core/_docs-layout.scss +756 -0
- data/_sass/core/_navbar.scss +522 -69
- data/_sass/core/_offcanvas-panels.scss +48 -0
- data/_sass/core/_syntax.scss +1 -51
- data/_sass/core/_theme.scss +2 -249
- data/_sass/core/_variables.scss +1 -54
- data/_sass/core/code-copy.scss +6 -6
- data/_sass/custom.scss +119 -133
- data/_sass/theme/_color-modes.scss +3 -0
- data/_sass/theme/_css-variables.scss +29 -0
- data/_sass/theme/_wizard-mode.scss +31 -0
- data/assets/css/custom.css +5 -120
- data/assets/css/main.scss +6 -2
- data/assets/css/stats.css +3 -0
- data/assets/css/theme-npm-entry.scss +6 -0
- data/assets/css/vendor/.gitkeep +0 -0
- data/assets/images/authors/bamr87.png +0 -0
- data/assets/js/auto-hide-nav.js +71 -20
- data/assets/js/color-modes.js +8 -2
- data/assets/js/halfmoon.js +8 -2
- data/assets/js/myScript.js +4 -12
- data/assets/js/navigation.js +174 -19
- data/assets/js/search-modal.js +50 -7
- data/assets/vendor/bootstrap/css/bootstrap.min.css +5 -0
- data/assets/vendor/bootstrap/js/bootstrap.bundle.min.js +6 -0
- data/assets/vendor/bootstrap-icons/font/bootstrap-icons.css +2018 -0
- data/assets/vendor/bootstrap-icons/font/fonts/bootstrap-icons.woff +0 -0
- data/assets/vendor/bootstrap-icons/font/fonts/bootstrap-icons.woff2 +0 -0
- data/assets/vendor/font-awesome/css/all.min.css +9 -0
- data/assets/vendor/font-awesome/webfonts/fa-brands-400.ttf +0 -0
- data/assets/vendor/font-awesome/webfonts/fa-brands-400.woff2 +0 -0
- data/assets/vendor/font-awesome/webfonts/fa-regular-400.ttf +0 -0
- data/assets/vendor/font-awesome/webfonts/fa-regular-400.woff2 +0 -0
- data/assets/vendor/font-awesome/webfonts/fa-solid-900.ttf +0 -0
- data/assets/vendor/font-awesome/webfonts/fa-solid-900.woff2 +0 -0
- data/assets/vendor/font-awesome/webfonts/fa-v4compatibility.ttf +0 -0
- data/assets/vendor/font-awesome/webfonts/fa-v4compatibility.woff2 +0 -0
- data/assets/vendor/github-calendar/github-calendar-responsive.css +231 -0
- data/assets/vendor/github-calendar/github-calendar.min.js +240 -0
- data/assets/vendor/jquery/jquery-3.7.1.min.js +2 -0
- data/assets/vendor/mathjax/es5/adaptors/liteDOM.js +1 -0
- data/assets/vendor/mathjax/es5/core.js +1 -0
- data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_AMS-Regular.woff +0 -0
- data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Calligraphic-Bold.woff +0 -0
- data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Calligraphic-Regular.woff +0 -0
- data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Fraktur-Bold.woff +0 -0
- data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Fraktur-Regular.woff +0 -0
- data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Main-Bold.woff +0 -0
- data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Main-Italic.woff +0 -0
- data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Main-Regular.woff +0 -0
- data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Math-BoldItalic.woff +0 -0
- data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Math-Italic.woff +0 -0
- data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Math-Regular.woff +0 -0
- data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_SansSerif-Bold.woff +0 -0
- data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_SansSerif-Italic.woff +0 -0
- data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_SansSerif-Regular.woff +0 -0
- data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Script-Regular.woff +0 -0
- data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Size1-Regular.woff +0 -0
- data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Size2-Regular.woff +0 -0
- data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Size3-Regular.woff +0 -0
- data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Size4-Regular.woff +0 -0
- data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Typewriter-Regular.woff +0 -0
- data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Vector-Bold.woff +0 -0
- data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Vector-Regular.woff +0 -0
- data/assets/vendor/mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Zero.woff +0 -0
- data/assets/vendor/mathjax/es5/startup.js +1 -0
- data/assets/vendor/mathjax/es5/tex-mml-chtml.js +1 -0
- data/assets/vendor/mermaid/mermaid.min.js +2029 -0
- data/scripts/bin/build +12 -2
- data/scripts/lib/version.sh +41 -0
- data/scripts/test/integration/mermaid +1 -1
- data/scripts/vendor-install.sh +196 -0
- metadata +62 -3
- data/_sass/core/_docs.scss +0 -3219
data/assets/js/auto-hide-nav.js
CHANGED
|
@@ -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 (
|
|
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 =
|
|
16
|
-
const SCROLL_DELTA =
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
//
|
|
60
|
-
if (scrollTop
|
|
61
|
-
|
|
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
|
-
|
|
90
|
+
function onScroll() {
|
|
70
91
|
if (!ticking) {
|
|
71
92
|
window.requestAnimationFrame(handleScroll);
|
|
72
93
|
ticking = true;
|
|
73
94
|
}
|
|
74
|
-
}
|
|
95
|
+
}
|
|
96
|
+
window.addEventListener('scroll', onScroll, { passive: true });
|
|
75
97
|
|
|
76
|
-
//
|
|
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
|
})();
|
data/assets/js/color-modes.js
CHANGED
|
@@ -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
|
|
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
|
|
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) {
|
data/assets/js/halfmoon.js
CHANGED
|
@@ -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
|
|
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
|
|
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) {
|
data/assets/js/myScript.js
CHANGED
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
+
*/
|
data/assets/js/navigation.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* NAVIGATION SCRIPTS - Zer0-Mistakes Theme
|
|
4
4
|
* ==============================================================================
|
|
5
5
|
*
|
|
6
|
-
* Handles offcanvas navigation, dropdowns,
|
|
7
|
-
*
|
|
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
|
-
*
|
|
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
|
-
//
|
|
90
|
+
// Enhanced arrow key navigation
|
|
81
91
|
dropdown.addEventListener('keydown', (e) => {
|
|
82
|
-
if (!menu.classList.contains('show'))
|
|
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
|
-
|
|
110
|
+
const nextIndex = (currentIndex + 1) % items.length;
|
|
111
|
+
items[nextIndex]?.focus();
|
|
90
112
|
} else if (e.key === 'ArrowUp') {
|
|
91
113
|
e.preventDefault();
|
|
92
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
})();
|
data/assets/js/search-modal.js
CHANGED
|
@@ -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
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
45
|
+
const showSearchModal = () => {
|
|
46
|
+
if (typeof bootstrap === 'undefined') return;
|
|
47
|
+
bootstrap.Modal.getOrCreateInstance(modalEl).show();
|
|
48
|
+
};
|
|
26
49
|
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|