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.
@@ -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, '&lt;')
156
+ .replace(/>/g, '&gt;')
157
+ .replace(/"/g, '&quot;')
158
+ .replace(/'/g, '&#39;');
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();