jekyll-theme-zer0 0.16.0 → 0.17.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.
@@ -0,0 +1,149 @@
1
+ /**
2
+ * ===================================================================
3
+ * NAVIGATION CONFIG - Shared Configuration for Navigation Modules
4
+ * ===================================================================
5
+ *
6
+ * File: config.js
7
+ * Path: assets/js/modules/navigation/config.js
8
+ * Purpose: Centralized configuration for all navigation modules
9
+ *
10
+ * Usage:
11
+ * import { config } from './config.js';
12
+ * const toc = document.querySelector(config.selectors.toc);
13
+ *
14
+ * ===================================================================
15
+ */
16
+
17
+ export const config = {
18
+ // ===================================================================
19
+ // DOM SELECTORS
20
+ // ===================================================================
21
+ selectors: {
22
+ // Table of Contents
23
+ toc: '#TableOfContents',
24
+ tocLinks: '#TableOfContents a',
25
+ tocContainer: '.bd-toc .offcanvas-body',
26
+
27
+ // Sidebars
28
+ leftSidebar: '#bdSidebar',
29
+ rightSidebar: '#tocContents',
30
+
31
+ // Content areas
32
+ mainContent: '.bd-content',
33
+
34
+ // Navigation elements
35
+ navTree: '.nav-tree',
36
+ navTreeToggle: '[data-nav-toggle]',
37
+
38
+ // Offcanvas
39
+ offcanvas: '.offcanvas'
40
+ },
41
+
42
+ // ===================================================================
43
+ // SCROLL SPY SETTINGS
44
+ // ===================================================================
45
+ scrollSpy: {
46
+ // Root margin for Intersection Observer
47
+ // Negative values account for fixed headers
48
+ rootMargin: '-80px 0px -80px 0px',
49
+
50
+ // Intersection threshold levels
51
+ threshold: [0, 0.25, 0.5, 0.75, 1]
52
+ },
53
+
54
+ // ===================================================================
55
+ // SMOOTH SCROLL SETTINGS
56
+ // ===================================================================
57
+ smoothScroll: {
58
+ // Offset for fixed headers when scrolling to anchor
59
+ offset: 80,
60
+
61
+ // Scroll behavior
62
+ behavior: 'smooth'
63
+ },
64
+
65
+ // ===================================================================
66
+ // KEYBOARD SHORTCUTS
67
+ // ===================================================================
68
+ keyboard: {
69
+ // Enable/disable keyboard shortcuts
70
+ enabled: true,
71
+
72
+ // Key mappings
73
+ keys: {
74
+ previousSection: '[',
75
+ nextSection: ']',
76
+ search: '/',
77
+ toggleSidebar: 'b',
78
+ toggleToc: 't'
79
+ }
80
+ },
81
+
82
+ // ===================================================================
83
+ // SWIPE GESTURES
84
+ // ===================================================================
85
+ gestures: {
86
+ // Enable/disable swipe gestures
87
+ enabled: true,
88
+
89
+ // Minimum distance (px) for swipe to register
90
+ threshold: 50,
91
+
92
+ // Edge detection zone (px) for sidebar swipes
93
+ edgeZone: 50
94
+ },
95
+
96
+ // ===================================================================
97
+ // SIDEBAR STATE PERSISTENCE
98
+ // ===================================================================
99
+ state: {
100
+ // localStorage key prefix
101
+ storagePrefix: 'zer0-nav-',
102
+
103
+ // Keys for different state values
104
+ keys: {
105
+ expandedNodes: 'expanded-nodes',
106
+ sidebarOpen: 'sidebar-open',
107
+ tocOpen: 'toc-open'
108
+ }
109
+ },
110
+
111
+ // ===================================================================
112
+ // DEBOUNCE/THROTTLE TIMINGS
113
+ // ===================================================================
114
+ timing: {
115
+ debounceDelay: 100,
116
+ scrollDebounce: 50
117
+ },
118
+
119
+ // ===================================================================
120
+ // BREAKPOINTS (match Bootstrap 5)
121
+ // ===================================================================
122
+ breakpoints: {
123
+ sm: 576,
124
+ md: 768,
125
+ lg: 992,
126
+ xl: 1200,
127
+ xxl: 1400
128
+ }
129
+ };
130
+
131
+ /**
132
+ * Check if viewport is below a breakpoint
133
+ * @param {string} breakpoint - Breakpoint name (sm, md, lg, xl, xxl)
134
+ * @returns {boolean}
135
+ */
136
+ export function isBelowBreakpoint(breakpoint) {
137
+ return window.innerWidth < config.breakpoints[breakpoint];
138
+ }
139
+
140
+ /**
141
+ * Check if viewport is at or above a breakpoint
142
+ * @param {string} breakpoint - Breakpoint name (sm, md, lg, xl, xxl)
143
+ * @returns {boolean}
144
+ */
145
+ export function isAtOrAboveBreakpoint(breakpoint) {
146
+ return window.innerWidth >= config.breakpoints[breakpoint];
147
+ }
148
+
149
+ export default config;
@@ -0,0 +1,189 @@
1
+ /**
2
+ * ===================================================================
3
+ * FOCUS MANAGER - Accessibility Enhancements
4
+ * ===================================================================
5
+ *
6
+ * File: focus.js
7
+ * Path: assets/js/modules/navigation/focus.js
8
+ * Purpose: Focus management for accessibility in navigation components
9
+ *
10
+ * Features:
11
+ * - Returns focus to trigger element when offcanvas closes
12
+ * - Manages focus trap in modal/offcanvas contexts
13
+ * - Provides focus ring styling hooks
14
+ *
15
+ * Usage:
16
+ * import { FocusManager } from './focus.js';
17
+ * const focus = new FocusManager();
18
+ *
19
+ * ===================================================================
20
+ */
21
+
22
+ import { config } from './config.js';
23
+
24
+ /**
25
+ * Get all elements safely
26
+ * @param {string} selector - CSS selector
27
+ * @returns {NodeList}
28
+ */
29
+ function getElements(selector) {
30
+ try {
31
+ return document.querySelectorAll(selector);
32
+ } catch (error) {
33
+ return [];
34
+ }
35
+ }
36
+
37
+ export class FocusManager {
38
+ constructor() {
39
+ this._boundHandleHidden = this._handleOffcanvasHidden.bind(this);
40
+ this._init();
41
+ }
42
+
43
+ /**
44
+ * Initialize focus management
45
+ * @private
46
+ */
47
+ _init() {
48
+ // Handle focus return when offcanvas closes
49
+ const offcanvasElements = getElements(config.selectors.offcanvas);
50
+
51
+ offcanvasElements.forEach(offcanvas => {
52
+ offcanvas.addEventListener('hidden.bs.offcanvas', this._boundHandleHidden);
53
+ });
54
+
55
+ // Add focus-visible polyfill behavior
56
+ this._setupFocusVisible();
57
+
58
+ console.log(`FocusManager: Initialized with ${offcanvasElements.length} offcanvas elements`);
59
+ }
60
+
61
+ /**
62
+ * Handle offcanvas hidden event - return focus to trigger
63
+ * @private
64
+ * @param {Event} event
65
+ */
66
+ _handleOffcanvasHidden(event) {
67
+ const offcanvas = event.target;
68
+ this.returnFocus(offcanvas);
69
+ }
70
+
71
+ /**
72
+ * Return focus to the trigger element that opened an offcanvas
73
+ * @param {Element} offcanvas - The offcanvas element
74
+ */
75
+ returnFocus(offcanvas) {
76
+ const triggerId = offcanvas.id;
77
+
78
+ // Find the trigger button that opened this offcanvas
79
+ const trigger = document.querySelector(
80
+ `[data-bs-target="#${triggerId}"], [href="#${triggerId}"]`
81
+ );
82
+
83
+ if (trigger) {
84
+ // Small delay to ensure offcanvas animation completes
85
+ requestAnimationFrame(() => {
86
+ trigger.focus();
87
+ });
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Setup focus-visible behavior for keyboard users
93
+ * @private
94
+ */
95
+ _setupFocusVisible() {
96
+ // Add class to body when user is navigating with keyboard
97
+ let hadKeyboardEvent = false;
98
+
99
+ document.addEventListener('keydown', (e) => {
100
+ if (e.key === 'Tab') {
101
+ hadKeyboardEvent = true;
102
+ document.body.classList.add('keyboard-nav');
103
+ }
104
+ });
105
+
106
+ document.addEventListener('mousedown', () => {
107
+ hadKeyboardEvent = false;
108
+ document.body.classList.remove('keyboard-nav');
109
+ });
110
+ }
111
+
112
+ /**
113
+ * Focus the first focusable element within a container
114
+ * @param {Element} container
115
+ */
116
+ focusFirst(container) {
117
+ const focusable = container.querySelector(
118
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
119
+ );
120
+ if (focusable) {
121
+ focusable.focus();
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Focus the last focusable element within a container
127
+ * @param {Element} container
128
+ */
129
+ focusLast(container) {
130
+ const focusables = container.querySelectorAll(
131
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
132
+ );
133
+ if (focusables.length > 0) {
134
+ focusables[focusables.length - 1].focus();
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Trap focus within a container (for modals/offcanvas)
140
+ * @param {Element} container
141
+ * @returns {Function} Cleanup function to remove trap
142
+ */
143
+ trapFocus(container) {
144
+ const focusableElements = container.querySelectorAll(
145
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
146
+ );
147
+
148
+ const firstFocusable = focusableElements[0];
149
+ const lastFocusable = focusableElements[focusableElements.length - 1];
150
+
151
+ const handleKeydown = (e) => {
152
+ if (e.key !== 'Tab') return;
153
+
154
+ if (e.shiftKey) {
155
+ // Shift + Tab
156
+ if (document.activeElement === firstFocusable) {
157
+ e.preventDefault();
158
+ lastFocusable.focus();
159
+ }
160
+ } else {
161
+ // Tab
162
+ if (document.activeElement === lastFocusable) {
163
+ e.preventDefault();
164
+ firstFocusable.focus();
165
+ }
166
+ }
167
+ };
168
+
169
+ container.addEventListener('keydown', handleKeydown);
170
+
171
+ // Return cleanup function
172
+ return () => {
173
+ container.removeEventListener('keydown', handleKeydown);
174
+ };
175
+ }
176
+
177
+ /**
178
+ * Cleanup event listeners
179
+ */
180
+ destroy() {
181
+ const offcanvasElements = getElements(config.selectors.offcanvas);
182
+ offcanvasElements.forEach(offcanvas => {
183
+ offcanvas.removeEventListener('hidden.bs.offcanvas', this._boundHandleHidden);
184
+ });
185
+ console.log('FocusManager: Destroyed');
186
+ }
187
+ }
188
+
189
+ export default FocusManager;
@@ -0,0 +1,179 @@
1
+ /**
2
+ * ===================================================================
3
+ * SWIPE GESTURES - Mobile Offcanvas Control
4
+ * ===================================================================
5
+ *
6
+ * File: gestures.js
7
+ * Path: assets/js/modules/navigation/gestures.js
8
+ * Purpose: Touch gesture support for sidebar navigation on mobile
9
+ *
10
+ * Features:
11
+ * - Swipe from left edge to open left sidebar
12
+ * - Swipe from right edge to open TOC sidebar
13
+ * - Configurable threshold and edge zones
14
+ *
15
+ * Usage:
16
+ * import { SwipeGestures } from './gestures.js';
17
+ * const gestures = new SwipeGestures();
18
+ *
19
+ * ===================================================================
20
+ */
21
+
22
+ import { config, isBelowBreakpoint } from './config.js';
23
+
24
+ export class SwipeGestures {
25
+ constructor() {
26
+ if (!config.gestures.enabled) {
27
+ console.log('SwipeGestures: Disabled via config');
28
+ return;
29
+ }
30
+
31
+ this.startX = 0;
32
+ this.startY = 0;
33
+ this.distX = 0;
34
+ this.distY = 0;
35
+
36
+ // Bound handlers for cleanup
37
+ this._boundTouchStart = this._handleTouchStart.bind(this);
38
+ this._boundTouchMove = this._handleTouchMove.bind(this);
39
+ this._boundTouchEnd = this._handleTouchEnd.bind(this);
40
+
41
+ this._init();
42
+ }
43
+
44
+ /**
45
+ * Initialize touch event listeners
46
+ * @private
47
+ */
48
+ _init() {
49
+ document.addEventListener('touchstart', this._boundTouchStart, { passive: true });
50
+ document.addEventListener('touchmove', this._boundTouchMove, { passive: true });
51
+ document.addEventListener('touchend', this._boundTouchEnd);
52
+
53
+ console.log('SwipeGestures: Initialized');
54
+ }
55
+
56
+ /**
57
+ * Handle touch start
58
+ * @private
59
+ * @param {TouchEvent} event
60
+ */
61
+ _handleTouchStart(event) {
62
+ const touch = event.touches[0];
63
+ this.startX = touch.clientX;
64
+ this.startY = touch.clientY;
65
+ this.distX = 0;
66
+ this.distY = 0;
67
+ }
68
+
69
+ /**
70
+ * Handle touch move
71
+ * @private
72
+ * @param {TouchEvent} event
73
+ */
74
+ _handleTouchMove(event) {
75
+ if (!this.startX || !this.startY) return;
76
+
77
+ const touch = event.touches[0];
78
+ this.distX = touch.clientX - this.startX;
79
+ this.distY = touch.clientY - this.startY;
80
+ }
81
+
82
+ /**
83
+ * Handle touch end
84
+ * @private
85
+ * @param {TouchEvent} event
86
+ */
87
+ _handleTouchEnd(event) {
88
+ const { threshold, edgeZone } = config.gestures;
89
+
90
+ // Check if swipe distance meets threshold
91
+ if (Math.abs(this.distX) < threshold) {
92
+ this._reset();
93
+ return;
94
+ }
95
+
96
+ // Only handle horizontal swipes (not vertical scroll)
97
+ if (Math.abs(this.distX) > Math.abs(this.distY)) {
98
+ if (this.distX > 0) {
99
+ this._handleSwipeRight();
100
+ } else {
101
+ this._handleSwipeLeft();
102
+ }
103
+ }
104
+
105
+ this._reset();
106
+ }
107
+
108
+ /**
109
+ * Handle swipe right (open left sidebar)
110
+ * @private
111
+ */
112
+ _handleSwipeRight() {
113
+ const { edgeZone } = config.gestures;
114
+
115
+ // Only if swipe started from left edge
116
+ if (this.startX > edgeZone) return;
117
+
118
+ // Only on mobile
119
+ if (!isBelowBreakpoint('lg')) return;
120
+
121
+ const leftSidebar = document.querySelector(config.selectors.leftSidebar);
122
+ if (leftSidebar && typeof bootstrap !== 'undefined') {
123
+ const bsOffcanvas = bootstrap.Offcanvas.getOrCreateInstance(leftSidebar);
124
+ bsOffcanvas.show();
125
+
126
+ document.dispatchEvent(new CustomEvent('navigation:swipe', {
127
+ detail: { direction: 'right', sidebar: 'left' }
128
+ }));
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Handle swipe left (open right/TOC sidebar)
134
+ * @private
135
+ */
136
+ _handleSwipeLeft() {
137
+ const { edgeZone } = config.gestures;
138
+ const windowWidth = window.innerWidth;
139
+
140
+ // Only if swipe started from right edge
141
+ if (this.startX < windowWidth - edgeZone) return;
142
+
143
+ // Only on mobile
144
+ if (!isBelowBreakpoint('lg')) return;
145
+
146
+ const rightSidebar = document.querySelector(config.selectors.rightSidebar);
147
+ if (rightSidebar && typeof bootstrap !== 'undefined') {
148
+ const bsOffcanvas = bootstrap.Offcanvas.getOrCreateInstance(rightSidebar);
149
+ bsOffcanvas.show();
150
+
151
+ document.dispatchEvent(new CustomEvent('navigation:swipe', {
152
+ detail: { direction: 'left', sidebar: 'toc' }
153
+ }));
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Reset touch tracking state
159
+ * @private
160
+ */
161
+ _reset() {
162
+ this.startX = 0;
163
+ this.startY = 0;
164
+ this.distX = 0;
165
+ this.distY = 0;
166
+ }
167
+
168
+ /**
169
+ * Cleanup event listeners
170
+ */
171
+ destroy() {
172
+ document.removeEventListener('touchstart', this._boundTouchStart);
173
+ document.removeEventListener('touchmove', this._boundTouchMove);
174
+ document.removeEventListener('touchend', this._boundTouchEnd);
175
+ console.log('SwipeGestures: Destroyed');
176
+ }
177
+ }
178
+
179
+ export default SwipeGestures;