diff --git a/css/layout/_header.css b/css/layout/_header.css
index b2c8837f..f68a25c0 100644
--- a/css/layout/_header.css
+++ b/css/layout/_header.css
@@ -15,7 +15,9 @@
/* 1. Glavni kontejner za glavo */
.dropdown-header-navigation {
- position: absolute;
+ position: fixed;
+ top: 0;
+ left: 0;
width: 100%;
height: 80px;
background: #FFFFFF;
diff --git a/css/pages/_article.css b/css/pages/_article.css
index 4f1dbdb9..6456da69 100644
--- a/css/pages/_article.css
+++ b/css/pages/_article.css
@@ -51,15 +51,36 @@
width: 288px;
flex-shrink: 0;
position: sticky;
- top: 120px;
- height: calc(100vh - 240px);
+ top: 112px;
+ /* max-height (not fixed height) so short lists size to content and never clip;
+ the list only scrolls when the viewport is genuinely too short. */
+ max-height: calc(100vh - 140px);
+ display: flex;
+ flex-direction: column;
+}
+
+/* Article title above the table of contents, so the reader always knows
+ which article they are reading. Injected by main.js from the page
. */
+.article-sidebar-title {
+ flex-shrink: 0;
+ margin: 0 0 16px 0;
+ padding: 0 0 16px 20px;
+ font-size: 16px;
+ font-weight: 600;
+ line-height: 24px;
+ color: #101828;
+ border-bottom: 1px solid #EAECF0;
}
.scrollspy-nav {
- height: 100%;
+ /* Fill the remaining sidebar height and scroll internally when needed.
+ min-height:0 lets a flex child actually shrink so overflow works. */
+ flex: 1 1 auto;
+ min-height: 0;
overflow-y: auto;
- display: flex;
- align-items: center;
+ /* No flex centering here: centering inside a scroll container pushed the first
+ item out of reach at 100% zoom / short viewports. Content now flows from the top. */
+ padding: 4px 0;
}
.scrollspy-nav ul {
@@ -68,15 +89,16 @@
margin: 0;
display: flex;
flex-direction: column;
- gap: 40px;
+ gap: 8px;
border-left: 2px solid #EAECF0;
}
.scrollspy-nav li a {
display: block;
- padding: 12px 24px;
+ padding: 8px 20px;
text-decoration: none;
- font-size: 16px;
+ font-size: 14px;
+ line-height: 20px;
font-weight: 500;
color: #667085;
border-left: 2px solid transparent;
diff --git a/main.js b/main.js
index fdcc0970..e90352f4 100644
--- a/main.js
+++ b/main.js
@@ -275,39 +275,134 @@ document.addEventListener('DOMContentLoaded', function() {
try {
const articlePage = document.querySelector('.article-page');
if (articlePage) {
- const scrollspyNav = document.querySelector('.scrollspy-nav');
- const mobileHeader = document.querySelector('.mobile-article-header');
- const mobileTitle = document.getElementById('mobile-article-title');
- const mobileDropdown = document.querySelector('.mobile-scrollspy-dropdown');
- const mobileLinksContainer = document.getElementById('mobile-scrollspy-links');
- const progressBar = document.querySelector('.progress-bar');
-
- const headings = Array.from(articlePage.querySelectorAll('.article-body h2'));
- const desktopNavLinks = scrollspyNav ? Array.from(scrollspyNav.querySelectorAll('a')) : [];
+ const articleContainer = articlePage.querySelector('.article-container');
+ const articleBody = articlePage.querySelector('.article-body');
+ const headings = articleBody ? Array.from(articleBody.querySelectorAll('h2')) : [];
+ const articleTitleEl = articlePage.querySelector('.article-header h1');
+ const articleTitle = articleTitleEl ? articleTitleEl.textContent.trim()
+ : (document.title || '').trim();
- // --- Dinamično ustvarjanje mobilnega spustnega seznama ---
- if (headings.length > 0 && mobileLinksContainer) {
+ // Label for a heading in the table of contents. An optional
+ // data-nav-title attribute lets an article override the full
+ // text with a shorter label; otherwise we use the heading text.
+ const navLabel = (heading) =>
+ (heading.getAttribute('data-nav-title') || heading.textContent).trim();
+
+ // --- 5a. Zagotovi, da ima vsak naslov enoličen id (za sidra) ---
+ // Transliterate then slugify so anchors are readable in every language:
+ // Slovenian diacritics (č/š/ž/…) and Macedonian Cyrillic map to ASCII.
+ // Anything still unmapped is dropped and falls back to section-N below.
+ const TRANSLIT = {
+ // Slovenian / Latin diacritics
+ 'č':'c','ć':'c','š':'s','ž':'z','đ':'dj',
+ 'á':'a','à':'a','â':'a','ä':'a','ã':'a','é':'e','è':'e','ê':'e','ë':'e',
+ 'í':'i','ì':'i','î':'i','ï':'i','ó':'o','ò':'o','ô':'o','ö':'o','õ':'o',
+ 'ú':'u','ù':'u','û':'u','ü':'u','ñ':'n','ç':'c','ß':'ss',
+ // Macedonian Cyrillic
+ 'а':'a','б':'b','в':'v','г':'g','д':'d','ѓ':'gj','е':'e','ж':'zh','з':'z',
+ 'ѕ':'dz','и':'i','ј':'j','к':'k','л':'l','љ':'lj','м':'m','н':'n','њ':'nj',
+ 'о':'o','п':'p','р':'r','с':'s','т':'t','ќ':'kj','у':'u','ф':'f','х':'h',
+ 'ц':'c','ч':'ch','џ':'dj','ш':'sh'
+ };
+ const usedIds = new Set();
+ const transliterate = (text) =>
+ text.replace(/[^\x00-\x7F]/g, (ch) => (ch in TRANSLIT ? TRANSLIT[ch] : ''));
+ const slugify = (text) => transliterate(text.toLowerCase())
+ .replace(/[^a-z0-9]+/g, '-')
+ .replace(/^-+|-+$/g, '');
+ headings.forEach((heading, i) => {
+ let id = heading.id || slugify(heading.textContent);
+ if (id.length < 2) id = 'section-' + (i + 1);
+ let unique = id, n = 2;
+ while (usedIds.has(unique)) { unique = id + '-' + (n++); }
+ usedIds.add(unique);
+ heading.id = unique;
+ });
+
+ // --- 5b. Zagotovi postavitveni ovoj in stransko vrstico (Desktop) ---
+ // Older article pages (SI/MK and some EN) ship without the TOC
+ // scaffolding, so we build it here from the page's own headings.
+ let layoutContainer = articlePage.querySelector('.article-layout-container');
+ if (!layoutContainer && articleContainer) {
+ layoutContainer = document.createElement('div');
+ layoutContainer.className = 'article-layout-container';
+ articleContainer.parentNode.insertBefore(layoutContainer, articleContainer);
+ layoutContainer.appendChild(articleContainer);
+ }
+
+ let sidebar = articlePage.querySelector('.article-sidebar');
+ if (!sidebar && layoutContainer && headings.length > 0) {
+ sidebar = document.createElement('aside');
+ sidebar.className = 'article-sidebar';
+ sidebar.innerHTML = ' ';
+ layoutContainer.insertBefore(sidebar, layoutContainer.firstChild);
+ }
+
+ const scrollspyNav = sidebar ? sidebar.querySelector('.scrollspy-nav') : null;
+ const desktopUl = scrollspyNav ? scrollspyNav.querySelector('ul') : null;
+
+ // --- 5c. Napolni namizni seznam, če je prazen (ohrani ročno napisanega) ---
+ if (desktopUl && desktopUl.children.length === 0 && headings.length > 0) {
+ headings.forEach(heading => {
+ const li = document.createElement('li');
+ const a = document.createElement('a');
+ a.href = '#' + heading.id;
+ a.textContent = navLabel(heading);
+ li.appendChild(a);
+ desktopUl.appendChild(li);
+ });
+ }
+
+ // --- 5d. Naslov članka nad kazalom (Desktop) ---
+ if (sidebar && !sidebar.querySelector('.article-sidebar-title') && articleTitle) {
+ const titleEl = document.createElement('p');
+ titleEl.className = 'article-sidebar-title';
+ titleEl.textContent = articleTitle;
+ sidebar.insertBefore(titleEl, sidebar.firstChild);
+ }
+
+ // --- 5e. Zagotovi lepljivo mobilno glavo z naslovom + progress bar ---
+ let mobileHeader = articlePage.querySelector('.mobile-article-header');
+ if (!mobileHeader && headings.length > 0) {
+ mobileHeader = document.createElement('div');
+ mobileHeader.className = 'mobile-article-header';
+ mobileHeader.innerHTML =
+ ' ' +
+ ' ' +
+ '' +
+ '';
+ articlePage.insertBefore(mobileHeader, articlePage.firstChild);
+ }
+
+ const mobileTitle = document.getElementById('mobile-article-title');
+ if (mobileTitle && !mobileTitle.textContent.trim()) {
+ mobileTitle.textContent = articleTitle; // known at load; scrollspy refines it
+ }
+ const mobileDropdown = articlePage.querySelector('.mobile-scrollspy-dropdown');
+ const mobileLinksContainer = document.getElementById('mobile-scrollspy-links');
+ const progressBar = articlePage.querySelector('.progress-bar');
+ const desktopNavLinks = desktopUl ? Array.from(desktopUl.querySelectorAll('a')) : [];
+
+ // --- 5f. Napolni mobilni spustni seznam ---
+ if (headings.length > 0 && mobileLinksContainer && mobileLinksContainer.children.length === 0) {
headings.forEach(heading => {
const listItem = document.createElement('li');
const link = document.createElement('a');
- link.href = `#${heading.id}`;
- link.textContent = heading.textContent;
+ link.href = '#' + heading.id;
+ link.textContent = navLabel(heading);
listItem.appendChild(link);
mobileLinksContainer.appendChild(listItem);
});
}
-
- // --- Logika za odpiranje/zapiranje mobilnega spustnega seznama ---
+
+ // --- 5g. Odpiranje/zapiranje mobilnega spustnega seznama ---
if (mobileHeader && mobileDropdown) {
mobileHeader.addEventListener('click', (e) => {
- // Preprečimo, da se meni zapre, če kliknemo na povezavo znotraj njega
if (!e.target.closest('a')) {
- mobileHeader.classList.toggle('open');
- mobileDropdown.classList.toggle('open');
+ mobileHeader.classList.toggle('open');
+ mobileDropdown.classList.toggle('open');
}
});
-
- // Dodamo dogodek za zapiranje menija ob kliku na povezavo
mobileDropdown.querySelectorAll('a').forEach(link => {
link.addEventListener('click', () => {
mobileHeader.classList.remove('open');
@@ -316,56 +411,55 @@ document.addEventListener('DOMContentLoaded', function() {
});
}
- // --- Logika za Progress Bar ---
+ // --- 5h. Progress Bar ---
function updateProgressBar() {
if (!progressBar) return;
const scrollableHeight = document.documentElement.scrollHeight - window.innerHeight;
const scrollTop = window.scrollY;
-
- if (scrollableHeight > 0) {
- const progress = (scrollTop / scrollableHeight) * 100;
- progressBar.style.width = `${progress}%`;
- } else {
- progressBar.style.width = '0%';
- }
+ progressBar.style.width = scrollableHeight > 0
+ ? `${(scrollTop / scrollableHeight) * 100}%`
+ : '0%';
}
- // --- Logika za Scrollspy ---
+ // --- 5i. Scrollspy (posodobi navigacijo, naslov in URL sidro) ---
if (headings.length > 0) {
+ let activeId = null;
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
- const id = entry.target.getAttribute('id');
- const desktopNavLink = desktopNavLinks.find(link => link.getAttribute('href') === `#${id}`);
- const mobileNavLink = mobileLinksContainer ? mobileLinksContainer.querySelector(`a[href="#${id}"]`) : null;
+ if (!entry.isIntersecting) return;
+ const id = entry.target.id;
+ if (id === activeId) return;
+ activeId = id;
- if (entry.isIntersecting) {
- // Posodobi namizno navigacijo
- if (desktopNavLink) {
- desktopNavLinks.forEach(link => link.classList.remove('active'));
- desktopNavLink.classList.add('active');
- }
- // Posodobi mobilni naslov in aktivno povezavo v spustnem seznamu
- if (mobileTitle) {
- mobileTitle.textContent = entry.target.textContent;
- }
+ // Namizna navigacija
+ const desktopNavLink = desktopNavLinks.find(link => link.getAttribute('href') === `#${id}`);
+ if (desktopNavLink) {
+ desktopNavLinks.forEach(link => link.classList.remove('active'));
+ desktopNavLink.classList.add('active');
+ }
+ // Mobilni naslov + aktivna povezava
+ if (mobileTitle) mobileTitle.textContent = navLabel(entry.target);
+ if (mobileLinksContainer) {
+ const mobileNavLink = mobileLinksContainer.querySelector(`a[href="#${id}"]`);
if (mobileNavLink) {
mobileLinksContainer.querySelectorAll('a').forEach(link => link.classList.remove('active'));
mobileNavLink.classList.add('active');
}
}
+ // Posodobi URL sidro brez skoka in brez polnjenja zgodovine
+ if (window.history && history.replaceState) {
+ history.replaceState(null, '', '#' + id);
+ }
});
}, {
- rootMargin: "-100px 0px -50% 0px" // Sproži, ko je naslov v zgornjem delu zaslona
+ rootMargin: "-100px 0px -55% 0px" // Sproži, ko je naslov v zgornjem delu zaslona
});
- headings.forEach(heading => {
- observer.observe(heading);
- });
+ headings.forEach(heading => observer.observe(heading));
}
- // Dodaj poslušalca dogodkov za vrstico napredka
window.addEventListener('scroll', updateProgressBar);
- updateProgressBar(); // Klic ob nalaganju strani
+ updateProgressBar();
}
} catch (error) {
console.error('Error in Article Scrollspy/Progress Bar setup:', error);