feat: Improve UI with early breadcrumbs, sidebar auto-expansion, and mobile swipe gestures, fix image and TOC rendering, and add an Introduction page.

This commit is contained in:
Fabien POLLY
2025-12-09 12:23:42 +01:00
parent 3d56944550
commit 32deb0752e
2 changed files with 316 additions and 14 deletions

View File

@@ -661,11 +661,21 @@
<script>
// --- 1. CONFIG & STATE ---
const renderer = new marked.Renderer();
// Custom Image Rendering for Lazy Loading and styling
// --- 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 `<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) {
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>`;
@@ -813,7 +823,6 @@
toggleVersionsPage(null, false);
} else if (pageParam) {
// pageParam format: "Folder/Filename.md" or similar
// We need to find the title for this file
let found = false;
for (const [folder, files] of Object.entries(STATE.wikiData)) {
for (const [title, file] of Object.entries(files)) {
@@ -901,6 +910,13 @@
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 = `
<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>
`;
// UI Reset
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>`;
@@ -955,13 +971,6 @@
generateTOC();
renderPagination(folder, title);
// 5. Breadcrumbs
const cleanFolder = folder.replace(/^\d+_/, '').replace(/_/g, ' ');
document.getElementById('breadcrumbs').innerHTML = `
<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>
`;
// 6. Navigation / History
if (pushHistory) {
const newUrl = `?page=${folder}/${filename}`;
@@ -969,6 +978,8 @@
}
// 7. Scroll
STATE.expandedSections = new Set([folder]);
renderSidebar();
if (window.location.hash) {
const el = document.getElementById(window.location.hash.substring(1));
if (el) el.scrollIntoView();
@@ -995,6 +1006,9 @@
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>`;
}
}
@@ -1049,7 +1063,7 @@
}
function enhanceMarkdownContent() {
// Anchor Links
// 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, '');
@@ -1066,6 +1080,34 @@
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)
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);
}
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));
@@ -1091,13 +1133,29 @@
// External Links
document.querySelectorAll('#markdown-viewer a').forEach(a => {
if (a.hostname !== window.location.hostname && !a.className.includes('anchor-link')) {
// 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 (
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.target = '_blank';
a.rel = 'noopener noreferrer';
// Add external icon
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>';
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>';
}
});
}
function renderPagination(currentFolder, currentTitle) {
@@ -1336,8 +1394,76 @@
}
});
// Initialize UI Logic (Scroll Bar) immediately
function initUI() {
const scrollContainer = document.getElementById('scroll-container');
const progressBar = document.getElementById('reading-progress-bar');
const scrollBtn = document.getElementById('scroll-top-btn');
if (scrollContainer && progressBar) {
scrollContainer.addEventListener('scroll', () => {
const scrollTop = scrollContainer.scrollTop;
const scrollHeight = scrollContainer.scrollHeight - scrollContainer.clientHeight;
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');
});
}
if (scrollBtn) {
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 = 40; // swipe must start within 40px from screen left
document.addEventListener("touchstart", (e) => {
const t = e.changedTouches[0];
touchStartX = t.clientX;
touchStartY = t.clientY;
});
document.addEventListener("touchmove", (e) => {
const t = e.changedTouches[0];
touchEndX = t.clientX;
touchEndY = t.clientY;
});
document.addEventListener("touchend", () => {
const dx = touchEndX - touchStartX;
const dy = Math.abs(touchEndY - touchStartY);
// 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();
}
// 2. SWIPE → LEFT = CLOSE MENU
if (
dx < -MIN_SWIPE_DISTANCE && // swipe left
dy < MAX_VERTICAL_DRIFT // again avoid scroll false positives
) {
closeMenu();
}
});
// INIT
window.onload = () => {
initUI();
fetchLatestVersion();
initWiki();
lucide.createIcons();