diff --git a/CLAUDE.md b/CLAUDE.md index 78e55c2..cad1d52 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,13 +8,17 @@ Every merge into `main` is a release. Before committing any change to `mdcms.py` ## Branching convention +Only two branches exist in this repository: **`main`** and **`development`**. No other branches should be created or left alive. + - **`main`** is the release branch. Every merge to `main` is a release. Never commit work-in-progress directly to `main`. -- **`development`** is the default branch for all development, including all Claude-driven work. Create it from `main` if it doesn't exist. Do not create a new branch per conversation. -- **Phased branches** (`claude/`) are allowed when a large feature needs staged review, but the final merge target is always `main` via `development`. +- **`development`** is the default branch for all development, including all Claude-driven work. Always commit to `development` — never create a new branch per conversation or feature. - **Documentation only** (`CLAUDE.md`, `docs/`) — may be pushed directly to `main`. +- **If a non-canonical branch is created** (e.g. for a large staged feature), it must be deleted immediately after it is merged. The repo returns to `main` + `development` only. In practice: check out `development`, do the work, push to `development`, PR `development` → `main` when ready to release. +**When a branch isn't visible locally:** always run `git fetch origin ` before concluding a branch doesn't exist. Never create a new branch if the user names one — fetch it from the remote first. + ## Unreleased changelog `docs/unreleased.md` is a living document that tracks every fix or feature on `development` that has not yet been merged to `main`. Keep it current: whenever a change lands on `development`, add or update an entry in `unreleased.md` in the same commit (or a follow-up commit to `development`). When a batch of changes is merged to `main` and released, clear the entries that were released and leave the file in place for the next round of work. @@ -97,7 +101,7 @@ Single-module Python script. Logical layers in order: 5. **Category system** — `identify_variant()` splits `.md` paths into `(base, category_code)`. A suffix is only treated as a category code if it appears in the declared code list. 6. **Scanner** (`scan_and_categorize`) — walks a directory, skips drafts, returns records with the first 5000 chars of body for search indexing. Paths are relative to `site_root`. 7. **Nav/search generators** — `generate_nav_yml()` emits a fixed-format YAML subset. `generate_search_json()` emits a JSON array. `merge_sections()` preserves existing section metadata on rebuild. -8. **Core build** (`run_build`) — orchestrates the full build: version check → config read → scan → merge → write nav.yml and search.json. +8. **Core build** (`run_build`) — orchestrates the full build: version check → config read → scan → merge → write nav.yml and search.json → patch `` in `index.html` with `sitename` → generate PWA files if enabled. The `<title>` patch ensures crawlers and link-preview scrapers (WhatsApp, Slack, etc.) see the correct site name in the static HTML before any JavaScript runs. 9. **Template download** (`download_template`) — fetches `app/` from GitHub via the Contents API using `urllib` + `certifi` for SSL. Recursively downloads files and directories. 10. **CLI commands** (`register`, `delete`, `view`, `build`) — implemented with `click`. Entry point: `main()` → `cli()`. diff --git a/app/index.html b/app/index.html index d811dc9..de8a6ab 100644 --- a/app/index.html +++ b/app/index.html @@ -21,7 +21,7 @@ <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> -<title>MD-CMS + @@ -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; } @@ -952,7 +1092,7 @@ body { // Category state (phase 3) let categoriesUse = false; - let categoriesList = []; // [{code, name, direction, message, notfoundmessage, pagenotfoundmessage, font, ...}] + let categoriesList = []; // [{code, name, direction, message, notfoundmessage, pagenotfoundmessage, visibilityifnocontent, font, ...}] let categoriesByCode = {}; // code → category object let defaultCategoryCode = null; let activeCategory = null; // current code @@ -1135,11 +1275,19 @@ body { if (b) b.remove(); } + function _isMdResponse(r) { + // Reject HTML responses — servers with SPA routing (e.g. Cloudflare Pages with + // "/* /index.html 200") return index.html with 200 for missing files, which would + // be mistaken for a found markdown file. + const ct = r.headers.get('content-type') || ''; + return !ct.startsWith('text/html'); + } + async function fetchPageFile(conceptualFile) { // conceptualFile like "pages/foo.md". Returns { ok, text, resolvedFile } or { ok: false }. if (!categoriesUse) { const r = await fetch(conceptualFile); - if (r.ok) return { ok: true, text: await r.text(), resolvedFile: conceptualFile }; + if (r.ok && _isMdResponse(r)) return { ok: true, text: await r.text(), resolvedFile: conceptualFile }; return { ok: false }; } const base = conceptualFile.replace(/\.md$/, ''); @@ -1169,7 +1317,7 @@ body { if (seen.has(url)) continue; seen.add(url); const r = await fetch(url); - if (r.ok) return { ok: true, text: await r.text(), resolvedFile: url }; + if (r.ok && _isMdResponse(r)) return { ok: true, text: await r.text(), resolvedFile: url }; } return { ok: false }; } @@ -1202,13 +1350,15 @@ body { // - Home page: always show (per config.homepage or default 'pages/home.md') // - Variant exists for active category: show // - Active category has notfoundmessage: show (renderer falls back to default language) + // - Active category has visibilityifnocontent: visible: show (renderer shows pagenotfoundmessage) // - Otherwise: hide if (!categoriesUse) return true; if (page.file === defaultPage()) return true; + if (page.uncategorized) return true; const variants = page.variants || []; if (variants.includes(activeCategory)) return true; const cat = categoriesByCode[activeCategory]; - return !!(cat && cat.notfoundmessage); + return !!(cat && (cat.notfoundmessage || cat.visibilityifnocontent === 'visible')); } // ─── Theme ──────────────────────────────────────────────── @@ -1226,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() { @@ -1457,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 '
'; } @@ -1607,7 +1762,7 @@ function fmtDatetime(dtStr) { // Category filter if (categoriesUse && activeCategory) { - posts = posts.filter(function(e) { return e.category === activeCategory; }); + posts = posts.filter(function(e) { return !e.category || e.category === activeCategory; }); } // Field filter @@ -1946,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; }); @@ -2002,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 { @@ -2010,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); } @@ -2243,16 +2628,17 @@ function fmtDatetime(dtStr) { function visibleCategoryCodesForCurrentPage() { // Which categories should appear in the dropdown: // - the variant exists for this page, OR - // - the category has a notfoundmessage + // - the category has a notfoundmessage (fallback to default content), OR + // - the category has visibilityifnocontent: visible (shows pagenotfoundmessage instead) // - always include the active category so user can see what they're on const out = new Set(); const page = currentPage ? navData.find(p => p.file === currentPage) : null; categoriesList.forEach(cat => { - const hasVariant = !page || !page.variants || page.variants.includes(cat.code); - const hasMsg = !!cat.notfoundmessage; - if (hasVariant || hasMsg || cat.code === activeCategory) out.add(cat.code); + const hasVariant = !page || page.uncategorized || !(page.variants && page.variants.length) || page.variants.includes(cat.code); + const alwaysVisible = !!cat.notfoundmessage || cat.visibilityifnocontent === 'visible'; + if (hasVariant || alwaysVisible || cat.code === activeCategory) out.add(cat.code); }); return out; } @@ -2277,7 +2663,7 @@ function fmtDatetime(dtStr) { 'data-code': cat.code }); option.appendChild(document.createTextNode(primary)); - const hasVariant = !page || !page.variants || page.variants.includes(cat.code); + const hasVariant = !page || page.uncategorized || !(page.variants && page.variants.length) || page.variants.includes(cat.code); if (!hasVariant && cat.notfoundmessage) { option.appendChild(el('span', { className: 'secondary', textContent: cat.notfoundmessage })); } else if (secondary) { @@ -2642,6 +3028,24 @@ function fmtDatetime(dtStr) { const contentEl = document.getElementById('pageContent'); highlightNav(file); + // If the active category is "hidden" (no notfoundmessage, not visibilityifnocontent:visible) + // and this page has no variant for it, silently switch to the default category instead of + // showing an error. + if (categoriesUse && activeCategory !== defaultCategoryCode && file !== defaultPage()) { + const cat = categoriesByCode[activeCategory]; + const isHidden = cat && !cat.notfoundmessage && cat.visibilityifnocontent !== 'visible'; + if (isHidden) { + const pageEntry = navData.find(p => p.file === file); + const hasVariant = !pageEntry || pageEntry.uncategorized + || !(pageEntry.variants && pageEntry.variants.length) + || pageEntry.variants.includes(activeCategory); + if (!hasVariant) { + setActiveCategory(defaultCategoryCode); + return; + } + } + } + // Build a clean URL: keep origin + path, set ?cat only when non-default, set hash to conceptual file const u = new URL(window.location); if (categoriesUse && activeCategory && activeCategory !== defaultCategoryCode) { @@ -2656,7 +3060,7 @@ function fmtDatetime(dtStr) { const result = await fetchPageFile(file); if (!result.ok) { - const offlineMsg = localStorage.getItem('mdcms-offline'); + const offlineMsg = !navigator.onLine && localStorage.getItem('mdcms-offline'); const bodyMsg = offlineMsg ? `

${offlineMsg}

` : `

${pageNotFoundMessage()}

`; 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/knownbugs.md b/docs/knownbugs.md new file mode 100644 index 0000000..e341d28 --- /dev/null +++ b/docs/knownbugs.md @@ -0,0 +1,35 @@ +# Known bugs + +Bugs that have been identified but not yet fixed. Fixed bugs are moved to the release notes. + +--- + +## Fixed in development (not yet released) + +### Category-variant pages fail to load on servers with SPA routing + +**Symptom:** On Cloudflare Pages (and any other server configured to serve `index.html` with HTTP 200 for missing paths), clicking a nav item whose page only exists as a category-variant file (e.g. `page.current.md`, no plain `page.md`) showed garbled content — the raw HTML of `index.html` rendered as markdown, with the site's `` text visible in the content area. + +**Root cause:** `fetchPageFile` tried the base filename (`pages/page.md`) first. Servers with SPA routing return this with HTTP 200 (serving `index.html`), so `r.ok` was true and the function returned without trying the actual variant file (`pages/page.current.md`). + +**Fix:** `fetchPageFile` now checks the `Content-Type` response header and skips any response with `text/html`, continuing to the next candidate URL. + +--- + +### Stale service worker not removed when `pwa: no` + +**Symptom:** After changing a site from `pwa: yes` to `pwa: no` and rebuilding, the old service worker remained active in browsers that had previously visited the site. Cached responses from the old build continued to be served. + +**Root cause:** `mdcms build` stopped generating PWA files when `pwa: no`, but `index.html` unconditionally registers `service-worker.js` on every page load. With no new SW to replace it, the old worker stayed installed indefinitely. + +**Fix:** `mdcms build` now writes a self-unregistering stub `service-worker.js` when `pwa: no`. On the visitor's next visit, the browser installs the stub which immediately calls `self.registration.unregister()`, evicting the stale worker. `manifest.json` is also deleted if present. + +--- + +### `config.yml` YAML parse errors were silently swallowed + +**Symptom:** A malformed `config.yml` (e.g. a stray tab character, which YAML forbids as a token starter) caused `read_config` to catch the `YAMLError` and return an empty dict. The build would proceed with no config — categories disabled, no default category code — producing a `nav.yml` that omitted `variants` fields and listed category variant files (e.g. `page.current.md`) as plain pages. Pages with category variants would not appear in the sidebar. + +**Root cause:** `read_config` caught `(OSError, yaml.YAMLError)` in a single block and silently returned `{}` on any error. + +**Fix:** `read_config` now raises `click.ClickException` on both `OSError` and `yaml.YAMLError`, aborting the build with a descriptive error message instead of continuing with an empty config. diff --git a/docs/reference-config.md b/docs/reference-config.md index 6c91062..0c0d829 100644 --- a/docs/reference-config.md +++ b/docs/reference-config.md @@ -151,16 +151,46 @@ categories-use: yes # Enable the category system. Default: no. default-category: # The category used when no ?cat= parameter is in the URL. code: en # Short code. Used in filenames (page.en.md) and URL params. - name: English # Display name shown in the category selector. + name: English # Display name shown in the category dropdown list. + message: English # Label shown on the selector bar (trigger button). Falls back to name. + name-latin: English # Secondary label shown in the dropdown alongside name. Use when name + # is in a non-Latin script (e.g. Arabic, Devanagari) to aid recognition. + # Omit if name is already Latin or identical to name. direction: ltr # Text direction. ltr or rtl. Default: ltr. + # rtl flips the nav position and content text direction. + notfoundmessage: "Not available in this language" + # Short note shown in the dropdown when no variant exists for the + # current page. Also enables fallback: the renderer will fall back to + # the default-category content instead of hiding the page. + # Omit to hide the category from the dropdown when no variant exists. + visibilityifnocontent: hidden # hidden (default) or visible. + # hidden: category disappears from the selector when no variant exists + # for the current page (unless notfoundmessage is also set). + # visible: category stays in the selector regardless. When the user + # navigates to a page with no variant, pagenotfoundmessage is shown + # in the content area. No fallback to default-category content. + pagenotfoundmessage: "This page is not yet available in English." + # Message shown in the content area when a page cannot be fetched for + # this category. Overrides the top-level pagenotfoundmessage. + font: NotoNastaliqUrdu-Regular.ttf + # Font filename inside assets/fonts/. Loaded on demand when this + # category is activated. Useful for scripts that need a specific font. + line-height: 2.8 # Line height override for this category. Useful for scripts like + # Nastaliq that need extra vertical space. Restores to theme default + # when switching away. -categories: # Additional categories. +categories: # Additional categories. Each entry supports the same keys as + # default-category above. - code: nb name: Norsk direction: ltr - code: ar name: عربي - direction: rtl # RTL flips nav position and content text direction. + name-latin: Arabic + direction: rtl + notfoundmessage: "غير متاح" + font: NotoNastaliqUrdu-Regular.ttf + line-height: 2.8 categories-sectionnames: same # How section names are shown per category. # same: all categories share one section name (defaultname in nav.yml). @@ -170,6 +200,21 @@ categories-selecticon: globe # Icon shown in the category selector bar. SVG na categories-selecttext: "Language" # Label shown next to the icon in the category selector bar. ``` +### Per-category keys summary + +| Key | Required | Description | +|---|---|---| +| `code` | Yes | Short identifier used in filenames (`page.nb.md`) and the `?cat=` URL param. | +| `name` | Yes | Display name shown in the dropdown list. | +| `message` | No | Label shown on the selector trigger button. Falls back to `name`. | +| `name-latin` | No | Secondary label in the dropdown, shown alongside `name` when `name` uses a non-Latin script. | +| `direction` | No | `ltr` or `rtl`. Default: `ltr`. RTL flips nav and content direction. | +| `notfoundmessage` | No | Short note shown in the dropdown when no variant exists for the current page. Also enables fallback to default-category content. | +| `visibilityifnocontent` | No | `hidden` (default) or `visible`. `visible` keeps the category in the selector when no variant exists; navigating to it shows `pagenotfoundmessage` with no fallback to default content. | +| `pagenotfoundmessage` | No | Message shown in the content area when a page cannot be fetched for this category. Overrides the top-level `pagenotfoundmessage`. | +| `font` | No | Font filename from `assets/fonts/`. Loaded on demand when this category is activated. | +| `line-height` | No | Body line height override for this category. Restores to theme default when switching away. | + --- ## Reusable callout messages @@ -228,6 +273,30 @@ offline-message: nb: "Du er frakoblet. Koble til og last inn på nytt." language: en +pagenotfoundmessage: "Please select a page to continue." + +categories-use: yes +default-category: + code: en + name: English + direction: ltr +categories: + - code: nb + name: Norsk + direction: ltr + visibilityifnocontent: visible + pagenotfoundmessage: "Denne siden er ikke tilgjengelig på norsk ennå." + - code: ar + name: عربي + name-latin: Arabic + direction: rtl + notfoundmessage: "غير متاح" + pagenotfoundmessage: "هذه الصفحة غير متاحة." + font: NotoNastaliqUrdu-Regular.ttf + line-height: 2.8 +categories-sectionnames: same +categories-selecticon: globe +categories-selecttext: "Language" callouts: aitranslation: diff --git a/docs/reference-pages.md b/docs/reference-pages.md index 6598ad9..287a336 100644 --- a/docs/reference-pages.md +++ b/docs/reference-pages.md @@ -167,6 +167,92 @@ paginate: yes # Pagination mode: --- +### Tabs — `tab-underline`, `tab-filled`, `tab` + +A horizontal tab strip with a single visible content panel. The active tab is set with `default: selected`; if no item carries that value the first item is selected automatically. + +| Tag name | Appearance | +|---|---| +| `tab-underline` | Labels in a row; active tab marked with a 2 px underline in the accent colour. | +| `tab` | Alias for `tab-underline`. | +| `tab-filled` | Each label is a chip with a filled background; active chip inverts to the page background with an accent border. | + +The body of the block is YAML. It must start with `items:` followed by a list of item objects. + +````markdown +```mdcms tab-underline +items: + - title: npm + default: selected + content: | + ```bash + npm install mdcms + ``` + - title: pnpm + content: | + ```bash + pnpm add mdcms + ``` + - title: yarn + content: | + ```bash + yarn add mdcms + ``` +``` +```` + +**Per-item keys:** + +| Key | Required | Notes | +|---|---|---| +| `title` | yes | Label on the tab button. Plain text only. | +| `content` | yes | Tab panel body. Full Markdown, use `\|` for multi-line. | +| `default` | no | `selected` — open on load. If no item is `selected`, the first item is used. | +| `title-style` | no | Heading level for screen readers. One of `"#"` … `"######"` or `""` (default). Does not affect visual size. | + +--- + +### Accordions — `accordion-underline`, `accordion-filled`, `accordion` + +Stacked collapsible items. Each item has a clickable header and a body that expands below it. Any number of items can be open simultaneously. + +| Tag name | Appearance | +|---|---| +| `accordion-underline` | Header separated from the content by a 2 px bar in the accent or nav colour; open content has a matching 1 px border on three sides. | +| `accordion` | Alias for `accordion-underline`. | +| `accordion-filled` | Closed header is a filled chip; when open the item becomes a single bordered card with the header fill at the top and the page background below. | + +````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. +``` +```` + +**Per-item keys:** + +| Key | Required | Notes | +|---|---|---| +| `title` | yes | Header label. Plain text only. | +| `content` | yes | Body shown when expanded. Full Markdown, use `\|` for multi-line. | +| `default` | no | `open` — expanded on load. `closed` or omitted — collapsed. Multiple items may be `open`. | +| `title-style` | no | Heading level for screen readers. One of `"#"` … `"######"` or `""` (default). Does not affect visual size. | + +**How the colour adapts to themes:** The bar/border colour and the chip fill are derived automatically from the active theme. On themes where the sidebar background is visually distinct from the page (dark nav on a light page, or a coloured nav), the components use the nav colour as their fill. On subtle themes where sidebar and page backgrounds are near-identical, the accent colour is used instead. No per-theme config is needed. + +--- + ## Markdown features Standard CommonMark plus GFM (GitHub-flavoured) extensions: diff --git a/docs/unreleased.md b/docs/unreleased.md new file mode 100644 index 0000000..646d081 --- /dev/null +++ b/docs/unreleased.md @@ -0,0 +1,204 @@ +# Unreleased changes + +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 <type>`. 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 `<h2>` 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 `<title>` 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. + +--- + +## Untranslated posts now visible in all categories + +**Status:** On `development`, pending release. + +### What was broken + +When the category system is enabled, a post file without a category suffix (e.g. `posts/my-post.md`) was silently assigned to the default category only. Switching to any other category caused those posts to disappear from the nav and from `posts-*` tag listings — even though no translated version existed. If you wrote posts without a language suffix, they simply vanished the moment a visitor switched category. + +Pages without a category suffix are unaffected: they continue to be assigned to the default category, which is the correct behaviour for pages. + +### What it does now + +Posts without a category suffix are treated as uncategorised — meaning they appear in every category. A post called `my-post.md` now shows up regardless of which category is active. A post called `my-post.en.md` still appears only in the `en` category as before. + +Mixed situations work as expected: if you have both `my-post.md` and `my-post.nb.md`, the Norwegian variant is shown when the `nb` category is active, and the bare `my-post.md` is shown for every other category. + +### What changes in the build output + +After rebuilding a site with `mdcms build`, affected post entries in `nav.yml` gain an `uncategorized: true` field: + +```yaml +- file: posts/my-post.md + title: My Post + sort: 100 + uncategorized: true +``` + +In `search.json`, these entries carry `"category": null` instead of the default category code. This is what tells the renderer to include them universally. + +A rebuild is required for existing sites to pick up the change. + +--- + +## Fix: category-variant pages fail to load on servers with SPA routing (e.g. Cloudflare Pages) + +When a site uses category-suffixed page files (e.g. `page.current.md`) and is hosted on a server configured with SPA fallback routing (serving `index.html` with HTTP 200 for any unknown path), the renderer's `fetchPageFile` mistook the HTML fallback for a found markdown file. It returned `index.html` content instead of falling through to try the `.current.md` variant. The page rendered the raw HTML of `index.html` as markdown, showing the `<title>` text (`sitename`) in the content area. + +`fetchPageFile` now checks the `Content-Type` response header and rejects any response with `text/html`, continuing to the next candidate URL instead. + +--- + +## Fix: stale service worker not removed when `pwa: no` + +`index.html` unconditionally registers `service-worker.js` on every page load. When a site switched from `pwa: yes` to `pwa: no`, `mdcms build` stopped generating a new service worker, but the old one remained active in browsers that had visited the site before. The stale worker continued to serve cached responses from the old build. + +`mdcms build` now writes a self-unregistering `service-worker.js` when `pwa: no`. On the visitor's next page load, the browser installs this stub worker, which immediately unregisters itself and evicts any previously cached content. `manifest.json` is also removed if present. + +--- + +## Fix: `config.yml` YAML parse errors now abort the build with a clear message + +A malformed `config.yml` (e.g. a stray tab character, which YAML forbids as a token starter) previously caused `read_config` to silently return an empty dict. The build would proceed with no config — categories disabled, no default category code — producing a broken `nav.yml` with wrong filenames and missing `variants` fields, so category-variant pages would not appear in the sidebar. + +`read_config` now raises `ClickException` on both `OSError` and `yaml.YAMLError`, aborting the build with a descriptive error message instead of continuing silently with an empty config. diff --git a/mdcms.py b/mdcms.py index 7050c9e..ce4316e 100644 --- a/mdcms.py +++ b/mdcms.py @@ -113,9 +113,12 @@ def read_config(site_path: Path) -> dict: return {} try: text = config_file.read_text(encoding="utf-8") + except OSError as e: + raise click.ClickException(f"Could not read config.yml: {e}") + try: return yaml.safe_load(text) or {} - except (OSError, yaml.YAMLError): - return {} + except yaml.YAMLError as e: + raise click.ClickException(f"config.yml is not valid YAML: {e}") def get_category_info(cfg: dict) -> dict: @@ -265,11 +268,19 @@ def build_page_nav( "sort": sort, } if categories_use: + is_post = file.startswith("posts/") covered = {} + has_uncategorized = False for code, record in variants.items(): - key = code if code is not None else default_code - if key: - covered[key] = record.get("title", "") + if code is None: + if is_post: + has_uncategorized = True + elif default_code: + covered[default_code] = record.get("title", "") + else: + covered[code] = record.get("title", "") + if has_uncategorized: + entry["uncategorized"] = True entry["variants"] = sorted(covered.keys()) entry["titles"] = covered out.append(entry) @@ -313,6 +324,8 @@ def generate_nav_yml(sections: list, pages: list, categories_use: bool = False) if p.get("section-id"): lines.append(f" section-id: {p['section-id']}") lines.append(f" sort: {p.get('sort', 100)}") + if categories_use and p.get("uncategorized"): + lines.append(" uncategorized: true") if categories_use and p.get("variants"): lines.append(f" variants: [{', '.join(p['variants'])}]") if categories_use and p.get("titles"): @@ -345,7 +358,13 @@ def generate_search_json( } if categories_use: code = r.get("code") - entry["category"] = code if code is not None else default_code + is_post = r.get("file", "").startswith("posts/") + if code is not None: + entry["category"] = code + elif is_post: + entry["category"] = None # null = show in all categories + else: + entry["category"] = default_code out.append(entry) return json.dumps(out, indent=2, ensure_ascii=False) @@ -412,6 +431,19 @@ def validate_assets(site_path: Path, cfg: dict) -> list: # ─── Core build logic ───────────────────────────────────────── +_TITLE_RE = re.compile(r"<title>[^<]*") + + +def _patch_html_title(site_path: Path, sitename: str) -> None: + index = site_path / "index.html" + if not index.exists(): + return + html = index.read_text(encoding="utf-8") + new_html = _TITLE_RE.sub(f"{sitename}", html, count=1) + if new_html != html: + index.write_text(new_html, encoding="utf-8") + + def run_build(site_path: Path): """Scan pages/ and posts/, write nav.yml and search.json. Raises ClickException on failure.""" if not site_path.is_dir(): @@ -491,9 +523,13 @@ def run_build(site_path: Path): ) click.echo(f" Wrote search.json ({len(live_pages) + len(post_records)} entries)") + _patch_html_title(site_path, cfg.get("sitename", "")) + pwa_enabled = str(cfg.get("pwa", "no")).lower() in ("yes", "true") if pwa_enabled: generate_pwa(site_path, cfg) + else: + cleanup_pwa(site_path) asset_warnings = validate_assets(site_path, cfg) for w in asset_warnings: @@ -509,6 +545,29 @@ def run_build(site_path: Path): # ─── PWA generation ─────────────────────────────────────────── +def cleanup_pwa(site_path: Path): + """When pwa: no, write a self-unregistering service worker and remove manifest.json. + + Browsers keep the previously installed service worker active until a new one is + installed. Writing a stub that immediately unregisters itself ensures any stale + caching worker is evicted on the next visit after a pwa: yes → pwa: no change. + """ + sw = site_path / "service-worker.js" + sw.write_text( + "// mdcms: PWA disabled — unregisters any previously installed service worker.\n" + "self.addEventListener('install', () => self.skipWaiting());\n" + "self.addEventListener('activate', event => {\n" + " event.waitUntil(self.registration.unregister());\n" + "});\n", + encoding="utf-8", + ) + manifest = site_path / "manifest.json" + if manifest.exists(): + manifest.unlink() + click.echo(" Removed manifest.json (pwa: no)") + click.echo(" Wrote service-worker.js (self-unregistering stub, pwa: no)") + + def generate_pwa(site_path: Path, cfg: dict): """Generate manifest.json and service-worker.js when pwa: yes.""" pwa_name = cfg.get("pwa-name", cfg.get("sitename", "MD-CMS Site")) diff --git a/themes/Operating Systems/.gitkeep b/themes/Operating Systems/.gitkeep new file mode 100644 index 0000000..8d1c8b6 --- /dev/null +++ b/themes/Operating Systems/.gitkeep @@ -0,0 +1 @@ + diff --git a/themes/Operating Systems/os-adwaita.yaml b/themes/Operating Systems/os-adwaita.yaml new file mode 100644 index 0000000..ab8907d --- /dev/null +++ b/themes/Operating Systems/os-adwaita.yaml @@ -0,0 +1,76 @@ +# mdcms v0.4 | DO NOT REMOVE THIS COMMENT +# mdcms theme — os-adwaita +# GNOME / Adwaita feel. Warm near-white paper, soft window-bg chrome, +# the familiar Adwaita blue accent. Cantarell typeface throughout. + +# ────────────────────────────────── +# Colours — based on published libadwaita tokens +# light: window_bg_color #fafafa, view_bg #ffffff, accent #1c71d8 (blue 4), +# fg ~rgba(0,0,0,0.8), dim ~rgba(0,0,0,0.55) +# dark: window_bg_color #242424, view_bg #1e1e1e, accent #78aeed (blue 1) +# ────────────────────────────────── +light: + accent: "#1C71D8" + background: "#FFFFFF" + nav-background: "#FAFAFA" + text: "#202020" + text-muted: "#5E5C64" + +dark: + accent: "#78AEED" + background: "#1E1E1E" + nav-background: "#242424" + text: "#FFFFFF" + text-muted: "#C0BFBC" + +# ────────────────────────────────── +# Semantic colours — Adwaita "named colors": green-3, yellow-5, red-3 +# ────────────────────────────────── +colours-semantic: + info: "#1C71D8" + warning: "#E5A50A" + success: "#26A269" + error: "#C01C28" + +colours-semantic-dark: + info: "#78AEED" + warning: "#F8E45C" + success: "#57E389" + error: "#F66151" + +# ────────────────────────────────── +# Callout defaults +# ────────────────────────────────── +callouts: + info: + icon: info + primary-colour: "#1C71D8" + background-colour: "#1C71D8" + warning: + icon: warning + primary-colour: "#E5A50A" + background-colour: "#E5A50A" + success: + icon: success + primary-colour: "#26A269" + background-colour: "#26A269" + error: + icon: error + primary-colour: "#C01C28" + background-colour: "#C01C28" + +# ────────────────────────────────── +# Typography +# Cantarell is GNOME's UI typeface — humanist sans, slightly tall x-height. +# Available on Google Fonts. +# ────────────────────────────────── +font-body: "google:Cantarell:400" +font-heading: "google:Cantarell:700" +font-size: 1.00 +line-height: 1.65 + +# ────────────────────────────────── +# Layout +# ────────────────────────────────── +main-width: 78em +nav-width: 20em diff --git a/themes/Operating Systems/os-aero.yaml b/themes/Operating Systems/os-aero.yaml new file mode 100644 index 0000000..57bddd5 --- /dev/null +++ b/themes/Operating Systems/os-aero.yaml @@ -0,0 +1,82 @@ +# mdcms v0.4 | DO NOT REMOVE THIS COMMENT +# mdcms theme — os-aero +# Windows Vista / 7 "Aero Glass" feel. Pale glass-tinted blue chrome, +# translucent sidebar vibes, bright sky-blue accent. The desktop your +# laptop sweated to render in 2009. +# +# Colours approximated from the default Aero theme palette: +# accent (taskbar / button glow) #1A78D4 +# glass tint #B8D6F0 (frosted blue) +# window face #F0F4F9 +# text #1B1B1B +# Aero Dark / "Aero Black" variant uses the same accent over near-black. + +# ────────────────────────────────── +# Colours +# ────────────────────────────────── +light: + accent: "#1A78D4" + background: "#F4F9FE" + nav-background: "#B8D6F0" + text: "#1B1B1B" + text-muted: "#525E6E" + +dark: + accent: "#4FC3F7" + background: "#0F1A2A" + nav-background: "#1A2A40" + text: "#EAF2FC" + text-muted: "#8FA8C4" + +# ────────────────────────────────── +# Semantic colours — Vista/7 standard hues +# ────────────────────────────────── +colours-semantic: + info: "#1A78D4" + warning: "#E59400" + success: "#1E8C3F" + error: "#C42B1C" + +colours-semantic-dark: + info: "#4FC3F7" + warning: "#FFC74A" + success: "#7AD18F" + error: "#FF7A7A" + +# ────────────────────────────────── +# Callout defaults +# ────────────────────────────────── +callouts: + info: + icon: info + primary-colour: "#1A78D4" + background-colour: "#1A78D4" + warning: + icon: warning + primary-colour: "#E59400" + background-colour: "#E59400" + success: + icon: success + primary-colour: "#1E8C3F" + background-colour: "#1E8C3F" + error: + icon: error + primary-colour: "#C42B1C" + background-colour: "#C42B1C" + +# ────────────────────────────────── +# Typography +# Inter portable default. Preferred: Segoe UI (Vista/7 default — first +# Microsoft OS to ship it). Open Segoe-metric-compatible alternative: +# "Selawik". Drop your TTFs in /fonts and swap font-body / font-heading. +# ────────────────────────────────── +font-body: "bunny:Inter:400" +font-heading: "bunny:Inter:600" +font-size: 1.00 +line-height: 1.55 + +# ────────────────────────────────── +# Layout +# ────────────────────────────────── +main-width: 78em +nav-width: 20em diff --git a/themes/Operating Systems/os-amiga.yaml b/themes/Operating Systems/os-amiga.yaml new file mode 100644 index 0000000..b802ecc --- /dev/null +++ b/themes/Operating Systems/os-amiga.yaml @@ -0,0 +1,82 @@ +# mdcms v0.4 | DO NOT REMOVE THIS COMMENT +# mdcms theme — os-amiga +# Workbench 1.3 revival. Iconic blue/orange/white/black on Workbench grey. +# Pixel-screen energy. The most idiosyncratic theme in the set. +# +# Original Workbench 1.x palette (4 colours, hardware-fixed): +# #0055AA blue (window chrome, background) +# #FFFFFF white +# #000000 black +# #FF8800 orange (highlights) +# 2.x onward added the warm grey #AAAAAA. + +# ────────────────────────────────── +# Colours +# ────────────────────────────────── +light: + accent: "#FF8800" + background: "#FFFFFF" + nav-background: "#0055AA" + text: "#000000" + text-muted: "#555555" + +dark: + accent: "#FF8800" + background: "#0055AA" + nav-background: "#003D7A" + text: "#FFFFFF" + text-muted: "#AAC4E0" + +# ────────────────────────────────── +# Semantic colours +# ────────────────────────────────── +colours-semantic: + info: "#0055AA" + warning: "#FF8800" + success: "#00AA55" + error: "#CC0000" + +colours-semantic-dark: + info: "#7FB2E0" + warning: "#FFB04A" + success: "#7FD9A4" + error: "#FF6B6B" + +# ────────────────────────────────── +# Callout defaults +# ────────────────────────────────── +callouts: + info: + icon: info + primary-colour: "#0055AA" + background-colour: "#0055AA" + warning: + icon: warning + primary-colour: "#FF8800" + background-colour: "#FF8800" + success: + icon: success + primary-colour: "#00AA55" + background-colour: "#00AA55" + error: + icon: error + primary-colour: "#CC0000" + background-colour: "#CC0000" + +# ────────────────────────────────── +# Typography +# VT323 portable default for the pixel-screen feel. +# Preferred for true Workbench-fidelity: "Topaz" or "Topaz New" (free +# pixel font replicas of the Amiga system font, widely available as TTF). +# For a more readable modern take, swap to "bunny:IBM Plex Mono:400". +# ────────────────────────────────── +font-body: "google:VT323:400" +font-heading: "google:VT323:400" +font-size: 1.15 +line-height: 1.45 + +# ────────────────────────────────── +# Layout +# ────────────────────────────────── +main-width: 76em +nav-width: 20em diff --git a/themes/Operating Systems/os-beos.yaml b/themes/Operating Systems/os-beos.yaml new file mode 100644 index 0000000..134bf5f --- /dev/null +++ b/themes/Operating Systems/os-beos.yaml @@ -0,0 +1,80 @@ +# mdcms v0.4 | DO NOT REMOVE THIS COMMENT +# mdcms theme — os-beos +# BeOS / Haiku revival. Iconic yellow window tab, cream paper, +# navy text. The friendly weird desktop of 1996 that won't quit. +# +# Colours from Haiku's default "Beige" palette: +# panel background #DCDCDC +# document-tab yellow #FFCB00 +# text #000000 +# accent (link/button) #336699 navy + +# ────────────────────────────────── +# Colours +# ────────────────────────────────── +light: + accent: "#336699" + background: "#F8F8E8" + nav-background: "#FFCB00" + text: "#000000" + text-muted: "#4A4A3E" + +dark: + accent: "#FFCB00" + background: "#1A1A14" + nav-background: "#2A2515" + text: "#F8F8E8" + text-muted: "#A89E70" + +# ────────────────────────────────── +# Semantic colours +# ────────────────────────────────── +colours-semantic: + info: "#336699" + warning: "#CC7700" + success: "#3F8F3F" + error: "#B22222" + +colours-semantic-dark: + info: "#7FB2E0" + warning: "#FFCB00" + success: "#86C58B" + error: "#E07A7A" + +# ────────────────────────────────── +# Callout defaults +# ────────────────────────────────── +callouts: + info: + icon: info + primary-colour: "#336699" + background-colour: "#336699" + warning: + icon: warning + primary-colour: "#CC7700" + background-colour: "#CC7700" + success: + icon: success + primary-colour: "#3F8F3F" + background-colour: "#3F8F3F" + error: + icon: error + primary-colour: "#B22222" + background-colour: "#B22222" + +# ────────────────────────────────── +# Typography +# Noto Sans is the portable default and Haiku's actual UI font. +# Preferred (BeOS original): Swis721 BT / "Be Sans" (proprietary, paid). +# DejaVu Sans is a very close free alternative. +# ────────────────────────────────── +font-body: "bunny:Noto Sans:400" +font-heading: "bunny:Noto Sans:700" +font-size: 1.00 +line-height: 1.55 + +# ────────────────────────────────── +# Layout +# ────────────────────────────────── +main-width: 78em +nav-width: 20em diff --git a/themes/Operating Systems/os-breeze.yaml b/themes/Operating Systems/os-breeze.yaml new file mode 100644 index 0000000..9160913 --- /dev/null +++ b/themes/Operating Systems/os-breeze.yaml @@ -0,0 +1,86 @@ +# mdcms v0.4 | DO NOT REMOVE THIS COMMENT +# mdcms theme — os-breeze +# KDE Plasma / Breeze feel. Cool neutral chrome, the famous Plasma blue +# accent. Breeze is intentionally subtle — light, low-saturation, with +# just a touch of cool grey. +# +# Colours from KDE's Breeze stylesheet (qss + colour scheme): +# accent (Highlight) #3DAEE9 +# view-background-color #FCFCFC +# window-background-color #EFF0F1 +# foreground (Text) #232629 +# foreground-inactive #7F8C8D +# Breeze Dark: +# view-background-color #1B1E20 +# window-background-color #232629 +# foreground #FCFCFC + +# ────────────────────────────────── +# Colours +# ────────────────────────────────── +light: + accent: "#3DAEE9" + background: "#FCFCFC" + nav-background: "#EFF0F1" + text: "#232629" + text-muted: "#7F8C8D" + +dark: + accent: "#3DAEE9" + background: "#1B1E20" + nav-background: "#232629" + text: "#FCFCFC" + text-muted: "#A1A9B1" + +# ────────────────────────────────── +# Semantic colours — Breeze "positive / neutral / negative" tones +# ────────────────────────────────── +colours-semantic: + info: "#3DAEE9" + warning: "#F67400" + success: "#27AE60" + error: "#DA4453" + +colours-semantic-dark: + info: "#61C1F0" + warning: "#F8A04A" + success: "#56C883" + error: "#ED7077" + +# ────────────────────────────────── +# Callout defaults +# ────────────────────────────────── +callouts: + info: + icon: info + primary-colour: "#3DAEE9" + background-colour: "#3DAEE9" + warning: + icon: warning + primary-colour: "#F67400" + background-colour: "#F67400" + success: + icon: success + primary-colour: "#27AE60" + background-colour: "#27AE60" + error: + icon: error + primary-colour: "#DA4453" + background-colour: "#DA4453" + +# ────────────────────────────────── +# Typography +# Noto Sans is the portable default and KDE's current UI font. +# Preferred (classic Plasma 4 era): "Oxygen Sans" — open SIL-licensed, +# available on Google Fonts as "Oxygen". +# ────────────────────────────────── +font-body: "bunny:Noto Sans:400" +font-heading: "bunny:Noto Sans:600" +font-size: 1.00 +line-height: 1.6 + +# ────────────────────────────────── +# Layout +# ────────────────────────────────── +main-width: 78em +nav-width: 20em diff --git a/themes/Operating Systems/os-chromeos.yaml b/themes/Operating Systems/os-chromeos.yaml new file mode 100644 index 0000000..58d45ba --- /dev/null +++ b/themes/Operating Systems/os-chromeos.yaml @@ -0,0 +1,85 @@ +# mdcms v0.4 | DO NOT REMOVE THIS COMMENT +# mdcms theme — os-chromeos +# ChromeOS feel. Bright white shelf, soft Google-grey surface, +# Google Blue accent. Material-rooted but its own dialect. +# +# Colours from Google's public ChromeOS / Material reference: +# Google Blue 600 #1A73E8 (accent light) +# Google Blue 200 #8AB4F8 (accent dark) +# Surface #FFFFFF / #202124 +# Surface variant #F1F3F4 / #292A2D +# On-surface #202124 / #E8EAED + +# ────────────────────────────────── +# Colours +# ────────────────────────────────── +light: + accent: "#1A73E8" + background: "#FFFFFF" + nav-background: "#F1F3F4" + text: "#202124" + text-muted: "#5F6368" + +dark: + accent: "#8AB4F8" + background: "#202124" + nav-background: "#292A2D" + text: "#E8EAED" + text-muted: "#9AA0A6" + +# ────────────────────────────────── +# Semantic colours — Google standard hues +# Green 700 #1E8E3E / Green 300 #81C995 +# Yellow 700 #F29900 / Yellow 300 #FDD663 +# Red 600 #D93025 / Red 300 #F28B82 +# ────────────────────────────────── +colours-semantic: + info: "#1A73E8" + warning: "#F29900" + success: "#1E8E3E" + error: "#D93025" + +colours-semantic-dark: + info: "#8AB4F8" + warning: "#FDD663" + success: "#81C995" + error: "#F28B82" + +# ────────────────────────────────── +# Callout defaults +# ────────────────────────────────── +callouts: + info: + icon: info + primary-colour: "#1A73E8" + background-colour: "#1A73E8" + warning: + icon: warning + primary-colour: "#F29900" + background-colour: "#F29900" + success: + icon: success + primary-colour: "#1E8E3E" + background-colour: "#1E8E3E" + error: + icon: error + primary-colour: "#D93025" + background-colour: "#D93025" + +# ────────────────────────────────── +# Typography +# Roboto is the portable default and ChromeOS's body font. +# Preferred for headings: "Google Sans" (proprietary, restricted). +# Open near-equivalent for Google Sans display: "Product Sans" +# — also restricted; use Roboto for both and you'll be fine. +# ────────────────────────────────── +font-body: "bunny:Roboto:400" +font-heading: "bunny:Roboto:500" +font-size: 1.00 +line-height: 1.55 + +# ────────────────────────────────── +# Layout +# ────────────────────────────────── +main-width: 78em +nav-width: 20em diff --git a/themes/Operating Systems/os-cupertino-graphite.yaml b/themes/Operating Systems/os-cupertino-graphite.yaml new file mode 100644 index 0000000..9edd420 --- /dev/null +++ b/themes/Operating Systems/os-cupertino-graphite.yaml @@ -0,0 +1,72 @@ +# mdcms v0.4 | DO NOT REMOVE THIS COMMENT +# mdcms theme — os-cupertino-graphite +# Mac desktop, Graphite accent variant — for people who switch the system +# tint to "Graphite" because they're serious. Pure neutral chrome. + +# ────────────────────────────────── +# Colours +# ────────────────────────────────── +light: + accent: "#6E6E73" + background: "#FFFFFF" + nav-background: "#F2F2F7" + text: "#1D1D1F" + text-muted: "#6E6E73" + +dark: + accent: "#98989D" + background: "#1E1E1E" + nav-background: "#2C2C2E" + text: "#F5F5F7" + text-muted: "#98989D" + +# ────────────────────────────────── +# Semantic colours +# ────────────────────────────────── +colours-semantic: + info: "#6E6E73" + warning: "#FF9500" + success: "#34C759" + error: "#FF3B30" + +colours-semantic-dark: + info: "#98989D" + warning: "#FF9F0A" + success: "#30D158" + error: "#FF453A" + +# ────────────────────────────────── +# Callout defaults +# ────────────────────────────────── +callouts: + info: + icon: info + primary-colour: "#6E6E73" + background-colour: "#6E6E73" + warning: + icon: warning + primary-colour: "#FF9500" + background-colour: "#FF9500" + success: + icon: success + primary-colour: "#34C759" + background-colour: "#34C759" + error: + icon: error + primary-colour: "#FF3B30" + background-colour: "#FF3B30" + +# ────────────────────────────────── +# Typography +# Inter portable default. Preferred: SF Pro Text / SF Pro Display. +# ────────────────────────────────── +font-body: "bunny:Inter:400" +font-heading: "bunny:Inter:600" +font-size: 1.00 +line-height: 1.55 + +# ────────────────────────────────── +# Layout +# ────────────────────────────────── +main-width: 78em +nav-width: 20em diff --git a/themes/Operating Systems/os-cupertino.yaml b/themes/Operating Systems/os-cupertino.yaml new file mode 100644 index 0000000..5eb1248 --- /dev/null +++ b/themes/Operating Systems/os-cupertino.yaml @@ -0,0 +1,85 @@ +# mdcms v0.4 | DO NOT REMOVE THIS COMMENT +# mdcms theme — os-cupertino +# Mac desktop feel. Bright white paper, light-platinum sidebar, +# vivid system blue accent. Dark mode goes near-black with brighter blue. +# +# Colours from Apple's publicly-documented system colour palette +# (developer.apple.com → Human Interface Guidelines → Color): +# systemBlue light #007AFF dark #0A84FF +# secondarySystemBackground (light) #F2F2F7 +# systemBackground (dark) #000000 +# secondarySystemBackground (dark) #1C1C1E + +# ────────────────────────────────── +# Colours +# ────────────────────────────────── +light: + accent: "#007AFF" + background: "#FFFFFF" + nav-background: "#F2F2F7" + text: "#1D1D1F" + text-muted: "#6E6E73" + +dark: + accent: "#0A84FF" + background: "#1E1E1E" + nav-background: "#2C2C2E" + text: "#F5F5F7" + text-muted: "#98989D" + +# ────────────────────────────────── +# Semantic colours — Apple system colours (light / dark) +# green #34C759 / #30D158 +# orange #FF9500 / #FF9F0A +# red #FF3B30 / #FF453A +# ────────────────────────────────── +colours-semantic: + info: "#007AFF" + warning: "#FF9500" + success: "#34C759" + error: "#FF3B30" + +colours-semantic-dark: + info: "#0A84FF" + warning: "#FF9F0A" + success: "#30D158" + error: "#FF453A" + +# ────────────────────────────────── +# Callout defaults +# ────────────────────────────────── +callouts: + info: + icon: info + primary-colour: "#007AFF" + background-colour: "#007AFF" + warning: + icon: warning + primary-colour: "#FF9500" + background-colour: "#FF9500" + success: + icon: success + primary-colour: "#34C759" + background-colour: "#34C759" + error: + icon: error + primary-colour: "#FF3B30" + background-colour: "#FF3B30" + +# ────────────────────────────────── +# Typography +# Inter is the portable default — close metrics to SF. +# Preferred on Apple platforms: SF Pro Text (body), SF Pro Display (headings). +# Drop your own TTFs in /fonts and change font-body / font-heading to +# "local:SF Pro Text:400" etc. +# ────────────────────────────────── +font-body: "bunny:Inter:400" +font-heading: "bunny:Inter:600" +font-size: 1.00 +line-height: 1.55 + +# ────────────────────────────────── +# Layout +# ────────────────────────────────── +main-width: 78em +nav-width: 20em diff --git a/themes/Operating Systems/os-elementary.yaml b/themes/Operating Systems/os-elementary.yaml new file mode 100644 index 0000000..6aec4d5 --- /dev/null +++ b/themes/Operating Systems/os-elementary.yaml @@ -0,0 +1,81 @@ +# mdcms v0.4 | DO NOT REMOVE THIS COMMENT +# mdcms theme — os-elementary +# elementary OS feel. Calm paper, "Slate" silver chrome, +# Blueberry-blue accent. Inter is their actual UI typeface (Inter Variable). +# +# Colours from elementary's published Stylesheet (Granite/Pantheon): +# Blueberry 500 #3689E6 (accent) +# Slate 100 #F4F4F4 / Slate 700 #333333 +# Strawberry/Lime/Banana/Cherry are the named semantic palette. + +# ────────────────────────────────── +# Colours +# ────────────────────────────────── +light: + accent: "#3689E6" + background: "#FAFAFA" + nav-background: "#F4F4F4" + text: "#333333" + text-muted: "#7E8087" + +dark: + accent: "#64BAFF" + background: "#1A1A1A" + nav-background: "#262626" + text: "#FFFFFF" + text-muted: "#A6A6A6" + +# ────────────────────────────────── +# Semantic colours — elementary named palette +# Lime 500 #68B723 success +# Banana 500 #F9C440 warning +# Strawberry 500 #C6262E error +# Blueberry 500 #3689E6 info +# ────────────────────────────────── +colours-semantic: + info: "#3689E6" + warning: "#F9C440" + success: "#68B723" + error: "#C6262E" + +colours-semantic-dark: + info: "#64BAFF" + warning: "#FFD66B" + success: "#9BDB4D" + error: "#E14852" + +# ────────────────────────────────── +# Callout defaults +# ────────────────────────────────── +callouts: + info: + icon: info + primary-colour: "#3689E6" + background-colour: "#3689E6" + warning: + icon: warning + primary-colour: "#F9C440" + background-colour: "#F9C440" + success: + icon: success + primary-colour: "#68B723" + background-colour: "#68B723" + error: + icon: error + primary-colour: "#C6262E" + background-colour: "#C6262E" + +# ────────────────────────────────── +# Typography +# Inter is elementary's actual UI typeface — used as-is. +# ────────────────────────────────── +font-body: "bunny:Inter:400" +font-heading: "bunny:Inter:600" +font-size: 1.00 +line-height: 1.55 + +# ────────────────────────────────── +# Layout +# ────────────────────────────────── +main-width: 78em +nav-width: 20em diff --git a/themes/Operating Systems/os-fluent-dark.yaml b/themes/Operating Systems/os-fluent-dark.yaml new file mode 100644 index 0000000..0c834a3 --- /dev/null +++ b/themes/Operating Systems/os-fluent-dark.yaml @@ -0,0 +1,71 @@ +# mdcms v0.4 | DO NOT REMOVE THIS COMMENT +# mdcms theme — os-fluent-dark +# Windows 11 dark mica. Same accent system, dark-first defaults. + +# ────────────────────────────────── +# Colours +# ────────────────────────────────── +light: + accent: "#005FB8" + background: "#202020" + nav-background: "#2C2C2C" + text: "#FFFFFF" + text-muted: "#C7C7C7" + +dark: + accent: "#60CDFF" + background: "#1A1A1A" + nav-background: "#202020" + text: "#FFFFFF" + text-muted: "#C7C7C7" + +# ────────────────────────────────── +# Semantic colours +# ────────────────────────────────── +colours-semantic: + info: "#60CDFF" + warning: "#FCE100" + success: "#6CCB5F" + error: "#FF99A4" + +colours-semantic-dark: + info: "#60CDFF" + warning: "#FCE100" + success: "#6CCB5F" + error: "#FF99A4" + +# ────────────────────────────────── +# Callout defaults +# ────────────────────────────────── +callouts: + info: + icon: info + primary-colour: "#60CDFF" + background-colour: "#60CDFF" + warning: + icon: warning + primary-colour: "#FCE100" + background-colour: "#FCE100" + success: + icon: success + primary-colour: "#6CCB5F" + background-colour: "#6CCB5F" + error: + icon: error + primary-colour: "#FF99A4" + background-colour: "#FF99A4" + +# ────────────────────────────────── +# Typography +# Inter portable default. Preferred: Segoe UI Variable / Selawik. +# ────────────────────────────────── +font-body: "bunny:Inter:400" +font-heading: "bunny:Inter:600" +font-size: 1.00 +line-height: 1.55 + +# ────────────────────────────────── +# Layout +# ────────────────────────────────── +main-width: 78em +nav-width: 20em diff --git a/themes/Operating Systems/os-fluent.yaml b/themes/Operating Systems/os-fluent.yaml new file mode 100644 index 0000000..5ade995 --- /dev/null +++ b/themes/Operating Systems/os-fluent.yaml @@ -0,0 +1,81 @@ +# mdcms v0.4 | DO NOT REMOVE THIS COMMENT +# mdcms theme — os-fluent +# Windows 11 / Fluent feel. Cool near-white "Mica" paper, light gray sidebar, +# Windows accent blue. Dark mode uses the dark mica neutrals. +# +# Colours from Microsoft's public Fluent 2 design tokens: +# accent (light): #005FB8 accent (dark): #60CDFF +# neutralBackground1 light #F9F9F9 / sidebar #F3F3F3 +# neutralBackground1 dark #202020 / sidebar #2C2C2C +# neutralForeground1 light #1A1A1A / dark #FFFFFF + +# ────────────────────────────────── +# Colours +# ────────────────────────────────── +light: + accent: "#005FB8" + background: "#F9F9F9" + nav-background: "#F3F3F3" + text: "#1A1A1A" + text-muted: "#5C5C5C" + +dark: + accent: "#60CDFF" + background: "#202020" + nav-background: "#2C2C2C" + text: "#FFFFFF" + text-muted: "#C7C7C7" + +# ────────────────────────────────── +# Semantic colours — Fluent persona / shared colours +# ────────────────────────────────── +colours-semantic: + info: "#005FB8" + warning: "#9D5D00" + success: "#107C10" + error: "#C42B1C" + +colours-semantic-dark: + info: "#60CDFF" + warning: "#FCE100" + success: "#6CCB5F" + error: "#FF99A4" + +# ────────────────────────────────── +# Callout defaults +# ────────────────────────────────── +callouts: + info: + icon: info + primary-colour: "#005FB8" + background-colour: "#005FB8" + warning: + icon: warning + primary-colour: "#9D5D00" + background-colour: "#9D5D00" + success: + icon: success + primary-colour: "#107C10" + background-colour: "#107C10" + error: + icon: error + primary-colour: "#C42B1C" + background-colour: "#C42B1C" + +# ────────────────────────────────── +# Typography +# Inter portable default. Preferred on Windows: Segoe UI Variable Text +# (body) / Segoe UI Variable Display (headings). Open alternatives: +# "Selawik" or "Selawik Semilight" (Microsoft's Segoe-metric-compatible +# release). Drop TTFs in /fonts and swap to "local:Segoe UI Variable:400". +# ────────────────────────────────── +font-body: "bunny:Inter:400" +font-heading: "bunny:Inter:600" +font-size: 1.00 +line-height: 1.55 + +# ────────────────────────────────── +# Layout +# ────────────────────────────────── +main-width: 78em +nav-width: 20em diff --git a/themes/Operating Systems/os-ios.yaml b/themes/Operating Systems/os-ios.yaml new file mode 100644 index 0000000..72a5692 --- /dev/null +++ b/themes/Operating Systems/os-ios.yaml @@ -0,0 +1,80 @@ +# mdcms v0.4 | DO NOT REMOVE THIS COMMENT +# mdcms theme — os-ios +# iPhone feel. Crisp white paper, grouped-table grey sidebar, +# iOS system blue. Dark mode goes true-black like the OLED dark mode. +# +# Colours from Apple's iOS system colour palette: +# systemBlue light #007AFF / dark #0A84FF +# systemBackground light #FFFFFF / dark #000000 +# secondarySystemBackground light #F2F2F7 / dark #1C1C1E +# label light #000000 / dark #FFFFFF +# secondaryLabel light #3C3C43 60% / dark #EBEBF5 60% + +# ────────────────────────────────── +# Colours +# ────────────────────────────────── +light: + accent: "#007AFF" + background: "#FFFFFF" + nav-background: "#F2F2F7" + text: "#000000" + text-muted: "#8E8E93" + +dark: + accent: "#0A84FF" + background: "#000000" + nav-background: "#1C1C1E" + text: "#FFFFFF" + text-muted: "#8E8E93" + +# ────────────────────────────────── +# Semantic colours — iOS system colours +# ────────────────────────────────── +colours-semantic: + info: "#007AFF" + warning: "#FF9500" + success: "#34C759" + error: "#FF3B30" + +colours-semantic-dark: + info: "#0A84FF" + warning: "#FF9F0A" + success: "#30D158" + error: "#FF453A" + +# ────────────────────────────────── +# Callout defaults +# ────────────────────────────────── +callouts: + info: + icon: info + primary-colour: "#007AFF" + background-colour: "#007AFF" + warning: + icon: warning + primary-colour: "#FF9500" + background-colour: "#FF9500" + success: + icon: success + primary-colour: "#34C759" + background-colour: "#34C759" + error: + icon: error + primary-colour: "#FF3B30" + background-colour: "#FF3B30" + +# ────────────────────────────────── +# Typography +# Inter portable default. Preferred on iOS: SF Pro Text (body), +# SF Pro Display (headings ≥20pt), SF Pro Rounded for friendly UI. +# ────────────────────────────────── +font-body: "bunny:Inter:400" +font-heading: "bunny:Inter:600" +font-size: 1.00 +line-height: 1.5 + +# ────────────────────────────────── +# Layout +# ────────────────────────────────── +main-width: 78em +nav-width: 20em diff --git a/themes/Operating Systems/os-material-you.yaml b/themes/Operating Systems/os-material-you.yaml new file mode 100644 index 0000000..048885e --- /dev/null +++ b/themes/Operating Systems/os-material-you.yaml @@ -0,0 +1,80 @@ +# mdcms v0.4 | DO NOT REMOVE THIS COMMENT +# mdcms theme — os-material-you +# Android / Material 3 feel. Tonal-palette neutrals built on the M3 +# baseline purple (#6750A4). Pale lavender paper, soft surface chrome. +# +# Colours from the Material 3 baseline scheme: +# primary light #6750A4 / dark #D0BCFF +# surface light #FEF7FF / dark #141218 +# surface-container-low light #F7F2FA / dark #1D1B20 +# on-surface light #1D1B20 / dark #E6E0E9 +# on-surface-variant light #49454F / dark #CAC4D0 + +# ────────────────────────────────── +# Colours +# ────────────────────────────────── +light: + accent: "#6750A4" + background: "#FEF7FF" + nav-background: "#F7F2FA" + text: "#1D1B20" + text-muted: "#49454F" + +dark: + accent: "#D0BCFF" + background: "#141218" + nav-background: "#1D1B20" + text: "#E6E0E9" + text-muted: "#CAC4D0" + +# ────────────────────────────────── +# Semantic colours — M3 baseline error + standard tertiary/green/yellow +# ────────────────────────────────── +colours-semantic: + info: "#6750A4" + warning: "#9A6700" + success: "#386A20" + error: "#B3261E" + +colours-semantic-dark: + info: "#D0BCFF" + warning: "#EFBE6E" + success: "#A6D388" + error: "#F2B8B5" + +# ────────────────────────────────── +# Callout defaults +# ────────────────────────────────── +callouts: + info: + icon: info + primary-colour: "#6750A4" + background-colour: "#6750A4" + warning: + icon: warning + primary-colour: "#9A6700" + background-colour: "#9A6700" + success: + icon: success + primary-colour: "#386A20" + background-colour: "#386A20" + error: + icon: error + primary-colour: "#B3261E" + background-colour: "#B3261E" + +# ────────────────────────────────── +# Typography +# Roboto is the portable default and the Material default. +# Preferred: Roboto Flex (variable) or Google Sans for headings. +# ────────────────────────────────── +font-body: "bunny:Roboto:400" +font-heading: "bunny:Roboto:500" +font-size: 1.00 +line-height: 1.55 + +# ────────────────────────────────── +# Layout +# ────────────────────────────────── +main-width: 78em +nav-width: 20em diff --git a/themes/Operating Systems/os-nextstep.yaml b/themes/Operating Systems/os-nextstep.yaml new file mode 100644 index 0000000..e458aef --- /dev/null +++ b/themes/Operating Systems/os-nextstep.yaml @@ -0,0 +1,84 @@ +# mdcms v0.4 | DO NOT REMOVE THIS COMMENT +# mdcms theme — os-nextstep +# NeXTSTEP / OPENSTEP revival. Cool 50% greys everywhere, jet-black +# title chrome, and the famous NeXT magenta as the accent. Heavy, +# considered, very 1989 Cube energy. +# +# Colours approximated from NeXTSTEP's 2-bit greyscale + colour passes: +# #555555 dark window chrome (title bars, scrollbar wells) +# #AAAAAA panel face (50% grey) +# #DDDDDD highlight +# #000000 ink +# #C72A86 NeXT magenta (used in logo + accents) + +# ────────────────────────────────── +# Colours +# ────────────────────────────────── +light: + accent: "#C72A86" + background: "#DDDDDD" + nav-background: "#555555" + text: "#000000" + text-muted: "#5A5A5A" + +dark: + accent: "#E579B5" + background: "#1A1A1A" + nav-background: "#000000" + text: "#DDDDDD" + text-muted: "#A0A0A0" + +# ────────────────────────────────── +# Semantic colours — kept restrained, in keeping with the grey-on-grey +# NeXTSTEP discipline. +# ────────────────────────────────── +colours-semantic: + info: "#3A6FA5" + warning: "#A06A00" + success: "#3E7A3E" + error: "#A02828" + +colours-semantic-dark: + info: "#88AED9" + warning: "#D9B36B" + success: "#86C58B" + error: "#E07A7A" + +# ────────────────────────────────── +# Callout defaults +# ────────────────────────────────── +callouts: + info: + icon: info + primary-colour: "#3A6FA5" + background-colour: "#3A6FA5" + warning: + icon: warning + primary-colour: "#A06A00" + background-colour: "#A06A00" + success: + icon: success + primary-colour: "#3E7A3E" + background-colour: "#3E7A3E" + error: + icon: error + primary-colour: "#A02828" + background-colour: "#A02828" + +# ────────────────────────────────── +# Typography +# Inter portable default. NeXTSTEP used Helvetica system-wide. +# Preferred: "Helvetica Neue" (Apple system) — falls back to Inter. +# For the more brutalist OPENSTEP feel try "bunny:Helvetica:400" +# if you have it installed locally. +# ────────────────────────────────── +font-body: "bunny:Inter:400" +font-heading: "bunny:Inter:700" +font-size: 1.00 +line-height: 1.55 + +# ────────────────────────────────── +# Layout +# ────────────────────────────────── +main-width: 76em +nav-width: 20em diff --git a/themes/Operating Systems/os-pop.yaml b/themes/Operating Systems/os-pop.yaml new file mode 100644 index 0000000..7fd5b33 --- /dev/null +++ b/themes/Operating Systems/os-pop.yaml @@ -0,0 +1,79 @@ +# mdcms v0.4 | DO NOT REMOVE THIS COMMENT +# mdcms theme — os-pop +# Pop!_OS feel. Warm Cosmic-grey chrome, signature Pop orange accent. +# System76's GTK-rooted desktop with its own distinctive warmth. +# +# Colours from System76's public Pop palette: +# Pop Orange #FAA41A (primary accent) +# Cosmic Light bg #F2F2F2 / surface #FAFAFA +# Cosmic Dark bg #2D2D2D / surface #232323 +# Text light #181818 / dark #F2F2F2 + +# ────────────────────────────────── +# Colours +# ────────────────────────────────── +light: + accent: "#FAA41A" + background: "#FAFAFA" + nav-background: "#F2F2F2" + text: "#181818" + text-muted: "#5C5C5C" + +dark: + accent: "#FAA41A" + background: "#232323" + nav-background: "#2D2D2D" + text: "#F2F2F2" + text-muted: "#A8A8A8" + +# ────────────────────────────────── +# Semantic colours — Pop palette greens/yellows/reds with the warm cast +# ────────────────────────────────── +colours-semantic: + info: "#1B6091" + warning: "#FAA41A" + success: "#73C48F" + error: "#F15D22" + +colours-semantic-dark: + info: "#88B8DC" + warning: "#FFC664" + success: "#9BD7AF" + error: "#FF8A5C" + +# ────────────────────────────────── +# Callout defaults +# ────────────────────────────────── +callouts: + info: + icon: info + primary-colour: "#1B6091" + background-colour: "#1B6091" + warning: + icon: warning + primary-colour: "#FAA41A" + background-colour: "#FAA41A" + success: + icon: success + primary-colour: "#73C48F" + background-colour: "#73C48F" + error: + icon: error + primary-colour: "#F15D22" + background-colour: "#F15D22" + +# ────────────────────────────────── +# Typography +# Fira Sans is the portable default and Pop!_OS's actual UI font. +# Available on Google Fonts. Pop also ships Fira Mono for code. +# ────────────────────────────────── +font-body: "bunny:Fira Sans:400" +font-heading: "bunny:Fira Sans:600" +font-size: 1.00 +line-height: 1.6 + +# ────────────────────────────────── +# Layout +# ────────────────────────────────── +main-width: 78em +nav-width: 20em diff --git a/themes/Operating Systems/os-system-7.yaml b/themes/Operating Systems/os-system-7.yaml new file mode 100644 index 0000000..dbd3729 --- /dev/null +++ b/themes/Operating Systems/os-system-7.yaml @@ -0,0 +1,84 @@ +# mdcms v0.4 | DO NOT REMOVE THIS COMMENT +# mdcms theme — os-system-7 +# Classic Macintosh System 7 / early Mac OS. Black on white with the +# Platinum-grey window chrome that arrived around System 7.5. Very flat, +# very calm, very monochrome. +# +# Colours: +# #FFFFFF paper (white) +# #DDDDDD Platinum window chrome +# #000000 ink (1-bit Mac heritage) +# #B0B0B0 shadow grey + +# ────────────────────────────────── +# Colours +# ────────────────────────────────── +light: + accent: "#000000" + background: "#FFFFFF" + nav-background: "#DDDDDD" + text: "#000000" + text-muted: "#555555" + +dark: + accent: "#FFFFFF" + background: "#1A1A1A" + nav-background: "#262626" + text: "#FFFFFF" + text-muted: "#A0A0A0" + +# ────────────────────────────────── +# Semantic colours — restrained, since System 7 was a 1-bit interface +# until colour Macs. Kept muted and "drawn-in-MacPaint". +# ────────────────────────────────── +colours-semantic: + info: "#000000" + warning: "#7A5A00" + success: "#1F5A1F" + error: "#8B0000" + +colours-semantic-dark: + info: "#FFFFFF" + warning: "#E5C36B" + success: "#7FB87F" + error: "#E08585" + +# ────────────────────────────────── +# Callout defaults +# ────────────────────────────────── +callouts: + info: + icon: info + primary-colour: "#000000" + background-colour: "#000000" + warning: + icon: warning + primary-colour: "#7A5A00" + background-colour: "#7A5A00" + success: + icon: success + primary-colour: "#1F5A1F" + background-colour: "#1F5A1F" + error: + icon: error + primary-colour: "#8B0000" + background-colour: "#8B0000" + +# ────────────────────────────────── +# Typography +# Inter portable default. Preferred for true System 7 fidelity: +# "ChicagoFLF" (free Chicago revival, body) for headings +# "Geneva" (system) for body — or any free Geneva-alike like +# "Charcoal CY" or "ArkPixel". +# For an authentic 1-bit look, try a pixel font like "VT323". +# ────────────────────────────────── +font-body: "bunny:Inter:400" +font-heading: "bunny:Inter:700" +font-size: 1.00 +line-height: 1.6 + +# ────────────────────────────────── +# Layout +# ────────────────────────────────── +main-width: 76em +nav-width: 20em