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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +115 -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/cookie-consent.html +81 -9
- data/_includes/components/js-cdn.html +2 -2
- data/_includes/components/mermaid.html +260 -14
- data/_includes/core/head.html +1 -0
- 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 +21 -21
- data/_layouts/notebook.html +4 -4
- data/_layouts/root.html +2 -2
- data/_sass/core/_nav-tree.scss +145 -0
- data/_sass/core/code-copy.scss +45 -6
- data/_sass/custom.scss +541 -1
- data/assets/js/code-copy.js +79 -13
- 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/assets/js/ui-enhancements.js +194 -0
- data/scripts/migrate-nav-modes.sh +146 -0
- metadata +20 -7
- 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
|
+
|