@kenjura/ursa 0.10.0 → 0.32.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.
package/meta/menu.js ADDED
@@ -0,0 +1,371 @@
1
+ document.addEventListener('DOMContentLoaded', () => {
2
+ const navMain = document.querySelector('nav#nav-main');
3
+ if (!navMain) return;
4
+
5
+ // Load menu data from embedded JSON
6
+ const menuDataScript = document.getElementById('menu-data');
7
+ if (!menuDataScript) return;
8
+
9
+ let menuData;
10
+ try {
11
+ menuData = JSON.parse(menuDataScript.textContent);
12
+ } catch (e) {
13
+ console.error('Failed to parse menu data:', e);
14
+ return;
15
+ }
16
+
17
+ // State
18
+ let currentPath = []; // Array of path segments representing current directory
19
+ let expandedLevel1 = new Set(); // Track which level-1 items are expanded
20
+
21
+ // DOM elements
22
+ const breadcrumb = navMain.querySelector('.menu-breadcrumb');
23
+ const backButton = navMain.querySelector('.menu-back');
24
+ const homeButton = navMain.querySelector('.menu-home');
25
+ const currentPathSpan = navMain.querySelector('.menu-current-path');
26
+ const menuContainer = navMain.querySelector('.menu-level');
27
+
28
+ // Helper to check if we're on mobile
29
+ const isMobile = () => window.matchMedia('(max-width: 800px)').matches;
30
+
31
+ // Get items at a specific path
32
+ function getItemsAtPath(path) {
33
+ let items = menuData;
34
+ for (const segment of path) {
35
+ const folder = items.find(item => item.path === (path.slice(0, path.indexOf(segment) + 1).join('/') || segment));
36
+ if (folder && folder.children) {
37
+ items = folder.children;
38
+ } else {
39
+ return [];
40
+ }
41
+ }
42
+ return items;
43
+ }
44
+
45
+ // Find item by path
46
+ function findItemByPath(pathString) {
47
+ const segments = pathString.split('/').filter(Boolean);
48
+ let items = menuData;
49
+ let item = null;
50
+
51
+ for (let i = 0; i < segments.length; i++) {
52
+ const targetPath = segments.slice(0, i + 1).join('/');
53
+ item = items.find(it => it.path === targetPath);
54
+ if (!item) return null;
55
+ if (item.children && i < segments.length - 1) {
56
+ items = item.children;
57
+ }
58
+ }
59
+ return item;
60
+ }
61
+
62
+ // Check if current page is within an item's tree
63
+ function isCurrentPageInTree(item) {
64
+ if (isCurrentPage(item)) return true;
65
+ if (item.children) {
66
+ for (const child of item.children) {
67
+ if (isCurrentPageInTree(child)) return true;
68
+ }
69
+ }
70
+ return false;
71
+ }
72
+
73
+ // Find the current page item within an item's children (level 2)
74
+ function findCurrentPageChild(item) {
75
+ if (!item.children) return null;
76
+ for (const child of item.children) {
77
+ if (isCurrentPage(child)) return child;
78
+ // Also check grandchildren in case current page is deeper
79
+ if (isCurrentPageInTree(child)) return child;
80
+ }
81
+ return null;
82
+ }
83
+
84
+ // Track which level-1 item contains the current page (for special collapse behavior)
85
+ let currentPageParentPath = null;
86
+
87
+ // Render menu at current path
88
+ function renderMenu() {
89
+ // Get items for current level (level 1)
90
+ let level1Items;
91
+ if (currentPath.length === 0) {
92
+ level1Items = menuData;
93
+ } else {
94
+ const currentPathString = currentPath.join('/');
95
+ const currentFolder = findItemByPath(currentPathString);
96
+ level1Items = currentFolder?.children || [];
97
+ }
98
+
99
+ // Find which level-1 item contains the current page
100
+ currentPageParentPath = null;
101
+ for (const item of level1Items) {
102
+ if (item.hasChildren && isCurrentPageInTree(item)) {
103
+ currentPageParentPath = item.path;
104
+ break;
105
+ }
106
+ }
107
+
108
+ // Build HTML for level 1 and level 2
109
+ let html = '';
110
+ for (const item of level1Items) {
111
+ const isActive = isCurrentPage(item);
112
+ const activeClass = isActive ? ' current-menu-item' : '';
113
+ const hasChildrenClass = item.hasChildren ? ' has-children' : '';
114
+
115
+ // Level-1 items with children get a caret, not triple-dot
116
+ // Expanded if: manually expanded, or current page is in this tree
117
+ const isExpanded = expandedLevel1.has(item.path) || isCurrentPageInTree(item);
118
+ const expandedClass = isExpanded ? ' expanded' : '';
119
+ const caretIndicator = item.hasChildren
120
+ ? `<span class="menu-caret">${isExpanded ? '▼' : '▶'}</span>`
121
+ : '';
122
+
123
+ const labelHtml = item.href
124
+ ? `<a href="${item.href}" class="menu-label">${item.label}</a>`
125
+ : `<span class="menu-label">${item.label}</span>`;
126
+
127
+ html += `
128
+ <li class="menu-item level-1${hasChildrenClass}${activeClass}${expandedClass}" data-path="${item.path}">
129
+ <div class="menu-item-row">
130
+ ${item.icon}
131
+ ${labelHtml}
132
+ ${caretIndicator}
133
+ </div>`;
134
+
135
+ // Determine which children to render
136
+ let childrenToRender = [];
137
+ if (item.children && item.children.length > 0) {
138
+ if (isExpanded) {
139
+ // Fully expanded - show all children
140
+ childrenToRender = item.children;
141
+ } else if (item.path === currentPageParentPath) {
142
+ // Collapsed but contains current page - show only current page
143
+ const currentChild = findCurrentPageChild(item);
144
+ if (currentChild) {
145
+ childrenToRender = [currentChild];
146
+ }
147
+ }
148
+ }
149
+
150
+ if (childrenToRender.length > 0) {
151
+ html += '<ul class="menu-sublevel">';
152
+ for (const child of childrenToRender) {
153
+ const childActive = isCurrentPage(child);
154
+ const childActiveClass = childActive ? ' current-menu-item' : '';
155
+ const childHasChildren = child.hasChildren ? ' has-children' : '';
156
+ // Level-2 items with children get triple-dot
157
+ const childMoreIndicator = child.hasChildren ? '<span class="menu-more" title="Has sub-items">⋮</span>' : '';
158
+
159
+ const childLabelHtml = child.href
160
+ ? `<a href="${child.href}" class="menu-label">${child.label}</a>`
161
+ : `<span class="menu-label">${child.label}</span>`;
162
+
163
+ html += `
164
+ <li class="menu-item level-2${childHasChildren}${childActiveClass}" data-path="${child.path}">
165
+ <div class="menu-item-row">
166
+ ${child.icon}
167
+ ${childLabelHtml}
168
+ ${childMoreIndicator}
169
+ </div>
170
+ </li>`;
171
+ }
172
+ html += '</ul>';
173
+ }
174
+
175
+ html += '</li>';
176
+ }
177
+
178
+ menuContainer.innerHTML = html;
179
+
180
+ // Update breadcrumb
181
+ if (currentPath.length > 0) {
182
+ breadcrumb.style.display = 'flex';
183
+ currentPathSpan.textContent = currentPath[currentPath.length - 1];
184
+ } else {
185
+ breadcrumb.style.display = 'none';
186
+ }
187
+
188
+ // Attach click handlers
189
+ attachClickHandlers();
190
+ }
191
+
192
+ // Check if an item matches the current page
193
+ function isCurrentPage(item) {
194
+ if (!item.href) return false;
195
+ const currentHref = window.location.pathname;
196
+ // Normalize paths for comparison
197
+ const normalizedItemHref = item.href.replace(/\/index\.html$/, '').replace(/\.html$/, '');
198
+ const normalizedCurrentHref = currentHref.replace(/\/index\.html$/, '').replace(/\.html$/, '');
199
+ return normalizedItemHref === normalizedCurrentHref;
200
+ }
201
+
202
+ // Navigate to a folder
203
+ function navigateToFolder(pathString) {
204
+ currentPath = pathString.split('/').filter(Boolean);
205
+ renderMenu();
206
+ }
207
+
208
+ // Go back one level
209
+ function goBack() {
210
+ if (currentPath.length > 0) {
211
+ currentPath.pop();
212
+ renderMenu();
213
+ }
214
+ }
215
+
216
+ // Go to root
217
+ function goToRoot() {
218
+ currentPath = [];
219
+ renderMenu();
220
+ }
221
+
222
+ // Attach click handlers to menu items
223
+ function attachClickHandlers() {
224
+ const menuItems = menuContainer.querySelectorAll('.menu-item');
225
+ menuItems.forEach(li => {
226
+ const row = li.querySelector('.menu-item-row');
227
+ const caret = li.querySelector('.menu-caret');
228
+ const moreBtn = li.querySelector('.menu-more');
229
+ const link = li.querySelector('a.menu-label');
230
+ const isLevel1 = li.classList.contains('level-1');
231
+ const isLevel2 = li.classList.contains('level-2');
232
+
233
+ if (li.classList.contains('has-children')) {
234
+ if (isLevel1) {
235
+ // Level-1: clicking caret or row (not link) toggles expand/collapse
236
+ const toggleExpand = (e) => {
237
+ e.preventDefault();
238
+ e.stopPropagation();
239
+ const path = li.dataset.path;
240
+ const hasLink = !!link;
241
+
242
+ if (expandedLevel1.has(path)) {
243
+ // Collapsing this item
244
+ expandedLevel1.delete(path);
245
+ } else {
246
+ // Expanding this item
247
+ // If this item has no link (non-navigable folder), collapse others
248
+ if (!hasLink) {
249
+ expandedLevel1.clear();
250
+ }
251
+ expandedLevel1.add(path);
252
+ }
253
+ renderMenu();
254
+ };
255
+
256
+ if (caret) {
257
+ caret.addEventListener('click', toggleExpand);
258
+ }
259
+
260
+ row.addEventListener('click', (e) => {
261
+ if (!e.target.closest('a')) {
262
+ toggleExpand(e);
263
+ }
264
+ });
265
+ } else if (isLevel2) {
266
+ // Level-2 with children: clicking ⋮ or row navigates into folder
267
+ if (moreBtn) {
268
+ moreBtn.addEventListener('click', (e) => {
269
+ e.preventDefault();
270
+ e.stopPropagation();
271
+ navigateToFolder(li.dataset.path);
272
+ });
273
+ }
274
+
275
+ row.addEventListener('click', (e) => {
276
+ if (!e.target.closest('a')) {
277
+ e.preventDefault();
278
+ navigateToFolder(li.dataset.path);
279
+ }
280
+ });
281
+ }
282
+ }
283
+ });
284
+ }
285
+
286
+ // Breadcrumb navigation
287
+ if (backButton) {
288
+ backButton.addEventListener('click', goBack);
289
+ }
290
+ if (homeButton) {
291
+ homeButton.addEventListener('click', goToRoot);
292
+ }
293
+
294
+ // Hamburger menu button toggle
295
+ const menuButton = document.querySelector('button.menu-button');
296
+ if (menuButton) {
297
+ const updateButtonIcon = (isOpen) => {
298
+ menuButton.textContent = isOpen ? '✕' : '☰';
299
+ menuButton.setAttribute('aria-expanded', isOpen);
300
+ menuButton.setAttribute('aria-label', isOpen ? 'Close menu' : 'Open menu');
301
+ };
302
+
303
+ menuButton.addEventListener('click', (e) => {
304
+ e.preventDefault();
305
+ e.stopPropagation();
306
+
307
+ if (isMobile()) {
308
+ navMain.classList.toggle('active');
309
+ const isExpanded = navMain.classList.contains('active');
310
+ updateButtonIcon(isExpanded);
311
+ } else {
312
+ navMain.classList.toggle('collapsed');
313
+ const isExpanded = !navMain.classList.contains('collapsed');
314
+ menuButton.setAttribute('aria-expanded', isExpanded);
315
+ }
316
+ });
317
+
318
+ document.addEventListener('click', (e) => {
319
+ if (isMobile() &&
320
+ navMain.classList.contains('active') &&
321
+ !navMain.contains(e.target) &&
322
+ !menuButton.contains(e.target)) {
323
+ navMain.classList.remove('active');
324
+ updateButtonIcon(false);
325
+ }
326
+ });
327
+
328
+ document.addEventListener('keydown', (e) => {
329
+ if (e.key === 'Escape') {
330
+ if (isMobile() && navMain.classList.contains('active')) {
331
+ navMain.classList.remove('active');
332
+ updateButtonIcon(false);
333
+ menuButton.focus();
334
+ }
335
+ }
336
+ });
337
+ }
338
+
339
+ // Initialize: find current page and set appropriate path
340
+ function initializeFromCurrentPage() {
341
+ const currentHref = window.location.pathname;
342
+ const pathParts = currentHref.split('/').filter(Boolean);
343
+
344
+ // Try to find the deepest matching folder
345
+ if (pathParts.length > 1) {
346
+ // Navigate to parent folder of current page
347
+ const parentPath = pathParts.slice(0, -1);
348
+
349
+ // Check if this path exists in menu data
350
+ let testPath = [];
351
+ for (const part of parentPath) {
352
+ testPath.push(part);
353
+ const item = findItemByPath(testPath.join('/'));
354
+ if (!item || !item.hasChildren) {
355
+ // Path doesn't exist or isn't a folder, stop here
356
+ testPath.pop();
357
+ break;
358
+ }
359
+ }
360
+
361
+ // If we found a valid folder path that's more than 1 level deep, navigate there
362
+ if (testPath.length > 1) {
363
+ currentPath = testPath.slice(0, -1); // Go to grandparent so current folder is visible
364
+ }
365
+ }
366
+
367
+ renderMenu();
368
+ }
369
+
370
+ initializeFromCurrentPage();
371
+ });
package/meta/search.js ADDED
@@ -0,0 +1,208 @@
1
+ // Global search functionality with typeahead
2
+ class GlobalSearch {
3
+ constructor() {
4
+ this.searchInput = document.getElementById('global-search');
5
+ this.searchResults = null;
6
+ this.searchIndex = window.SEARCH_INDEX || [];
7
+ this.currentSelection = -1;
8
+
9
+ if (!this.searchInput) return;
10
+
11
+ this.init();
12
+ }
13
+
14
+ init() {
15
+ this.createResultsContainer();
16
+ this.bindEvents();
17
+ }
18
+
19
+ createResultsContainer() {
20
+ this.searchResults = document.createElement('div');
21
+ this.searchResults.id = 'search-results';
22
+ this.searchResults.className = 'search-results hidden';
23
+ this.searchInput.parentNode.appendChild(this.searchResults);
24
+ }
25
+
26
+ bindEvents() {
27
+ // Input events
28
+ this.searchInput.addEventListener('input', (e) => {
29
+ this.handleSearch(e.target.value);
30
+ });
31
+
32
+ // Keyboard navigation
33
+ this.searchInput.addEventListener('keydown', (e) => {
34
+ this.handleKeydown(e);
35
+ });
36
+
37
+ // Focus events
38
+ this.searchInput.addEventListener('focus', () => {
39
+ if (this.searchInput.value.trim()) {
40
+ this.handleSearch(this.searchInput.value);
41
+ }
42
+ });
43
+
44
+ this.searchInput.addEventListener('blur', (e) => {
45
+ // Delay hiding to allow click on results
46
+ setTimeout(() => {
47
+ this.hideResults();
48
+ }, 150);
49
+ });
50
+
51
+ // Click outside to close
52
+ document.addEventListener('click', (e) => {
53
+ if (!this.searchInput.contains(e.target) && !this.searchResults.contains(e.target)) {
54
+ this.hideResults();
55
+ }
56
+ });
57
+ }
58
+
59
+ handleSearch(query) {
60
+ if (!query || query.length < 2) {
61
+ this.hideResults();
62
+ return;
63
+ }
64
+
65
+ const results = this.search(query);
66
+ this.displayResults(results);
67
+ }
68
+
69
+ search(query) {
70
+ const normalizedQuery = query.toLowerCase().trim();
71
+ const results = [];
72
+
73
+ // Search through the index
74
+ this.searchIndex.forEach(item => {
75
+ const titleMatch = item.title.toLowerCase().includes(normalizedQuery);
76
+ const pathMatch = item.path.toLowerCase().includes(normalizedQuery);
77
+ const contentMatch = item.content && item.content.toLowerCase().includes(normalizedQuery);
78
+
79
+ if (titleMatch || pathMatch || contentMatch) {
80
+ let score = 0;
81
+
82
+ // Boost exact title matches
83
+ if (item.title.toLowerCase() === normalizedQuery) score += 100;
84
+ else if (item.title.toLowerCase().startsWith(normalizedQuery)) score += 50;
85
+ else if (titleMatch) score += 25;
86
+
87
+ // Boost path matches
88
+ if (pathMatch) score += 10;
89
+
90
+ // Content matches get lower score
91
+ if (contentMatch) score += 5;
92
+
93
+ results.push({ ...item, score });
94
+ }
95
+ });
96
+
97
+ // Sort by score, then by title
98
+ return results
99
+ .sort((a, b) => {
100
+ if (b.score !== a.score) return b.score - a.score;
101
+ return a.title.localeCompare(b.title);
102
+ })
103
+ .slice(0, 10); // Limit to 10 results
104
+ }
105
+
106
+ displayResults(results) {
107
+ if (results.length === 0) {
108
+ this.hideResults();
109
+ return;
110
+ }
111
+
112
+ this.searchResults.innerHTML = '';
113
+ this.currentSelection = -1;
114
+
115
+ results.forEach((result, index) => {
116
+ const item = document.createElement('div');
117
+ item.className = 'search-result-item';
118
+ item.dataset.index = index;
119
+
120
+ const title = document.createElement('div');
121
+ title.className = 'search-result-title';
122
+ title.textContent = result.title;
123
+
124
+ const path = document.createElement('div');
125
+ path.className = 'search-result-path';
126
+ path.textContent = result.path;
127
+
128
+ item.appendChild(title);
129
+ item.appendChild(path);
130
+
131
+ // Click handler
132
+ item.addEventListener('click', () => {
133
+ this.navigateToResult(result);
134
+ });
135
+
136
+ this.searchResults.appendChild(item);
137
+ });
138
+
139
+ this.showResults();
140
+ }
141
+
142
+ showResults() {
143
+ this.searchResults.classList.remove('hidden');
144
+ }
145
+
146
+ hideResults() {
147
+ this.searchResults.classList.add('hidden');
148
+ this.currentSelection = -1;
149
+ }
150
+
151
+ handleKeydown(e) {
152
+ const items = this.searchResults.querySelectorAll('.search-result-item');
153
+
154
+ switch (e.key) {
155
+ case 'ArrowDown':
156
+ e.preventDefault();
157
+ this.currentSelection = Math.min(this.currentSelection + 1, items.length - 1);
158
+ this.updateSelection();
159
+ break;
160
+
161
+ case 'ArrowUp':
162
+ e.preventDefault();
163
+ this.currentSelection = Math.max(this.currentSelection - 1, -1);
164
+ this.updateSelection();
165
+ break;
166
+
167
+ case 'Enter':
168
+ e.preventDefault();
169
+ if (this.currentSelection >= 0 && items[this.currentSelection]) {
170
+ const index = items[this.currentSelection].dataset.index;
171
+ const results = this.getLastSearchResults();
172
+ if (results && results[index]) {
173
+ this.navigateToResult(results[index]);
174
+ }
175
+ }
176
+ break;
177
+
178
+ case 'Escape':
179
+ this.hideResults();
180
+ this.searchInput.blur();
181
+ break;
182
+ }
183
+ }
184
+
185
+ updateSelection() {
186
+ const items = this.searchResults.querySelectorAll('.search-result-item');
187
+ items.forEach((item, index) => {
188
+ item.classList.toggle('selected', index === this.currentSelection);
189
+ });
190
+ }
191
+
192
+ getLastSearchResults() {
193
+ // Store last search results for keyboard navigation
194
+ if (!this._lastResults) {
195
+ this._lastResults = this.search(this.searchInput.value);
196
+ }
197
+ return this._lastResults;
198
+ }
199
+
200
+ navigateToResult(result) {
201
+ window.location.href = result.url;
202
+ }
203
+ }
204
+
205
+ // Initialize when DOM is loaded
206
+ document.addEventListener('DOMContentLoaded', () => {
207
+ new GlobalSearch();
208
+ });
@@ -0,0 +1,36 @@
1
+ document.addEventListener('DOMContentLoaded', () => {
2
+ const article = document.querySelector('article#main-content');
3
+ if (!article) return;
4
+
5
+ const children = Array.from(article.children);
6
+ let sections = [];
7
+ let currentSection = document.createElement('section');
8
+ currentSection.classList.add('sectionOuter');
9
+
10
+ for (let i = 0; i < children.length; i++) {
11
+ const el = children[i];
12
+ if (el.tagName === 'H1' && currentSection.childNodes.length > 0) {
13
+ sections.push(currentSection);
14
+ currentSection = document.createElement('section');
15
+ currentSection.classList.add('sectionOuter');
16
+ }
17
+ currentSection.appendChild(el);
18
+ }
19
+ if (currentSection.childNodes.length > 0) {
20
+ sections.push(currentSection);
21
+ }
22
+
23
+ // Remove all existing children
24
+ while (article.firstChild) {
25
+ article.removeChild(article.firstChild);
26
+ }
27
+
28
+ // Append new sections
29
+ sections.forEach(section => article.appendChild(section));
30
+
31
+
32
+ // Optional: Add section numbers or other decorations
33
+ Array.from(article.querySelectorAll('section.sectionOuter')).forEach((section, index) => {
34
+ section.style.setProperty('--section-index', index + 1);
35
+ });
36
+ });
package/meta/sticky.js ADDED
@@ -0,0 +1,73 @@
1
+ document.addEventListener('DOMContentLoaded', () => {
2
+ const article = document.querySelector('article#main-content');
3
+ if (!article) return;
4
+
5
+ const headings = article.querySelectorAll('h1, h2, h3');
6
+
7
+ function updateStuckState() {
8
+ let currentStuckHeading = null;
9
+
10
+ headings.forEach(el => {
11
+ const wasStuck = el.classList.contains('stuck');
12
+ const style = window.getComputedStyle(el);
13
+ if (style.position === 'sticky' && style.top !== 'auto') {
14
+ const rect = el.getBoundingClientRect();
15
+ const top = parseInt(style.top, 10) || 0;
16
+ if (rect.top <= top && rect.bottom > top) {
17
+ el.classList.add('stuck');
18
+ currentStuckHeading = el;
19
+ } else {
20
+ el.classList.remove('stuck');
21
+ }
22
+ } else {
23
+ el.classList.remove('stuck');
24
+ }
25
+
26
+ // Dispatch event if stuck state changed
27
+ if (wasStuck !== el.classList.contains('stuck')) {
28
+ const event = new CustomEvent('headingStuckStateChanged', {
29
+ detail: {
30
+ heading: el,
31
+ stuck: el.classList.contains('stuck'),
32
+ currentStuckHeading: currentStuckHeading
33
+ }
34
+ });
35
+ document.dispatchEvent(event);
36
+ }
37
+
38
+ // Handle text updates for H1 elements
39
+ if (el.tagName === 'H1') {
40
+ // Store original text if not already stored
41
+ if (!el.dataset.originalText) {
42
+ el.dataset.originalText = el.textContent;
43
+ }
44
+
45
+ // Only update text if this H1 is stuck
46
+ if (el.classList.contains('stuck')) {
47
+ // Find the last stuck h2 and h3 (i.e., the "current" ones)
48
+ const stuckH2s = Array.from(headings).filter(h => h.tagName === 'H2' && h.classList.contains('stuck'));
49
+ const stuckH3s = Array.from(headings).filter(h => h.tagName === 'H3' && h.classList.contains('stuck'));
50
+ const stuckH2 = stuckH2s.length ? stuckH2s[stuckH2s.length - 1] : null;
51
+ const stuckH3 = stuckH3s.length ? stuckH3s[stuckH3s.length - 1] : null;
52
+
53
+ let newText = el.dataset.originalText;
54
+
55
+ if (stuckH3 && stuckH2) {
56
+ newText += ' > ' + (stuckH2.dataset.originalText || stuckH2.textContent) + ' > ' + (stuckH3.dataset.originalText || stuckH3.textContent);
57
+ } else if (stuckH2) {
58
+ newText += ' > ' + (stuckH2.dataset.originalText || stuckH2.textContent);
59
+ }
60
+
61
+ el.textContent = newText;
62
+ } else {
63
+ // Restore original text if not stuck
64
+ el.textContent = el.dataset.originalText;
65
+ }
66
+ }
67
+ });
68
+ }
69
+
70
+ updateStuckState();
71
+ window.addEventListener('scroll', updateStuckState, { passive: true });
72
+ window.addEventListener('resize', updateStuckState);
73
+ });