Update template

This commit is contained in:
Fabien POLLY
2026-01-23 10:48:35 +01:00
parent 57d4b5b540
commit 695cf6671b
7 changed files with 342 additions and 103 deletions

View File

@@ -324,10 +324,13 @@
.nav-link.active {
background-color: var(--accent-dim);
color: var(--accent-green);
border-left: 2px solid var(--accent-green);
padding-left: 0.5rem !important;
}
.nav-link.active span {
background-color: var(--accent-green) !important;
}
/* TOC & Scroll */
.toc-link.active {
color: var(--accent-green);
@@ -464,6 +467,19 @@
}
}
/* Hide scrollbar for breadcrumbs */
#breadcrumbs {
-ms-overflow-style: none;
/* IE and Edge */
scrollbar-width: none;
/* Firefox */
}
#breadcrumbs::-webkit-scrollbar {
display: none;
/* Chrome, Safari and Opera */
}
/* Toast Notification */
#toast-container {
position: fixed;
@@ -700,8 +716,9 @@
<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 id="label-mobile-toc"
class="text-xs font-bold text-gray-500 uppercase tracking-widest font-mono">Table of Contents</span>
<div id="label-mobile-toc"
class="text-[10px] font-bold text-hack-green uppercase tracking-widest px-3 py-1 bg-hack-greenDim border border-hack-border rounded-full font-mono truncate max-w-[200px]">
</div>
<button id="close-toc-btn" class="text-gray-400 hover:text-white">
<i data-lucide="x" class="w-5 h-5"></i>
</button>
@@ -717,7 +734,7 @@
<div class="flex-1 min-w-0 content-transition-area">
<div
class="flex flex-col md:flex-row md:items-center justify-between mb-6 gap-2 border-b border-hack-border/50 pb-4">
<div class="flex items-center gap-2 text-xs font-mono text-gray-500 overflow-x-auto whitespace-nowrap pb-2 md:pb-0"
<div class="flex items-center gap-2 text-xs font-mono text-gray-500 overflow-x-auto whitespace-nowrap md:pb-0"
id="breadcrumbs"></div>
<div class="flex items-center gap-4">
@@ -744,15 +761,18 @@
<aside class="hidden xl:block w-64 shrink-0">
<div class="sticky top-6">
<h4 id="label-on-this-page"
class="text-xs font-bold text-gray-500 uppercase tracking-widest mb-4 border-b border-hack-border pb-2">
On this page</h4>
<div class="mb-6">
<button id="label-on-this-page"
class="text-[10px] font-bold text-hack-green uppercase tracking-widest px-3 py-1.5 bg-hack-greenDim border border-hack-border rounded-full transition-all hover:bg-hack-green hover:text-white text-left max-w-full truncate"
onclick="document.getElementById('scroll-container').scrollTo({top: 0, behavior: 'smooth'})">
</button>
</div>
<div class="relative">
<svg id="toc-svg">
<path id="toc-path" fill="none" stroke="var(--accent-green)" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round" />
</svg>
<ul id="toc-container" class="space-y-2 text-xs border-l border-hack-border pl-3"></ul>
<ul id="toc-container" class="space-y-2 text-xs pl-3"></ul>
</div>
</div>
</aside>
@@ -793,7 +813,10 @@
searchIndex: [],
expandedSections: new Set(),
repo: CONFIG.repo,
branch: CONFIG.branch
branch: CONFIG.branch,
currentTitle: "",
currentFolder: "",
currentFilename: ""
};
// --- 1.5 INITIALIZE CONFIG ---
@@ -1074,20 +1097,24 @@
if (pageParam === 'changelog') {
toggleVersionsPage(null, false);
} else if (pageParam) {
let found = false;
for (const [folder, files] of Object.entries(STATE.wikiData)) {
for (const [title, file] of Object.entries(files)) {
if (`${folder}/${file}` === pageParam || file === pageParam) {
loadContent(folder, title, file, false);
STATE.expandedSections.add(folder);
renderSidebar();
found = true;
break;
const flatList = getFlatPageList();
const found = flatList.find(item => `${item.folder}/${item.file}` === pageParam || item.file === pageParam);
if (found) {
loadContent(found.folder, found.title, found.file, false);
// Expand all parent folders
const parts = found.folder.split('/');
let current = "";
parts.forEach(p => {
if (p) {
current = current ? `${current}/${p}` : p;
STATE.expandedSections.add(current);
}
}
if (found) break;
});
renderSidebar();
} else {
loadDefault();
}
if (!found) loadDefault();
} else {
loadDefault();
}
@@ -1095,37 +1122,59 @@
async function buildSearchIndex() {
const promises = [];
for (const [folder, files] of Object.entries(STATE.wikiData)) {
for (const [title, filename] of Object.entries(files)) {
if (!filename.endsWith('.md')) continue;
promises.push(
fetch(`./wiki/${folder}/${filename}`)
.then(res => { if (!res.ok) return ''; return res.text(); })
.then(text => {
if (!text) return;
STATE.searchIndex.push({
folder, title, filename,
content: text.toLowerCase(),
titleLower: title.toLowerCase()
});
})
.catch(err => console.log("Indexing skip:", filename))
);
function indexRecursive(data, currentPath = '') {
for (const [key, value] of Object.entries(data)) {
if (typeof value === 'object' && value !== null) {
const folderPath = currentPath ? `${currentPath}/${key}` : key;
indexRecursive(value, folderPath);
} else if (typeof value === 'string' && value.endsWith('.md')) {
const filename = value;
const title = key;
const folder = currentPath;
promises.push(
fetch(`./wiki/${folder}/${filename}`)
.then(res => { if (!res.ok) return ''; return res.text(); })
.then(text => {
if (!text) return;
STATE.searchIndex.push({
folder, title, filename,
content: text.toLowerCase(),
titleLower: title.toLowerCase()
});
})
.catch(err => console.log("Indexing skip:", filename))
);
}
}
}
indexRecursive(STATE.wikiData);
await Promise.all(promises);
}
function loadDefault() {
const folders = Object.keys(STATE.wikiData);
if (folders.length === 0) return;
const firstFolder = folders[0];
const files = Object.keys(STATE.wikiData[firstFolder]);
if (files.length === 0) return;
function getFlatPageList() {
const flatList = [];
function traverse(data, currentPath = '') {
for (const [key, value] of Object.entries(data)) {
if (typeof value === 'object' && value !== null) {
const folderPath = currentPath ? `${currentPath}/${key}` : key;
traverse(value, folderPath);
} else if (typeof value === 'string' && value.endsWith('.md')) {
flatList.push({ folder: currentPath, title: key, file: value });
}
}
}
traverse(STATE.wikiData);
return flatList;
}
const firstTitle = files[0];
loadContent(firstFolder, firstTitle, STATE.wikiData[firstFolder][firstTitle], true);
function loadDefault() {
const flatList = getFlatPageList();
if (flatList.length === 0) return;
const first = flatList[0];
loadContent(first.folder, first.title, first.file, true);
}
function showErrorState(msg) {
@@ -1155,11 +1204,63 @@
const pageNav = document.getElementById('page-nav');
const scrollContainer = document.getElementById('scroll-container');
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>
STATE.currentTitle = title;
STATE.currentFolder = folder;
STATE.currentFilename = filename;
document.getElementById('label-on-this-page').innerText = title;
document.getElementById('label-mobile-toc').innerText = title;
const flatList = getFlatPageList();
const idx = flatList.findIndex(item => item.folder === folder && item.title === title);
const prev = idx > 0 ? flatList[idx - 1] : null;
const next = idx < flatList.length - 1 ? flatList[idx + 1] : null;
const segments = folder.split('/').filter(s => s).map(s => s.replace(/^\d+_/, '').replace(/_/g, ' '));
const breadcrumbs = document.getElementById('breadcrumbs');
let breadcrumbParts = [];
breadcrumbParts.push(`<span class="hover:text-hack-heading cursor-pointer" onclick="loadDefault()">wiki</span>`);
if (segments.length > 2) {
breadcrumbParts.push(`<span class="opacity-50 cursor-help" title="${segments.slice(0, -1).join(' / ')}">...</span>`);
// For the last folder segment, find its first page
const lastFolderKey = folder.split('/').pop();
const folderData = folder.split('/').reduce((obj, key) => obj[key], STATE.wikiData);
const firstPage = Object.entries(folderData).find(([k, v]) => typeof v === 'string');
if (firstPage) {
breadcrumbParts.push(`<span class="hover:text-hack-heading cursor-pointer" onclick="loadContent('${folder}', '${firstPage[0]}', '${firstPage[1]}')">${segments[segments.length - 1]}</span>`);
} else {
breadcrumbParts.push(`<span>${segments[segments.length - 1]}</span>`);
}
} else {
let currentPath = "";
const folderParts = folder.split('/').filter(s => s);
segments.forEach((seg, i) => {
const partKey = folderParts[i];
currentPath = currentPath ? `${currentPath}/${partKey}` : partKey;
// Find first page in this specific folder level
const folderData = currentPath.split('/').reduce((obj, key) => obj[key], STATE.wikiData);
const firstPage = Object.entries(folderData).find(([k, v]) => typeof v === 'string');
if (firstPage) {
breadcrumbParts.push(`<span class="hover:text-hack-heading cursor-pointer" onclick="loadContent('${currentPath}', '${firstPage[0]}', '${firstPage[1]}')">${seg}</span>`);
} else {
breadcrumbParts.push(`<span>${seg}</span>`);
}
});
}
breadcrumbParts.push(`<span class="text-hack-green font-bold">${title}</span>`);
breadcrumbs.innerHTML = `
${prev ? `<button onclick="loadContent('${prev.folder}', '${prev.title}', '${prev.file}')" class="hover:text-hack-green transition-colors mr-1" title="Previous: ${prev.title}"><i data-lucide="chevron-left" class="w-3.5 h-3.5"></i></button>` : ''}
${breadcrumbParts.join(' <span class="opacity-30">/</span> ')}
${next ? `<button onclick="loadContent('${next.folder}', '${next.title}', '${next.file}')" class="hover:text-hack-green transition-colors ml-1" title="Next: ${next.title}"><i data-lucide="chevron-right" class="w-3.5 h-3.5"></i></button>` : ''}
`;
lucide.createIcons();
// Only show loader if not in RAM cache
if (!STATE.contentCache[`${folder}/${filename}`]) {
@@ -1303,12 +1404,7 @@
}
function preloadAdjacent(currentFolder, currentTitle) {
let flatList = [];
for (const [folder, files] of Object.entries(STATE.wikiData)) {
for (const [title, file] of Object.entries(files)) {
flatList.push({ folder, title, file });
}
}
const flatList = getFlatPageList();
const idx = flatList.findIndex(item => item.folder === currentFolder && item.title === currentTitle);
[idx - 1, idx + 1].forEach(i => {
@@ -1403,12 +1499,7 @@
function renderPagination(currentFolder, currentTitle) {
const navContainer = document.getElementById('page-nav');
let flatList = [];
for (const [folder, files] of Object.entries(STATE.wikiData)) {
for (const [title, file] of Object.entries(files)) {
flatList.push({ folder, title, file });
}
}
const flatList = getFlatPageList();
const idx = flatList.findIndex(item => item.folder === currentFolder && item.title === currentTitle);
if (idx === -1) return;
@@ -1440,8 +1531,18 @@
const createTOCItem = (h, isMobile) => {
const li = document.createElement('li');
const link = document.createElement('a');
link.textContent = h.childNodes[0].textContent;
link.href = `#${h.id}`;
// Extract text content excluding anchor links and other UI elements
let text = "";
h.childNodes.forEach(node => {
if (node.nodeType === Node.TEXT_NODE) {
text += node.textContent;
} else if (node.nodeType === Node.ELEMENT_NODE && !node.classList.contains('anchor-link')) {
text += node.innerText || node.textContent;
}
});
link.textContent = text.trim();
link.href = h.id ? `#${h.id}` : '#';
// Indentation based on tag
let indentClass = '';
@@ -1457,8 +1558,13 @@
link.onclick = (e) => {
e.preventDefault();
if (isMobile) closeTOC();
document.getElementById(h.id).scrollIntoView({ behavior: 'smooth' });
history.pushState(null, null, `#${h.id}`);
if (h.id) {
document.getElementById(h.id).scrollIntoView({ behavior: 'smooth' });
history.pushState(null, null, `#${h.id}`);
} else {
document.getElementById('scroll-container').scrollTo({ top: 0, behavior: 'smooth' });
history.pushState(null, null, window.location.pathname + window.location.search);
}
};
li.appendChild(link);
return li;
@@ -1567,50 +1673,64 @@
container.innerHTML = '';
let hasContent = false;
Object.keys(STATE.wikiData).forEach(folder => {
if (searchResults && !searchResults[folder]) return;
function renderRecursive(data, parentContainer, currentPath = '', level = 0) {
Object.keys(data).forEach(key => {
const value = data[key];
const isFolder = typeof value === 'object' && value !== null;
const cleanName = key.replace(/^\d+_/, '').replace(/_/g, ' ');
const cleanName = folder.replace(/^\d+_/, '').replace(/_/g, ' ');
const group = document.createElement('div');
group.className = 'nav-group mb-1';
if (isFolder) {
const folderPath = currentPath ? `${currentPath}/${key}` : key;
if (searchResults && !searchResults[key]) return;
const btn = document.createElement('button');
const isExpanded = STATE.expandedSections.has(folder) || searchResults;
btn.className = `section-header w-full flex items-center justify-between px-2 py-2 text-gray-500 hover:text-hack-heading transition-colors rounded hover:bg-hack-bg focus:outline-none focus:bg-hack-bg ${isExpanded ? 'active' : ''}`;
btn.innerHTML = `
<span class="flex items-center gap-2 text-[11px] font-bold uppercase tracking-widest font-mono"><i data-lucide="folder" class="w-3 h-3"></i> ${cleanName}</span>
<i data-lucide="chevron-right" class="section-arrow w-3 h-3 transition-transform"></i>
`;
btn.onclick = () => {
if (STATE.expandedSections.has(folder)) STATE.expandedSections.delete(folder);
else STATE.expandedSections.add(folder);
renderSidebar(searchResults);
};
const group = document.createElement('div');
group.className = `nav-group mb-1 ${level > 0 ? 'ml-2' : ''}`;
const list = document.createElement('div');
list.className = `nav-list pl-2 border-l border-hack-border/30 ml-1 ${isExpanded ? 'expanded' : 'collapsed'}`;
const btn = document.createElement('button');
const isExpanded = STATE.expandedSections.has(folderPath) || searchResults;
btn.className = `section-header w-full flex items-center justify-between px-2 py-2 text-gray-500 hover:text-hack-heading transition-colors rounded hover:bg-hack-bg focus:outline-none focus:bg-hack-bg ${isExpanded ? 'active' : ''}`;
btn.innerHTML = `
<span class="flex items-center gap-2 text-[11px] font-bold uppercase tracking-widest font-mono"><i data-lucide="${level === 0 ? 'folder' : 'folder-open'}" class="w-3 h-3"></i> ${cleanName}</span>
<i data-lucide="chevron-right" class="section-arrow w-3 h-3 transition-transform"></i>
`;
btn.onclick = () => {
if (STATE.expandedSections.has(folderPath)) STATE.expandedSections.delete(folderPath);
else STATE.expandedSections.add(folderPath);
renderSidebar(searchResults);
};
Object.keys(STATE.wikiData[folder]).forEach(title => {
if (searchResults && !searchResults[folder][title]) return;
hasContent = true;
const filename = STATE.wikiData[folder][title];
const link = document.createElement('a');
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 = `<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) => {
e.preventDefault();
loadContent(folder, title, filename);
document.querySelectorAll('.nav-link').forEach(l => l.classList.remove('active'));
link.classList.add('active');
};
list.appendChild(link);
const list = document.createElement('div');
list.className = `nav-list ${isExpanded ? 'expanded' : 'collapsed'}`;
renderRecursive(value, list, folderPath, level + 1);
group.appendChild(btn);
group.appendChild(list);
parentContainer.appendChild(group);
} else {
if (searchResults && !searchResults[currentPath]?.[key]) return;
hasContent = true;
const filename = value;
const link = document.createElement('a');
const fullPath = currentPath ? `${currentPath}/${filename}` : filename;
link.href = `?page=${fullPath}`;
const isActive = STATE.currentFolder === currentPath && STATE.currentFilename === 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 ${isActive ? 'active' : ''}`;
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> ${key}`;
link.onclick = (e) => {
e.preventDefault();
loadContent(currentPath, key, filename);
document.querySelectorAll('.nav-link').forEach(l => l.classList.remove('active'));
link.classList.add('active');
};
parentContainer.appendChild(link);
}
});
}
group.appendChild(btn);
group.appendChild(list);
container.appendChild(group);
});
renderRecursive(STATE.wikiData, container);
if (!hasContent && searchResults) noResults.classList.remove('hidden');
else noResults.classList.add('hidden');
@@ -1673,12 +1793,27 @@
async function performToggleVersionsPage(btn, pushHistory = true) {
const viewer = document.getElementById('markdown-viewer');
// Update state and UI labels
STATE.currentFolder = "";
STATE.currentFilename = "";
STATE.currentTitle = CONFIG.ui.changelogTitle;
document.getElementById('label-on-this-page').innerText = CONFIG.ui.changelogTitle;
document.getElementById('label-mobile-toc').innerText = CONFIG.ui.changelogTitle;
document.getElementById('breadcrumbs').innerHTML = `<span class="hover:text-hack-heading cursor-pointer" onclick="loadDefault()">${CONFIG.projectName}</span> <span>/</span> <span class="text-hack-green font-bold">${CONFIG.ui.changelogTitle}</span>`;
document.getElementById('page-nav').innerHTML = '';
document.getElementById('reading-time').innerHTML = '';
document.getElementById('last-updated').innerHTML = '';
viewer.innerHTML = `<h1>${CONFIG.ui.changelogTitle}</h1><div id="versions-list" class="space-y-4 mt-6"><div class="animate-pulse text-hack-green">${CONFIG.ui.fetchingReleasesText}</div></div>`;
// Update TOC with initial H1
enhanceMarkdownContent();
generateTOC();
// Update sidebar to clear active links
renderSidebar();
if (pushHistory) window.history.pushState({ page: 'versions' }, "", "?page=changelog");
try {
@@ -1704,8 +1839,15 @@
`;
list.appendChild(div);
});
// Update TOC with new H2s
enhanceMarkdownContent();
generateTOC();
} catch (e) {
document.getElementById('versions-list').innerHTML = `<p class="text-red-400">Unable to load changelog. Check back later.</p>`;
enhanceMarkdownContent();
generateTOC();
}
}

View File

@@ -0,0 +1 @@
# Test1

View File

@@ -0,0 +1 @@
# Test1

View File

@@ -0,0 +1,72 @@
# Long Page Test CSS
This page is designed to test the CSS rendering of various markdown elements, especially headings and long content.
## Level 2 Heading
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
### Level 3 Heading
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
#### Level 4 Heading
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
##### Level 5 Heading
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
###### Level 6 Heading
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium.
## Lists and Formatting
### Unordered List
- Item 1
- Item 2
- Sub-item 2.1
- Sub-item 2.2
- Item 3
### Ordered List
1. First item
2. Second item
3. Third item
### Blockquotes
> This is a blockquote.
> It can span multiple lines.
> -- Someone Famous
### Code Blocks
```javascript
function helloWorld() {
console.log("Hello, world!");
}
```
Inline code: `const x = 10;`
### Tables
| Header 1 | Header 2 | Header 3 |
|----------|----------|----------|
| Row 1 Col 1 | Row 1 Col 2 | Row 1 Col 3 |
| Row 2 Col 1 | Row 2 Col 2 | Row 2 Col 3 |
## More Content to Test Scrolling
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia ac.
### Another H3
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.
#### Another H4
Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus.

View File

@@ -0,0 +1,7 @@
# Sub Page
This is a sub page located in a sub-category to test nested navigation.
## Content
Testing nested sidebar levels.

View File

@@ -0,0 +1,7 @@
# Sub Sub Page
This is a sub-sub page located in a sub-sub-category to test deep nested navigation.
## Content
Testing deep nested sidebar levels (3 levels deep).

View File

@@ -4,5 +4,14 @@
},
"02_Setup": {
"Installation": "Installation.md"
},
"03_Test_CSS": {
"Long Page": "Long_Page.md",
"Sub_Category": {
"Sub Page": "Sub_Page.md",
"Sub_Sub_Category": {
"Sub Sub Page": "Sub_Sub_Page.md"
}
}
}
}