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,227 @@
1
+ /**
2
+ * ===================================================================
3
+ * NAVIGATION - Main Entry Point
4
+ * ===================================================================
5
+ *
6
+ * File: index.js
7
+ * Path: assets/js/modules/navigation/index.js
8
+ * Purpose: Main orchestrator for all navigation modules
9
+ *
10
+ * This module imports and initializes all navigation-related modules:
11
+ * - ScrollSpy: Highlights current section in TOC
12
+ * - SmoothScroll: Smooth anchor navigation
13
+ * - KeyboardShortcuts: Keyboard navigation
14
+ * - SwipeGestures: Mobile gesture support
15
+ * - FocusManager: Accessibility focus management
16
+ * - SidebarState: Persistence of sidebar state
17
+ *
18
+ * Usage:
19
+ * <script type="module" src="/assets/js/modules/navigation/index.js"></script>
20
+ *
21
+ * Or import programmatically:
22
+ * import { Navigation } from './modules/navigation/index.js';
23
+ * const nav = new Navigation();
24
+ *
25
+ * ===================================================================
26
+ */
27
+
28
+ import { config } from './config.js';
29
+ import { ScrollSpy } from './scroll-spy.js';
30
+ import { SmoothScroll } from './smooth-scroll.js';
31
+ import { KeyboardShortcuts } from './keyboard.js';
32
+ import { SwipeGestures } from './gestures.js';
33
+ import { FocusManager } from './focus.js';
34
+ import { SidebarState } from './sidebar-state.js';
35
+
36
+ /**
37
+ * Navigation Controller - Orchestrates all navigation modules
38
+ */
39
+ export class Navigation {
40
+ constructor() {
41
+ this.modules = {};
42
+ this._initialized = false;
43
+ }
44
+
45
+ /**
46
+ * Initialize all navigation modules
47
+ * @returns {Navigation} this instance for chaining
48
+ */
49
+ init() {
50
+ if (this._initialized) {
51
+ console.warn('Navigation: Already initialized');
52
+ return this;
53
+ }
54
+
55
+ try {
56
+ // Initialize sidebar state first (other modules may depend on it)
57
+ this.modules.state = new SidebarState();
58
+
59
+ // Initialize TOC-related modules only if TOC exists
60
+ const toc = document.querySelector(config.selectors.toc);
61
+ if (toc) {
62
+ this.modules.scrollSpy = new ScrollSpy();
63
+ this.modules.smoothScroll = new SmoothScroll();
64
+ } else {
65
+ console.log('Navigation: No TOC found, skipping scroll spy and smooth scroll');
66
+ }
67
+
68
+ // Initialize keyboard shortcuts
69
+ this.modules.keyboard = new KeyboardShortcuts();
70
+
71
+ // Initialize gesture support
72
+ this.modules.gestures = new SwipeGestures();
73
+
74
+ // Initialize focus management
75
+ this.modules.focus = new FocusManager();
76
+
77
+ this._initialized = true;
78
+
79
+ // Dispatch ready event
80
+ document.dispatchEvent(new CustomEvent('navigation:ready', {
81
+ detail: {
82
+ modules: Object.keys(this.modules)
83
+ }
84
+ }));
85
+
86
+ console.log('Navigation: Successfully initialized modules:', Object.keys(this.modules));
87
+
88
+ } catch (error) {
89
+ console.error('Navigation: Initialization error', error);
90
+ }
91
+
92
+ return this;
93
+ }
94
+
95
+ /**
96
+ * Get a specific module instance
97
+ * @param {string} name - Module name
98
+ * @returns {Object|undefined}
99
+ */
100
+ getModule(name) {
101
+ return this.modules[name];
102
+ }
103
+
104
+ /**
105
+ * Get configuration
106
+ * @returns {Object}
107
+ */
108
+ getConfig() {
109
+ return config;
110
+ }
111
+
112
+ /**
113
+ * Scroll to a specific element
114
+ * @param {string|Element} target - Element ID or Element
115
+ * @param {number} [offset] - Optional scroll offset
116
+ */
117
+ scrollTo(target, offset) {
118
+ if (!this.modules.smoothScroll) return;
119
+
120
+ if (typeof target === 'string') {
121
+ this.modules.smoothScroll.scrollToId(target, offset);
122
+ } else if (target instanceof Element) {
123
+ this.modules.smoothScroll.scrollToElement(target, offset);
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Expand sidebar tree to show a specific item
129
+ * @param {string} targetId - ID of element to reveal
130
+ */
131
+ expandTo(targetId) {
132
+ if (this.modules.state) {
133
+ this.modules.state.expandPathTo(targetId);
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Expand all sidebar tree nodes
139
+ */
140
+ expandAll() {
141
+ if (this.modules.state) {
142
+ this.modules.state.expandAll();
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Collapse all sidebar tree nodes
148
+ */
149
+ collapseAll() {
150
+ if (this.modules.state) {
151
+ this.modules.state.collapseAll();
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Get keyboard shortcuts for help display
157
+ * @returns {Object|null}
158
+ */
159
+ getShortcuts() {
160
+ if (this.modules.keyboard && this.modules.keyboard.getShortcuts) {
161
+ return this.modules.keyboard.getShortcuts();
162
+ }
163
+ return null;
164
+ }
165
+
166
+ /**
167
+ * Destroy all modules and cleanup
168
+ */
169
+ destroy() {
170
+ Object.values(this.modules).forEach(module => {
171
+ if (module && typeof module.destroy === 'function') {
172
+ module.destroy();
173
+ }
174
+ });
175
+
176
+ this.modules = {};
177
+ this._initialized = false;
178
+
179
+ document.dispatchEvent(new CustomEvent('navigation:destroyed'));
180
+ console.log('Navigation: Destroyed all modules');
181
+ }
182
+ }
183
+
184
+ // ===================================================================
185
+ // AUTO-INITIALIZATION
186
+ // ===================================================================
187
+
188
+ /**
189
+ * Create and initialize navigation when DOM is ready
190
+ */
191
+ function initNavigation() {
192
+ // Create global instance
193
+ window.zer0Navigation = new Navigation();
194
+
195
+ // Check if Bootstrap is available
196
+ if (typeof bootstrap === 'undefined') {
197
+ console.warn('Navigation: Bootstrap not found, waiting for load event');
198
+ window.addEventListener('load', () => {
199
+ window.zer0Navigation.init();
200
+ });
201
+ } else {
202
+ window.zer0Navigation.init();
203
+ }
204
+ }
205
+
206
+ // Wait for DOM to be ready
207
+ if (document.readyState === 'loading') {
208
+ document.addEventListener('DOMContentLoaded', initNavigation);
209
+ } else {
210
+ // DOM already loaded
211
+ initNavigation();
212
+ }
213
+
214
+ // ===================================================================
215
+ // EXPORTS
216
+ // ===================================================================
217
+
218
+ // Export individual modules for advanced usage
219
+ export { config } from './config.js';
220
+ export { ScrollSpy } from './scroll-spy.js';
221
+ export { SmoothScroll } from './smooth-scroll.js';
222
+ export { KeyboardShortcuts } from './keyboard.js';
223
+ export { SwipeGestures } from './gestures.js';
224
+ export { FocusManager } from './focus.js';
225
+ export { SidebarState } from './sidebar-state.js';
226
+
227
+ export default Navigation;
@@ -0,0 +1,237 @@
1
+ /**
2
+ * ===================================================================
3
+ * KEYBOARD SHORTCUTS - Navigation Enhancements
4
+ * ===================================================================
5
+ *
6
+ * File: keyboard.js
7
+ * Path: assets/js/modules/navigation/keyboard.js
8
+ * Purpose: Keyboard shortcuts for navigation and accessibility
9
+ *
10
+ * Default Shortcuts:
11
+ * - [ : Navigate to previous section
12
+ * - ] : Navigate to next section
13
+ * - / : Focus search (when implemented)
14
+ * - b : Toggle left sidebar
15
+ * - t : Toggle TOC sidebar
16
+ *
17
+ * Usage:
18
+ * import { KeyboardShortcuts } from './keyboard.js';
19
+ * const shortcuts = new KeyboardShortcuts();
20
+ *
21
+ * ===================================================================
22
+ */
23
+
24
+ import { config, isBelowBreakpoint } from './config.js';
25
+
26
+ /**
27
+ * Get all elements safely
28
+ * @param {string} selector - CSS selector
29
+ * @returns {NodeList}
30
+ */
31
+ function getElements(selector) {
32
+ try {
33
+ return document.querySelectorAll(selector);
34
+ } catch (error) {
35
+ return [];
36
+ }
37
+ }
38
+
39
+ export class KeyboardShortcuts {
40
+ constructor() {
41
+ if (!config.keyboard.enabled) {
42
+ console.log('KeyboardShortcuts: Disabled via config');
43
+ return;
44
+ }
45
+
46
+ this.tocLinks = Array.from(getElements(config.selectors.tocLinks));
47
+ this.currentIndex = -1;
48
+ this._boundHandler = this._handleKeydown.bind(this);
49
+
50
+ this._init();
51
+ }
52
+
53
+ /**
54
+ * Initialize keyboard event listeners
55
+ * @private
56
+ */
57
+ _init() {
58
+ document.addEventListener('keydown', this._boundHandler);
59
+ console.log('KeyboardShortcuts: Initialized');
60
+ }
61
+
62
+ /**
63
+ * Handle keydown events
64
+ * @private
65
+ * @param {KeyboardEvent} event
66
+ */
67
+ _handleKeydown(event) {
68
+ // Ignore if user is typing in an input
69
+ if (event.target.matches('input, textarea, select, [contenteditable="true"]')) {
70
+ return;
71
+ }
72
+
73
+ const { keys } = config.keyboard;
74
+
75
+ switch(event.key) {
76
+ case keys.previousSection:
77
+ event.preventDefault();
78
+ this._navigatePrevious();
79
+ break;
80
+
81
+ case keys.nextSection:
82
+ event.preventDefault();
83
+ this._navigateNext();
84
+ break;
85
+
86
+ case keys.search:
87
+ event.preventDefault();
88
+ this._focusSearch();
89
+ break;
90
+
91
+ case keys.toggleSidebar:
92
+ // Only handle if not typing
93
+ if (!event.ctrlKey && !event.metaKey && !event.altKey) {
94
+ event.preventDefault();
95
+ this._toggleSidebar();
96
+ }
97
+ break;
98
+
99
+ case keys.toggleToc:
100
+ // Only handle if not typing
101
+ if (!event.ctrlKey && !event.metaKey && !event.altKey) {
102
+ event.preventDefault();
103
+ this._toggleToc();
104
+ }
105
+ break;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Navigate to previous section
111
+ * @private
112
+ */
113
+ _navigatePrevious() {
114
+ this._updateCurrentIndex();
115
+
116
+ if (this.currentIndex > 0) {
117
+ this.currentIndex--;
118
+ this._navigateToIndex(this.currentIndex);
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Navigate to next section
124
+ * @private
125
+ */
126
+ _navigateNext() {
127
+ this._updateCurrentIndex();
128
+
129
+ if (this.currentIndex < this.tocLinks.length - 1) {
130
+ this.currentIndex++;
131
+ this._navigateToIndex(this.currentIndex);
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Update current index based on active link
137
+ * @private
138
+ */
139
+ _updateCurrentIndex() {
140
+ const activeLink = document.querySelector(`${config.selectors.tocLinks}.active`);
141
+ if (activeLink) {
142
+ this.currentIndex = this.tocLinks.indexOf(activeLink);
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Navigate to a specific index
148
+ * @private
149
+ * @param {number} index
150
+ */
151
+ _navigateToIndex(index) {
152
+ const link = this.tocLinks[index];
153
+ if (link) {
154
+ link.click();
155
+
156
+ // Dispatch event for tracking
157
+ document.dispatchEvent(new CustomEvent('navigation:keyboardNav', {
158
+ detail: {
159
+ direction: index > this.currentIndex ? 'next' : 'previous',
160
+ index: index
161
+ }
162
+ }));
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Focus search input
168
+ * @private
169
+ */
170
+ _focusSearch() {
171
+ const searchInput = document.querySelector('#search-input, [data-search-input]');
172
+ if (searchInput) {
173
+ searchInput.focus();
174
+ } else {
175
+ console.log('KeyboardShortcuts: Search not yet implemented');
176
+ // Dispatch event so other modules can handle
177
+ document.dispatchEvent(new CustomEvent('navigation:searchRequest'));
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Toggle left sidebar
183
+ * @private
184
+ */
185
+ _toggleSidebar() {
186
+ const sidebar = document.querySelector(config.selectors.leftSidebar);
187
+ if (sidebar && typeof bootstrap !== 'undefined') {
188
+ const bsOffcanvas = bootstrap.Offcanvas.getOrCreateInstance(sidebar);
189
+ bsOffcanvas.toggle();
190
+
191
+ document.dispatchEvent(new CustomEvent('navigation:sidebarToggle', {
192
+ detail: { sidebar: 'left' }
193
+ }));
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Toggle TOC sidebar
199
+ * @private
200
+ */
201
+ _toggleToc() {
202
+ const toc = document.querySelector(config.selectors.rightSidebar);
203
+ if (toc && typeof bootstrap !== 'undefined') {
204
+ const bsOffcanvas = bootstrap.Offcanvas.getOrCreateInstance(toc);
205
+ bsOffcanvas.toggle();
206
+
207
+ document.dispatchEvent(new CustomEvent('navigation:sidebarToggle', {
208
+ detail: { sidebar: 'toc' }
209
+ }));
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Get available shortcuts for help display
215
+ * @returns {Object} Map of key to description
216
+ */
217
+ getShortcuts() {
218
+ const { keys } = config.keyboard;
219
+ return {
220
+ [keys.previousSection]: 'Previous section',
221
+ [keys.nextSection]: 'Next section',
222
+ [keys.search]: 'Focus search',
223
+ [keys.toggleSidebar]: 'Toggle sidebar',
224
+ [keys.toggleToc]: 'Toggle table of contents'
225
+ };
226
+ }
227
+
228
+ /**
229
+ * Cleanup event listeners
230
+ */
231
+ destroy() {
232
+ document.removeEventListener('keydown', this._boundHandler);
233
+ console.log('KeyboardShortcuts: Destroyed');
234
+ }
235
+ }
236
+
237
+ export default KeyboardShortcuts;
@@ -0,0 +1,219 @@
1
+ /**
2
+ * ===================================================================
3
+ * SCROLL SPY - Intersection Observer Implementation
4
+ * ===================================================================
5
+ *
6
+ * File: scroll-spy.js
7
+ * Path: assets/js/modules/navigation/scroll-spy.js
8
+ * Purpose: Track visible sections and highlight corresponding TOC links
9
+ *
10
+ * Features:
11
+ * - Intersection Observer-based (better performance than scroll events)
12
+ * - Active link highlighting with smooth transitions
13
+ * - Auto-scroll TOC to keep active link visible
14
+ *
15
+ * Usage:
16
+ * import { ScrollSpy } from './scroll-spy.js';
17
+ * const scrollSpy = new ScrollSpy();
18
+ *
19
+ * ===================================================================
20
+ */
21
+
22
+ import { config } from './config.js';
23
+
24
+ /**
25
+ * Get element safely with error handling
26
+ * @param {string} selector - CSS selector
27
+ * @returns {Element|null}
28
+ */
29
+ function getElement(selector) {
30
+ try {
31
+ return document.querySelector(selector);
32
+ } catch (error) {
33
+ console.warn(`ScrollSpy: Element not found - ${selector}`);
34
+ return null;
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Get all elements safely with error handling
40
+ * @param {string} selector - CSS selector
41
+ * @returns {NodeList}
42
+ */
43
+ function getElements(selector) {
44
+ try {
45
+ return document.querySelectorAll(selector);
46
+ } catch (error) {
47
+ console.warn(`ScrollSpy: Elements not found - ${selector}`);
48
+ return [];
49
+ }
50
+ }
51
+
52
+ export class ScrollSpy {
53
+ constructor() {
54
+ this.tocLinks = getElements(config.selectors.tocLinks);
55
+ this.headings = this._getHeadings();
56
+ this.currentActive = null;
57
+ this.observer = null;
58
+
59
+ if (this.headings.length === 0 || this.tocLinks.length === 0) {
60
+ console.log('ScrollSpy: No TOC or headings found, skipping initialization');
61
+ return;
62
+ }
63
+
64
+ this._init();
65
+ }
66
+
67
+ /**
68
+ * Get all headings that have corresponding TOC links
69
+ * @private
70
+ * @returns {Array<{element: Element, link: Element, id: string}>}
71
+ */
72
+ _getHeadings() {
73
+ const headings = [];
74
+ this.tocLinks.forEach(link => {
75
+ const href = link.getAttribute('href');
76
+ if (href && href.startsWith('#')) {
77
+ const id = href.substring(1);
78
+ const heading = document.getElementById(id);
79
+ if (heading) {
80
+ headings.push({
81
+ element: heading,
82
+ link: link,
83
+ id: id
84
+ });
85
+ }
86
+ }
87
+ });
88
+ return headings;
89
+ }
90
+
91
+ /**
92
+ * Initialize Intersection Observer
93
+ * @private
94
+ */
95
+ _init() {
96
+ const observerOptions = {
97
+ root: null,
98
+ rootMargin: config.scrollSpy.rootMargin,
99
+ threshold: config.scrollSpy.threshold
100
+ };
101
+
102
+ this.observer = new IntersectionObserver(
103
+ entries => this._handleIntersection(entries),
104
+ observerOptions
105
+ );
106
+
107
+ // Observe all headings
108
+ this.headings.forEach(heading => {
109
+ this.observer.observe(heading.element);
110
+ });
111
+
112
+ console.log(`ScrollSpy: Observing ${this.headings.length} headings`);
113
+ }
114
+
115
+ /**
116
+ * Handle intersection events
117
+ * @private
118
+ * @param {IntersectionObserverEntry[]} entries
119
+ */
120
+ _handleIntersection(entries) {
121
+ // Find the most visible heading
122
+ let mostVisible = null;
123
+ let maxRatio = 0;
124
+
125
+ entries.forEach(entry => {
126
+ if (entry.isIntersecting && entry.intersectionRatio > maxRatio) {
127
+ maxRatio = entry.intersectionRatio;
128
+ mostVisible = this.headings.find(h => h.element === entry.target);
129
+ }
130
+ });
131
+
132
+ // If we found a visible heading, activate it
133
+ if (mostVisible) {
134
+ this._setActiveLink(mostVisible.link);
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Set active link with visual feedback
140
+ * @private
141
+ * @param {Element} link
142
+ */
143
+ _setActiveLink(link) {
144
+ if (this.currentActive === link) return;
145
+
146
+ // Remove previous active state
147
+ this.tocLinks.forEach(l => l.classList.remove('active'));
148
+
149
+ // Add active state
150
+ if (link) {
151
+ link.classList.add('active');
152
+ this.currentActive = link;
153
+
154
+ // Scroll TOC to show active link (if needed)
155
+ this._scrollTocToActiveLink(link);
156
+
157
+ // Dispatch custom event for other modules
158
+ document.dispatchEvent(new CustomEvent('navigation:sectionChange', {
159
+ detail: {
160
+ link: link,
161
+ href: link.getAttribute('href')
162
+ }
163
+ }));
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Scroll TOC container to show active link
169
+ * @private
170
+ * @param {Element} link
171
+ */
172
+ _scrollTocToActiveLink(link) {
173
+ const tocContainer = getElement(config.selectors.tocContainer);
174
+ if (!tocContainer) return;
175
+
176
+ const linkRect = link.getBoundingClientRect();
177
+ const containerRect = tocContainer.getBoundingClientRect();
178
+
179
+ // Check if link is out of view
180
+ if (linkRect.top < containerRect.top || linkRect.bottom > containerRect.bottom) {
181
+ link.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Manually set active section by ID
187
+ * @param {string} id - Heading ID to activate
188
+ */
189
+ setActiveById(id) {
190
+ const heading = this.headings.find(h => h.id === id);
191
+ if (heading) {
192
+ this._setActiveLink(heading.link);
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Get current active heading
198
+ * @returns {{element: Element, link: Element, id: string}|null}
199
+ */
200
+ getActive() {
201
+ if (!this.currentActive) return null;
202
+ return this.headings.find(h => h.link === this.currentActive) || null;
203
+ }
204
+
205
+ /**
206
+ * Cleanup observer
207
+ */
208
+ destroy() {
209
+ if (this.observer) {
210
+ this.observer.disconnect();
211
+ this.observer = null;
212
+ }
213
+ this.tocLinks.forEach(l => l.classList.remove('active'));
214
+ this.currentActive = null;
215
+ console.log('ScrollSpy: Destroyed');
216
+ }
217
+ }
218
+
219
+ export default ScrollSpy;