diff --git a/app/index.html b/app/index.html
index d2324f7..f8dff1a 100644
--- a/app/index.html
+++ b/app/index.html
@@ -371,6 +371,33 @@ body {
}
.topbar-nav .nav-item.active { border-left: none; background: var(--nav-active-bg); }
+/* ─── Topbar grouped navigation (dropdowns) ─── */
+.topbar-nav .nav-group { position: relative; }
+.topbar-nav .nav-trigger {
+ display: flex; align-items: center; gap: 0.25rem;
+ padding: 0.35rem 0.75rem; border-radius: 5px;
+ background: none; border: none; cursor: pointer;
+ color: var(--font-colour); font-size: 0.85rem;
+ font-family: inherit; white-space: nowrap; text-decoration: none; line-height: inherit;
+}
+.topbar-nav .nav-trigger:hover,
+.topbar-nav .nav-group.open > .nav-trigger { background: var(--nav-hover-bg); }
+.topbar-nav .nav-group.has-active > .nav-trigger { background: var(--nav-active-bg); }
+.topbar-nav .nav-caret { font-size: 0.6rem; color: var(--font-colour-muted); opacity: 0.55; line-height: 1; }
+.topbar-nav .nav-dropdown {
+ display: none; position: absolute; top: calc(100% + 4px); left: 0;
+ background: var(--bg-nav); border: 1px solid var(--divider);
+ border-radius: 6px; box-shadow: 0 4px 16px rgba(0,0,0,0.1);
+ min-width: 160px; z-index: 200; padding: 0.25rem 0;
+}
+.topbar-nav .nav-group.open > .nav-dropdown { display: block; }
+.topbar-nav .nav-dropdown .nav-item {
+ display: block; padding: 0.45rem 1rem;
+ border-left: none; border-radius: 0; white-space: nowrap;
+}
+.topbar-nav .nav-dropdown .nav-item:hover { background: var(--nav-hover-bg); }
+.topbar-nav .nav-dropdown .nav-item.active { background: var(--nav-active-bg); font-weight: 600; }
+
.topbar-actions { display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; }
.topbar-search { position: relative; }
@@ -732,6 +759,19 @@ body {
border-left: none;
}
.layout-topbar .mobile-nav-panel .nav-item.active { border-left: none; font-weight: 600; }
+ .layout-topbar .mobile-nav-panel .nav-group-row { display: flex; align-items: stretch; }
+ .layout-topbar .mobile-nav-panel .nav-group-row .nav-item { flex: 1; }
+ .layout-topbar .mobile-nav-panel .nav-section-label {
+ flex: 1; display: flex; align-items: center;
+ padding: 0.6rem 1.25rem; font-size: 1rem; color: var(--font-colour); font-weight: 500;
+ }
+ .layout-topbar .mobile-nav-panel .nav-expand-btn {
+ background: none; border: none; cursor: pointer;
+ color: var(--font-colour-muted); padding: 0 1.25rem; font-size: 1.2rem; line-height: 1; flex-shrink: 0;
+ }
+ .layout-topbar .mobile-nav-panel .nav-group-children { display: none; }
+ .layout-topbar .mobile-nav-panel .nav-group-children.open { display: block; }
+ .layout-topbar .mobile-nav-panel .nav-group-children .nav-item { padding-left: 2.5rem; }
}
@media (max-width: 480px) {
@@ -1800,6 +1840,10 @@ function fmtDatetime(dtStr) {
if (panel) panel.classList.remove('open');
hamburger.innerHTML = ICONS.menu;
};
+
+ document.addEventListener('click', () => {
+ document.querySelectorAll('.topbar-nav .nav-group.open').forEach(g => g.classList.remove('open'));
+ });
}
function buildSearchWidget() {
@@ -2100,18 +2144,141 @@ function fmtDatetime(dtStr) {
tree.forEach(root => renderTreeSection(container, root, 0, groups));
}
- function renderFlat(container) {
- // Topbar inline: pages only, sorted by global sort, draft-section pages excluded.
+ function buildTopbarNavItems() {
const byCode = {};
navSections.forEach(s => { if (s.code) byCode[s.code] = s; });
- const visible = navData.filter(p => {
- const sid = p['section-id'];
- return !sid || !isDraftSection(sid, byCode);
+ const items = [];
+
+ // Sections (non-draft), each becomes a dropdown trigger
+ navSections.forEach(s => {
+ if (!s.code || isDraftSection(s.code, byCode)) return;
+ const pages = navData.filter(p => p['section-id'] === s.code && pageShouldDisplay(p));
+ pages.sort((a, b) => ((a.sort ?? 999) - (b.sort ?? 999)) || a.file.localeCompare(b.file));
+ if (!pages.length) return;
+ items.push({ type: 'section', sort: s.sort ?? 999, section: s, pages });
});
- visible.sort((a, b) => ((a.sort ?? 999) - (b.sort ?? 999)) || a.file.localeCompare(b.file));
- visible.forEach(p => {
- const item = makeNavItem(p, 0);
- if (item) container.appendChild(item);
+
+ // Unsectioned pages (or pages whose section isn't in nav), grouped by sort century
+ const unsectioned = navData.filter(p => {
+ if (!pageShouldDisplay(p)) return false;
+ const sid = p['section-id'];
+ return !sid || !byCode[sid];
+ });
+ unsectioned.sort((a, b) => ((a.sort ?? 999) - (b.sort ?? 999)) || a.file.localeCompare(b.file));
+ const centuryMap = new Map();
+ unsectioned.forEach(p => {
+ const c = Math.floor((p.sort ?? 999) / 100);
+ if (!centuryMap.has(c)) centuryMap.set(c, []);
+ centuryMap.get(c).push(p);
+ });
+ for (const [, pgs] of centuryMap) {
+ items.push({ type: 'group', sort: pgs[0].sort ?? 999, primary: pgs[0], children: pgs.slice(1) });
+ }
+
+ items.sort((a, b) => a.sort - b.sort);
+ return items;
+ }
+
+ function makeTopbarPageGroup({ primary, children }, isMobile) {
+ const group = el('div', { className: 'nav-group' });
+ const hasChildren = children.length > 0;
+
+ if (isMobile) {
+ const row = el('div', { className: 'nav-group-row' });
+ const link = makeNavItem(primary, 0);
+ if (link) row.appendChild(link);
+ if (hasChildren) {
+ const childrenEl = el('div', { className: 'nav-group-children' });
+ children.forEach(p => { const it = makeNavItem(p, 1); if (it) childrenEl.appendChild(it); });
+ const btn = el('button', { className: 'nav-expand-btn', 'aria-label': 'Expand', textContent: '+' });
+ btn.addEventListener('click', () => {
+ const open = childrenEl.classList.toggle('open');
+ btn.textContent = open ? '−' : '+';
+ });
+ row.appendChild(btn);
+ group.appendChild(row);
+ group.appendChild(childrenEl);
+ } else {
+ group.appendChild(row);
+ }
+ } else {
+ const title = pageDisplayTitle(primary);
+ const trigger = el('a', { className: 'nav-trigger', href: '#' + primary.file, 'data-file': primary.file });
+ trigger.appendChild(el('span', { textContent: title }));
+ if (hasChildren) trigger.appendChild(el('span', { className: 'nav-caret', textContent: '▾' }));
+ group.appendChild(trigger);
+
+ if (hasChildren) {
+ const dropdown = el('div', { className: 'nav-dropdown' });
+ children.forEach(p => { const it = makeNavItem(p, 0); if (it) dropdown.appendChild(it); });
+ group.appendChild(dropdown);
+ group.addEventListener('mouseenter', () => group.classList.add('open'));
+ group.addEventListener('mouseleave', () => group.classList.remove('open'));
+ trigger.addEventListener('click', e => {
+ e.preventDefault();
+ e.stopPropagation();
+ group.classList.toggle('open');
+ navigateTo(primary.file);
+ if (window._closeMobileMenu) window._closeMobileMenu();
+ });
+ } else {
+ trigger.addEventListener('click', e => {
+ e.preventDefault();
+ navigateTo(primary.file);
+ if (window._closeMobileMenu) window._closeMobileMenu();
+ });
+ }
+ }
+
+ return group;
+ }
+
+ function makeTopbarSection({ section, pages }, isMobile) {
+ const group = el('div', { className: 'nav-group' });
+ const isHidden = section.pagesvisibility === 'hidden';
+ const name = sectionDisplayName(section);
+
+ if (isMobile) {
+ const row = el('div', { className: 'nav-group-row' });
+ row.appendChild(el('span', { className: 'nav-section-label', textContent: name }));
+ const childrenEl = el('div', { className: 'nav-group-children' + (isHidden ? '' : ' open') });
+ pages.forEach(p => { const it = makeNavItem(p, 1); if (it) childrenEl.appendChild(it); });
+ const btn = el('button', { className: 'nav-expand-btn', 'aria-label': 'Expand', textContent: isHidden ? '+' : '−' });
+ btn.addEventListener('click', () => {
+ const open = childrenEl.classList.toggle('open');
+ btn.textContent = open ? '−' : '+';
+ });
+ row.appendChild(btn);
+ group.appendChild(row);
+ group.appendChild(childrenEl);
+ } else {
+ const trigger = el('button', { className: 'nav-trigger', type: 'button' });
+ trigger.appendChild(el('span', { textContent: name }));
+ trigger.appendChild(el('span', { className: 'nav-caret', textContent: '▾' }));
+ trigger.addEventListener('click', e => { e.stopPropagation(); group.classList.toggle('open'); });
+ group.appendChild(trigger);
+
+ const dropdown = el('div', { className: 'nav-dropdown' });
+ pages.forEach(p => { const it = makeNavItem(p, 0); if (it) dropdown.appendChild(it); });
+ group.appendChild(dropdown);
+
+ if (!isHidden) {
+ group.addEventListener('mouseenter', () => group.classList.add('open'));
+ group.addEventListener('mouseleave', () => group.classList.remove('open'));
+ }
+ }
+
+ return group;
+ }
+
+ function renderTopbarGrouped(container, isMobile) {
+ const items = buildTopbarNavItems();
+ items.forEach(item => {
+ container.appendChild(
+ item.type === 'group'
+ ? makeTopbarPageGroup(item, isMobile)
+ : makeTopbarSection(item, isMobile)
+ );
});
}
@@ -2121,19 +2288,23 @@ function fmtDatetime(dtStr) {
const mobile = document.getElementById('mobileNavLinks');
if (main) {
main.innerHTML = '';
- if (topbar) renderFlat(main);
+ if (topbar) renderTopbarGrouped(main, false);
else renderTree(main);
}
if (mobile) {
mobile.innerHTML = '';
- renderTree(mobile);
+ if (topbar) renderTopbarGrouped(mobile, true);
+ else renderTree(mobile);
}
}
function highlightNav(file) {
- document.querySelectorAll('.nav-item').forEach(item => {
+ document.querySelectorAll('.nav-item, .nav-trigger[data-file]').forEach(item => {
item.classList.toggle('active', item.getAttribute('data-file') === file);
});
+ document.querySelectorAll('.topbar-nav .nav-group').forEach(group => {
+ group.classList.toggle('has-active', !!group.querySelector('[data-file].active'));
+ });
}
// ─── Page loading ─────────────────────────────────────────