diff --git a/app/index.html b/app/index.html
index ca0b5c4..df1a053 100644
--- a/app/index.html
+++ b/app/index.html
@@ -793,6 +793,39 @@ body {
.main-content { padding: 1rem 1rem 3rem; }
}
+/* ═══════════════════════════════════════════
+ TAG SYSTEM: CALLOUTS
+ ═══════════════════════════════════════════ */
+.mdcms-callout {
+ border-left: 4px solid;
+ border-radius: 0 6px 6px 0;
+ padding: 0.85rem 1rem 0.85rem 1rem;
+ margin: 1.25rem 0;
+}
+.mdcms-callout-title {
+ display: flex;
+ align-items: center;
+ gap: 0.4rem;
+ font-weight: 700;
+ font-size: 0.95rem;
+ margin-bottom: 0.45rem;
+}
+.mdcms-callout-title .mdcms-icon { font-size: 1.1em; }
+.mdcms-callout-body { font-size: 0.95rem; }
+.mdcms-callout-body > *:first-child { margin-top: 0; }
+.mdcms-callout-body > *:last-child { margin-bottom: 0; }
+
+/* ═══════════════════════════════════════════
+ TAG SYSTEM: TABLE OF CONTENTS
+ ═══════════════════════════════════════════ */
+.mdcms-toc { margin: 1rem 0; }
+.mdcms-toc-section { font-size: 1rem; font-weight: 600; margin: 1.5rem 0 0.4rem; color: var(--font-colour-muted); border-bottom: 1px solid var(--divider); padding-bottom: 0.25rem; }
+.mdcms-toc-list { list-style: none; padding: 0; margin: 0 0 0.5rem; }
+.mdcms-toc-list li { padding: 0.2rem 0; border-bottom: 1px solid var(--divider); }
+.mdcms-toc-list li:last-child { border-bottom: none; }
+.mdcms-toc-list a { color: var(--accent); text-decoration: none; font-size: 0.95rem; }
+.mdcms-toc-list a:hover { text-decoration: underline; }
+
/* ═══════════════════════════════════════════
TAG SYSTEM: POST LISTINGS
═══════════════════════════════════════════ */
@@ -870,32 +903,6 @@ body {
}
.post-load-more:hover { background: var(--nav-hover-bg); }
-/* ── Callout tags ──────────────────────────────────────── */
-.mdcms-callout {
- border-left: 4px solid var(--callout-primary, var(--accent));
- background: var(--callout-bg, transparent);
- border-radius: 0 0.4rem 0.4rem 0;
- padding: 0.75rem 1rem;
- margin: 1rem 0;
-}
-.mdcms-callout-title {
- display: flex;
- align-items: center;
- gap: 0.45rem;
- font-weight: 700;
- color: var(--callout-primary, var(--accent));
- margin-bottom: 0.4rem;
-}
-.mdcms-callout-title .mdcms-icon svg {
- fill: var(--callout-primary, var(--accent));
- width: 1.2em;
- height: 1.2em;
- display: block;
-}
-.mdcms-callout-body { margin: 0; }
-.mdcms-callout-body > :first-child { margin-top: 0; }
-.mdcms-callout-body > :last-child { margin-bottom: 0; }
-
@media print {
.sidebar, .topbar, .scroll-top, .hamburger,
.mobile-header, .theme-toggle, .search-container { display: none !important; }
@@ -1322,6 +1329,7 @@ body {
// ─── Fonts ────────────────────────────────────────────────
function loadFonts(tc) {
+ if (document.querySelector('link[data-mdcms-fonts]')) return;
function parseFont(spec) {
if (!spec) return null;
const parts = spec.split(':');
@@ -1897,12 +1905,70 @@ function fmtDatetime(dtStr) {
return 'rgba(' + r + ',' + g + ',' + b + ',' + alpha + ')';
}
+ function renderTocTag(container) {
+ const byCode = {};
+ navSections.forEach(s => { byCode[s.code] = s; });
+
+ const sortedSections = navSections
+ .filter(s => !isDraftSection(s.code, byCode))
+ .sort((a, b) => ((a.sort ?? 999) - (b.sort ?? 999)) || (a.code || '').localeCompare(b.code || ''));
+
+ const visiblePages = navData.filter(p => {
+ if (p.file === currentPage) return false;
+ if (!pageShouldDisplay(p)) return false;
+ const sid = p['section-id'];
+ if (sid && isDraftSection(sid, byCode)) return false;
+ return true;
+ });
+
+ const bySection = {};
+ const unsectioned = [];
+ visiblePages.forEach(p => {
+ const sid = p['section-id'] || null;
+ if (sid) { (bySection[sid] = bySection[sid] || []).push(p); }
+ else unsectioned.push(p);
+ });
+
+ function sortPages(pages) {
+ return [...pages].sort((a, b) =>
+ ((a.sort ?? 999) - (b.sort ?? 999)) || a.file.localeCompare(b.file));
+ }
+
+ function makeList(pages) {
+ const ul = document.createElement('ul');
+ ul.className = 'mdcms-toc-list';
+ pages.forEach(p => {
+ const a = el('a', { href: '#' + p.file, textContent: pageDisplayTitle(p) });
+ a.addEventListener('click', e => { e.preventDefault(); navigateTo(p.file); });
+ ul.appendChild(el('li', {}, a));
+ });
+ return ul;
+ }
+
+ const div = el('div', { className: 'mdcms-toc' });
+
+ if (unsectioned.length) div.appendChild(makeList(sortPages(unsectioned)));
+
+ sortedSections.forEach(section => {
+ const pages = bySection[section.code];
+ if (!pages || !pages.length) return;
+ div.appendChild(el('h3', { className: 'mdcms-toc-section', textContent: sectionDisplayName(section) }));
+ div.appendChild(makeList(sortPages(pages)));
+ });
+
+ if (!div.children.length) div.textContent = 'No pages found.';
+
+ container.replaceWith(div);
+ }
+
function hydrateMdcmsTags() {
document.querySelectorAll('.mdcms-tag').forEach(function(tagEl) {
try {
var cfg = JSON.parse(tagEl.getAttribute('data-config'));
if (/^callout-(info|warning|success|error)$/.test(cfg.tagName)) {
renderCalloutTag(tagEl, cfg);
+ } else if (cfg.tagName === 'toc') {
+ renderTocTag(tagEl);
} else {
renderPostTag(tagEl, cfg);
}
@@ -2648,7 +2714,6 @@ function fmtDatetime(dtStr) {
} catch (e) { /* fall back to hardcoded CSS defaults */ }
}
- // Write offline message to localStorage for SW offline fallback
const offlineMsgCfg = config['offline-message'];
if (offlineMsgCfg) {
const offlineText = typeof offlineMsgCfg === 'string'