@kenjura/ursa 0.72.0 → 0.75.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/CHANGELOG.md +19 -0
- package/meta/default-template.html +32 -6
- package/meta/default.css +365 -175
- package/meta/menu.js +153 -80
- package/meta/search.js +7 -13
- package/meta/sectionify.js +10 -0
- package/meta/toc-generator.js +12 -6
- package/meta/widgets.js +376 -0
- package/package.json +1 -1
- package/src/dev.js +39 -11
- package/src/helper/automenu.js +102 -12
- package/src/helper/breadcrumbs.js +42 -0
- package/src/helper/build/autoIndex.js +80 -23
- package/src/helper/build/menu.js +4 -4
- package/src/helper/customMenu.js +118 -29
- package/src/helper/imageProcessor.js +38 -8
- package/src/jobs/generate.js +52 -9
package/meta/menu.js
CHANGED
|
@@ -5,13 +5,14 @@
|
|
|
5
5
|
* Each column represents one level of the folder hierarchy.
|
|
6
6
|
*
|
|
7
7
|
* Also supports top menu position for horizontal navigation with dropdowns.
|
|
8
|
+
* Top menu is the default; side menu is used only when explicitly set.
|
|
8
9
|
*/
|
|
9
10
|
document.addEventListener('DOMContentLoaded', () => {
|
|
10
11
|
const navMain = document.querySelector('nav#nav-main');
|
|
11
12
|
const navMainTop = document.querySelector('nav#nav-main-top');
|
|
12
|
-
const menuPosition = document.body.dataset.menuPosition || '
|
|
13
|
+
const menuPosition = document.body.dataset.menuPosition || 'top';
|
|
13
14
|
|
|
14
|
-
// If menu position is top, handle differently
|
|
15
|
+
// If menu position is top (default), handle differently
|
|
15
16
|
if (menuPosition === 'top') {
|
|
16
17
|
initTopMenu();
|
|
17
18
|
return;
|
|
@@ -541,27 +542,170 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
541
542
|
|
|
542
543
|
/**
|
|
543
544
|
* Initialize top menu (horizontal navigation with dropdowns)
|
|
545
|
+
* Also handles: home button on desktop, hamburger → mobile side menu
|
|
544
546
|
*/
|
|
545
547
|
function initTopMenu() {
|
|
546
548
|
const navMainTop = document.querySelector('nav#nav-main-top');
|
|
549
|
+
const navMain = document.querySelector('nav#nav-main');
|
|
550
|
+
const globalNav = document.querySelector('nav#nav-global');
|
|
551
|
+
const menuButton = globalNav?.querySelector('.menu-button');
|
|
547
552
|
const customMenuPath = document.body.dataset.customMenu;
|
|
548
553
|
|
|
549
|
-
if (!navMainTop
|
|
554
|
+
if (!navMainTop) return;
|
|
550
555
|
|
|
551
|
-
//
|
|
552
|
-
|
|
556
|
+
// Determine which URL to load — custom menu or default auto-menu
|
|
557
|
+
const menuUrl = customMenuPath || '/public/menu-data.json';
|
|
558
|
+
|
|
559
|
+
// Load menu data and render both top menu and mobile menu
|
|
560
|
+
fetch(menuUrl)
|
|
553
561
|
.then(response => response.json())
|
|
554
562
|
.then(data => {
|
|
555
|
-
// Handle new JSON format with menuData and menuPosition
|
|
556
563
|
const menuData = data.menuData || data;
|
|
557
564
|
renderTopMenu(navMainTop, menuData);
|
|
565
|
+
buildMobileMenu(menuData, navMain);
|
|
558
566
|
})
|
|
559
567
|
.catch(error => {
|
|
560
568
|
console.error('Failed to load top menu data:', error);
|
|
561
569
|
});
|
|
562
570
|
|
|
563
|
-
// Set up
|
|
564
|
-
|
|
571
|
+
// Set up home button (desktop) / hamburger (mobile)
|
|
572
|
+
setupMenuButton(menuButton, navMain);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Set up the menu button:
|
|
577
|
+
* - Desktop: shows 🏠 (home icon), navigates to "/" on click
|
|
578
|
+
* - Mobile: shows ☰ (hamburger), toggles mobile side menu on click
|
|
579
|
+
*/
|
|
580
|
+
function setupMenuButton(menuButton, navMain) {
|
|
581
|
+
if (!menuButton) return;
|
|
582
|
+
|
|
583
|
+
const isMobile = () => window.matchMedia('(max-width: 50rem)').matches;
|
|
584
|
+
|
|
585
|
+
function updateButtonIcon() {
|
|
586
|
+
if (isMobile()) {
|
|
587
|
+
const isActive = navMain?.classList.contains('active');
|
|
588
|
+
menuButton.textContent = isActive ? '✕' : '☰';
|
|
589
|
+
menuButton.setAttribute('aria-label', isActive ? 'Close menu' : 'Menu');
|
|
590
|
+
} else {
|
|
591
|
+
menuButton.textContent = '🏠';
|
|
592
|
+
menuButton.setAttribute('aria-label', 'Home');
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Set initial icon
|
|
597
|
+
updateButtonIcon();
|
|
598
|
+
|
|
599
|
+
// Update icon on resize
|
|
600
|
+
window.addEventListener('resize', updateButtonIcon);
|
|
601
|
+
|
|
602
|
+
menuButton.addEventListener('click', (e) => {
|
|
603
|
+
e.stopPropagation();
|
|
604
|
+
|
|
605
|
+
if (isMobile()) {
|
|
606
|
+
// Mobile: toggle side menu
|
|
607
|
+
if (navMain) {
|
|
608
|
+
const isNowActive = navMain.classList.toggle('active');
|
|
609
|
+
menuButton.textContent = isNowActive ? '✕' : '☰';
|
|
610
|
+
menuButton.setAttribute('aria-label', isNowActive ? 'Close menu' : 'Menu');
|
|
611
|
+
}
|
|
612
|
+
} else {
|
|
613
|
+
// Desktop: navigate to home
|
|
614
|
+
window.location.href = '/';
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
// Close mobile menu on outside click
|
|
619
|
+
document.addEventListener('click', (e) => {
|
|
620
|
+
if (isMobile() && navMain?.classList.contains('active') &&
|
|
621
|
+
!navMain.contains(e.target) && !menuButton.contains(e.target)) {
|
|
622
|
+
navMain.classList.remove('active');
|
|
623
|
+
updateButtonIcon();
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
// Close mobile menu on Escape
|
|
628
|
+
document.addEventListener('keydown', (e) => {
|
|
629
|
+
if (e.key === 'Escape' && isMobile() && navMain?.classList.contains('active')) {
|
|
630
|
+
navMain.classList.remove('active');
|
|
631
|
+
updateButtonIcon();
|
|
632
|
+
menuButton.focus();
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Build the mobile side menu from the same data as the top menu.
|
|
639
|
+
* Repurposes #nav-main as a vertical list with a "Home" link at top.
|
|
640
|
+
*/
|
|
641
|
+
function buildMobileMenu(menuData, navMain) {
|
|
642
|
+
if (!navMain) return;
|
|
643
|
+
|
|
644
|
+
// Clear existing content
|
|
645
|
+
navMain.innerHTML = '';
|
|
646
|
+
|
|
647
|
+
const ul = document.createElement('ul');
|
|
648
|
+
ul.className = 'mobile-menu-list';
|
|
649
|
+
|
|
650
|
+
// Add "Home" item at the top
|
|
651
|
+
const homeLi = document.createElement('li');
|
|
652
|
+
homeLi.className = 'mobile-menu-item mobile-menu-home';
|
|
653
|
+
const homeLink = document.createElement('a');
|
|
654
|
+
homeLink.href = '/';
|
|
655
|
+
homeLink.className = 'mobile-menu-label';
|
|
656
|
+
homeLink.textContent = '🏠 Home';
|
|
657
|
+
homeLi.appendChild(homeLink);
|
|
658
|
+
ul.appendChild(homeLi);
|
|
659
|
+
|
|
660
|
+
// Build menu items recursively
|
|
661
|
+
buildMobileMenuItems(menuData, ul, 0);
|
|
662
|
+
|
|
663
|
+
navMain.appendChild(ul);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Recursively build mobile menu items as a flat indented list
|
|
668
|
+
*/
|
|
669
|
+
function buildMobileMenuItems(items, parentUl, depth) {
|
|
670
|
+
for (const item of items) {
|
|
671
|
+
const li = document.createElement('li');
|
|
672
|
+
li.className = 'mobile-menu-item';
|
|
673
|
+
if (depth > 0) {
|
|
674
|
+
li.classList.add('mobile-menu-depth-' + Math.min(depth, 4));
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const el = item.href
|
|
678
|
+
? document.createElement('a')
|
|
679
|
+
: document.createElement('span');
|
|
680
|
+
el.className = 'mobile-menu-label';
|
|
681
|
+
el.textContent = item.label;
|
|
682
|
+
if (item.href) el.href = item.href;
|
|
683
|
+
|
|
684
|
+
// Highlight current page
|
|
685
|
+
if (item.href && isCurrentTopMenuPage(item.href)) {
|
|
686
|
+
li.classList.add('mobile-menu-current');
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
li.appendChild(el);
|
|
690
|
+
parentUl.appendChild(li);
|
|
691
|
+
|
|
692
|
+
// Recurse into children
|
|
693
|
+
if (item.children && item.children.length > 0) {
|
|
694
|
+
buildMobileMenuItems(item.children, parentUl, depth + 1);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Check if an href matches the current page
|
|
701
|
+
*/
|
|
702
|
+
function isCurrentTopMenuPage(href) {
|
|
703
|
+
if (!href || href === '#') return false;
|
|
704
|
+
const current = decodeURIComponent(window.location.pathname)
|
|
705
|
+
.replace(/\/index\.html$/, '').replace(/\.html$/, '').replace(/\/$/, '');
|
|
706
|
+
const target = decodeURIComponent(href)
|
|
707
|
+
.replace(/\/index\.html$/, '').replace(/\.html$/, '').replace(/\/$/, '');
|
|
708
|
+
return current === target;
|
|
565
709
|
}
|
|
566
710
|
|
|
567
711
|
/**
|
|
@@ -685,75 +829,4 @@ function createTopMenuFlyout(items) {
|
|
|
685
829
|
return ul;
|
|
686
830
|
}
|
|
687
831
|
|
|
688
|
-
|
|
689
|
-
* Initialize search functionality for top menu mode
|
|
690
|
-
*/
|
|
691
|
-
function initTopMenuSearch() {
|
|
692
|
-
const searchButton = document.querySelector('nav#nav-global .search-button');
|
|
693
|
-
const searchInput = document.querySelector('#global-search');
|
|
694
|
-
const searchWrapper = searchInput?.closest('.search-wrapper');
|
|
695
|
-
const searchResults = document.querySelector('#search-results');
|
|
696
|
-
|
|
697
|
-
if (!searchButton || !searchInput || !searchWrapper) return;
|
|
698
|
-
|
|
699
|
-
// Create search overlay backdrop (just the dark background)
|
|
700
|
-
const backdrop = document.createElement('div');
|
|
701
|
-
backdrop.className = 'search-backdrop';
|
|
702
|
-
document.body.appendChild(backdrop);
|
|
703
|
-
|
|
704
|
-
// Create container for search that floats above the nav
|
|
705
|
-
const floatingSearch = document.createElement('div');
|
|
706
|
-
floatingSearch.className = 'search-floating';
|
|
707
|
-
document.body.appendChild(floatingSearch);
|
|
708
|
-
|
|
709
|
-
// Toggle search on button click
|
|
710
|
-
searchButton.addEventListener('click', () => {
|
|
711
|
-
backdrop.classList.add('active');
|
|
712
|
-
floatingSearch.classList.add('active');
|
|
713
|
-
|
|
714
|
-
// Move the search wrapper into the floating container
|
|
715
|
-
floatingSearch.appendChild(searchWrapper);
|
|
716
|
-
|
|
717
|
-
// Move search results into floating container too (if exists)
|
|
718
|
-
if (searchResults) {
|
|
719
|
-
floatingSearch.appendChild(searchResults);
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
searchInput.value = '';
|
|
723
|
-
searchInput.focus();
|
|
724
|
-
});
|
|
725
|
-
|
|
726
|
-
function closeSearch() {
|
|
727
|
-
backdrop.classList.remove('active');
|
|
728
|
-
floatingSearch.classList.remove('active');
|
|
729
|
-
|
|
730
|
-
// Move search wrapper back to nav-center
|
|
731
|
-
const navCenter = document.querySelector('.nav-center');
|
|
732
|
-
if (navCenter && searchWrapper) {
|
|
733
|
-
navCenter.appendChild(searchWrapper);
|
|
734
|
-
}
|
|
735
|
-
// Move search results back too
|
|
736
|
-
if (searchResults && searchWrapper) {
|
|
737
|
-
searchWrapper.parentNode.appendChild(searchResults);
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
// Close on backdrop click
|
|
742
|
-
backdrop.addEventListener('click', closeSearch);
|
|
743
|
-
|
|
744
|
-
// Close on escape
|
|
745
|
-
document.addEventListener('keydown', (e) => {
|
|
746
|
-
if (e.key === 'Escape' && backdrop.classList.contains('active')) {
|
|
747
|
-
closeSearch();
|
|
748
|
-
}
|
|
749
|
-
});
|
|
750
|
-
|
|
751
|
-
// Close when a search result is clicked (navigation will happen)
|
|
752
|
-
if (searchResults) {
|
|
753
|
-
searchResults.addEventListener('click', (e) => {
|
|
754
|
-
if (e.target.closest('.search-result-item')) {
|
|
755
|
-
closeSearch();
|
|
756
|
-
}
|
|
757
|
-
});
|
|
758
|
-
}
|
|
759
|
-
}
|
|
832
|
+
// (Search functionality is now handled by widgets.js)
|
package/meta/search.js
CHANGED
|
@@ -157,12 +157,6 @@ class GlobalSearch {
|
|
|
157
157
|
|
|
158
158
|
this.searchInput.addEventListener('blur', (e) => {
|
|
159
159
|
// Delay hiding to allow click on results
|
|
160
|
-
// Check if we're inside the floating search container (top menu mode)
|
|
161
|
-
const floatingSearch = this.searchInput.closest('.search-floating');
|
|
162
|
-
if (floatingSearch && floatingSearch.classList.contains('active')) {
|
|
163
|
-
// In floating mode, don't auto-hide on blur - let the backdrop click handle it
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
160
|
setTimeout(() => {
|
|
167
161
|
this.hideResults();
|
|
168
162
|
}, 150);
|
|
@@ -170,11 +164,6 @@ class GlobalSearch {
|
|
|
170
164
|
|
|
171
165
|
// Click outside to close
|
|
172
166
|
document.addEventListener('click', (e) => {
|
|
173
|
-
// If inside floating search container, don't close on internal clicks
|
|
174
|
-
const floatingSearch = this.searchInput.closest('.search-floating');
|
|
175
|
-
if (floatingSearch && floatingSearch.contains(e.target)) {
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
167
|
if (!this.searchInput.contains(e.target) && !this.searchResults.contains(e.target)) {
|
|
179
168
|
this.hideResults();
|
|
180
169
|
}
|
|
@@ -185,8 +174,13 @@ class GlobalSearch {
|
|
|
185
174
|
// Check for Cmd+P (Mac) or Ctrl+P (Windows/Linux)
|
|
186
175
|
if ((e.metaKey || e.ctrlKey) && e.key === 'p') {
|
|
187
176
|
e.preventDefault();
|
|
188
|
-
|
|
189
|
-
|
|
177
|
+
// If widget system is available, open the search widget
|
|
178
|
+
if (window.widgetManager) {
|
|
179
|
+
window.widgetManager.open('search');
|
|
180
|
+
} else {
|
|
181
|
+
this.searchInput.focus();
|
|
182
|
+
this.searchInput.select();
|
|
183
|
+
}
|
|
190
184
|
}
|
|
191
185
|
});
|
|
192
186
|
}
|
package/meta/sectionify.js
CHANGED
|
@@ -9,6 +9,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
9
9
|
|
|
10
10
|
for (let i = 0; i < children.length; i++) {
|
|
11
11
|
const el = children[i];
|
|
12
|
+
// Skip breadcrumb nav — it stays outside sections
|
|
13
|
+
if (el.classList && el.classList.contains('breadcrumbs')) continue;
|
|
12
14
|
if (el.tagName === 'H1' && currentSection.childNodes.length > 0) {
|
|
13
15
|
sections.push(currentSection);
|
|
14
16
|
currentSection = document.createElement('section');
|
|
@@ -20,11 +22,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
20
22
|
sections.push(currentSection);
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
// Preserve breadcrumb nav before clearing
|
|
26
|
+
const breadcrumbs = article.querySelector('.breadcrumbs');
|
|
27
|
+
|
|
23
28
|
// Remove all existing children
|
|
24
29
|
while (article.firstChild) {
|
|
25
30
|
article.removeChild(article.firstChild);
|
|
26
31
|
}
|
|
27
32
|
|
|
33
|
+
// Re-insert breadcrumbs at the top, outside any section
|
|
34
|
+
if (breadcrumbs) {
|
|
35
|
+
article.appendChild(breadcrumbs);
|
|
36
|
+
}
|
|
37
|
+
|
|
28
38
|
// Append new sections
|
|
29
39
|
sections.forEach(section => article.appendChild(section));
|
|
30
40
|
|
package/meta/toc-generator.js
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
// Table of Contents Generator
|
|
2
2
|
document.addEventListener('DOMContentLoaded', () => {
|
|
3
|
-
|
|
3
|
+
// Generate TOC into widget panel if available, otherwise fallback to nav-toc
|
|
4
|
+
const tocTarget = document.getElementById('widget-content-toc') || document.getElementById('nav-toc');
|
|
4
5
|
const article = document.querySelector('article#main-content');
|
|
5
6
|
|
|
6
|
-
if (!
|
|
7
|
+
if (!tocTarget || !article) return;
|
|
7
8
|
|
|
8
9
|
// Find all headings in the article
|
|
9
10
|
const headings = article.querySelectorAll('h1, h2, h3');
|
|
10
11
|
|
|
11
12
|
if (headings.length === 0) {
|
|
12
|
-
|
|
13
|
+
// Hide the TOC widget button if no headings
|
|
14
|
+
const tocButton = document.querySelector('.widget-button[data-widget="toc"]');
|
|
15
|
+
if (tocButton) tocButton.style.display = 'none';
|
|
16
|
+
tocTarget.style.display = 'none';
|
|
13
17
|
return;
|
|
14
18
|
}
|
|
15
19
|
|
|
@@ -39,7 +43,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
39
43
|
tocList.appendChild(listItem);
|
|
40
44
|
});
|
|
41
45
|
|
|
42
|
-
|
|
46
|
+
// Add an id=toc wrapper for the toc.js sentinel-based highlighter
|
|
47
|
+
tocList.id = 'toc';
|
|
48
|
+
tocTarget.appendChild(tocList);
|
|
43
49
|
|
|
44
50
|
// Handle TOC link clicks for smooth scrolling
|
|
45
51
|
function handleTocClick(e) {
|
|
@@ -84,13 +90,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
84
90
|
|
|
85
91
|
// Update TOC active state
|
|
86
92
|
function updateTocActiveState(activeHeading) {
|
|
87
|
-
const tocLinks =
|
|
93
|
+
const tocLinks = tocTarget.querySelectorAll('a');
|
|
88
94
|
tocLinks.forEach(link => {
|
|
89
95
|
link.classList.remove('active');
|
|
90
96
|
});
|
|
91
97
|
|
|
92
98
|
if (activeHeading) {
|
|
93
|
-
const activeLink =
|
|
99
|
+
const activeLink = tocTarget.querySelector(`a[href="#${activeHeading.id}"]`);
|
|
94
100
|
if (activeLink) {
|
|
95
101
|
activeLink.classList.add('active');
|
|
96
102
|
}
|