mirror of
https://github.com/infinition/Bjorn.git
synced 2025-12-13 16:14:57 +00:00
feat: Add a mobile table of contents sidebar and remove the wiki setup guide and extraneous comments.
This commit is contained in:
252
index.html
252
index.html
@@ -402,6 +402,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#sidebar,
|
#sidebar,
|
||||||
|
#mobile-toc-sidebar,
|
||||||
header,
|
header,
|
||||||
#theme-toggle-desktop,
|
#theme-toggle-desktop,
|
||||||
#scroll-top-btn,
|
#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">
|
class="fixed inset-0 bg-black/80 backdrop-blur-[2px] z-[65] hidden transition-opacity opacity-0 md:hidden">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Sidebar -->
|
<!-- Mobile Sidebar (Left - Menu) -->
|
||||||
<aside id="sidebar"
|
<aside id="sidebar"
|
||||||
class="sidebar-mobile fixed top-0 bottom-0 left-0 z-[70] w-[280px] bg-hack-sidebar border-r border-hack-border transform -translate-x-full md:translate-x-0 md:static md:h-full flex flex-col shadow-2xl md:shadow-none transition-transform duration-300">
|
class="sidebar-mobile fixed top-0 bottom-0 left-0 z-[70] w-[280px] bg-hack-sidebar border-r border-hack-border transform -translate-x-full md:translate-x-0 md:static md:h-full flex flex-col shadow-2xl md:shadow-none transition-transform duration-300">
|
||||||
|
|
||||||
@@ -608,6 +609,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
<!-- Mobile Sidebar (Right - TOC) -->
|
||||||
|
<aside id="mobile-toc-sidebar"
|
||||||
|
class="sidebar-mobile fixed top-0 bottom-0 right-0 z-[70] w-[280px] bg-hack-sidebar border-l border-hack-border transform translate-x-full md:hidden flex flex-col shadow-2xl transition-transform duration-300">
|
||||||
|
<div class="p-4 border-b border-hack-border flex justify-between items-center bg-hack-bg/50">
|
||||||
|
<span class="text-xs font-bold text-gray-500 uppercase tracking-widest font-mono">Table of Contents</span>
|
||||||
|
<button id="close-toc-btn" class="text-gray-400 hover:text-white">
|
||||||
|
<i data-lucide="x" class="w-5 h-5"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 overflow-y-auto p-4">
|
||||||
|
<ul id="mobile-toc-list" class="space-y-3 text-sm"></ul>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
<div class="flex-1 flex flex-col h-full overflow-hidden relative">
|
<div class="flex-1 flex flex-col h-full overflow-hidden relative">
|
||||||
<main class="flex-1 overflow-y-auto bg-hack-bg scroll-smooth relative w-full" id="scroll-container">
|
<main class="flex-1 overflow-y-auto bg-hack-bg scroll-smooth relative w-full" id="scroll-container">
|
||||||
<div class="max-w-6xl mx-auto px-5 py-8 md:py-12 md:px-10 flex gap-10">
|
<div class="max-w-6xl mx-auto px-5 py-8 md:py-12 md:px-10 flex gap-10">
|
||||||
@@ -664,19 +679,15 @@
|
|||||||
|
|
||||||
// --- FIXED IMAGE RENDERER (Handles Marked 5.0+ Object Token) ---
|
// --- FIXED IMAGE RENDERER (Handles Marked 5.0+ Object Token) ---
|
||||||
renderer.image = function (href, title, text) {
|
renderer.image = function (href, title, text) {
|
||||||
// New Marked versions pass an object as the first argument
|
|
||||||
if (typeof href === 'object' && href !== null) {
|
if (typeof href === 'object' && href !== null) {
|
||||||
const token = href;
|
const token = href;
|
||||||
href = token.href;
|
href = token.href;
|
||||||
title = token.title;
|
title = token.title;
|
||||||
text = token.text;
|
text = token.text;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle relative paths for GitHub Pages if needed, usually browser handles it.
|
|
||||||
return `<img src="${href}" alt="${text || ''}" title="${title || ''}" loading="lazy" class="rounded-lg border border-hack-border bg-black/20">`;
|
return `<img src="${href}" alt="${text || ''}" title="${title || ''}" loading="lazy" class="rounded-lg border border-hack-border bg-black/20">`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Table styling
|
|
||||||
renderer.table = function (header, body) {
|
renderer.table = function (header, body) {
|
||||||
return `<div class="overflow-x-auto my-4 border border-hack-border rounded-lg"><table class="w-full text-left text-sm border-collapse"><thead>${header}</thead><tbody>${body}</tbody></table></div>`;
|
return `<div class="overflow-x-auto my-4 border border-hack-border rounded-lg"><table class="w-full text-left text-sm border-collapse"><thead>${header}</thead><tbody>${body}</tbody></table></div>`;
|
||||||
};
|
};
|
||||||
@@ -726,8 +737,6 @@
|
|||||||
initTheme();
|
initTheme();
|
||||||
|
|
||||||
// --- 3. FETCH DATA (GitHub API) ---
|
// --- 3. FETCH DATA (GitHub API) ---
|
||||||
// Includes Cache to avoid GitHub API Rate Limiting (60 requests/hr for unauth)
|
|
||||||
|
|
||||||
async function fetchLatestVersion() {
|
async function fetchLatestVersion() {
|
||||||
const CACHE_KEY = 'bjorn_ver_data';
|
const CACHE_KEY = 'bjorn_ver_data';
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
@@ -744,14 +753,12 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
let ver = "v2.0";
|
let ver = "v2.0";
|
||||||
// Try Releases first
|
|
||||||
let res = await fetch(`https://api.github.com/repos/${STATE.repo}/releases/latest`);
|
let res = await fetch(`https://api.github.com/repos/${STATE.repo}/releases/latest`);
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
ver = data.tag_name;
|
ver = data.tag_name;
|
||||||
} else {
|
} else {
|
||||||
// Fallback to tags
|
|
||||||
res = await fetch(`https://api.github.com/repos/${STATE.repo}/tags`);
|
res = await fetch(`https://api.github.com/repos/${STATE.repo}/tags`);
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const tags = await res.json();
|
const tags = await res.json();
|
||||||
@@ -763,7 +770,6 @@
|
|||||||
updateVersionUI(ver, true);
|
updateVersionUI(ver, true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("GitHub API Limit or Network Error", 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);
|
if (cached) updateVersionUI(JSON.parse(cached).version, true);
|
||||||
else updateVersionUI("v2.0", false);
|
else updateVersionUI("v2.0", false);
|
||||||
}
|
}
|
||||||
@@ -779,7 +785,6 @@
|
|||||||
// --- 4. WIKI CORE ---
|
// --- 4. WIKI CORE ---
|
||||||
async function initWiki() {
|
async function initWiki() {
|
||||||
try {
|
try {
|
||||||
// IMPORTANT: Fetch relative to index.html
|
|
||||||
const res = await fetch('./wiki/structure.json');
|
const res = await fetch('./wiki/structure.json');
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
@@ -789,20 +794,17 @@
|
|||||||
|
|
||||||
STATE.wikiData = await res.json();
|
STATE.wikiData = await res.json();
|
||||||
|
|
||||||
// Expand first folder by default
|
|
||||||
const firstFolder = Object.keys(STATE.wikiData)[0];
|
const firstFolder = Object.keys(STATE.wikiData)[0];
|
||||||
if (firstFolder) STATE.expandedSections.add(firstFolder);
|
if (firstFolder) STATE.expandedSections.add(firstFolder);
|
||||||
|
|
||||||
renderSidebar();
|
renderSidebar();
|
||||||
buildSearchIndex(); // Background
|
buildSearchIndex();
|
||||||
|
|
||||||
// History handling
|
|
||||||
window.onpopstate = (event) => {
|
window.onpopstate = (event) => {
|
||||||
if (event.state) {
|
if (event.state) {
|
||||||
if (event.state.page === 'versions') toggleVersionsPage(null, false);
|
if (event.state.page === 'versions') toggleVersionsPage(null, false);
|
||||||
else loadContent(event.state.folder, event.state.title, event.state.filename, false);
|
else loadContent(event.state.folder, event.state.title, event.state.filename, false);
|
||||||
} else {
|
} else {
|
||||||
// Check URL params on back button to root
|
|
||||||
handleInitialRoute();
|
handleInitialRoute();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -822,11 +824,9 @@
|
|||||||
if (pageParam === 'changelog') {
|
if (pageParam === 'changelog') {
|
||||||
toggleVersionsPage(null, false);
|
toggleVersionsPage(null, false);
|
||||||
} else if (pageParam) {
|
} else if (pageParam) {
|
||||||
// pageParam format: "Folder/Filename.md" or similar
|
|
||||||
let found = false;
|
let found = false;
|
||||||
for (const [folder, files] of Object.entries(STATE.wikiData)) {
|
for (const [folder, files] of Object.entries(STATE.wikiData)) {
|
||||||
for (const [title, file] of Object.entries(files)) {
|
for (const [title, file] of Object.entries(files)) {
|
||||||
// Check strict match or loose match
|
|
||||||
if (`${folder}/${file}` === pageParam || file === pageParam) {
|
if (`${folder}/${file}` === pageParam || file === pageParam) {
|
||||||
loadContent(folder, title, file, false);
|
loadContent(folder, title, file, false);
|
||||||
STATE.expandedSections.add(folder);
|
STATE.expandedSections.add(folder);
|
||||||
@@ -847,7 +847,6 @@
|
|||||||
const promises = [];
|
const promises = [];
|
||||||
for (const [folder, files] of Object.entries(STATE.wikiData)) {
|
for (const [folder, files] of Object.entries(STATE.wikiData)) {
|
||||||
for (const [title, filename] of Object.entries(files)) {
|
for (const [title, filename] of Object.entries(files)) {
|
||||||
// Skip if not markdown
|
|
||||||
if (!filename.endsWith('.md')) continue;
|
if (!filename.endsWith('.md')) continue;
|
||||||
|
|
||||||
promises.push(
|
promises.push(
|
||||||
@@ -888,18 +887,6 @@
|
|||||||
document.getElementById('markdown-viewer').innerHTML = `
|
document.getElementById('markdown-viewer').innerHTML = `
|
||||||
<h1>Wiki Setup Guide</h1>
|
<h1>Wiki Setup Guide</h1>
|
||||||
<p>Welcome to your new Wiki Node. To enable content, please ensure your repository structure matches the config.</p>
|
<p>Welcome to your new Wiki Node. To enable content, please ensure your repository structure matches the config.</p>
|
||||||
<div class="p-4 bg-gray-800 rounded border border-gray-700">
|
|
||||||
<h3 class="text-white mt-0">Required Structure</h3>
|
|
||||||
<pre><code class="language-bash">/ (Root)
|
|
||||||
├── index.html
|
|
||||||
├── .nojekyll <-- IMPORTANT for GitHub Pages
|
|
||||||
├── assets/
|
|
||||||
│ └── bjorn.png
|
|
||||||
└── wiki/
|
|
||||||
├── structure.json
|
|
||||||
└── 01_General/
|
|
||||||
└── Introduction.md</code></pre>
|
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
hljs.highlightAll();
|
hljs.highlightAll();
|
||||||
}
|
}
|
||||||
@@ -910,14 +897,12 @@
|
|||||||
const pageNav = document.getElementById('page-nav');
|
const pageNav = document.getElementById('page-nav');
|
||||||
const scrollContainer = document.getElementById('scroll-container');
|
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, ' ');
|
const cleanFolder = folder.replace(/^\d+_/, '').replace(/_/g, ' ');
|
||||||
document.getElementById('breadcrumbs').innerHTML = `
|
document.getElementById('breadcrumbs').innerHTML = `
|
||||||
<span class="hover:text-hack-heading cursor-pointer" onclick="loadDefault()">wiki</span> <span>/</span>
|
<span class="hover:text-hack-heading cursor-pointer" onclick="loadDefault()">wiki</span> <span>/</span>
|
||||||
<span>${cleanFolder}</span> <span>/</span> <span class="text-hack-green font-bold">${title}</span>
|
<span>${cleanFolder}</span> <span>/</span> <span class="text-hack-green font-bold">${title}</span>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// UI Reset
|
|
||||||
if (!STATE.contentCache[`${folder}/${filename}`]) {
|
if (!STATE.contentCache[`${folder}/${filename}`]) {
|
||||||
viewer.innerHTML = `<div class="animate-pulse space-y-4 pt-10"><div class="h-8 bg-gray-800 rounded w-1/3 mb-6"></div><div class="h-4 bg-gray-800 rounded w-full"></div><div class="h-4 bg-gray-800 rounded w-5/6"></div><div class="h-4 bg-gray-800 rounded w-4/6"></div></div>`;
|
viewer.innerHTML = `<div class="animate-pulse space-y-4 pt-10"><div class="h-8 bg-gray-800 rounded w-1/3 mb-6"></div><div class="h-4 bg-gray-800 rounded w-full"></div><div class="h-4 bg-gray-800 rounded w-5/6"></div><div class="h-4 bg-gray-800 rounded w-4/6"></div></div>`;
|
||||||
}
|
}
|
||||||
@@ -929,11 +914,9 @@
|
|||||||
let text;
|
let text;
|
||||||
const cacheKey = `${folder}/${filename}`;
|
const cacheKey = `${folder}/${filename}`;
|
||||||
|
|
||||||
// 1. Get Markdown
|
|
||||||
if (STATE.contentCache[cacheKey]) {
|
if (STATE.contentCache[cacheKey]) {
|
||||||
text = STATE.contentCache[cacheKey];
|
text = STATE.contentCache[cacheKey];
|
||||||
} else {
|
} else {
|
||||||
// ATTEMPT 1: Structure Path
|
|
||||||
try {
|
try {
|
||||||
const path1 = `./wiki/${folder}/${filename}`;
|
const path1 = `./wiki/${folder}/${filename}`;
|
||||||
const res = await fetch(path1);
|
const res = await fetch(path1);
|
||||||
@@ -943,41 +926,33 @@
|
|||||||
throw new Error("404");
|
throw new Error("404");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} 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 path2 = `./wiki/${filename}`;
|
||||||
const res2 = await fetch(path2);
|
const res2 = await fetch(path2);
|
||||||
if (res2.ok) {
|
if (res2.ok) {
|
||||||
text = await res2.text();
|
text = await res2.text();
|
||||||
} else {
|
} else {
|
||||||
// If both fail, throw detailed error
|
throw new Error(`Content not found.`);
|
||||||
throw new Error(`Content not found. Tried:\n1. ./wiki/${folder}/${filename}\n2. ./wiki/${filename}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
STATE.contentCache[cacheKey] = text;
|
STATE.contentCache[cacheKey] = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Render
|
|
||||||
const cleanHTML = DOMPurify.sanitize(marked.parse(text));
|
const cleanHTML = DOMPurify.sanitize(marked.parse(text));
|
||||||
viewer.innerHTML = cleanHTML;
|
viewer.innerHTML = cleanHTML;
|
||||||
|
|
||||||
// 3. Stats
|
|
||||||
const wordCount = text.replace(/[#*`]/g, '').split(/\s+/).length;
|
const wordCount = text.replace(/[#*`]/g, '').split(/\s+/).length;
|
||||||
document.getElementById('reading-time').textContent = `~${Math.ceil(wordCount / 200)} min read`;
|
document.getElementById('reading-time').textContent = `~${Math.ceil(wordCount / 200)} min read`;
|
||||||
fetchLastUpdated(folder, filename);
|
fetchLastUpdated(folder, filename);
|
||||||
|
|
||||||
// 4. UI Enhancements
|
|
||||||
enhanceMarkdownContent();
|
enhanceMarkdownContent();
|
||||||
generateTOC();
|
generateTOC();
|
||||||
renderPagination(folder, title);
|
renderPagination(folder, title);
|
||||||
|
|
||||||
// 6. Navigation / History
|
|
||||||
if (pushHistory) {
|
if (pushHistory) {
|
||||||
const newUrl = `?page=${folder}/${filename}`;
|
const newUrl = `?page=${folder}/${filename}`;
|
||||||
window.history.pushState({ folder, title, filename }, "", newUrl);
|
window.history.pushState({ folder, title, filename }, "", newUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. Scroll
|
|
||||||
STATE.expandedSections = new Set([folder]);
|
STATE.expandedSections = new Set([folder]);
|
||||||
renderSidebar();
|
renderSidebar();
|
||||||
if (window.location.hash) {
|
if (window.location.hash) {
|
||||||
@@ -989,7 +964,6 @@
|
|||||||
|
|
||||||
if (window.innerWidth < 768) closeMenu();
|
if (window.innerWidth < 768) closeMenu();
|
||||||
|
|
||||||
// 8. Preload next/prev
|
|
||||||
preloadAdjacent(folder, title);
|
preloadAdjacent(folder, title);
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -999,20 +973,12 @@
|
|||||||
<h1 class="text-red-500 mb-2">Page Not Found</h1>
|
<h1 class="text-red-500 mb-2">Page Not Found</h1>
|
||||||
<div class="p-4 bg-red-900/20 border border-red-900 rounded text-sm font-mono text-red-300">
|
<div class="p-4 bg-red-900/20 border border-red-900 rounded text-sm font-mono text-red-300">
|
||||||
<strong>Error Details:</strong><br>${cleanError}
|
<strong>Error Details:</strong><br>${cleanError}
|
||||||
</div>
|
</div>`;
|
||||||
<p class="mt-4 text-gray-400 text-sm">
|
|
||||||
<strong>Troubleshooting:</strong><br>
|
|
||||||
1. Ensure the file exists in your <code>wiki/</code> folder.<br>
|
|
||||||
2. Check capitalization (Linux/GitHub is case-sensitive).<br>
|
|
||||||
3. Verify <code>.nojekyll</code> is at the root of your repo.
|
|
||||||
</p>`;
|
|
||||||
|
|
||||||
// Breadcrumbs Fallback
|
|
||||||
document.getElementById('breadcrumbs').innerHTML = `<span class="hover:text-hack-heading cursor-pointer" onclick="loadDefault()">wiki</span> <span>/</span> <span class="text-red-500">Error</span>`;
|
document.getElementById('breadcrumbs').innerHTML = `<span class="hover:text-hack-heading cursor-pointer" onclick="loadDefault()">wiki</span> <span>/</span> <span class="text-red-500">Error</span>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache last updated to prevent API ban
|
|
||||||
async function fetchLastUpdated(folder, filename) {
|
async function fetchLastUpdated(folder, filename) {
|
||||||
const CACHE_KEY = `bjorn_upd_${folder}_${filename}`;
|
const CACHE_KEY = `bjorn_upd_${folder}_${filename}`;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
@@ -1020,14 +986,13 @@
|
|||||||
const cached = localStorage.getItem(CACHE_KEY);
|
const cached = localStorage.getItem(CACHE_KEY);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
const data = JSON.parse(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}`;
|
document.getElementById('last-updated').textContent = `Updated: ${data.date}`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Rate limit protection
|
|
||||||
const path = `wiki/${folder}/${filename}`;
|
const path = `wiki/${folder}/${filename}`;
|
||||||
const res = await fetch(`https://api.github.com/repos/${STATE.repo}/commits?path=${path}&page=1&per_page=1`);
|
const res = await fetch(`https://api.github.com/repos/${STATE.repo}/commits?path=${path}&page=1&per_page=1`);
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
@@ -1038,9 +1003,7 @@
|
|||||||
localStorage.setItem(CACHE_KEY, JSON.stringify({ ts: now, date }));
|
localStorage.setItem(CACHE_KEY, JSON.stringify({ ts: now, date }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) { }
|
||||||
// Silent fail
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function preloadAdjacent(currentFolder, currentTitle) {
|
function preloadAdjacent(currentFolder, currentTitle) {
|
||||||
@@ -1063,7 +1026,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function enhanceMarkdownContent() {
|
function enhanceMarkdownContent() {
|
||||||
// Anchor Links Generation (Same ID logic as before)
|
|
||||||
document.querySelectorAll('#markdown-viewer h2, #markdown-viewer h3').forEach(h => {
|
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, '');
|
if (!h.id) h.id = h.textContent.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
|
||||||
|
|
||||||
@@ -1080,20 +1042,14 @@
|
|||||||
h.appendChild(anchor);
|
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 => {
|
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;
|
if (link.classList.contains('anchor-link')) return;
|
||||||
|
|
||||||
link.onclick = (e) => {
|
link.onclick = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const hash = link.getAttribute('href').substring(1); // remove #
|
closeTOC(); // Close mobile TOC if open
|
||||||
|
const hash = link.getAttribute('href').substring(1);
|
||||||
// 1. Try exact match (e.g. if ID matches TOC exactly)
|
|
||||||
let target = document.getElementById(hash);
|
let target = document.getElementById(hash);
|
||||||
|
|
||||||
// 2. Fallback: Try removing leading dashes (Fixes GitHub style TOC vs JS generated IDs)
|
|
||||||
if (!target) {
|
if (!target) {
|
||||||
const cleanHash = hash.replace(/^-+/, '');
|
const cleanHash = hash.replace(/^-+/, '');
|
||||||
target = document.getElementById(cleanHash);
|
target = document.getElementById(cleanHash);
|
||||||
@@ -1102,16 +1058,12 @@
|
|||||||
if (target) {
|
if (target) {
|
||||||
target.scrollIntoView({ behavior: 'smooth' });
|
target.scrollIntoView({ behavior: 'smooth' });
|
||||||
history.pushState(null, null, `#${hash}`);
|
history.pushState(null, null, `#${hash}`);
|
||||||
} else {
|
|
||||||
console.warn('Anchor target not found:', hash);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Syntax Highlight
|
|
||||||
document.querySelectorAll('pre code').forEach((el) => hljs.highlightElement(el));
|
document.querySelectorAll('pre code').forEach((el) => hljs.highlightElement(el));
|
||||||
|
|
||||||
// Copy Buttons
|
|
||||||
document.querySelectorAll('pre').forEach(pre => {
|
document.querySelectorAll('pre').forEach(pre => {
|
||||||
if (pre.querySelector('.copy-btn')) return;
|
if (pre.querySelector('.copy-btn')) return;
|
||||||
const btn = document.createElement('button');
|
const btn = document.createElement('button');
|
||||||
@@ -1126,31 +1078,20 @@
|
|||||||
pre.appendChild(btn);
|
pre.appendChild(btn);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Images -> Lightbox
|
|
||||||
document.querySelectorAll('#markdown-viewer img').forEach(img => {
|
document.querySelectorAll('#markdown-viewer img').forEach(img => {
|
||||||
img.onclick = () => openLightbox(img.src);
|
img.onclick = () => openLightbox(img.src);
|
||||||
});
|
});
|
||||||
|
|
||||||
// External Links
|
|
||||||
document.querySelectorAll('#markdown-viewer a').forEach(a => {
|
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 <img> → prevents icon under badges
|
|
||||||
if (
|
if (
|
||||||
a.hostname !== window.location.hostname &&
|
a.hostname !== window.location.hostname &&
|
||||||
!a.classList.contains('anchor-link') &&
|
!a.classList.contains('anchor-link') &&
|
||||||
a.getAttribute('href') &&
|
a.getAttribute('href') &&
|
||||||
!a.getAttribute('href').startsWith('#') &&
|
!a.getAttribute('href').startsWith('#') &&
|
||||||
!a.querySelector('img') // ⬅ NEW: do NOT add icon when link wraps an image
|
!a.querySelector('img')
|
||||||
) {
|
) {
|
||||||
a.target = '_blank';
|
a.target = '_blank';
|
||||||
a.rel = 'noopener noreferrer';
|
a.rel = 'noopener noreferrer';
|
||||||
|
|
||||||
// Add external icon
|
|
||||||
a.innerHTML +=
|
a.innerHTML +=
|
||||||
' <svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline-block opacity-50"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>';
|
' <svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline-block opacity-50"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>';
|
||||||
}
|
}
|
||||||
@@ -1170,7 +1111,6 @@
|
|||||||
const idx = flatList.findIndex(item => item.folder === currentFolder && item.title === currentTitle);
|
const idx = flatList.findIndex(item => item.folder === currentFolder && item.title === currentTitle);
|
||||||
if (idx === -1) return;
|
if (idx === -1) return;
|
||||||
|
|
||||||
// Prev
|
|
||||||
if (idx > 0) {
|
if (idx > 0) {
|
||||||
const prev = flatList[idx - 1];
|
const prev = flatList[idx - 1];
|
||||||
const btn = document.createElement('button');
|
const btn = document.createElement('button');
|
||||||
@@ -1180,7 +1120,6 @@
|
|||||||
navContainer.appendChild(btn);
|
navContainer.appendChild(btn);
|
||||||
} else { navContainer.appendChild(document.createElement('div')); }
|
} else { navContainer.appendChild(document.createElement('div')); }
|
||||||
|
|
||||||
// Next
|
|
||||||
if (idx < flatList.length - 1) {
|
if (idx < flatList.length - 1) {
|
||||||
const next = flatList[idx + 1];
|
const next = flatList[idx + 1];
|
||||||
const btn = document.createElement('button');
|
const btn = document.createElement('button');
|
||||||
@@ -1192,26 +1131,45 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function generateTOC() {
|
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');
|
const headings = document.querySelectorAll('#markdown-viewer h2, #markdown-viewer h3');
|
||||||
toc.innerHTML = '';
|
|
||||||
if (headings.length === 0) {
|
const createTOCItem = (h, isMobile) => {
|
||||||
toc.innerHTML = '<li class="text-gray-600 italic">No sections</li>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
headings.forEach(h => {
|
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
const link = document.createElement('a');
|
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.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) => {
|
link.onclick = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (isMobile) closeTOC();
|
||||||
document.getElementById(h.id).scrollIntoView({ behavior: 'smooth' });
|
document.getElementById(h.id).scrollIntoView({ behavior: 'smooth' });
|
||||||
history.pushState(null, null, `#${h.id}`);
|
history.pushState(null, null, `#${h.id}`);
|
||||||
};
|
};
|
||||||
li.appendChild(link);
|
li.appendChild(link);
|
||||||
toc.appendChild(li);
|
return li;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reset
|
||||||
|
desktopToc.innerHTML = '';
|
||||||
|
mobileToc.innerHTML = '';
|
||||||
|
|
||||||
|
if (headings.length === 0) {
|
||||||
|
desktopToc.innerHTML = '<li class="text-gray-600 italic">No sections</li>';
|
||||||
|
mobileToc.innerHTML = '<li class="text-gray-600 italic text-center py-4">No sections available for this page.</li>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
headings.forEach(h => {
|
||||||
|
desktopToc.appendChild(createTOCItem(h, false));
|
||||||
|
mobileToc.appendChild(createTOCItem(h, true));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1250,7 +1208,7 @@
|
|||||||
hasContent = true;
|
hasContent = true;
|
||||||
const filename = STATE.wikiData[folder][title];
|
const filename = STATE.wikiData[folder][title];
|
||||||
const link = document.createElement('a');
|
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.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 = `<span class="w-1.5 h-1.5 rounded-full bg-hack-border group-hover:bg-hack-green flex-shrink-0 transition-colors"></span> ${title}`;
|
link.innerHTML = `<span class="w-1.5 h-1.5 rounded-full bg-hack-border group-hover:bg-hack-green flex-shrink-0 transition-colors"></span> ${title}`;
|
||||||
link.onclick = (e) => {
|
link.onclick = (e) => {
|
||||||
@@ -1298,7 +1256,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Fallback
|
|
||||||
for (const [folder, files] of Object.entries(STATE.wikiData)) {
|
for (const [folder, files] of Object.entries(STATE.wikiData)) {
|
||||||
for (const [title, file] of Object.entries(files)) {
|
for (const [title, file] of Object.entries(files)) {
|
||||||
if (title.toLowerCase().includes(q)) {
|
if (title.toLowerCase().includes(q)) {
|
||||||
@@ -1330,7 +1287,6 @@
|
|||||||
if (pushHistory) window.history.pushState({ page: 'versions' }, "", "?page=changelog");
|
if (pushHistory) window.history.pushState({ page: 'versions' }, "", "?page=changelog");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get releases from API
|
|
||||||
const res = await fetch(`https://api.github.com/repos/${STATE.repo}/releases`);
|
const res = await fetch(`https://api.github.com/repos/${STATE.repo}/releases`);
|
||||||
if (!res.ok) throw new Error("API Limit");
|
if (!res.ok) throw new Error("API Limit");
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@@ -1354,7 +1310,7 @@
|
|||||||
list.appendChild(div);
|
list.appendChild(div);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
document.getElementById('versions-list').innerHTML = `<p class="text-red-400">Unable to load changelog (GitHub API Rate Limit might be reached). Please check back later.</p>`;
|
document.getElementById('versions-list').innerHTML = `<p class="text-red-400">Unable to load changelog. Check back later.</p>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1375,14 +1331,54 @@
|
|||||||
document.body.style.overflow = '';
|
document.body.style.overflow = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mobile Menu
|
// Mobile Menu & TOC Logic
|
||||||
const sidebar = document.getElementById('sidebar');
|
const sidebar = document.getElementById('sidebar');
|
||||||
|
const tocSidebar = document.getElementById('mobile-toc-sidebar');
|
||||||
const overlay = document.getElementById('overlay');
|
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('menu-btn').onclick = openMenu;
|
||||||
document.getElementById('close-sidebar-btn').onclick = closeMenu;
|
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
|
// Events
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
@@ -1390,7 +1386,10 @@
|
|||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
if (document.activeElement === searchInput) searchInput.blur();
|
if (document.activeElement === searchInput) searchInput.blur();
|
||||||
else if (lightbox.classList.contains('active')) closeLightbox();
|
else if (lightbox.classList.contains('active')) closeLightbox();
|
||||||
else closeMenu();
|
else {
|
||||||
|
closeMenu();
|
||||||
|
closeTOC();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1407,7 +1406,6 @@
|
|||||||
const scrollPercent = (scrollHeight > 0) ? (scrollTop / scrollHeight) * 100 : 0;
|
const scrollPercent = (scrollHeight > 0) ? (scrollTop / scrollHeight) * 100 : 0;
|
||||||
progressBar.style.width = scrollPercent + '%';
|
progressBar.style.width = scrollPercent + '%';
|
||||||
|
|
||||||
// Show/Hide Scroll Top Button
|
|
||||||
if (scrollTop > 300) scrollBtn.classList.remove('hidden');
|
if (scrollTop > 300) scrollBtn.classList.remove('hidden');
|
||||||
else scrollBtn.classList.add('hidden');
|
else scrollBtn.classList.add('hidden');
|
||||||
});
|
});
|
||||||
@@ -1416,16 +1414,17 @@
|
|||||||
scrollBtn.onclick = () => scrollContainer.scrollTo({ top: 0, behavior: 'smooth' });
|
scrollBtn.onclick = () => scrollContainer.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- MOBILE SWIPE GESTURES ---
|
// --- MOBILE SWIPE GESTURES ---
|
||||||
// Detect swipe from left edge → open menu
|
|
||||||
let touchStartX = 0;
|
let touchStartX = 0;
|
||||||
let touchStartY = 0;
|
let touchStartY = 0;
|
||||||
let touchEndX = 0;
|
let touchEndX = 0;
|
||||||
let touchEndY = 0;
|
let touchEndY = 0;
|
||||||
|
|
||||||
const MIN_SWIPE_DISTANCE = 50; // minimal movement to count as a swipe
|
const MIN_SWIPE_DISTANCE = 50;
|
||||||
const MAX_VERTICAL_DRIFT = 80; // tolerance to avoid accidental scroll/swipe mix
|
const MAX_VERTICAL_DRIFT = 80;
|
||||||
const EDGE_ZONE = 200; // swipe must start within 200px from screen left
|
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) => {
|
document.addEventListener("touchstart", (e) => {
|
||||||
const t = e.changedTouches[0];
|
const t = e.changedTouches[0];
|
||||||
@@ -1442,22 +1441,31 @@
|
|||||||
document.addEventListener("touchend", () => {
|
document.addEventListener("touchend", () => {
|
||||||
const dx = touchEndX - touchStartX;
|
const dx = touchEndX - touchStartX;
|
||||||
const dy = Math.abs(touchEndY - touchStartY);
|
const dy = Math.abs(touchEndY - touchStartY);
|
||||||
|
const screenW = window.innerWidth;
|
||||||
|
|
||||||
// 1. SWIPE → RIGHT = OPEN MENU
|
// 1. SWIPE LEFT -> RIGHT (Open Menu or Close TOC)
|
||||||
if (
|
if (dx > MIN_SWIPE_DISTANCE && dy < MAX_VERTICAL_DRIFT) {
|
||||||
touchStartX < EDGE_ZONE && // gesture started at screen left
|
// If TOC is open, close it
|
||||||
dx > MIN_SWIPE_DISTANCE && // swiped enough to the right
|
if (!tocSidebar.classList.contains('translate-x-full')) {
|
||||||
dy < MAX_VERTICAL_DRIFT // not a vertical scroll
|
closeTOC();
|
||||||
) {
|
}
|
||||||
openMenu();
|
// Else if swipe starts near left edge, open Menu
|
||||||
|
else if (touchStartX < EDGE_ZONE_LEFT) {
|
||||||
|
openMenu();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. SWIPE → LEFT = CLOSE MENU
|
// 2. SWIPE RIGHT -> LEFT (Open TOC or Close Menu)
|
||||||
if (
|
if (dx < -MIN_SWIPE_DISTANCE && dy < MAX_VERTICAL_DRIFT) {
|
||||||
dx < -MIN_SWIPE_DISTANCE && // swipe left
|
// If Menu is open, close it
|
||||||
dy < MAX_VERTICAL_DRIFT // again avoid scroll false positives
|
if (!sidebar.classList.contains('-translate-x-full')) {
|
||||||
) {
|
closeMenu();
|
||||||
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user