diff --git a/index.html b/index.html index 57fe989..87418c8 100644 --- a/index.html +++ b/index.html @@ -402,6 +402,7 @@ } #sidebar, + #mobile-toc-sidebar, header, #theme-toggle-desktop, #scroll-top-btn, @@ -512,7 +513,7 @@ class="fixed inset-0 bg-black/80 backdrop-blur-[2px] z-[65] hidden transition-opacity opacity-0 md:hidden"> - + + + +
@@ -664,19 +679,15 @@ // --- FIXED IMAGE RENDERER (Handles Marked 5.0+ Object Token) --- renderer.image = function (href, title, text) { - // New Marked versions pass an object as the first argument if (typeof href === 'object' && href !== null) { const token = href; href = token.href; title = token.title; text = token.text; } - - // Handle relative paths for GitHub Pages if needed, usually browser handles it. return `${text || ''}`; }; - // Table styling renderer.table = function (header, body) { return `
${header}${body}
`; }; @@ -726,8 +737,6 @@ initTheme(); // --- 3. FETCH DATA (GitHub API) --- - // Includes Cache to avoid GitHub API Rate Limiting (60 requests/hr for unauth) - async function fetchLatestVersion() { const CACHE_KEY = 'bjorn_ver_data'; const now = Date.now(); @@ -744,14 +753,12 @@ try { let ver = "v2.0"; - // Try Releases first let res = await fetch(`https://api.github.com/repos/${STATE.repo}/releases/latest`); if (res.ok) { const data = await res.json(); ver = data.tag_name; } else { - // Fallback to tags res = await fetch(`https://api.github.com/repos/${STATE.repo}/tags`); if (res.ok) { const tags = await res.json(); @@ -763,7 +770,6 @@ updateVersionUI(ver, true); } catch (e) { console.warn("GitHub API Limit or Network Error", e); - // Fallback to cached version even if old, or default if (cached) updateVersionUI(JSON.parse(cached).version, true); else updateVersionUI("v2.0", false); } @@ -779,7 +785,6 @@ // --- 4. WIKI CORE --- async function initWiki() { try { - // IMPORTANT: Fetch relative to index.html const res = await fetch('./wiki/structure.json'); if (!res.ok) { @@ -789,20 +794,17 @@ STATE.wikiData = await res.json(); - // Expand first folder by default const firstFolder = Object.keys(STATE.wikiData)[0]; if (firstFolder) STATE.expandedSections.add(firstFolder); renderSidebar(); - buildSearchIndex(); // Background + buildSearchIndex(); - // History handling window.onpopstate = (event) => { if (event.state) { if (event.state.page === 'versions') toggleVersionsPage(null, false); else loadContent(event.state.folder, event.state.title, event.state.filename, false); } else { - // Check URL params on back button to root handleInitialRoute(); } }; @@ -822,11 +824,9 @@ if (pageParam === 'changelog') { toggleVersionsPage(null, false); } else if (pageParam) { - // pageParam format: "Folder/Filename.md" or similar let found = false; for (const [folder, files] of Object.entries(STATE.wikiData)) { for (const [title, file] of Object.entries(files)) { - // Check strict match or loose match if (`${folder}/${file}` === pageParam || file === pageParam) { loadContent(folder, title, file, false); STATE.expandedSections.add(folder); @@ -847,7 +847,6 @@ const promises = []; for (const [folder, files] of Object.entries(STATE.wikiData)) { for (const [title, filename] of Object.entries(files)) { - // Skip if not markdown if (!filename.endsWith('.md')) continue; promises.push( @@ -888,18 +887,6 @@ document.getElementById('markdown-viewer').innerHTML = `

Wiki Setup Guide

Welcome to your new Wiki Node. To enable content, please ensure your repository structure matches the config.

-
-

Required Structure

-
/ (Root)
-├── index.html
-├── .nojekyll  <-- IMPORTANT for GitHub Pages
-├── assets/
-│   └── bjorn.png
-└── wiki/
-    ├── structure.json
-    └── 01_General/
-        └── Introduction.md
-
`; hljs.highlightAll(); } @@ -910,14 +897,12 @@ const pageNav = document.getElementById('page-nav'); const scrollContainer = document.getElementById('scroll-container'); - // Set Breadcrumbs early so they appear even if loading fails/takes time const cleanFolder = folder.replace(/^\d+_/, '').replace(/_/g, ' '); document.getElementById('breadcrumbs').innerHTML = ` wiki / ${cleanFolder} / ${title} `; - // UI Reset if (!STATE.contentCache[`${folder}/${filename}`]) { viewer.innerHTML = `
`; } @@ -929,11 +914,9 @@ let text; const cacheKey = `${folder}/${filename}`; - // 1. Get Markdown if (STATE.contentCache[cacheKey]) { text = STATE.contentCache[cacheKey]; } else { - // ATTEMPT 1: Structure Path try { const path1 = `./wiki/${folder}/${filename}`; const res = await fetch(path1); @@ -943,41 +926,33 @@ throw new Error("404"); } } catch (e) { - // ATTEMPT 2: Flat Path (Fallback) - "AUTO-FIX" - console.warn(`Attempt 1 failed for ${folder}/${filename}. Trying flat path...`); const path2 = `./wiki/${filename}`; const res2 = await fetch(path2); if (res2.ok) { text = await res2.text(); } else { - // If both fail, throw detailed error - throw new Error(`Content not found. Tried:\n1. ./wiki/${folder}/${filename}\n2. ./wiki/${filename}`); + throw new Error(`Content not found.`); } } STATE.contentCache[cacheKey] = text; } - // 2. Render const cleanHTML = DOMPurify.sanitize(marked.parse(text)); viewer.innerHTML = cleanHTML; - // 3. Stats const wordCount = text.replace(/[#*`]/g, '').split(/\s+/).length; document.getElementById('reading-time').textContent = `~${Math.ceil(wordCount / 200)} min read`; fetchLastUpdated(folder, filename); - // 4. UI Enhancements enhanceMarkdownContent(); generateTOC(); renderPagination(folder, title); - // 6. Navigation / History if (pushHistory) { const newUrl = `?page=${folder}/${filename}`; window.history.pushState({ folder, title, filename }, "", newUrl); } - // 7. Scroll STATE.expandedSections = new Set([folder]); renderSidebar(); if (window.location.hash) { @@ -989,7 +964,6 @@ if (window.innerWidth < 768) closeMenu(); - // 8. Preload next/prev preloadAdjacent(folder, title); } catch (e) { @@ -999,20 +973,12 @@

Page Not Found

Error Details:
${cleanError} -
-

- Troubleshooting:
- 1. Ensure the file exists in your wiki/ folder.
- 2. Check capitalization (Linux/GitHub is case-sensitive).
- 3. Verify .nojekyll is at the root of your repo. -

`; +
`; - // Breadcrumbs Fallback document.getElementById('breadcrumbs').innerHTML = `wiki / Error`; } } - // Cache last updated to prevent API ban async function fetchLastUpdated(folder, filename) { const CACHE_KEY = `bjorn_upd_${folder}_${filename}`; const now = Date.now(); @@ -1020,14 +986,13 @@ const cached = localStorage.getItem(CACHE_KEY); if (cached) { const data = JSON.parse(cached); - if (now - data.ts < 86400000) { // 24h cache + if (now - data.ts < 86400000) { document.getElementById('last-updated').textContent = `Updated: ${data.date}`; return; } } try { - // Rate limit protection const path = `wiki/${folder}/${filename}`; const res = await fetch(`https://api.github.com/repos/${STATE.repo}/commits?path=${path}&page=1&per_page=1`); if (res.ok) { @@ -1038,9 +1003,7 @@ localStorage.setItem(CACHE_KEY, JSON.stringify({ ts: now, date })); } } - } catch (e) { - // Silent fail - } + } catch (e) { } } function preloadAdjacent(currentFolder, currentTitle) { @@ -1063,7 +1026,6 @@ } function enhanceMarkdownContent() { - // Anchor Links Generation (Same ID logic as before) document.querySelectorAll('#markdown-viewer h2, #markdown-viewer h3').forEach(h => { if (!h.id) h.id = h.textContent.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''); @@ -1080,20 +1042,14 @@ h.appendChild(anchor); }); - // NEW: Fix Manual TOC Links (Smart Handler) - // This catches clicks on generated TOC links like [Introduction](#-introduction) document.querySelectorAll('#markdown-viewer a[href^="#"]').forEach(link => { - // Ignore the anchor links we just created above (class anchor-link) if (link.classList.contains('anchor-link')) return; link.onclick = (e) => { e.preventDefault(); - const hash = link.getAttribute('href').substring(1); // remove # - - // 1. Try exact match (e.g. if ID matches TOC exactly) + closeTOC(); // Close mobile TOC if open + const hash = link.getAttribute('href').substring(1); let target = document.getElementById(hash); - - // 2. Fallback: Try removing leading dashes (Fixes GitHub style TOC vs JS generated IDs) if (!target) { const cleanHash = hash.replace(/^-+/, ''); target = document.getElementById(cleanHash); @@ -1102,16 +1058,12 @@ if (target) { target.scrollIntoView({ behavior: 'smooth' }); history.pushState(null, null, `#${hash}`); - } else { - console.warn('Anchor target not found:', hash); } }; }); - // Syntax Highlight document.querySelectorAll('pre code').forEach((el) => hljs.highlightElement(el)); - // Copy Buttons document.querySelectorAll('pre').forEach(pre => { if (pre.querySelector('.copy-btn')) return; const btn = document.createElement('button'); @@ -1126,31 +1078,20 @@ pre.appendChild(btn); }); - // Images -> Lightbox document.querySelectorAll('#markdown-viewer img').forEach(img => { img.onclick = () => openLightbox(img.src); }); - // External Links document.querySelectorAll('#markdown-viewer a').forEach(a => { - - // Skip if: - // - link is internal - // - it's an anchor-link - // - it has no href - // - href starts with "#" - // - link contains an → prevents icon under badges if ( a.hostname !== window.location.hostname && !a.classList.contains('anchor-link') && a.getAttribute('href') && !a.getAttribute('href').startsWith('#') && - !a.querySelector('img') // ⬅ NEW: do NOT add icon when link wraps an image + !a.querySelector('img') ) { a.target = '_blank'; a.rel = 'noopener noreferrer'; - - // Add external icon a.innerHTML += ' '; } @@ -1170,7 +1111,6 @@ const idx = flatList.findIndex(item => item.folder === currentFolder && item.title === currentTitle); if (idx === -1) return; - // Prev if (idx > 0) { const prev = flatList[idx - 1]; const btn = document.createElement('button'); @@ -1180,7 +1120,6 @@ navContainer.appendChild(btn); } else { navContainer.appendChild(document.createElement('div')); } - // Next if (idx < flatList.length - 1) { const next = flatList[idx + 1]; const btn = document.createElement('button'); @@ -1192,26 +1131,45 @@ } function generateTOC() { - const toc = document.getElementById('toc-container'); + const desktopToc = document.getElementById('toc-container'); + const mobileToc = document.getElementById('mobile-toc-list'); const headings = document.querySelectorAll('#markdown-viewer h2, #markdown-viewer h3'); - toc.innerHTML = ''; - if (headings.length === 0) { - toc.innerHTML = '
  • No sections
  • '; - return; - } - headings.forEach(h => { + + const createTOCItem = (h, isMobile) => { const li = document.createElement('li'); const link = document.createElement('a'); - link.textContent = h.childNodes[0].textContent; // ignore anchor link + link.textContent = h.childNodes[0].textContent; link.href = `#${h.id}`; - link.className = `toc-link block py-1 pl-2 border-l-2 border-transparent hover:text-hack-green transition-colors truncate ${h.tagName === 'H3' ? 'ml-3 opacity-80' : ''}`; + // Different styling for desktop vs mobile + if (isMobile) { + link.className = `block py-2 text-gray-400 hover:text-hack-green border-l-2 border-transparent pl-3 transition-colors ${h.tagName === 'H3' ? 'ml-4 text-xs' : 'font-medium'}`; + } else { + link.className = `toc-link block py-1 pl-2 border-l-2 border-transparent hover:text-hack-green transition-colors truncate ${h.tagName === 'H3' ? 'ml-3 opacity-80' : ''}`; + } + link.onclick = (e) => { e.preventDefault(); + if (isMobile) closeTOC(); document.getElementById(h.id).scrollIntoView({ behavior: 'smooth' }); history.pushState(null, null, `#${h.id}`); }; li.appendChild(link); - toc.appendChild(li); + return li; + }; + + // Reset + desktopToc.innerHTML = ''; + mobileToc.innerHTML = ''; + + if (headings.length === 0) { + desktopToc.innerHTML = '
  • No sections
  • '; + mobileToc.innerHTML = '
  • No sections available for this page.
  • '; + return; + } + + headings.forEach(h => { + desktopToc.appendChild(createTOCItem(h, false)); + mobileToc.appendChild(createTOCItem(h, true)); }); } @@ -1250,7 +1208,7 @@ hasContent = true; const filename = STATE.wikiData[folder][title]; const link = document.createElement('a'); - link.href = `?page=${folder}/${filename}`; // Fallback href + link.href = `?page=${folder}/${filename}`; link.className = 'nav-link group flex items-center gap-3 px-2 py-1.5 rounded-md text-sm font-medium text-gray-400 hover:text-hack-heading hover:bg-hack-bg transition-all outline-none focus:bg-hack-bg'; link.innerHTML = ` ${title}`; link.onclick = (e) => { @@ -1298,7 +1256,6 @@ } }); } else { - // Fallback for (const [folder, files] of Object.entries(STATE.wikiData)) { for (const [title, file] of Object.entries(files)) { if (title.toLowerCase().includes(q)) { @@ -1330,7 +1287,6 @@ if (pushHistory) window.history.pushState({ page: 'versions' }, "", "?page=changelog"); try { - // Get releases from API const res = await fetch(`https://api.github.com/repos/${STATE.repo}/releases`); if (!res.ok) throw new Error("API Limit"); const data = await res.json(); @@ -1354,7 +1310,7 @@ list.appendChild(div); }); } catch (e) { - document.getElementById('versions-list').innerHTML = `

    Unable to load changelog (GitHub API Rate Limit might be reached). Please check back later.

    `; + document.getElementById('versions-list').innerHTML = `

    Unable to load changelog. Check back later.

    `; } } @@ -1375,14 +1331,54 @@ document.body.style.overflow = ''; } - // Mobile Menu + // Mobile Menu & TOC Logic const sidebar = document.getElementById('sidebar'); + const tocSidebar = document.getElementById('mobile-toc-sidebar'); const overlay = document.getElementById('overlay'); - function openMenu() { sidebar.classList.remove('-translate-x-full'); overlay.classList.remove('hidden'); setTimeout(() => overlay.classList.remove('opacity-0'), 10); } - function closeMenu() { sidebar.classList.add('-translate-x-full'); overlay.classList.add('opacity-0'); setTimeout(() => overlay.classList.add('hidden'), 300); } + + function openMenu() { + closeTOC(); // Close TOC if open + sidebar.classList.remove('-translate-x-full'); + overlay.classList.remove('hidden'); + setTimeout(() => overlay.classList.remove('opacity-0'), 10); + } + function closeMenu() { + sidebar.classList.add('-translate-x-full'); + checkOverlay(); + } + + function openTOC() { + // Only open if TOC has items + const list = document.getElementById('mobile-toc-list'); + if (!list.hasChildNodes() || list.innerHTML.includes('No sections')) return; + + closeMenu(); // Close Menu if open + tocSidebar.classList.remove('translate-x-full'); + overlay.classList.remove('hidden'); + setTimeout(() => overlay.classList.remove('opacity-0'), 10); + } + function closeTOC() { + tocSidebar.classList.add('translate-x-full'); + checkOverlay(); + } + + function checkOverlay() { + // Hide overlay only if both panels are closed + if (sidebar.classList.contains('-translate-x-full') && tocSidebar.classList.contains('translate-x-full')) { + overlay.classList.add('opacity-0'); + setTimeout(() => overlay.classList.add('hidden'), 300); + } + } + document.getElementById('menu-btn').onclick = openMenu; document.getElementById('close-sidebar-btn').onclick = closeMenu; - overlay.onclick = closeMenu; + document.getElementById('close-toc-btn').onclick = closeTOC; + + // Unified overlay click closes whichever is open + overlay.onclick = () => { + closeMenu(); + closeTOC(); + }; // Events document.addEventListener('keydown', (e) => { @@ -1390,7 +1386,10 @@ if (e.key === 'Escape') { if (document.activeElement === searchInput) searchInput.blur(); else if (lightbox.classList.contains('active')) closeLightbox(); - else closeMenu(); + else { + closeMenu(); + closeTOC(); + } } }); @@ -1407,7 +1406,6 @@ const scrollPercent = (scrollHeight > 0) ? (scrollTop / scrollHeight) * 100 : 0; progressBar.style.width = scrollPercent + '%'; - // Show/Hide Scroll Top Button if (scrollTop > 300) scrollBtn.classList.remove('hidden'); else scrollBtn.classList.add('hidden'); }); @@ -1416,16 +1414,17 @@ scrollBtn.onclick = () => scrollContainer.scrollTo({ top: 0, behavior: 'smooth' }); } } + // --- MOBILE SWIPE GESTURES --- - // Detect swipe from left edge → open menu let touchStartX = 0; let touchStartY = 0; let touchEndX = 0; let touchEndY = 0; - const MIN_SWIPE_DISTANCE = 50; // minimal movement to count as a swipe - const MAX_VERTICAL_DRIFT = 80; // tolerance to avoid accidental scroll/swipe mix - const EDGE_ZONE = 200; // swipe must start within 200px from screen left + const MIN_SWIPE_DISTANCE = 50; + const MAX_VERTICAL_DRIFT = 80; + const EDGE_ZONE_LEFT = 200; // Swipe from left triggers Menu + const EDGE_ZONE_RIGHT = 100; // Distance from right edge to likely trigger TOC document.addEventListener("touchstart", (e) => { const t = e.changedTouches[0]; @@ -1442,22 +1441,31 @@ document.addEventListener("touchend", () => { const dx = touchEndX - touchStartX; const dy = Math.abs(touchEndY - touchStartY); + const screenW = window.innerWidth; - // 1. SWIPE → RIGHT = OPEN MENU - if ( - touchStartX < EDGE_ZONE && // gesture started at screen left - dx > MIN_SWIPE_DISTANCE && // swiped enough to the right - dy < MAX_VERTICAL_DRIFT // not a vertical scroll - ) { - openMenu(); + // 1. SWIPE LEFT -> RIGHT (Open Menu or Close TOC) + if (dx > MIN_SWIPE_DISTANCE && dy < MAX_VERTICAL_DRIFT) { + // If TOC is open, close it + if (!tocSidebar.classList.contains('translate-x-full')) { + closeTOC(); + } + // Else if swipe starts near left edge, open Menu + else if (touchStartX < EDGE_ZONE_LEFT) { + openMenu(); + } } - // 2. SWIPE → LEFT = CLOSE MENU - if ( - dx < -MIN_SWIPE_DISTANCE && // swipe left - dy < MAX_VERTICAL_DRIFT // again avoid scroll false positives - ) { - closeMenu(); + // 2. SWIPE RIGHT -> LEFT (Open TOC or Close Menu) + if (dx < -MIN_SWIPE_DISTANCE && dy < MAX_VERTICAL_DRIFT) { + // If Menu is open, close it + if (!sidebar.classList.contains('-translate-x-full')) { + closeMenu(); + } + // Else if swipe starts anywhere (or strictly right side if preferred), open TOC + // User asked for "swipe right to left" generally, but preventing conflict with horizontal scroll tables is good + else { + openTOC(); + } } });