diff --git a/app/index.html b/app/index.html index 9ac8e70..de8a6ab 100644 --- a/app/index.html +++ b/app/index.html @@ -917,6 +917,146 @@ body { } .post-load-more:hover { background: var(--nav-hover-bg); } +/* ═══════════════════════════════════════════ + TAG SYSTEM: TABS + ═══════════════════════════════════════════ */ +.mdcms-tabs { margin: 1.25rem 0; } + +/* Underline variant */ +.mdcms-tabs-underline .mdcms-tabs-strip { + display: flex; + flex-wrap: wrap; + gap: 0 18px; + border-bottom: 1px solid var(--mdcms-strip-border, color-mix(in srgb, var(--font-colour) 12%, transparent)); +} +.mdcms-tabs-underline .mdcms-tab-btn { + padding: 8px 2px; + border: none; + border-bottom: 2px solid transparent; + margin-bottom: -1px; + background: transparent; + cursor: pointer; + font-size: inherit; + font-family: inherit; + font-weight: 500; + color: var(--font-colour-muted); + line-height: inherit; + transition: color 0.15s; +} +.mdcms-tabs-underline .mdcms-tab-btn:hover { color: var(--font-colour); } +.mdcms-tabs-underline .mdcms-tab-btn[aria-selected="true"] { + font-weight: 600; + color: var(--font-colour); + border-bottom-color: var(--accent); +} + +/* Filled variant */ +.mdcms-tabs-filled .mdcms-tabs-strip { display: flex; flex-wrap: wrap; gap: 4px; } +.mdcms-tabs-filled .mdcms-tab-btn { + padding: 6px 11px; + border-radius: 4px; + border: 1px solid var(--mdcms-filled-border, rgba(var(--accent-rgb), 0.30)); + background: var(--mdcms-filled-bg, rgba(var(--accent-rgb), 0.10)); + color: var(--mdcms-filled-fg-muted, var(--font-colour-muted)); + cursor: pointer; + font-size: inherit; + font-family: inherit; + font-weight: 400; + line-height: inherit; + transition: color 0.15s; +} +.mdcms-tabs-filled .mdcms-tab-btn:hover { color: var(--mdcms-filled-fg, var(--font-colour)); } +.mdcms-tabs-filled .mdcms-tab-btn[aria-selected="true"] { + background: var(--mdcms-bg, var(--bg-main)); + border-color: rgba(var(--accent-rgb), 0.55); + color: var(--accent); + font-weight: 600; +} + +/* Shared panel */ +.mdcms-tabs-panel { padding-top: 1rem; } +.mdcms-tabs-panel[hidden] { display: none; } +.mdcms-tabs-panel > *:first-child { margin-top: 0; } +.mdcms-tabs-panel > *:last-child { margin-bottom: 0; } + +/* ═══════════════════════════════════════════ + TAG SYSTEM: ACCORDIONS + ═══════════════════════════════════════════ */ +.mdcms-accordion { margin: 1.25rem 0; } +.mdcms-accordion-btn { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + border: none; + background: transparent; + cursor: pointer; + font-size: inherit; + font-family: inherit; + text-align: left; + line-height: inherit; +} +.mdcms-accordion-chevron { + display: inline-flex; + flex-shrink: 0; + width: 1.1em; + height: 1.1em; + transition: transform 0.2s ease; + transform: rotate(0deg); +} +.mdcms-accordion-item[data-open="false"] .mdcms-accordion-chevron { transform: rotate(-90deg); } +.mdcms-accordion-body[hidden] { display: none; } +.mdcms-accordion-body > *:first-child { margin-top: 0; } +.mdcms-accordion-body > *:last-child { margin-bottom: 0; } + +/* Underline variant */ +.mdcms-accordion-underline .mdcms-accordion-item { margin-bottom: 6px; } +.mdcms-accordion-underline .mdcms-accordion-btn { + padding: 8px 2px 9px; + border-bottom: 2px solid var(--mdcms-bar, var(--accent)); + font-weight: 600; + font-size: 0.75rem; + color: var(--font-colour); +} +.mdcms-accordion-underline .mdcms-accordion-chevron { color: var(--mdcms-bar, var(--accent)); } +.mdcms-accordion-underline .mdcms-accordion-body { + border-left: 1px solid var(--mdcms-bar, var(--accent)); + border-right: 1px solid var(--mdcms-bar, var(--accent)); + border-bottom: 1px solid var(--mdcms-bar, var(--accent)); + border-radius: 0 0 3px 3px; + padding: 8px 10px 9px; + color: var(--font-colour-muted); +} + +/* Filled variant — closed */ +.mdcms-accordion-filled .mdcms-accordion-item { margin-bottom: 6px; } +.mdcms-accordion-filled .mdcms-accordion-item[data-open="true"] { margin-bottom: 8px; } +.mdcms-accordion-filled .mdcms-accordion-btn { + padding: 8px 11px; + border-radius: 4px; + border: 1px solid var(--mdcms-filled-border, rgba(var(--accent-rgb), 0.30)); + background: var(--mdcms-filled-bg, rgba(var(--accent-rgb), 0.10)); + color: var(--mdcms-filled-fg, var(--font-colour)); +} +.mdcms-accordion-filled .mdcms-accordion-chevron { color: var(--mdcms-filled-fg, var(--font-colour)); } + +/* Filled variant — open: item becomes the outer frame */ +.mdcms-accordion-filled .mdcms-accordion-item[data-open="true"] { + border: 1px solid var(--mdcms-bar, var(--accent)); + border-radius: 4px; + overflow: hidden; +} +.mdcms-accordion-filled .mdcms-accordion-item[data-open="true"] > .mdcms-accordion-btn { + border: none; + border-radius: 0; +} +.mdcms-accordion-filled .mdcms-accordion-body { + background: var(--mdcms-bg, var(--bg-main)); + padding: 8px 11px 9px; + color: var(--font-colour-muted); +} + @media print { .sidebar, .topbar, .scroll-top, .hamburger, .mobile-header, .theme-toggle, .search-container { display: none !important; } @@ -1236,6 +1376,7 @@ body { btn.appendChild(iconEl(isDark ? 'light_mode' : 'dark_mode')); btn.appendChild(el('span', { textContent: isDark ? 'Light mode' : 'Dark mode' })); } + computeDerivedTokens(); } function getInitialTheme() { @@ -1467,6 +1608,10 @@ body { const fenceType = codeLang === 'mdcms' ? '' : codeLang.slice('mdcms '.length).trim(); const fullText = fenceType ? (fenceType + '\n' + (codeText || '')) : (codeText || ''); const tag = parseMdcmsTag(fullText); + // For tab/accordion blocks, preserve the raw fence body to avoid trim() breaking YAML indentation. + if (/^tab(-underline|-filled)?$|^accordion(-underline|-filled)?$/.test(tag.tagName)) { + tag.rawBody = codeText || ''; + } const encoded = JSON.stringify(tag).replace(/&/g, '&').replace(/"/g, '"'); return '
'; } @@ -1956,6 +2101,95 @@ function fmtDatetime(dtStr) { return 'rgba(' + r + ',' + g + ',' + b + ',' + alpha + ')'; } + function parseColorToHex(val) { + if (!val) return null; + val = val.trim(); + if (val.startsWith('#')) { + if (val.length === 4) return '#' + val[1]+val[1]+val[2]+val[2]+val[3]+val[3]; + return val.toLowerCase(); + } + var m = val.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/); + if (m) return '#' + [m[1],m[2],m[3]].map(function(n) { return parseInt(n).toString(16).padStart(2,'0'); }).join(''); + return null; + } + + function relativeLuminance(hex) { + hex = hex.replace('#',''); + if (hex.length === 3) hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2]; + var r = parseInt(hex.substr(0,2),16)/255; + var g = parseInt(hex.substr(2,2),16)/255; + var b = parseInt(hex.substr(4,2),16)/255; + function lin(c) { return c <= 0.04045 ? c/12.92 : Math.pow((c+0.055)/1.055, 2.4); } + return 0.2126*lin(r) + 0.7152*lin(g) + 0.0722*lin(b); + } + + function hexToHsl(hex) { + hex = hex.replace('#',''); + if (hex.length === 3) hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2]; + var r = parseInt(hex.substr(0,2),16)/255; + var g = parseInt(hex.substr(2,2),16)/255; + var b = parseInt(hex.substr(4,2),16)/255; + var max = Math.max(r,g,b), min = Math.min(r,g,b); + var h = 0, s = 0, l = (max+min)/2; + if (max !== min) { + var d = max - min; + s = l > 0.5 ? d/(2-max-min) : d/(max+min); + switch(max) { + case r: h = ((g-b)/d + (g 0.22 || (navC > 0.35 && Math.abs(navC - bgC) > 0.25); + var navIsInverted = Math.abs(bgL - navL) > 0.35; + + var navText = navIsInverted ? (navL < 0.5 ? '#F2EFE8' : '#161412') : textHex; + var navTextMuted = navIsInverted ? hexToRgba(navText, 0.6) : mutedHex; + + var filledBg = navIsAccent ? navHex : hexToRgba(accentHex, 0.10); + var filledBorder = navIsAccent ? hexToRgba(navText, 0.18) : hexToRgba(accentHex, 0.30); + var filledFg = navIsAccent ? navText : textHex; + var filledFgMuted = navIsAccent ? navTextMuted : mutedHex; + var barColor = navIsAccent ? navHex : accentHex; + var stripAlpha = isDark ? 0.14 : 0.10; + + var root = document.documentElement; + root.style.setProperty('--mdcms-bg', bgHex); + root.style.setProperty('--mdcms-accent', accentHex); + root.style.setProperty('--mdcms-filled-bg', filledBg); + root.style.setProperty('--mdcms-filled-border', filledBorder); + root.style.setProperty('--mdcms-filled-fg', filledFg); + root.style.setProperty('--mdcms-filled-fg-muted',filledFgMuted); + root.style.setProperty('--mdcms-bar', barColor); + root.style.setProperty('--mdcms-strip-border', hexToRgba(textHex, stripAlpha)); + } + function renderTocTag(container) { const byCode = {}; navSections.forEach(s => { byCode[s.code] = s; }); @@ -2012,6 +2246,143 @@ function fmtDatetime(dtStr) { container.replaceWith(div); } + function renderTabsTag(container, cfg) { + var variant = cfg.tagName === 'tab' ? 'tab-underline' : cfg.tagName; + var isFilled = variant === 'tab-filled'; + var varClass = isFilled ? 'filled' : 'underline'; + + var items = []; + try { + // Use rawBody (pre-trim YAML) when available; fall back to reconstructed form. + var rawYaml = cfg.rawBody !== undefined ? cfg.rawBody : ('items:\n' + (cfg.body || '')); + var parsed = jsyaml.load(rawYaml); + items = (parsed && parsed.items) || []; + } catch (e) { + container.textContent = 'Error parsing tab items.'; + return; + } + if (!items.length) { container.textContent = 'No tab items.'; return; } + + var selectedIdx = items.findIndex(function(it) { return it && it.default === 'selected'; }); + if (selectedIdx < 0) selectedIdx = 0; + + var wrapper = el('div', { className: 'mdcms-tabs mdcms-tabs-' + varClass }); + var strip = el('div', { className: 'mdcms-tabs-strip', role: 'tablist' }); + var panels = []; + + items.forEach(function(item, i) { + if (!item) return; + var isSelected = i === selectedIdx; + + var btn = el('button', { + className: 'mdcms-tab-btn', + role: 'tab', + type: 'button', + 'aria-selected': String(isSelected) + }); + var titleStyle = item['title-style'] || ''; + var lvlMatch = titleStyle.match(/^(#{1,6})$/); + var titleSpan; + if (lvlMatch) { + titleSpan = el('span', { role: 'heading', 'aria-level': String(lvlMatch[1].length) }); + titleSpan.textContent = item.title || ''; + } else { + titleSpan = el('span', { textContent: item.title || '' }); + } + btn.appendChild(titleSpan); + strip.appendChild(btn); + + var panel = el('div', { className: 'mdcms-tabs-panel', role: 'tabpanel' }); + panel.innerHTML = renderMarkdown(String(item.content || '')); + if (!isSelected) panel.setAttribute('hidden', ''); + panels.push(panel); + + btn.addEventListener('click', (function(idx) { + return function() { + strip.querySelectorAll('.mdcms-tab-btn').forEach(function(b, j) { + b.setAttribute('aria-selected', String(j === idx)); + if (j === idx) panels[j].removeAttribute('hidden'); + else panels[j].setAttribute('hidden', ''); + }); + }; + })(i)); + }); + + wrapper.appendChild(strip); + panels.forEach(function(p) { wrapper.appendChild(p); }); + container.replaceWith(wrapper); + } + + function renderAccordionTag(container, cfg) { + var variant = cfg.tagName === 'accordion' ? 'accordion-underline' : cfg.tagName; + var isFilled = variant === 'accordion-filled'; + var varClass = isFilled ? 'filled' : 'underline'; + + var items = []; + try { + var rawYaml = cfg.rawBody !== undefined ? cfg.rawBody : ('items:\n' + (cfg.body || '')); + var parsed = jsyaml.load(rawYaml); + items = (parsed && parsed.items) || []; + } catch (e) { + container.textContent = 'Error parsing accordion items.'; + return; + } + if (!items.length) { container.textContent = 'No accordion items.'; return; } + + var CHEVRON_SVG = ''; + + var wrapper = el('div', { className: 'mdcms-accordion mdcms-accordion-' + varClass }); + + items.forEach(function(item) { + if (!item) return; + var isOpen = item.default === 'open'; + + var itemEl = el('div', { className: 'mdcms-accordion-item' }); + itemEl.setAttribute('data-open', String(isOpen)); + + var btn = el('button', { + className: 'mdcms-accordion-btn', + type: 'button', + 'aria-expanded': String(isOpen) + }); + + var titleStyle = item['title-style'] || ''; + var lvlMatch = titleStyle.match(/^(#{1,6})$/); + var titleEl = el('span', { className: 'mdcms-accordion-title' }); + if (lvlMatch) { + var heading = el('span', { role: 'heading', 'aria-level': String(lvlMatch[1].length) }); + heading.textContent = item.title || ''; + titleEl.appendChild(heading); + } else { + titleEl.textContent = item.title || ''; + } + btn.appendChild(titleEl); + + var chevron = el('span', { className: 'mdcms-accordion-chevron' }); + chevron.innerHTML = CHEVRON_SVG; + btn.appendChild(chevron); + + var body = el('div', { className: 'mdcms-accordion-body' }); + body.innerHTML = renderMarkdown(String(item.content || '')); + if (!isOpen) body.setAttribute('hidden', ''); + + btn.addEventListener('click', function() { + var open = itemEl.getAttribute('data-open') === 'true'; + var next = !open; + itemEl.setAttribute('data-open', String(next)); + btn.setAttribute('aria-expanded', String(next)); + if (next) body.removeAttribute('hidden'); + else body.setAttribute('hidden', ''); + }); + + itemEl.appendChild(btn); + itemEl.appendChild(body); + wrapper.appendChild(itemEl); + }); + + container.replaceWith(wrapper); + } + function hydrateMdcmsTags() { document.querySelectorAll('.mdcms-tag').forEach(function(tagEl) { try { @@ -2020,6 +2391,10 @@ function fmtDatetime(dtStr) { renderCalloutTag(tagEl, cfg); } else if (cfg.tagName === 'toc') { renderTocTag(tagEl); + } else if (/^tab(-underline|-filled)?$/.test(cfg.tagName)) { + renderTabsTag(tagEl, cfg); + } else if (/^accordion(-underline|-filled)?$/.test(cfg.tagName)) { + renderAccordionTag(tagEl, cfg); } else { renderPostTag(tagEl, cfg); } diff --git a/app/nav.yml b/app/nav.yml index 655a3c5..331515a 100644 --- a/app/nav.yml +++ b/app/nav.yml @@ -16,3 +16,7 @@ pages: - file: pages/docs.md title: Docs sort: 300 + + - file: pages/tabs-accordions.md + title: Tabs & Accordions + sort: 400 diff --git a/app/pages/tabs-accordions.md b/app/pages/tabs-accordions.md new file mode 100644 index 0000000..d21a541 --- /dev/null +++ b/app/pages/tabs-accordions.md @@ -0,0 +1,78 @@ +--- +title: Tabs & Accordions +sort: 400 +--- + +# Tabs & Accordions + +## Tab — Underline variant + +```mdcms tab-underline +items: + - title: Install + default: selected + content: | + Install with `npm i mdcms` or `pnpm add mdcms`. + - title: Configure + content: | + Drop a `mdcms.config.yaml` next to your content folder. + - title: Deploy + content: | + Any static host. The build emits plain HTML. +``` + +## Tab — Filled variant + +```mdcms tab-filled +items: + - title: Overview + default: selected + content: | + MD-CMS is a markdown-based static site system with no build step. + - title: Features + content: | + - Sidebar navigation with sections + - Full-text search via Fuse.js + - PWA support with offline caching + - Dark / light theme toggle + - title: Architecture + content: | + Two parts: `mdcms.py` (CLI) and `app/index.html` (browser renderer). +``` + +## Accordion — Underline variant + +```mdcms accordion-underline +items: + - title: What is MD-CMS? + default: open + content: | + MD-CMS is a single-file browser renderer that reads markdown, config, + and nav at runtime entirely client-side. No build pipeline, no compilation. + - title: How do I install it? + content: | + Run `pip install mdcms` or download a binary from the GitHub releases page. + - title: Does it work offline? + content: | + Yes — run `mdcms fetch-deps` to bundle all vendor assets locally, then + enable `pwa: yes` in `config.yml` for full offline support. +``` + +## Accordion — Filled variant + +```mdcms accordion-filled +items: + - title: Can I use custom themes? + default: open + content: | + Yes. Create a `theme.yml` file and point to it with `theme: theme.yml` in + your `config.yml`. The theme controls colours, fonts, and layout. + - title: What markdown features are supported? + content: | + GFM (GitHub Flavored Markdown): tables, task lists, fenced code blocks, + strikethrough, and autolinks. Syntax highlighting via highlight.js. + - title: Can I nest categories? + content: | + Categories are flat (no nesting), but nav sections support a `parent:` + key for two-level sidebar grouping. +``` diff --git a/app/search.json b/app/search.json index d6dfd7a..cd54333 100644 --- a/app/search.json +++ b/app/search.json @@ -34,5 +34,17 @@ "modified": "", "language": "en", "body": "# Phase 7 — PWA Test\n\nThis page verifies the service worker and manifest generated by `mdcms build` when `pwa: yes` is set in `config.yml`.\n\n## Test procedure\n\n1. Run `python3 mdcms.py build --path app/` — confirm `manifest.json` and `service-worker.js` appear in `app/`\n2. Load `http://localhost:8800` — service worker registers on first load\n3. Navigate to the **About** and **Docs** pages so they are fetched and cached\n4. Stop the HTTP server (`Ctrl+C` in its terminal)\n5. Reload — site should load fully from the service worker cache\n6. Navigate between pages — all should work offline\n7. Check that a page not yet visited shows the offline message\n\n## What to look for\n\n- `manifest.json` and `service-worker.js` exist after build\n- DevTools → Application → Service Workers: status **activated and running**\n- DevTools → Application → Cache Storage: cache named `mdcms-xxxxxxxx` with all files listed\n- Site loads fully with server stopped\n- Offline message (`config.yml: offline-message`) appears for uncached pages\n" + }, + { + "file": "pages/tabs-accordions.md", + "title": "Tabs & Accordions", + "section-id": null, + "keywords": "", + "description": "", + "author": null, + "created": "", + "modified": "", + "language": "en", + "body": "# Tabs & Accordions\n\n## Tab — Underline variant\n\n```mdcms tab-underline\nitems:\n - title: Install\n default: selected\n content: |\n Install with `npm i mdcms` or `pnpm add mdcms`.\n - title: Configure\n content: |\n Drop a `mdcms.config.yaml` next to your content folder.\n - title: Deploy\n content: |\n Any static host. The build emits plain HTML.\n```\n\n## Tab — Filled variant\n\n```mdcms tab-filled\nitems:\n - title: Overview\n default: selected\n content: |\n MD-CMS is a markdown-based static site system with no build step.\n - title: Features\n content: |\n - Sidebar navigation with sections\n - Full-text search via Fuse.js\n - PWA support with offline caching\n - Dark / light theme toggle\n - title: Architecture\n content: |\n Two parts: `mdcms.py` (CLI) and `app/index.html` (browser renderer).\n```\n\n## Accordion — Underline variant\n\n```mdcms accordion-underline\nitems:\n - title: What is MD-CMS?\n default: open\n content: |\n MD-CMS is a single-file browser renderer that reads markdown, config,\n and nav at runtime entirely client-side. No build pipeline, no compilation.\n - title: How do I install it?\n content: |\n Run `pip install mdcms` or download a binary from the GitHub releases page.\n - title: Does it work offline?\n content: |\n Yes — run `mdcms fetch-deps` to bundle all vendor assets locally, then\n enable `pwa: yes` in `config.yml` for full offline support.\n```\n\n## Accordion — Filled variant\n\n```mdcms accordion-filled\nitems:\n - title: Can I use custom themes?\n default: open\n content: |\n Yes. Create a `theme.yml` file and point to it with `theme: theme.yml` in\n your `config.yml`. The theme controls colours, fonts, and layout.\n - title: What markdown features are supported?\n content: |\n GFM (GitHub Flavored Markdown): tables, task lists, fenced code blocks,\n strikethrough, and autolinks. Syntax highlighting via highlight.js.\n - title: Can I nest categories?\n content: |\n Categories are flat (no nesting), but nav sections support a `parent:`\n key for two-level sidebar grouping.\n```\n" } ] \ No newline at end of file diff --git a/app/service-worker.js b/app/service-worker.js index a357757..791adf2 100644 --- a/app/service-worker.js +++ b/app/service-worker.js @@ -1,5 +1,5 @@ // mdcms service worker — generated by mdcms build -const CACHE_NAME = 'mdcms-eb384247'; +const CACHE_NAME = 'mdcms-a1862733'; const PRECACHE_URLS = [ "index.html", "config.yml", @@ -9,20 +9,29 @@ const PRECACHE_URLS = [ "pages/about.md", "pages/docs.md", "pages/home.md", + "pages/tabs-accordions.md", "posts/.gitkeep", "assets/fonts/.gitkeep", "assets/icons/.gitkeep", + "assets/icons/add.svg", "assets/icons/arrow_drop_down.svg", "assets/icons/arrow_right.svg", + "assets/icons/collapse_content.svg", "assets/icons/dangerous.svg", "assets/icons/dark_mode.svg", "assets/icons/error.svg", "assets/icons/exclamation.svg", + "assets/icons/expand_content.svg", "assets/icons/history.svg", "assets/icons/info.svg", + "assets/icons/keyboard_arrow_down.svg", + "assets/icons/keyboard_arrow_right.svg", + "assets/icons/keyboard_double_arrow_down.svg", + "assets/icons/keyboard_double_arrow_right.svg", "assets/icons/language.svg", "assets/icons/light_mode.svg", "assets/icons/menu.svg", + "assets/icons/minimize.svg", "assets/icons/mobile_arrow_down.svg", "assets/icons/report.svg", "assets/icons/search.svg", diff --git a/docs/unreleased.md b/docs/unreleased.md index 36c6127..646d081 100644 --- a/docs/unreleased.md +++ b/docs/unreleased.md @@ -4,6 +4,144 @@ Changes merged into `development` that have not yet been released to `main`. --- +## Tabs & Accordions (`app/index.html`) + +Four new `mdcms` fenced-block types for rich content layout. All four variants read from the active theme automatically — no new config keys, no per-theme overrides needed. + +### Block types + +| Language tag | Alias for | Renders as | +|---|---|---| +| `tab-underline` | — | Tab strip, active tab marked with underline | +| `tab` | `tab-underline` | (same) | +| `tab-filled` | — | Tab strip, tabs as filled chips | +| `accordion-underline` | — | Stacked accordion, header underline style | +| `accordion` | `accordion-underline` | (same) | +| `accordion-filled` | — | Stacked accordion, filled card style | + +### Authoring syntax + +Open a fenced block with the language tag `mdcms `. The body is YAML with a single top-level key `items:`, whose value is a list of item objects. + +~~~markdown +```mdcms tab-underline +items: + - title: Install + default: selected + content: | + Install with `npm i mdcms` or `pnpm add mdcms`. + - title: Configure + content: | + Drop a `mdcms.config.yaml` next to your content folder. + - title: Deploy + content: | + Any static host. The build emits plain HTML. +``` +~~~ + +### Per-item keys + +| Key | Required | Type | Notes | +|---|---|---|---| +| `title` | yes | plain string | Label shown on the tab button or accordion header. Plain text only — no Markdown. | +| `content` | yes | Markdown block | Body content. Use the YAML literal block scalar (`\|`) for multi-line Markdown. Rendered with the same pipeline as the surrounding page (GFM, syntax highlighting, internal links). | +| `default` | no | string | **Tabs:** `selected` marks the tab that is open on load; if no item has `selected`, the first item is used. `notselected` (or omitting the key) leaves the tab inactive. Exactly one tab should be `selected`. **Accordions:** `open` makes the item expanded on load; `closed` (or omitting) leaves it collapsed. Any number of accordion items may be `open`. | +| `title-style` | no | string | Heading level for screen readers and external TOC tools. One of `"#"`, `"##"`, `"###"`, `"####"`, `"#####"`, `"######"`, or `""` (default). Visual size is always fixed by the component — this only changes the underlying ARIA role and level. Use a value when you want the item to be picked up as a heading by assistive technology. | + +### Examples + +**Tabs — underline (default)** + +~~~markdown +```mdcms tab +items: + - title: npm + default: selected + content: | + ```bash + npm install mdcms + ``` + - title: pnpm + content: | + ```bash + pnpm add mdcms + ``` + - title: yarn + content: | + ```bash + yarn add mdcms + ``` +``` +~~~ + +**Tabs — filled chips** + +~~~markdown +```mdcms tab-filled +items: + - title: Overview + default: selected + content: | + MD-CMS is a markdown-based static site system with no build step. + - title: Features + content: | + - Sidebar navigation + - Full-text search + - PWA + offline support + - Dark / light theme +``` +~~~ + +**Accordion — underline (default)** + +~~~markdown +```mdcms accordion +items: + - title: What is MD-CMS? + default: open + content: | + A single-file browser renderer. No build pipeline, no compilation, + no server required. + - title: How do I install it? + content: | + Run `pip install mdcms` or download a binary from the GitHub releases page. + - title: Does it work offline? + content: | + Yes — run `mdcms fetch-deps` to bundle vendor assets locally, then enable + `pwa: yes` in `config.yml` for full offline support. +``` +~~~ + +**Accordion — filled cards** + +~~~markdown +```mdcms accordion-filled +items: + - title: Can I use custom themes? + default: open + content: | + Yes. Create a `theme.yml` and reference it with `theme: theme.yml` in + `config.yml`. The theme controls colours, fonts, and layout. + - title: title-style example + title-style: "##" + content: | + This header is announced as an `

` to screen readers, even though + its visual size is set by the accordion component. +``` +~~~ + +### How the appearance adapts to themes + +The components derive their fill colours and bar/border colours from the active theme at runtime. No new keys in `config.yml` or `theme.yml` are needed. + +**Bold themes** (nav background is visually distinct from the page — e.g. a dark sidebar on a light page, or a coloured nav like red or navy): filled tabs and accordion headers use the nav background colour as their fill; the bar/border uses the nav colour. This makes the components look like an extension of the sidebar chrome. + +**Subtle themes** (nav background is almost identical to the page — e.g. both near-white or both near-dark): filled tabs use a light tint of the accent colour; the bar and border use the accent colour directly. This keeps the components visible without a strong nav background to borrow from. + +The switch between bold and subtle is automatic. The algorithm uses HSL chroma (`S × (1−|2L−1|)`) rather than raw HSL saturation, which would give false "bold" readings for near-white or near-black nav backgrounds. + +--- + ## `mdcms build` patches `` with sitename `mdcms build` now rewrites the `<title>` tag in `index.html` with the value of `sitename` from `config.yml`. Previously the tag was hardcoded (`MD-CMS`) in older templates, or blank in the starter template, so link previews in WhatsApp, Slack, and other crawlers that read static HTML showed the wrong name.