jekyll-theme-zer0 0.15.2 → 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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +138 -10
  3. data/README.md +7 -7
  4. data/_data/authors.yml +2 -2
  5. data/_data/features.yml +676 -0
  6. data/_data/navigation/README.md +54 -0
  7. data/_data/navigation/about.yml +7 -5
  8. data/_data/navigation/docs.yml +77 -31
  9. data/_data/navigation/home.yml +4 -3
  10. data/_data/navigation/main.yml +16 -7
  11. data/_data/navigation/quickstart.yml +4 -2
  12. data/_includes/components/js-cdn.html +2 -2
  13. data/_includes/components/preview-image.html +20 -3
  14. data/_includes/content/intro.html +11 -2
  15. data/_includes/content/seo.html +12 -1
  16. data/_includes/landing/landing-install-cards.html +8 -8
  17. data/_includes/landing/landing-quick-links.html +4 -4
  18. data/_includes/navigation/breadcrumbs.html +29 -6
  19. data/_includes/navigation/nav-tree.html +181 -0
  20. data/_includes/navigation/navbar.html +262 -9
  21. data/_includes/navigation/sidebar-left.html +22 -23
  22. data/_layouts/default.html +1 -1
  23. data/_layouts/landing.html +1 -1
  24. data/_layouts/notebook.html +4 -4
  25. data/_layouts/root.html +2 -2
  26. data/_plugins/preview_image_generator.rb +29 -8
  27. data/_sass/core/_nav-tree.scss +145 -0
  28. data/_sass/custom.scss +3 -0
  29. data/assets/images/previews/site-personalization-configuration.png +0 -0
  30. data/assets/js/modules/navigation/config.js +149 -0
  31. data/assets/js/modules/navigation/focus.js +189 -0
  32. data/assets/js/modules/navigation/gestures.js +179 -0
  33. data/assets/js/modules/navigation/index.js +227 -0
  34. data/assets/js/modules/navigation/keyboard.js +237 -0
  35. data/assets/js/modules/navigation/scroll-spy.js +219 -0
  36. data/assets/js/modules/navigation/sidebar-state.js +267 -0
  37. data/assets/js/modules/navigation/smooth-scroll.js +153 -0
  38. data/scripts/README.md +8 -1
  39. data/scripts/lib/preview_generator.py +164 -8
  40. data/scripts/migrate-nav-modes.sh +146 -0
  41. data/scripts/update-preview-paths.sh +145 -0
  42. metadata +17 -3
  43. data/assets/js/sidebar.js +0 -511
@@ -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;
@@ -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;