diff --git a/CLAUDE.md b/CLAUDE.md index 5bac109..e744044 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -221,6 +221,28 @@ Fenced `mdcms` block with `toc`. Renders a section-grouped list of all visible, ### Theme system (`theme.yml`) Presentational config separate from `config.yml`. Controls accent colour, dark/light mode palette, fonts, and layout. `index.html` loads it at runtime. +**Colour keys per mode** (`light:` and `dark:` blocks): + +| Key | CSS variable | Default | +|---|---|---| +| `accent` | `--accent` | `#2563EB` / `#60A5FA` | +| `background` | `--bg-main` | `#FFFFFF` / `#0F172A` | +| `nav-background` | `--bg-nav` | `#F8FAFC` / `#1E293B` | +| `text` | `--font-colour` | `#1E293B` / `#F1F5F9` | +| `text-muted` | `--font-colour-muted` | `#64748B` / `#94A3B8` | +| `nav-link` | `--nav-link-colour` | falls back to `text` | +| `nav-link-active` | `--nav-link-active-colour` | falls back to `accent` | +| `nav-section-heading` | `--nav-section-heading-colour` | falls back to `text-muted` | + +**When to use nav-link keys:** When `nav-background` matches or is very close to `accent`, the default behaviour (active link coloured with `accent`) makes links invisible. Set `nav-link`, `nav-link-active`, and `nav-section-heading` explicitly so all three are legible against `nav-background`. Example: a red nav background needs white (`#FFFFFF`) for all three nav colour keys. + +**Semantic colours:** + +- `colours-semantic` — applies to both light and dark modes. Use for colours that read on both backgrounds, or when you don't need per-mode control. +- `colours-semantic-dark` — overrides semantic colours in dark mode only. Use lighter/more saturated variants here so callout borders and tinted backgrounds remain legible on dark page backgrounds. + +Keys in both blocks: `info`, `warning`, `success`, `error`. + ### Icon system All UI icons served as local SVGs from `app/assets/icons/`. No Google Fonts or external icon font. Icon names are normalised (lowercase, spaces → hyphens). diff --git a/app/index.html b/app/index.html index 93b1673..2bcb270 100644 --- a/app/index.html +++ b/app/index.html @@ -56,6 +56,9 @@ if ('serviceWorker' in navigator) { --bg-main: #FFFFFF; --bg-nav: #F8FAFC; --nav-font-colour: var(--font-colour); + --nav-link-colour: var(--nav-font-colour); + --nav-link-active-colour: var(--accent); + --nav-section-heading-colour: var(--font-colour-muted); --nav-active-bg: rgba(var(--accent-rgb), 0.10); --nav-hover-bg: rgba(var(--accent-rgb), 0.05); --font-colour: #1E293B; @@ -87,6 +90,9 @@ if ('serviceWorker' in navigator) { --bg-main: #0F172A; --bg-nav: #1E293B; --nav-font-colour: #E2E8F0; + --nav-link-colour: var(--nav-font-colour); + --nav-link-active-colour: var(--accent); + --nav-section-heading-colour: var(--font-colour-muted); --nav-active-bg: rgba(96, 165, 250, 0.15); --nav-hover-bg: rgba(96, 165, 250, 0.08); --font-colour: #F1F5F9; @@ -275,7 +281,7 @@ body { font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; - color: var(--font-colour-muted); + color: var(--nav-section-heading-colour, var(--font-colour-muted)); padding: 1rem 1.25rem 0.35rem; user-select: none; border-left: 3px solid transparent; @@ -308,7 +314,7 @@ body { display: block; padding: 0.45rem 1.25rem; font-size: 0.875rem; - color: var(--nav-font-colour, var(--font-colour)); + color: var(--nav-link-colour, var(--nav-font-colour, var(--font-colour))); text-decoration: none; transition: background 0.1s, color 0.1s; border-left: 3px solid transparent; @@ -317,8 +323,8 @@ body { .nav-item:hover { background: var(--nav-hover-bg); } .nav-item.active { background: var(--nav-active-bg); - border-left-color: var(--accent); - color: var(--accent); + border-left-color: var(--nav-link-active-colour, var(--accent)); + color: var(--nav-link-active-colour, var(--accent)); font-weight: 600; } @@ -1250,18 +1256,34 @@ body { 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 (m['nav-link']) vars.push(`--nav-link-colour: ${m['nav-link']}`); + if (m['nav-link-active']) vars.push(`--nav-link-active-colour: ${m['nav-link-active']}`); + if (m['nav-section-heading']) vars.push(`--nav-section-heading-colour: ${m['nav-section-heading']}`); 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['colours-semantic'] || tc['colours-semantic-dark']) { + let semCss = ''; + 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) semCss += `:root { ${semVars.join('; ')}; }\n`; + } + if (tc['colours-semantic-dark']) { + const semD = tc['colours-semantic-dark']; + const semDVars = []; + if (semD.info) semDVars.push(`--colour-info: ${semD.info}`); + if (semD.warning) semDVars.push(`--colour-warning: ${semD.warning}`); + if (semD.success) semDVars.push(`--colour-success: ${semD.success}`); + if (semD.error) semDVars.push(`--colour-error: ${semD.error}`); + if (semDVars.length) semCss += `:root[data-theme="dark"] { ${semDVars.join('; ')}; }\n`; + } + if (semCss) getOrCreateStyle('theme-semantic').textContent = semCss; } if (tc['main-width']) root.style.setProperty('--main-width', tc['main-width']); diff --git a/app/theme.yml b/app/theme.yml index 963f8fc..82bd2e6 100644 --- a/app/theme.yml +++ b/app/theme.yml @@ -12,6 +12,9 @@ light: nav-background: "#F8FAFC" text: "#1E293B" text-muted: "#64748B" + # nav-link: "#1E293B" # inactive nav link text (defaults to text) + # nav-link-active: "#2563EB" # active nav link text (defaults to accent) + # nav-section-heading: "#64748B" # nav section label text (defaults to text-muted) dark: accent: "#60A5FA" @@ -19,11 +22,14 @@ dark: nav-background: "#1E293B" text: "#F1F5F9" text-muted: "#94A3B8" + # nav-link: "#E2E8F0" # inactive nav link text (defaults to text) + # nav-link-active: "#60A5FA" # active nav link text (defaults to accent) + # nav-section-heading: "#94A3B8" # nav section label text (defaults to text-muted) # ────────────────────────────────── # Semantic colours # Used by callout tags (info, warning, success, error). -# Choose values that work on both light and dark backgrounds. +# colours-semantic applies to both modes; colours-semantic-dark overrides for dark mode. # ────────────────────────────────── colours-semantic: info: "#2563EB" @@ -31,6 +37,12 @@ colours-semantic: success: "#16A34A" error: "#DC2626" +colours-semantic-dark: + info: "#60A5FA" + warning: "#F59E0B" + success: "#34D399" + error: "#F87171" + # ────────────────────────────────── # Callout defaults # ────────────────────────────────── diff --git a/docs/claude-design.md b/docs/claude-design.md new file mode 100644 index 0000000..8426313 --- /dev/null +++ b/docs/claude-design.md @@ -0,0 +1,184 @@ +# mdcms theme authoring guide for Claude Design + +This document explains the `theme.yml` format so that Claude Design can produce +complete, correct theme files that render well in all nav configurations and in +both light and dark mode. + +--- + +## Full theme.yml structure + +```yaml +# mdcms v0.4 | DO NOT REMOVE THIS COMMENT +# mdcms theme — + +# ────────────────────────────────── +# Colours +# ────────────────────────────────── +light: + accent: "#2563EB" # brand colour; used for links, active nav border, accents + background: "#FFFFFF" # main content area background + nav-background: "#F8FAFC" # sidebar/nav panel background + text: "#1E293B" # body text + text-muted: "#64748B" # secondary text, captions + nav-link: "#1E293B" # inactive nav link text + nav-link-active: "#2563EB" # active (current page) nav link text + nav-section-heading: "#64748B" # nav section label text (uppercase, small) + +dark: + accent: "#60A5FA" + background: "#0F172A" + nav-background: "#1E293B" + text: "#F1F5F9" + text-muted: "#94A3B8" + nav-link: "#E2E8F0" + nav-link-active: "#60A5FA" + nav-section-heading: "#94A3B8" + +# ────────────────────────────────── +# Semantic colours +# colours-semantic applies to both modes. +# colours-semantic-dark overrides for dark mode only. +# ────────────────────────────────── +colours-semantic: + info: "#2563EB" + warning: "#D97706" + success: "#16A34A" + error: "#DC2626" + +colours-semantic-dark: + info: "#60A5FA" + warning: "#F59E0B" + success: "#34D399" + error: "#F87171" + +# ────────────────────────────────── +# Callout defaults +# primary-colour → left border and icon +# background-colour → tinted background (rendered at ~8% opacity) +# ────────────────────────────────── +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:IBM Plex Sans:400" +font-heading: "bunny:IBM Plex 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 +``` + +--- + +## Critical rule: nav contrast + +The renderer defaults `nav-link-active` to `accent` and `nav-link` to `text`. +When `nav-background` and `accent` share the same hue (or are very close), +active nav links become invisible — the coloured text disappears into a +coloured background. + +**Always set all three nav colour keys explicitly** whenever `nav-background` +is anything other than a neutral near-white (light) or near-black (dark). + +### Pattern: accent-coloured nav (e.g. brand red, navy, forest green) + +```yaml +light: + accent: "#D00C33" + nav-background: "#D00C33" # same as accent — nav links MUST be overridden + nav-link: "#FFFFFF" + nav-link-active: "#FFFFFF" + nav-section-heading: "rgba(255,255,255,0.65)" + +dark: + accent: "#D00C33" + nav-background: "#000000" + nav-link: "#E2E2E2" + nav-link-active: "#FFFFFF" + nav-section-heading: "#888888" +``` + +### Pattern: dark nav in light mode (sidebar darker than content) + +```yaml +light: + nav-background: "#1E293B" + nav-link: "#CBD5E1" + nav-link-active: "#FFFFFF" + nav-section-heading: "#64748B" +``` + +### Pattern: transparent / very light nav (default behaviour) + +When `nav-background` is a light neutral, the defaults work fine. +You can omit `nav-link`, `nav-link-active`, and `nav-section-heading` +and the renderer will fall back to `text`, `accent`, and `text-muted`. + +--- + +## Semantic colours and dark mode + +`colours-semantic` values are applied globally (both modes). The callout +background is rendered at ~8% opacity, so a colour that looks fine on white +can wash out on a dark background — or conversely, a colour bright enough for +dark mode may be too vivid on white. + +The solution is `colours-semantic-dark`: it overrides semantic colours in dark +mode only. Typical approach: + +- **`colours-semantic`** — choose saturated but not neon values that work on white +- **`colours-semantic-dark`** — use lighter, more luminous variants of the same hues + +```yaml +colours-semantic: + info: "#1D4ED8" # deep blue — strong on white + warning: "#B45309" # amber — strong on white + success: "#15803D" # green — strong on white + error: "#B91C1C" # red — strong on white + +colours-semantic-dark: + info: "#93C5FD" # light blue — visible on dark background + warning: "#FCD34D" # light amber + success: "#6EE7B7" # light green + error: "#FCA5A5" # light red/pink +``` + +Match `callouts` `primary-colour` / `background-colour` values to +`colours-semantic` (light mode callout values), since the callout block +uses its own per-callout colour settings rather than the semantic variables. + +--- + +## Checklist before finalising a theme + +- [ ] `nav-link`, `nav-link-active`, `nav-section-heading` specified for both + `light` and `dark` whenever `nav-background` is non-neutral +- [ ] All three nav link colours contrast against `nav-background` (WCAG AA minimum) +- [ ] `colours-semantic-dark` provided with lighter variants of each colour +- [ ] `callouts` `primary-colour` matches `colours-semantic` values for consistency +- [ ] Dark mode `background` is not pure `#000000` unless intentional (use `#0A0A0A`+) +- [ ] `font-size` between `0.85` and `1.15`; `line-height` between `1.5` and `1.9` +- [ ] Version comment on line 1: `# mdcms v0.4 | DO NOT REMOVE THIS COMMENT`