jekyll-theme-zer0 0.17.2 → 0.18.1
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/js-cdn.html +4 -1
- data/_includes/components/search-modal.html +56 -0
- data/_includes/core/branding.html +12 -13
- data/_includes/core/header.html +38 -11
- data/_includes/navigation/navbar.html +31 -0
- data/_layouts/root.html +3 -0
- data/_sass/custom.scss +82 -0
- data/assets/js/modules/navigation/keyboard.js +9 -2
- data/assets/js/search-modal.js +239 -0
- data/assets/js/ui-enhancements.js +0 -30
- data/scripts/init_setup.sh +544 -0
- data/scripts/test-notebook-conversion.sh +105 -0
- metadata +6 -2
|
@@ -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
|
+
})();
|
|
@@ -42,35 +42,6 @@
|
|
|
42
42
|
});
|
|
43
43
|
}
|
|
44
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
45
|
/**
|
|
75
46
|
* Smooth scroll for anchor links
|
|
76
47
|
*/
|
|
@@ -181,7 +152,6 @@
|
|
|
181
152
|
}
|
|
182
153
|
|
|
183
154
|
initScrollAnimations();
|
|
184
|
-
initCardParallax();
|
|
185
155
|
initSmoothScroll();
|
|
186
156
|
initImageLoading();
|
|
187
157
|
initButtonRipples();
|