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.
@@ -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.ariaLabel = 'Copy code to clipboard';
10
- button.innerText = copyText;
11
- button.tabIndex = 1; // Add this line to make the button focusable with the keyboard
12
- button.addEventListener('click', function () {
13
- var code = pre.querySelector('code').innerText
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
- navigator.clipboard.writeText(code);
19
- button.innerText = 'Copied';
20
- setTimeout(function () {
21
- button.innerText = copyText;
22
- }, 4000);
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
- pre.appendChild(button);
25
- pre.classList.add('has-copy-button'); // Add a class to the pre element
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
- if (searchInput) {
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, '&amp;')
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
+ })();
@@ -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
+