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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +138 -10
- data/README.md +7 -7
- data/_data/authors.yml +2 -2
- data/_data/features.yml +676 -0
- data/_data/navigation/README.md +54 -0
- data/_data/navigation/about.yml +7 -5
- data/_data/navigation/docs.yml +77 -31
- data/_data/navigation/home.yml +4 -3
- data/_data/navigation/main.yml +16 -7
- data/_data/navigation/quickstart.yml +4 -2
- data/_includes/components/js-cdn.html +2 -2
- data/_includes/components/preview-image.html +20 -3
- data/_includes/content/intro.html +11 -2
- data/_includes/content/seo.html +12 -1
- data/_includes/landing/landing-install-cards.html +8 -8
- data/_includes/landing/landing-quick-links.html +4 -4
- data/_includes/navigation/breadcrumbs.html +29 -6
- data/_includes/navigation/nav-tree.html +181 -0
- data/_includes/navigation/navbar.html +262 -9
- data/_includes/navigation/sidebar-left.html +22 -23
- data/_layouts/default.html +1 -1
- data/_layouts/landing.html +1 -1
- data/_layouts/notebook.html +4 -4
- data/_layouts/root.html +2 -2
- data/_plugins/preview_image_generator.rb +29 -8
- data/_sass/core/_nav-tree.scss +145 -0
- data/_sass/custom.scss +3 -0
- data/assets/images/previews/site-personalization-configuration.png +0 -0
- data/assets/js/modules/navigation/config.js +149 -0
- data/assets/js/modules/navigation/focus.js +189 -0
- data/assets/js/modules/navigation/gestures.js +179 -0
- data/assets/js/modules/navigation/index.js +227 -0
- data/assets/js/modules/navigation/keyboard.js +237 -0
- data/assets/js/modules/navigation/scroll-spy.js +219 -0
- data/assets/js/modules/navigation/sidebar-state.js +267 -0
- data/assets/js/modules/navigation/smooth-scroll.js +153 -0
- data/scripts/README.md +8 -1
- data/scripts/lib/preview_generator.py +164 -8
- data/scripts/migrate-nav-modes.sh +146 -0
- data/scripts/update-preview-paths.sh +145 -0
- metadata +17 -3
- 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`
|