mirror of
https://github.com/kbenestad/mdcms.git
synced 2026-06-18 07:24:31 +00:00
v0.4 Phase 1: theme.yml — separate presentational config from config.yml
- Add app/theme.yml: colours (light/dark), semantic colours, callout defaults, fonts (with bunny: provider support), font-size, line-height, main-width, nav-width - app/config.yml: add `theme: theme.yml` reference; remove font and layout keys that now live in theme.yml - index.html: add applyThemeYml() that reads the new theme.yml format and derives all CSS custom properties (accent-rgb, nav-active-bg, nav-hover-bg, table-header-bg, link-colour) from the 5 user keys per mode; add --colour-info/warning/success/error CSS vars with hardcoded defaults; add --line-height-body CSS var - index.html: update loadFonts() to accept themeConfig, support provider:name:weight format for Bunny Fonts and Google Fonts - index.html: boot() fetches theme file named by config.theme, routes to applyThemeYml() for new format or applyConfigTheme() for legacy; falls back gracefully to hardcoded defaults when theme key is absent https://claude.ai/code/session_015XtsgTMi8UtmgxEgb5Qt2c
This commit is contained in:
parent
503eb3d83d
commit
6492329b27
3 changed files with 167 additions and 33 deletions
|
|
@ -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)
|
||||
# ──────────────────────────────────
|
||||
|
|
|
|||
122
app/index.html
122
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
|
||||
|
|
|
|||
66
app/theme.yml
Normal file
66
app/theme.yml
Normal file
|
|
@ -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
|
||||
Loading…
Reference in a new issue