diff --git a/app/config.yml b/app/config.yml index 86727da..3583105 100644 --- a/app/config.yml +++ b/app/config.yml @@ -22,6 +22,7 @@ # ────────────────────────────────── sitename: MD-CMS New Site navigation: topbar # sidebar | topbar +theme: theme.yml # presentational config — edit theme.yml to customise colours, fonts, and layout # homepage: pages/home.md # override the default landing page @@ -30,20 +31,13 @@ navigation: topbar # sidebar | topbar # favicon: favicon.png # footer: "© 2026 Your Name" -# ────────────────────────────────── -# Typography (optional) -# ────────────────────────────────── -# font-title: "Inter:700" -# font-body: Inter -# font-code: JetBrains Mono - # ────────────────────────────────── # Layout (optional) # ────────────────────────────────── -# main-width: 80em -# nav-width: 20em # nav-position: left # left | right (sidebar mode) +# Typography and colours are configured in theme.yml, not here. + # ────────────────────────────────── # Features (optional) # ────────────────────────────────── diff --git a/app/index.html b/app/index.html index a80ba18..0f13219 100644 --- a/app/index.html +++ b/app/index.html @@ -117,6 +117,11 @@ --font-body-weight: 400; --main-width: 80em; --nav-width: 20em; + --line-height-body: 1.7; + --colour-info: #2563EB; + --colour-warning: #D97706; + --colour-success: #16A34A; + --colour-error: #DC2626; } html { font-size: 16px; scroll-behavior: smooth; } @@ -126,7 +131,7 @@ body { font-weight: var(--font-body-weight); color: var(--font-colour); background: var(--bg-main); - line-height: 1.7; + line-height: var(--line-height-body); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; transition: background-color 0.2s ease, color 0.2s ease; @@ -1112,6 +1117,53 @@ body { applyTheme(current === 'dark' ? 'light' : 'dark'); } + function applyThemeYml(tc) { + if (!tc) return; + const root = document.documentElement; + const getOrCreateStyle = id => { + let s = document.getElementById(id); + if (!s) { s = document.createElement('style'); s.id = id; document.head.appendChild(s); } + return s; + }; + + let modeCss = ''; + ['light', 'dark'].forEach(mode => { + const m = tc[mode]; + if (!m) return; + const vars = []; + if (m.accent) { + const rgb = hexToRgb(m.accent); + vars.push(`--accent: ${m.accent}`); + vars.push(`--accent-rgb: ${rgb}`); + vars.push(`--nav-active-bg: rgba(${rgb}, 0.10)`); + vars.push(`--nav-hover-bg: rgba(${rgb}, 0.05)`); + vars.push(`--table-header-bg: rgba(${rgb}, 0.08)`); + vars.push(`--link-colour: ${m.accent}`); + } + if (m.background) { vars.push(`--bg-main: ${m.background}`); vars.push(`--search-bg: ${m.background}`); } + if (m['nav-background']) vars.push(`--bg-nav: ${m['nav-background']}`); + if (m.text) { vars.push(`--font-colour: ${m.text}`); vars.push(`--code-font: ${m.text}`); } + if (m['text-muted']) vars.push(`--font-colour-muted: ${m['text-muted']}`); + if (vars.length) modeCss += `:root[data-theme="${mode}"] { ${vars.join('; ')}; }\n`; + }); + if (modeCss) getOrCreateStyle('theme-overrides').textContent = modeCss; + + if (tc['colours-semantic']) { + const sem = tc['colours-semantic']; + const semVars = []; + if (sem.info) semVars.push(`--colour-info: ${sem.info}`); + if (sem.warning) semVars.push(`--colour-warning: ${sem.warning}`); + if (sem.success) semVars.push(`--colour-success: ${sem.success}`); + if (sem.error) semVars.push(`--colour-error: ${sem.error}`); + if (semVars.length) getOrCreateStyle('theme-semantic').textContent = `:root { ${semVars.join('; ')}; }`; + } + + if (tc['main-width']) root.style.setProperty('--main-width', tc['main-width']); + if (tc['nav-width']) root.style.setProperty('--nav-width', tc['nav-width']); + if (tc['line-height']) root.style.setProperty('--line-height-body', String(tc['line-height'])); + if (tc['font-size']) document.documentElement.style.fontSize = `${tc['font-size'] * 16}px`; + } + function applyConfigTheme() { const root = document.documentElement; ['light', 'dark'].forEach(mode => { @@ -1171,35 +1223,48 @@ body { } // ─── Fonts ──────────────────────────────────────────────── - function loadFonts() { - const fonts = {}; - ['font-title', 'font-body', 'font-code'].forEach(key => { - const val = config[key]; - if (!val) return; - const [name, weight] = val.split(':'); - fonts[key] = { name: name.trim(), weight: weight ? weight.trim() : '400' }; - }); - const googleFonts = []; - Object.entries(fonts).forEach(([, { name, weight }]) => { - googleFonts.push(`${name.replace(/ /g, '+')}:wght@${weight}`); - }); + function loadFonts(tc) { + function parseFont(spec) { + if (!spec) return null; + const parts = spec.split(':'); + if (parts.length >= 3) return { provider: parts[0].trim(), name: parts.slice(1, -1).join(':').trim(), weight: parts[parts.length - 1].trim() }; + if (parts.length === 2) return { provider: 'google', name: parts[0].trim(), weight: parts[1].trim() }; + return { provider: 'google', name: parts[0].trim(), weight: '400' }; + } + + const src = tc || {}; + const bodyFont = parseFont(src['font-body'] || config['font-body']); + const headingFont = parseFont(src['font-heading'] || src['font-title'] || config['font-title']); + const codeFont = parseFont(src['font-code'] || config['font-code']); + const allFonts = [bodyFont, headingFont, codeFont].filter(Boolean); + + const bunnyFonts = allFonts.filter(f => f.provider === 'bunny'); + const googleFonts = allFonts.filter(f => f.provider === 'google'); + + if (bunnyFonts.length) { + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = `https://fonts.bunny.net/css?family=${bunnyFonts.map(f => `${f.name.replace(/ /g, '+')}:${f.weight}`).join('&family=')}`; + document.head.appendChild(link); + } if (googleFonts.length) { const link = document.createElement('link'); link.rel = 'stylesheet'; - link.href = `https://fonts.googleapis.com/css2?${googleFonts.map(f => `family=${f}`).join('&')}&display=swap`; + link.href = `https://fonts.googleapis.com/css2?${googleFonts.map(f => `family=${f.name.replace(/ /g, '+')}:wght@${f.weight}`).join('&')}&display=swap`; document.head.appendChild(link); } + const root = document.documentElement; - if (fonts['font-title']) { - root.style.setProperty('--font-title', `"${fonts['font-title'].name}", system-ui, sans-serif`); - root.style.setProperty('--font-title-weight', fonts['font-title'].weight); + if (headingFont) { + root.style.setProperty('--font-title', `"${headingFont.name}", system-ui, sans-serif`); + root.style.setProperty('--font-title-weight', headingFont.weight); } - if (fonts['font-body']) { - root.style.setProperty('--font-body', `"${fonts['font-body'].name}", system-ui, sans-serif`); - root.style.setProperty('--font-body-weight', fonts['font-body'].weight); + if (bodyFont) { + root.style.setProperty('--font-body', `"${bodyFont.name}", system-ui, sans-serif`); + root.style.setProperty('--font-body-weight', bodyFont.weight); } - if (fonts['font-code']) { - root.style.setProperty('--font-code', `"${fonts['font-code'].name}", monospace`); + if (codeFont) { + root.style.setProperty('--font-code', `"${codeFont.name}", monospace`); } } @@ -2256,14 +2321,23 @@ function fmtDatetime(dtStr) { if (link) link.href = `assets/images/${config.logo}`; } - loadFonts(); + let themeConfig = {}; + if (config.theme) { + try { + const themeResp = await fetch(config.theme); + if (themeResp.ok) themeConfig = jsyaml.load(await themeResp.text()) || {}; + } catch (e) { /* fall back to hardcoded CSS defaults */ } + } + + loadFonts(themeConfig); initCategories(); const navMode = config.navigation || 'sidebar'; if (navMode === 'topbar') buildTopbar(); else buildSidebar(); - applyConfigTheme(); + if (config.theme) applyThemeYml(themeConfig); + else applyConfigTheme(); applyTheme(getInitialTheme()); // nav.yml — phase 2 expects `sections:` + `pages:` blocks; phase 1 flat diff --git a/app/theme.yml b/app/theme.yml new file mode 100644 index 0000000..d6fead7 --- /dev/null +++ b/app/theme.yml @@ -0,0 +1,66 @@ +# mdcms theme — default +# Edit colours, fonts, and layout here. See docs for full reference. + +# ────────────────────────────────── +# Colours +# ────────────────────────────────── +light: + accent: "#2563EB" + background: "#FFFFFF" + nav-background: "#F8FAFC" + text: "#1E293B" + text-muted: "#64748B" + +dark: + accent: "#60A5FA" + background: "#0F172A" + nav-background: "#1E293B" + text: "#F1F5F9" + text-muted: "#94A3B8" + +# ────────────────────────────────── +# Semantic colours +# Used by callout tags (info, warning, success, error). +# Choose values that work on both light and dark backgrounds. +# ────────────────────────────────── +colours-semantic: + info: "#2563EB" + warning: "#D97706" + success: "#16A34A" + error: "#DC2626" + +# ────────────────────────────────── +# Callout defaults +# ────────────────────────────────── +callouts: + info: + icon: info + primary-colour: "#2563EB" + background-colour: "#2563EB" + warning: + icon: warning + primary-colour: "#D97706" + background-colour: "#D97706" + success: + icon: success + primary-colour: "#16A34A" + background-colour: "#16A34A" + error: + icon: error + primary-colour: "#DC2626" + background-colour: "#DC2626" + +# ────────────────────────────────── +# Typography +# Format: "provider:Font Name:weight" (provider: bunny | google) +# ────────────────────────────────── +font-body: "bunny:Noto Sans:400" +font-heading: "bunny:Noto Sans:700" +font-size: 1.0 # unitless multiplier (1.0 = 16px base) +line-height: 1.7 # unitless multiplier + +# ────────────────────────────────── +# Layout +# ────────────────────────────────── +main-width: 80em +nav-width: 20em