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,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;
@@ -0,0 +1,267 @@
1
+ /**
2
+ * ===================================================================
3
+ * SIDEBAR STATE - Persistence and State Management
4
+ * ===================================================================
5
+ *
6
+ * File: sidebar-state.js
7
+ * Path: assets/js/modules/navigation/sidebar-state.js
8
+ * Purpose: Manage and persist navigation state across page loads
9
+ *
10
+ * Features:
11
+ * - Persist expanded/collapsed state of tree nodes in localStorage
12
+ * - Emit custom events for state changes
13
+ * - Restore state on page load
14
+ * - Track sidebar open/close state
15
+ *
16
+ * Usage:
17
+ * import { SidebarState } from './sidebar-state.js';
18
+ * const state = new SidebarState();
19
+ * state.setExpanded('docs-section', true);
20
+ *
21
+ * ===================================================================
22
+ */
23
+
24
+ import { config } from './config.js';
25
+
26
+ export class SidebarState {
27
+ constructor() {
28
+ this._storagePrefix = config.state.storagePrefix;
29
+ this._expandedNodes = new Set();
30
+
31
+ this._init();
32
+ }
33
+
34
+ /**
35
+ * Initialize state management
36
+ * @private
37
+ */
38
+ _init() {
39
+ // Load persisted state
40
+ this._loadState();
41
+
42
+ // Listen for collapse events from Bootstrap
43
+ this._setupCollapseListeners();
44
+
45
+ // Restore expanded states on page load
46
+ this._restoreExpandedStates();
47
+
48
+ console.log('SidebarState: Initialized');
49
+ }
50
+
51
+ /**
52
+ * Load state from localStorage
53
+ * @private
54
+ */
55
+ _loadState() {
56
+ try {
57
+ const key = this._storagePrefix + config.state.keys.expandedNodes;
58
+ const stored = localStorage.getItem(key);
59
+ if (stored) {
60
+ const parsed = JSON.parse(stored);
61
+ this._expandedNodes = new Set(parsed);
62
+ }
63
+ } catch (error) {
64
+ console.warn('SidebarState: Could not load state from localStorage', error);
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Save state to localStorage
70
+ * @private
71
+ */
72
+ _saveState() {
73
+ try {
74
+ const key = this._storagePrefix + config.state.keys.expandedNodes;
75
+ const value = JSON.stringify([...this._expandedNodes]);
76
+ localStorage.setItem(key, value);
77
+ } catch (error) {
78
+ console.warn('SidebarState: Could not save state to localStorage', error);
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Setup listeners for Bootstrap collapse events
84
+ * @private
85
+ */
86
+ _setupCollapseListeners() {
87
+ // Listen for collapse show events
88
+ document.addEventListener('show.bs.collapse', (event) => {
89
+ const nodeId = event.target.id;
90
+ if (nodeId && this._isNavNode(event.target)) {
91
+ this.setExpanded(nodeId, true);
92
+ }
93
+ });
94
+
95
+ // Listen for collapse hide events
96
+ document.addEventListener('hide.bs.collapse', (event) => {
97
+ const nodeId = event.target.id;
98
+ if (nodeId && this._isNavNode(event.target)) {
99
+ this.setExpanded(nodeId, false);
100
+ }
101
+ });
102
+ }
103
+
104
+ /**
105
+ * Check if element is a navigation node
106
+ * @private
107
+ * @param {Element} element
108
+ * @returns {boolean}
109
+ */
110
+ _isNavNode(element) {
111
+ // Check if element is within sidebar or has nav-related classes
112
+ return element.closest('.bd-sidebar, .nav-tree, [data-nav-tree]') !== null;
113
+ }
114
+
115
+ /**
116
+ * Restore expanded states from persisted data
117
+ * @private
118
+ */
119
+ _restoreExpandedStates() {
120
+ // Wait for DOM to be ready
121
+ requestAnimationFrame(() => {
122
+ this._expandedNodes.forEach(nodeId => {
123
+ const element = document.getElementById(nodeId);
124
+ if (element && typeof bootstrap !== 'undefined') {
125
+ // Show the collapse without animation
126
+ element.classList.add('show');
127
+
128
+ // Update the toggle button state
129
+ const toggle = document.querySelector(`[data-bs-target="#${nodeId}"]`);
130
+ if (toggle) {
131
+ toggle.classList.remove('collapsed');
132
+ toggle.setAttribute('aria-expanded', 'true');
133
+ }
134
+ }
135
+ });
136
+ });
137
+ }
138
+
139
+ /**
140
+ * Set expanded state for a node
141
+ * @param {string} nodeId - The ID of the collapse element
142
+ * @param {boolean} expanded - Whether the node should be expanded
143
+ */
144
+ setExpanded(nodeId, expanded) {
145
+ if (expanded) {
146
+ this._expandedNodes.add(nodeId);
147
+ } else {
148
+ this._expandedNodes.delete(nodeId);
149
+ }
150
+
151
+ this._saveState();
152
+
153
+ // Dispatch custom event
154
+ document.dispatchEvent(new CustomEvent('navigation:toggle', {
155
+ detail: {
156
+ nodeId: nodeId,
157
+ expanded: expanded
158
+ }
159
+ }));
160
+ }
161
+
162
+ /**
163
+ * Check if a node is expanded
164
+ * @param {string} nodeId
165
+ * @returns {boolean}
166
+ */
167
+ isExpanded(nodeId) {
168
+ return this._expandedNodes.has(nodeId);
169
+ }
170
+
171
+ /**
172
+ * Expand all nodes
173
+ */
174
+ expandAll() {
175
+ const collapses = document.querySelectorAll('.bd-sidebar .collapse, .nav-tree .collapse');
176
+ collapses.forEach(collapse => {
177
+ if (collapse.id) {
178
+ this._expandedNodes.add(collapse.id);
179
+ if (typeof bootstrap !== 'undefined') {
180
+ const bsCollapse = bootstrap.Collapse.getOrCreateInstance(collapse, { toggle: false });
181
+ bsCollapse.show();
182
+ }
183
+ }
184
+ });
185
+ this._saveState();
186
+
187
+ document.dispatchEvent(new CustomEvent('navigation:expandAll'));
188
+ }
189
+
190
+ /**
191
+ * Collapse all nodes
192
+ */
193
+ collapseAll() {
194
+ const collapses = document.querySelectorAll('.bd-sidebar .collapse.show, .nav-tree .collapse.show');
195
+ collapses.forEach(collapse => {
196
+ if (collapse.id) {
197
+ this._expandedNodes.delete(collapse.id);
198
+ if (typeof bootstrap !== 'undefined') {
199
+ const bsCollapse = bootstrap.Collapse.getInstance(collapse);
200
+ if (bsCollapse) {
201
+ bsCollapse.hide();
202
+ }
203
+ }
204
+ }
205
+ });
206
+ this._saveState();
207
+
208
+ document.dispatchEvent(new CustomEvent('navigation:collapseAll'));
209
+ }
210
+
211
+ /**
212
+ * Expand path to a specific element (expand all parents)
213
+ * @param {string} targetId - The ID of the target element to reveal
214
+ */
215
+ expandPathTo(targetId) {
216
+ const target = document.getElementById(targetId);
217
+ if (!target) return;
218
+
219
+ // Find all parent collapses
220
+ let parent = target.closest('.collapse');
221
+ while (parent) {
222
+ if (parent.id) {
223
+ this.setExpanded(parent.id, true);
224
+ if (typeof bootstrap !== 'undefined') {
225
+ const bsCollapse = bootstrap.Collapse.getOrCreateInstance(parent, { toggle: false });
226
+ bsCollapse.show();
227
+ }
228
+ }
229
+ parent = parent.parentElement?.closest('.collapse');
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Get all expanded node IDs
235
+ * @returns {string[]}
236
+ */
237
+ getExpandedNodes() {
238
+ return [...this._expandedNodes];
239
+ }
240
+
241
+ /**
242
+ * Clear all persisted state
243
+ */
244
+ clearState() {
245
+ this._expandedNodes.clear();
246
+
247
+ try {
248
+ Object.values(config.state.keys).forEach(key => {
249
+ localStorage.removeItem(this._storagePrefix + key);
250
+ });
251
+ } catch (error) {
252
+ console.warn('SidebarState: Could not clear localStorage', error);
253
+ }
254
+
255
+ document.dispatchEvent(new CustomEvent('navigation:stateCleared'));
256
+ }
257
+
258
+ /**
259
+ * Cleanup
260
+ */
261
+ destroy() {
262
+ // State is persisted, nothing to clean up
263
+ console.log('SidebarState: Destroyed');
264
+ }
265
+ }
266
+
267
+ export default SidebarState;
@@ -0,0 +1,153 @@
1
+ /**
2
+ * ===================================================================
3
+ * SMOOTH SCROLL - Enhanced Anchor Navigation
4
+ * ===================================================================
5
+ *
6
+ * File: smooth-scroll.js
7
+ * Path: assets/js/modules/navigation/smooth-scroll.js
8
+ * Purpose: Smooth scrolling to anchor links with header offset
9
+ *
10
+ * Features:
11
+ * - Smooth scroll with configurable offset for fixed headers
12
+ * - URL hash update without page jump
13
+ * - Closes mobile offcanvas after navigation
14
+ * - Accessibility-focused with proper focus management
15
+ *
16
+ * Usage:
17
+ * import { SmoothScroll } from './smooth-scroll.js';
18
+ * const smoothScroll = new SmoothScroll();
19
+ *
20
+ * ===================================================================
21
+ */
22
+
23
+ import { config, isBelowBreakpoint } from './config.js';
24
+
25
+ /**
26
+ * Get all elements safely
27
+ * @param {string} selector - CSS selector
28
+ * @returns {NodeList}
29
+ */
30
+ function getElements(selector) {
31
+ try {
32
+ return document.querySelectorAll(selector);
33
+ } catch (error) {
34
+ console.warn(`SmoothScroll: Elements not found - ${selector}`);
35
+ return [];
36
+ }
37
+ }
38
+
39
+ export class SmoothScroll {
40
+ constructor() {
41
+ this.tocLinks = getElements(config.selectors.tocLinks);
42
+ this._init();
43
+ }
44
+
45
+ /**
46
+ * Initialize click handlers
47
+ * @private
48
+ */
49
+ _init() {
50
+ this.tocLinks.forEach(link => {
51
+ link.addEventListener('click', e => this._handleClick(e));
52
+ });
53
+ console.log(`SmoothScroll: Initialized with ${this.tocLinks.length} links`);
54
+ }
55
+
56
+ /**
57
+ * Handle click on TOC link
58
+ * @private
59
+ * @param {Event} event
60
+ */
61
+ _handleClick(event) {
62
+ const href = event.currentTarget.getAttribute('href');
63
+
64
+ if (!href || !href.startsWith('#')) return;
65
+
66
+ event.preventDefault();
67
+
68
+ const targetId = href.substring(1);
69
+ const targetElement = document.getElementById(targetId);
70
+
71
+ if (!targetElement) {
72
+ console.warn(`SmoothScroll: Target element #${targetId} not found`);
73
+ return;
74
+ }
75
+
76
+ // Scroll to target
77
+ this.scrollToElement(targetElement);
78
+
79
+ // Update URL without jumping
80
+ if (history.pushState) {
81
+ history.pushState(null, null, href);
82
+ }
83
+
84
+ // Close mobile offcanvas if open
85
+ this._closeMobileOffcanvas();
86
+
87
+ // Dispatch custom event
88
+ document.dispatchEvent(new CustomEvent('navigation:scroll', {
89
+ detail: {
90
+ targetId: targetId,
91
+ targetElement: targetElement
92
+ }
93
+ }));
94
+ }
95
+
96
+ /**
97
+ * Scroll to an element with offset
98
+ * @param {Element} element - Target element
99
+ * @param {number} [offset] - Optional custom offset
100
+ */
101
+ scrollToElement(element, offset = config.smoothScroll.offset) {
102
+ const elementPosition = element.getBoundingClientRect().top;
103
+ const offsetPosition = elementPosition + window.pageYOffset - offset;
104
+
105
+ window.scrollTo({
106
+ top: offsetPosition,
107
+ behavior: config.smoothScroll.behavior
108
+ });
109
+
110
+ // Update focus for accessibility
111
+ element.setAttribute('tabindex', '-1');
112
+ element.focus({ preventScroll: true });
113
+ }
114
+
115
+ /**
116
+ * Scroll to element by ID
117
+ * @param {string} id - Element ID (without #)
118
+ * @param {number} [offset] - Optional custom offset
119
+ */
120
+ scrollToId(id, offset) {
121
+ const element = document.getElementById(id);
122
+ if (element) {
123
+ this.scrollToElement(element, offset);
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Close mobile offcanvas if viewport is below lg breakpoint
129
+ * @private
130
+ */
131
+ _closeMobileOffcanvas() {
132
+ if (!isBelowBreakpoint('lg')) return;
133
+
134
+ const tocOffcanvas = document.getElementById('tocContents');
135
+ if (tocOffcanvas && typeof bootstrap !== 'undefined') {
136
+ const bsOffcanvas = bootstrap.Offcanvas.getInstance(tocOffcanvas);
137
+ if (bsOffcanvas) {
138
+ bsOffcanvas.hide();
139
+ }
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Cleanup event listeners
145
+ */
146
+ destroy() {
147
+ // Note: We'd need to store bound handlers to properly remove them
148
+ // For now, this is a no-op since the page will reload anyway
149
+ console.log('SmoothScroll: Destroyed');
150
+ }
151
+ }
152
+
153
+ export default SmoothScroll;
data/scripts/README.md CHANGED
@@ -91,7 +91,14 @@ Options:
91
91
  --dry-run Preview without changes
92
92
  --collection TYPE Generate for specific collection (posts, docs, etc.)
93
93
  -f, --file PATH Process specific file
94
- --provider PROVIDER Use specific AI provider (openai)
94
+ --provider PROVIDER Use specific AI provider (openai, stability, xai)
95
+ --assets-prefix Custom assets path prefix (default: /assets)
96
+ --no-auto-prefix Disable automatic path prefixing
97
+
98
+ AI Providers:
99
+ openai - OpenAI DALL-E (requires OPENAI_API_KEY)
100
+ stability - Stability AI (requires STABILITY_API_KEY)
101
+ xai - xAI Grok image generation (requires XAI_API_KEY)
95
102
  ```
96
103
 
97
104
  #### `install-preview-generator`