@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/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 || 'side';
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 || !customMenuPath) return;
554
+ if (!navMainTop) return;
550
555
 
551
- // Load menu data and render top menu
552
- fetch(customMenuPath)
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 search button toggle
564
- initTopMenuSearch();
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
- this.searchInput.focus();
189
- this.searchInput.select();
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
  }
@@ -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
 
@@ -1,15 +1,19 @@
1
1
  // Table of Contents Generator
2
2
  document.addEventListener('DOMContentLoaded', () => {
3
- const tocNav = document.getElementById('nav-toc');
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 (!tocNav || !article) return;
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
- tocNav.style.display = 'none';
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
- tocNav.appendChild(tocList);
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 = tocNav.querySelectorAll('a');
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 = tocNav.querySelector(`a[href="#${activeHeading.id}"]`);
99
+ const activeLink = tocTarget.querySelector(`a[href="#${activeHeading.id}"]`);
94
100
  if (activeLink) {
95
101
  activeLink.classList.add('active');
96
102
  }