jekyll-theme-zer0 0.16.0 → 0.17.2

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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +115 -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/cookie-consent.html +81 -9
  13. data/_includes/components/js-cdn.html +2 -2
  14. data/_includes/components/mermaid.html +260 -14
  15. data/_includes/core/head.html +1 -0
  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 +21 -21
  24. data/_layouts/notebook.html +4 -4
  25. data/_layouts/root.html +2 -2
  26. data/_sass/core/_nav-tree.scss +145 -0
  27. data/_sass/core/code-copy.scss +45 -6
  28. data/_sass/custom.scss +541 -1
  29. data/assets/js/code-copy.js +79 -13
  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/assets/js/ui-enhancements.js +194 -0
  39. data/scripts/migrate-nav-modes.sh +146 -0
  40. metadata +20 -7
  41. data/assets/js/sidebar.js +0 -511
@@ -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;
@@ -0,0 +1,194 @@
1
+ /**
2
+ * UI/UX Enhancements for zer0-mistakes Theme
3
+ * Adds smooth scroll animations, intersection observer effects, and enhanced interactions
4
+ */
5
+
6
+ (function() {
7
+ 'use strict';
8
+
9
+ // Check for reduced motion preference
10
+ const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
11
+
12
+ /**
13
+ * Initialize scroll animations for elements with animate-on-scroll class
14
+ */
15
+ function initScrollAnimations() {
16
+ if (prefersReducedMotion) return;
17
+
18
+ const animatedElements = document.querySelectorAll('.animate-on-scroll');
19
+
20
+ if (!animatedElements.length) return;
21
+
22
+ const observerOptions = {
23
+ threshold: 0.1,
24
+ rootMargin: '0px 0px -50px 0px'
25
+ };
26
+
27
+ const observer = new IntersectionObserver((entries) => {
28
+ entries.forEach(entry => {
29
+ if (entry.isIntersecting) {
30
+ entry.target.style.opacity = '1';
31
+ entry.target.style.transform = 'translateY(0)';
32
+ observer.unobserve(entry.target);
33
+ }
34
+ });
35
+ }, observerOptions);
36
+
37
+ animatedElements.forEach(el => {
38
+ el.style.opacity = '0';
39
+ el.style.transform = 'translateY(30px)';
40
+ el.style.transition = 'opacity 0.6s ease-out, transform 0.6s ease-out';
41
+ observer.observe(el);
42
+ });
43
+ }
44
+
45
+ /**
46
+ * Enhance card interactions with parallax effect
47
+ */
48
+ function initCardParallax() {
49
+ if (prefersReducedMotion) return;
50
+
51
+ const cards = document.querySelectorAll('.feature-card, .card');
52
+
53
+ cards.forEach(card => {
54
+ card.addEventListener('mousemove', function(e) {
55
+ const rect = card.getBoundingClientRect();
56
+ const x = e.clientX - rect.left;
57
+ const y = e.clientY - rect.top;
58
+
59
+ const centerX = rect.width / 2;
60
+ const centerY = rect.height / 2;
61
+
62
+ const rotateX = (y - centerY) / 20;
63
+ const rotateY = (centerX - x) / 20;
64
+
65
+ card.style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) translateY(-8px)`;
66
+ });
67
+
68
+ card.addEventListener('mouseleave', function() {
69
+ card.style.transform = '';
70
+ });
71
+ });
72
+ }
73
+
74
+ /**
75
+ * Smooth scroll for anchor links
76
+ */
77
+ function initSmoothScroll() {
78
+ document.querySelectorAll('a[href^="#"]').forEach(anchor => {
79
+ anchor.addEventListener('click', function(e) {
80
+ const href = this.getAttribute('href');
81
+ if (href === '#' || href === '') return;
82
+
83
+ const target = document.querySelector(href);
84
+ if (target) {
85
+ e.preventDefault();
86
+ const offsetTop = target.offsetTop - 80; // Account for fixed navbar
87
+
88
+ window.scrollTo({
89
+ top: offsetTop,
90
+ behavior: prefersReducedMotion ? 'auto' : 'smooth'
91
+ });
92
+ }
93
+ });
94
+ });
95
+ }
96
+
97
+ /**
98
+ * Add loading state to images
99
+ */
100
+ function initImageLoading() {
101
+ const images = document.querySelectorAll('img[loading="lazy"]');
102
+
103
+ images.forEach(img => {
104
+ if (img.complete) {
105
+ img.classList.add('loaded');
106
+ } else {
107
+ img.addEventListener('load', function() {
108
+ this.classList.add('loaded');
109
+ });
110
+ }
111
+ });
112
+ }
113
+
114
+ /**
115
+ * Enhance button ripple effect
116
+ */
117
+ function initButtonRipples() {
118
+ if (prefersReducedMotion) return;
119
+
120
+ document.querySelectorAll('.btn').forEach(button => {
121
+ button.addEventListener('click', function(e) {
122
+ const ripple = document.createElement('span');
123
+ const rect = this.getBoundingClientRect();
124
+ const size = Math.max(rect.width, rect.height);
125
+ const x = e.clientX - rect.left - size / 2;
126
+ const y = e.clientY - rect.top - size / 2;
127
+
128
+ ripple.style.width = ripple.style.height = size + 'px';
129
+ ripple.style.left = x + 'px';
130
+ ripple.style.top = y + 'px';
131
+ ripple.classList.add('ripple');
132
+
133
+ this.appendChild(ripple);
134
+
135
+ setTimeout(() => {
136
+ ripple.remove();
137
+ }, 600);
138
+ });
139
+ });
140
+ }
141
+
142
+ /**
143
+ * Add active state to navigation links on scroll
144
+ */
145
+ function initScrollSpy() {
146
+ const sections = document.querySelectorAll('section[id], [id^="get-started"], [id^="features"]');
147
+ const navLinks = document.querySelectorAll('a[href^="#"]');
148
+
149
+ if (!sections.length || !navLinks.length) return;
150
+
151
+ const observerOptions = {
152
+ threshold: 0.3,
153
+ rootMargin: '-80px 0px -50% 0px'
154
+ };
155
+
156
+ const observer = new IntersectionObserver((entries) => {
157
+ entries.forEach(entry => {
158
+ if (entry.isIntersecting) {
159
+ const id = entry.target.getAttribute('id');
160
+ navLinks.forEach(link => {
161
+ link.classList.remove('active');
162
+ if (link.getAttribute('href') === `#${id}`) {
163
+ link.classList.add('active');
164
+ }
165
+ });
166
+ }
167
+ });
168
+ }, observerOptions);
169
+
170
+ sections.forEach(section => observer.observe(section));
171
+ }
172
+
173
+ /**
174
+ * Initialize all enhancements
175
+ */
176
+ function init() {
177
+ // Wait for DOM to be fully loaded
178
+ if (document.readyState === 'loading') {
179
+ document.addEventListener('DOMContentLoaded', init);
180
+ return;
181
+ }
182
+
183
+ initScrollAnimations();
184
+ initCardParallax();
185
+ initSmoothScroll();
186
+ initImageLoading();
187
+ initButtonRipples();
188
+ initScrollSpy();
189
+ }
190
+
191
+ // Start initialization
192
+ init();
193
+ })();
194
+