jekyll-theme-zer0 0.17.0 → 0.18.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 +74 -0
- data/README.md +1 -1
- data/_includes/components/cookie-consent.html +81 -9
- data/_includes/components/js-cdn.html +4 -1
- data/_includes/components/mermaid.html +260 -14
- data/_includes/components/search-modal.html +56 -0
- data/_includes/core/branding.html +12 -13
- data/_includes/core/head.html +1 -0
- data/_includes/core/header.html +38 -11
- data/_includes/navigation/navbar.html +31 -0
- data/_layouts/landing.html +21 -21
- data/_layouts/root.html +3 -0
- data/_sass/core/code-copy.scss +45 -6
- data/_sass/custom.scss +620 -1
- data/assets/js/code-copy.js +79 -13
- data/assets/js/modules/navigation/keyboard.js +9 -2
- data/assets/js/search-modal.js +239 -0
- data/assets/js/ui-enhancements.js +164 -0
- data/scripts/init_setup.sh +544 -0
- data/scripts/test-notebook-conversion.sh +105 -0
- metadata +11 -6
data/assets/js/code-copy.js
CHANGED
|
@@ -1,27 +1,93 @@
|
|
|
1
1
|
document.addEventListener('DOMContentLoaded', function () {
|
|
2
|
+
// Enhanced code copy functionality with better UX
|
|
2
3
|
document
|
|
3
|
-
.querySelectorAll('pre.highlight')
|
|
4
|
+
.querySelectorAll('pre.highlight, pre code')
|
|
4
5
|
.forEach(function (pre) {
|
|
6
|
+
// Skip if already has copy button
|
|
7
|
+
if (pre.querySelector('.copy')) return;
|
|
8
|
+
|
|
9
|
+
// Find the actual pre element (might be parent)
|
|
10
|
+
var preElement = pre.tagName === 'PRE' ? pre : pre.closest('pre');
|
|
11
|
+
if (!preElement) return;
|
|
12
|
+
|
|
5
13
|
var button = document.createElement('button');
|
|
6
14
|
var copyText = 'Copy';
|
|
15
|
+
var copiedText = 'Copied!';
|
|
7
16
|
button.className = 'copy';
|
|
8
17
|
button.type = 'button';
|
|
9
|
-
button.
|
|
10
|
-
button.
|
|
11
|
-
button.
|
|
12
|
-
button.
|
|
13
|
-
|
|
18
|
+
button.setAttribute('aria-label', 'Copy code to clipboard');
|
|
19
|
+
button.setAttribute('title', 'Copy code to clipboard');
|
|
20
|
+
button.innerHTML = '<i class="bi bi-clipboard me-1"></i>' + copyText;
|
|
21
|
+
button.tabIndex = 0;
|
|
22
|
+
|
|
23
|
+
// Enhanced click handler with better feedback
|
|
24
|
+
button.addEventListener('click', function (e) {
|
|
25
|
+
e.preventDefault();
|
|
26
|
+
e.stopPropagation();
|
|
27
|
+
|
|
28
|
+
var codeElement = preElement.querySelector('code');
|
|
29
|
+
if (!codeElement) return;
|
|
30
|
+
|
|
31
|
+
var code = codeElement.innerText
|
|
14
32
|
.split('\n')
|
|
15
33
|
.filter(line => !line.trim().startsWith('#'))
|
|
16
34
|
.join('\n')
|
|
17
35
|
.trim();
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
36
|
+
|
|
37
|
+
// Use modern Clipboard API with fallback
|
|
38
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
39
|
+
navigator.clipboard.writeText(code).then(function() {
|
|
40
|
+
// Success feedback
|
|
41
|
+
button.innerHTML = '<i class="bi bi-check-circle me-1"></i>' + copiedText;
|
|
42
|
+
button.classList.add('copied');
|
|
43
|
+
|
|
44
|
+
setTimeout(function () {
|
|
45
|
+
button.innerHTML = '<i class="bi bi-clipboard me-1"></i>' + copyText;
|
|
46
|
+
button.classList.remove('copied');
|
|
47
|
+
}, 2000);
|
|
48
|
+
}).catch(function(err) {
|
|
49
|
+
console.error('Failed to copy:', err);
|
|
50
|
+
fallbackCopy(code, button, copyText);
|
|
51
|
+
});
|
|
52
|
+
} else {
|
|
53
|
+
fallbackCopy(code, button, copyText);
|
|
54
|
+
}
|
|
23
55
|
});
|
|
24
|
-
|
|
25
|
-
|
|
56
|
+
|
|
57
|
+
// Ensure pre has position relative for absolute positioning
|
|
58
|
+
if (getComputedStyle(preElement).position === 'static') {
|
|
59
|
+
preElement.style.position = 'relative';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
preElement.appendChild(button);
|
|
63
|
+
preElement.classList.add('has-copy-button');
|
|
26
64
|
});
|
|
65
|
+
|
|
66
|
+
// Fallback copy method for older browsers
|
|
67
|
+
function fallbackCopy(text, button, copyText) {
|
|
68
|
+
var textArea = document.createElement('textarea');
|
|
69
|
+
textArea.value = text;
|
|
70
|
+
textArea.style.position = 'fixed';
|
|
71
|
+
textArea.style.opacity = '0';
|
|
72
|
+
document.body.appendChild(textArea);
|
|
73
|
+
textArea.select();
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
document.execCommand('copy');
|
|
77
|
+
button.innerHTML = '<i class="bi bi-check-circle me-1"></i>Copied!';
|
|
78
|
+
button.classList.add('copied');
|
|
79
|
+
setTimeout(function () {
|
|
80
|
+
button.innerHTML = '<i class="bi bi-clipboard me-1"></i>' + copyText;
|
|
81
|
+
button.classList.remove('copied');
|
|
82
|
+
}, 2000);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
console.error('Fallback copy failed:', err);
|
|
85
|
+
button.innerHTML = '<i class="bi bi-x-circle me-1"></i>Failed';
|
|
86
|
+
setTimeout(function () {
|
|
87
|
+
button.innerHTML = '<i class="bi bi-clipboard me-1"></i>' + copyText;
|
|
88
|
+
}, 2000);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
document.body.removeChild(textArea);
|
|
92
|
+
}
|
|
27
93
|
});
|
|
@@ -103,6 +103,11 @@ export class KeyboardShortcuts {
|
|
|
103
103
|
this._toggleToc();
|
|
104
104
|
}
|
|
105
105
|
break;
|
|
106
|
+
default:
|
|
107
|
+
if (event.code === 'Slash' && keys.search === '/') {
|
|
108
|
+
event.preventDefault();
|
|
109
|
+
this._focusSearch();
|
|
110
|
+
}
|
|
106
111
|
}
|
|
107
112
|
}
|
|
108
113
|
|
|
@@ -169,10 +174,12 @@ export class KeyboardShortcuts {
|
|
|
169
174
|
*/
|
|
170
175
|
_focusSearch() {
|
|
171
176
|
const searchInput = document.querySelector('#search-input, [data-search-input]');
|
|
172
|
-
|
|
177
|
+
const searchModal = searchInput?.closest('.modal');
|
|
178
|
+
const modalVisible = searchModal?.classList.contains('show');
|
|
179
|
+
|
|
180
|
+
if (searchInput && (!searchModal || modalVisible)) {
|
|
173
181
|
searchInput.focus();
|
|
174
182
|
} else {
|
|
175
|
-
console.log('KeyboardShortcuts: Search not yet implemented');
|
|
176
183
|
// Dispatch event so other modules can handle
|
|
177
184
|
document.dispatchEvent(new CustomEvent('navigation:searchRequest'));
|
|
178
185
|
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search Modal Controller
|
|
3
|
+
* - Opens modal on navigation:searchRequest event ("/" shortcut)
|
|
4
|
+
* - Focuses search input on open
|
|
5
|
+
*/
|
|
6
|
+
(function() {
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
function initSearchModal() {
|
|
10
|
+
const modalEl = document.getElementById('siteSearchModal');
|
|
11
|
+
if (!modalEl) return;
|
|
12
|
+
|
|
13
|
+
const searchInput = modalEl.querySelector('[data-search-input]');
|
|
14
|
+
const searchForm = modalEl.querySelector('[data-search-form]');
|
|
15
|
+
const resultsContainer = modalEl.querySelector('[data-search-results]');
|
|
16
|
+
const emptyState = modalEl.querySelector('[data-search-empty]');
|
|
17
|
+
const searchIndexUrl = new URL('/search.json', window.location.origin);
|
|
18
|
+
let searchIndex = null;
|
|
19
|
+
let searchIndexPromise = null;
|
|
20
|
+
let searchTimeout = null;
|
|
21
|
+
|
|
22
|
+
const openModal = () => {
|
|
23
|
+
const modalInstance = typeof bootstrap !== 'undefined'
|
|
24
|
+
? bootstrap.Modal.getOrCreateInstance(modalEl)
|
|
25
|
+
: null;
|
|
26
|
+
|
|
27
|
+
if (modalInstance) {
|
|
28
|
+
modalInstance.show();
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Open modal when keyboard shortcut requests search
|
|
33
|
+
document.addEventListener('navigation:searchRequest', openModal);
|
|
34
|
+
|
|
35
|
+
// Open modal when clicking a search toggle button
|
|
36
|
+
document.querySelectorAll('[data-search-toggle]').forEach((button) => {
|
|
37
|
+
button.addEventListener('click', (event) => {
|
|
38
|
+
event.preventDefault();
|
|
39
|
+
openModal();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Fallback keyboard shortcut ("/" or Cmd/Ctrl+K) in case other modules are unavailable
|
|
44
|
+
document.addEventListener('keydown', (event) => {
|
|
45
|
+
if (event.target.matches('input, textarea, select, [contenteditable="true"]')) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const isSearchSlash = event.key === '/' || event.code === 'Slash';
|
|
49
|
+
const isSearchShortcut = (event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k';
|
|
50
|
+
if (isSearchSlash || isSearchShortcut) {
|
|
51
|
+
event.preventDefault();
|
|
52
|
+
openModal();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Ensure focus on input once modal is shown
|
|
57
|
+
modalEl.addEventListener('shown.bs.modal', () => {
|
|
58
|
+
if (searchInput) {
|
|
59
|
+
searchInput.focus();
|
|
60
|
+
searchInput.select();
|
|
61
|
+
}
|
|
62
|
+
if (searchInput && searchInput.value.trim()) {
|
|
63
|
+
triggerSearch();
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Clear input when modal closes
|
|
68
|
+
modalEl.addEventListener('hidden.bs.modal', () => {
|
|
69
|
+
if (searchInput) {
|
|
70
|
+
searchInput.value = '';
|
|
71
|
+
}
|
|
72
|
+
clearResults();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Prevent empty submissions
|
|
76
|
+
if (searchForm && searchInput) {
|
|
77
|
+
searchForm.addEventListener('submit', (event) => {
|
|
78
|
+
if (!searchInput.value.trim()) {
|
|
79
|
+
event.preventDefault();
|
|
80
|
+
searchInput.focus();
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (searchInput) {
|
|
86
|
+
searchInput.addEventListener('input', () => {
|
|
87
|
+
clearTimeout(searchTimeout);
|
|
88
|
+
searchTimeout = setTimeout(() => triggerSearch(), 200);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function clearResults() {
|
|
93
|
+
if (!resultsContainer) return;
|
|
94
|
+
resultsContainer.innerHTML = '';
|
|
95
|
+
if (emptyState) {
|
|
96
|
+
emptyState.textContent = 'Start typing to see results.';
|
|
97
|
+
emptyState.classList.remove('d-none');
|
|
98
|
+
resultsContainer.appendChild(emptyState);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function renderResults(items, query) {
|
|
103
|
+
if (!resultsContainer) return;
|
|
104
|
+
resultsContainer.innerHTML = '';
|
|
105
|
+
|
|
106
|
+
if (!query) {
|
|
107
|
+
clearResults();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!items.length) {
|
|
112
|
+
const empty = document.createElement('div');
|
|
113
|
+
empty.className = 'text-muted small';
|
|
114
|
+
empty.textContent = 'No results found.';
|
|
115
|
+
resultsContainer.appendChild(empty);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const list = document.createElement('div');
|
|
120
|
+
list.className = 'list-group';
|
|
121
|
+
|
|
122
|
+
items.slice(0, 8).forEach((item) => {
|
|
123
|
+
const link = document.createElement('a');
|
|
124
|
+
link.className = 'list-group-item list-group-item-action';
|
|
125
|
+
link.href = item.url;
|
|
126
|
+
|
|
127
|
+
const title = document.createElement('div');
|
|
128
|
+
title.className = 'fw-semibold';
|
|
129
|
+
title.innerHTML = highlightText(item.title || 'Untitled', query);
|
|
130
|
+
link.appendChild(title);
|
|
131
|
+
|
|
132
|
+
const snippet = buildSnippet(item, query);
|
|
133
|
+
if (snippet) {
|
|
134
|
+
const desc = document.createElement('div');
|
|
135
|
+
desc.className = 'small text-muted';
|
|
136
|
+
desc.innerHTML = highlightText(snippet, query);
|
|
137
|
+
link.appendChild(desc);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
list.appendChild(link);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
resultsContainer.appendChild(list);
|
|
144
|
+
|
|
145
|
+
const viewAll = document.createElement('a');
|
|
146
|
+
viewAll.className = 'd-block mt-2 small';
|
|
147
|
+
viewAll.href = `/sitemap/?q=${encodeURIComponent(query)}`;
|
|
148
|
+
viewAll.textContent = 'View all results';
|
|
149
|
+
resultsContainer.appendChild(viewAll);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function escapeHtml(value) {
|
|
153
|
+
return String(value)
|
|
154
|
+
.replace(/&/g, '&')
|
|
155
|
+
.replace(/</g, '<')
|
|
156
|
+
.replace(/>/g, '>')
|
|
157
|
+
.replace(/"/g, '"')
|
|
158
|
+
.replace(/'/g, ''');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function highlightText(text, query) {
|
|
162
|
+
if (!query) return escapeHtml(text);
|
|
163
|
+
const escaped = escapeHtml(text);
|
|
164
|
+
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
165
|
+
const regex = new RegExp(`(${escapedQuery})`, 'ig');
|
|
166
|
+
return escaped.replace(regex, '<mark>$1</mark>');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function buildSnippet(item, query) {
|
|
170
|
+
const description = item.description || '';
|
|
171
|
+
const content = item.content || '';
|
|
172
|
+
if (!description && !content) return '';
|
|
173
|
+
|
|
174
|
+
const lowerQuery = query.toLowerCase();
|
|
175
|
+
const lowerContent = content.toLowerCase();
|
|
176
|
+
const lowerDescription = description.toLowerCase();
|
|
177
|
+
|
|
178
|
+
let sourceText = content;
|
|
179
|
+
let index = lowerContent.indexOf(lowerQuery);
|
|
180
|
+
|
|
181
|
+
if (index === -1 && description) {
|
|
182
|
+
sourceText = description;
|
|
183
|
+
index = lowerDescription.indexOf(lowerQuery);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (index === -1) {
|
|
187
|
+
const fallback = content || description;
|
|
188
|
+
return fallback.length > 140 ? `${fallback.slice(0, 140)}...` : fallback;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const start = Math.max(0, index - 60);
|
|
192
|
+
const end = Math.min(sourceText.length, index + 80);
|
|
193
|
+
const prefix = start > 0 ? '... ' : '';
|
|
194
|
+
const suffix = end < sourceText.length ? ' ...' : '';
|
|
195
|
+
return `${prefix}${sourceText.slice(start, end)}${suffix}`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function loadSearchIndex() {
|
|
199
|
+
if (searchIndex) return Promise.resolve(searchIndex);
|
|
200
|
+
if (!searchIndexPromise) {
|
|
201
|
+
searchIndexPromise = fetch(searchIndexUrl.toString())
|
|
202
|
+
.then((response) => (response.ok ? response.json() : []))
|
|
203
|
+
.then((data) => {
|
|
204
|
+
searchIndex = Array.isArray(data) ? data : [];
|
|
205
|
+
return searchIndex;
|
|
206
|
+
})
|
|
207
|
+
.catch(() => []);
|
|
208
|
+
}
|
|
209
|
+
return searchIndexPromise;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function triggerSearch() {
|
|
213
|
+
if (!searchInput) return;
|
|
214
|
+
const query = searchInput.value.trim().toLowerCase();
|
|
215
|
+
|
|
216
|
+
if (!query) {
|
|
217
|
+
renderResults([], '');
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
loadSearchIndex().then((index) => {
|
|
222
|
+
const matches = index.filter((item) => {
|
|
223
|
+
const title = (item.title || '').toLowerCase();
|
|
224
|
+
const description = (item.description || '').toLowerCase();
|
|
225
|
+
const content = (item.content || '').toLowerCase();
|
|
226
|
+
return title.includes(query) || description.includes(query) || content.includes(query);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
renderResults(matches, query);
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (document.readyState === 'loading') {
|
|
235
|
+
document.addEventListener('DOMContentLoaded', initSearchModal);
|
|
236
|
+
} else {
|
|
237
|
+
initSearchModal();
|
|
238
|
+
}
|
|
239
|
+
})();
|
|
@@ -0,0 +1,164 @@
|
|
|
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
|
+
* Smooth scroll for anchor links
|
|
47
|
+
*/
|
|
48
|
+
function initSmoothScroll() {
|
|
49
|
+
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
|
50
|
+
anchor.addEventListener('click', function(e) {
|
|
51
|
+
const href = this.getAttribute('href');
|
|
52
|
+
if (href === '#' || href === '') return;
|
|
53
|
+
|
|
54
|
+
const target = document.querySelector(href);
|
|
55
|
+
if (target) {
|
|
56
|
+
e.preventDefault();
|
|
57
|
+
const offsetTop = target.offsetTop - 80; // Account for fixed navbar
|
|
58
|
+
|
|
59
|
+
window.scrollTo({
|
|
60
|
+
top: offsetTop,
|
|
61
|
+
behavior: prefersReducedMotion ? 'auto' : 'smooth'
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Add loading state to images
|
|
70
|
+
*/
|
|
71
|
+
function initImageLoading() {
|
|
72
|
+
const images = document.querySelectorAll('img[loading="lazy"]');
|
|
73
|
+
|
|
74
|
+
images.forEach(img => {
|
|
75
|
+
if (img.complete) {
|
|
76
|
+
img.classList.add('loaded');
|
|
77
|
+
} else {
|
|
78
|
+
img.addEventListener('load', function() {
|
|
79
|
+
this.classList.add('loaded');
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Enhance button ripple effect
|
|
87
|
+
*/
|
|
88
|
+
function initButtonRipples() {
|
|
89
|
+
if (prefersReducedMotion) return;
|
|
90
|
+
|
|
91
|
+
document.querySelectorAll('.btn').forEach(button => {
|
|
92
|
+
button.addEventListener('click', function(e) {
|
|
93
|
+
const ripple = document.createElement('span');
|
|
94
|
+
const rect = this.getBoundingClientRect();
|
|
95
|
+
const size = Math.max(rect.width, rect.height);
|
|
96
|
+
const x = e.clientX - rect.left - size / 2;
|
|
97
|
+
const y = e.clientY - rect.top - size / 2;
|
|
98
|
+
|
|
99
|
+
ripple.style.width = ripple.style.height = size + 'px';
|
|
100
|
+
ripple.style.left = x + 'px';
|
|
101
|
+
ripple.style.top = y + 'px';
|
|
102
|
+
ripple.classList.add('ripple');
|
|
103
|
+
|
|
104
|
+
this.appendChild(ripple);
|
|
105
|
+
|
|
106
|
+
setTimeout(() => {
|
|
107
|
+
ripple.remove();
|
|
108
|
+
}, 600);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Add active state to navigation links on scroll
|
|
115
|
+
*/
|
|
116
|
+
function initScrollSpy() {
|
|
117
|
+
const sections = document.querySelectorAll('section[id], [id^="get-started"], [id^="features"]');
|
|
118
|
+
const navLinks = document.querySelectorAll('a[href^="#"]');
|
|
119
|
+
|
|
120
|
+
if (!sections.length || !navLinks.length) return;
|
|
121
|
+
|
|
122
|
+
const observerOptions = {
|
|
123
|
+
threshold: 0.3,
|
|
124
|
+
rootMargin: '-80px 0px -50% 0px'
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const observer = new IntersectionObserver((entries) => {
|
|
128
|
+
entries.forEach(entry => {
|
|
129
|
+
if (entry.isIntersecting) {
|
|
130
|
+
const id = entry.target.getAttribute('id');
|
|
131
|
+
navLinks.forEach(link => {
|
|
132
|
+
link.classList.remove('active');
|
|
133
|
+
if (link.getAttribute('href') === `#${id}`) {
|
|
134
|
+
link.classList.add('active');
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}, observerOptions);
|
|
140
|
+
|
|
141
|
+
sections.forEach(section => observer.observe(section));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Initialize all enhancements
|
|
146
|
+
*/
|
|
147
|
+
function init() {
|
|
148
|
+
// Wait for DOM to be fully loaded
|
|
149
|
+
if (document.readyState === 'loading') {
|
|
150
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
initScrollAnimations();
|
|
155
|
+
initSmoothScroll();
|
|
156
|
+
initImageLoading();
|
|
157
|
+
initButtonRipples();
|
|
158
|
+
initScrollSpy();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Start initialization
|
|
162
|
+
init();
|
|
163
|
+
})();
|
|
164
|
+
|