diff --git a/CLAUDE.md b/CLAUDE.md index ef042c7..1ba4a59 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -122,8 +122,7 @@ sort: 100 # controls nav ordering (lower = higher) section-id: blog # assigns page to a nav section draft: true # exclude from nav and search author: Name -date: 2025-01-01 -datetime: 2025-01-01 13:00 # use this for posts (not `date` alone — see known limitations) +created: 2025-01-01 13:00 modified: 2025-01-15 09:00 keywords: foo, bar description: Short description for search @@ -153,13 +152,13 @@ Embed post lists in pages using fenced blocks: ````markdown ```mdcms -posts-datetime-reversechronological +posts-created-reversechronological limit: 10 paginate: yes ``` ```` -Reliable tags (others are known-broken): `posts-datetime-chronological-byyearmonth`, `posts-datetime-reversechronological`. Use `datetime` frontmatter (format: `YYYY-MM-DD HH:MM`) for posts — `date` alone does not work reliably. +Reliable tags (others are known-broken): `posts-created-chronological-byyearmonth`, `posts-created-reversechronological`. Use `created` frontmatter (format: `YYYY-MM-DD HH:MM`) for posts. ## Release workflow diff --git a/app/index.html b/app/index.html index a80ba18..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) { @@ -1369,14 +1409,14 @@ function fmtDatetime(dtStr) { function parsePostTagName(name) { var m = name.match( - /^posts-(date|datetime)-(chronological|reversechronological)(?:-(byyear|byyearmonth|lastyear|lastmonth))?$/ + /^posts-created-(chronological|reversechronological)(?:-(byyear|byyearmonth|lastyear|lastmonth))?$/ ); if (!m) return null; - return { field: m[1], order: m[2], modifier: m[3] || null }; + return { order: m[1], modifier: m[2] || null }; } function getPostEntries(parsed, options) { - const { field, order, modifier } = parsed; + const { order, modifier } = parsed; // Start with posts from search index let posts = (searchIndex || []).filter(function(e) { @@ -1389,11 +1429,7 @@ function fmtDatetime(dtStr) { } // Field filter - if (field === 'datetime') { - posts = posts.filter(function(e) { return !!e.datetime; }); - } else { - posts = posts.filter(function(e) { return !!e.date; }); - } + posts = posts.filter(function(e) { return !!e.created; }); // Time-window filter if (modifier === 'lastyear' || modifier === 'lastmonth') { @@ -1402,15 +1438,13 @@ function fmtDatetime(dtStr) { if (modifier === 'lastyear') cutoff.setDate(cutoff.getDate() - 365); else cutoff.setDate(cutoff.getDate() - 30); posts = posts.filter(function(e) { - var raw = field === 'datetime' ? e.datetime.replace(' ', 'T') : e.date; - return new Date(raw) >= cutoff; + return new Date(e.created.replace(' ', 'T')) >= cutoff; }); } // Sort - var sortKey = field === 'datetime' ? 'datetime' : 'date'; posts.sort(function(a, b) { - var da = a[sortKey] || '', db = b[sortKey] || ''; + var da = a.created || '', db = b.created || ''; return order === 'chronological' ? da.localeCompare(db) : db.localeCompare(da); }); @@ -1523,7 +1557,6 @@ function fmtDatetime(dtStr) { var opts = tag.options; var posts = getPostEntries(parsed, opts); - var field = parsed.field; var modifier = parsed.modifier; var paginate = opts.paginate || 'no'; var limitVal = opts.limit || 'all'; @@ -1532,7 +1565,7 @@ function fmtDatetime(dtStr) { // Format each entry var formatted = posts.map(function(p) { return { - display: field === 'datetime' ? fmtDatetime(p.datetime) : fmtDate(p.date), + display: fmtDatetime(p.created), title: p.title, file: p.file }; @@ -1565,12 +1598,10 @@ function fmtDatetime(dtStr) { // Grouped by year (or year+month) function getYear(p) { - var d = field === 'datetime' ? p.datetime : p.date; - return d ? d.substring(0, 4) : 'Unknown'; + return p.created ? p.created.substring(0, 4) : 'Unknown'; } function getYearMonth(p) { - var d = field === 'datetime' ? p.datetime : p.date; - return d ? d.substring(0, 7) : 'Unknown'; + return p.created ? p.created.substring(0, 7) : 'Unknown'; } function monthLabel(ym) { var m = parseInt(ym.substring(5, 7), 10); @@ -1809,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() { @@ -2109,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) + ); }); } @@ -2130,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 ───────────────────────────────────────── @@ -2189,12 +2351,12 @@ function fmtDatetime(dtStr) { hydrateMdcmsTags(); const firstH = contentEl.querySelector('.md-content h1, .md-content h2'); - if (firstH && (meta.author || meta.created || meta.date || meta.datetime)) { + if (firstH && (meta.author || meta.created)) { const metaEl = document.createElement('div'); metaEl.className = 'page-meta'; let metaText = ''; if (meta.author) metaText += meta.author; - const displayDate = meta.datetime || meta.date || meta.created; + const displayDate = meta.created; if (displayDate) { if (metaText) metaText += ' | '; metaText += 'Published ' + formatDate(displayDate); diff --git a/app/pages/home.md b/app/pages/home.md index 5b41f1f..ccf41dc 100644 --- a/app/pages/home.md +++ b/app/pages/home.md @@ -18,7 +18,7 @@ If you want to test `MD-CMS` you can grab `samplesite` from the repo and place t ## Reverse chronological (newest first) ```mdcms -posts-date-reversechronological +posts-created-reversechronological limit: 3 paginate: no ``` @@ -26,25 +26,25 @@ paginate: no ## Chronological (oldest first) ```mdcms -posts-date-chronological +posts-created-chronological limit: all paginate: none ``` -## By year (date, reverse chrono) +## By year (reverse chrono) ```mdcms -posts-date-reversechronological-byyear +posts-created-reversechronological-byyear limit: all defaultyear: current selectyear: yes paginate: none ``` -## By year+month (datetime, chrono) +## By year+month (chrono) ```mdcms -posts-datetime-chronological-byyearmonth +posts-created-chronological-byyearmonth limit: all defaultyear: 2024 selectyear: yes @@ -53,7 +53,7 @@ selectyear: yes ## Last 30 days ```mdcms -posts-date-reversechronological-lastmonth +posts-created-reversechronological-lastmonth limit: all paginate: none ``` @@ -61,7 +61,7 @@ paginate: none ## Paginated (2 per page) ```mdcms -posts-datetime-reversechronological +posts-created-reversechronological limit: 2 paginate: yes ``` diff --git a/mdcms.py b/mdcms.py index ee7cf84..5a7ad57 100644 --- a/mdcms.py +++ b/mdcms.py @@ -175,8 +175,6 @@ def scan_and_categorize(directory: Path, site_root: Path, known_codes: set) -> l "sort": meta.get("sort"), "section-id": meta.get("section-id"), "author": meta.get("author"), - "date": str(meta.get("date", "")), - "datetime": str(meta.get("datetime", "")), "created": str(meta.get("created", "")), "modified": str(meta.get("modified", "")), "language": meta.get("language", "en"), @@ -328,8 +326,8 @@ def generate_search_json( "keywords": r.get("keywords", ""), "description": r.get("description", ""), "author": r.get("author"), - "date": r.get("date", ""), - "datetime": r.get("datetime", ""), + "created": r.get("created", ""), + "modified": r.get("modified", ""), "language": r.get("language", "en"), "body": r.get("body", ""), } diff --git a/samplesite/pages/blog.md b/samplesite/pages/blog.md index e43716e..dc1b4ad 100644 --- a/samplesite/pages/blog.md +++ b/samplesite/pages/blog.md @@ -11,7 +11,7 @@ Stay up to date with announcements, product updates, and industry insights from ## All Posts ```mdcms -posts-date-reversechronological-byyear +posts-created-reversechronological-byyear limit: all defaultyear: current selectyear: yes diff --git a/samplesite/pages/blog.nb.md b/samplesite/pages/blog.nb.md index 23c1f01..5962716 100644 --- a/samplesite/pages/blog.nb.md +++ b/samplesite/pages/blog.nb.md @@ -11,7 +11,7 @@ Bli oppdatert med kunngjøringer, produktoppdateringer og bransjeinnsikter fra A ## Alle innlegg ```mdcms -posts-date-reversechronological-byyear +posts-created-reversechronological-byyear limit: all defaultyear: current selectyear: yes diff --git a/samplesite/posts/2022-q4-report.md b/samplesite/posts/2022-q4-report.md index c24a474..37cf475 100644 --- a/samplesite/posts/2022-q4-report.md +++ b/samplesite/posts/2022-q4-report.md @@ -1,7 +1,6 @@ --- title: Q4 2022 Performance Report -date: 2022-11-15 -datetime: 2022-11-15 09:00 +created: 2022-11-15 09:00 author: Sarah Chen --- diff --git a/samplesite/posts/2023-analytics-launch.md b/samplesite/posts/2023-analytics-launch.md index 96dd7d6..264e94a 100644 --- a/samplesite/posts/2023-analytics-launch.md +++ b/samplesite/posts/2023-analytics-launch.md @@ -1,7 +1,6 @@ --- title: Introducing Advanced Analytics Dashboard -date: 2023-03-22 -datetime: 2023-03-22 14:30 +created: 2023-03-22 14:30 author: David Okonkwo --- diff --git a/samplesite/posts/2023-security-update.md b/samplesite/posts/2023-security-update.md index 32e5169..e6046f4 100644 --- a/samplesite/posts/2023-security-update.md +++ b/samplesite/posts/2023-security-update.md @@ -1,7 +1,6 @@ --- title: Security Update - November 2023 -date: 2023-11-10 -datetime: 2023-11-10 11:45 +created: 2023-11-10 11:45 author: Security Team --- diff --git a/samplesite/posts/2024-roadmap.md b/samplesite/posts/2024-roadmap.md index b082033..1f0997e 100644 --- a/samplesite/posts/2024-roadmap.md +++ b/samplesite/posts/2024-roadmap.md @@ -1,7 +1,6 @@ --- title: 2024 Product Roadmap -date: 2024-01-30 -datetime: 2024-01-30 10:00 +created: 2024-01-30 10:00 author: David Okonkwo --- diff --git a/samplesite/posts/2024-success-stories.md b/samplesite/posts/2024-success-stories.md index 69be3fb..16a46bd 100644 --- a/samplesite/posts/2024-success-stories.md +++ b/samplesite/posts/2024-success-stories.md @@ -1,7 +1,6 @@ --- title: Q2 2024 Customer Success Stories -date: 2024-07-08 -datetime: 2024-07-08 15:20 +created: 2024-07-08 15:20 author: Maria Garcia --- diff --git a/samplesite/posts/2026-v9-release.md b/samplesite/posts/2026-v9-release.md index 63dfec1..20f37d5 100644 --- a/samplesite/posts/2026-v9-release.md +++ b/samplesite/posts/2026-v9-release.md @@ -1,7 +1,6 @@ --- title: Version 9.0 Released — A New Era -date: 2026-04-10 -datetime: 2026-04-10 13:00 +created: 2026-04-10 13:00 author: Sarah Chen ---