Merge claude/add-nav-link-colors-wz6IU: nav-link colour keys and per-mode semantic colours

Fixes theme designs (e.g. accent-coloured nav backgrounds) where active nav
links were invisible because nav-link-active defaulted to accent on an
accent-coloured background. Adds colours-semantic-dark for dark mode callout
legibility.

https://claude.ai/code/session_01NQKywehSj8Ku4yKhwB4VNB
This commit is contained in:
Claude 2026-05-18 10:38:05 +00:00
commit 92af867da2
No known key found for this signature in database
4 changed files with 253 additions and 13 deletions

View file

@ -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).

View file

@ -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,10 +1256,15 @@ 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'] || tc['colours-semantic-dark']) {
let semCss = '';
if (tc['colours-semantic']) {
const sem = tc['colours-semantic'];
const semVars = [];
@ -1261,7 +1272,18 @@ body {
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 (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']);

View file

@ -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
# ──────────────────────────────────

184
docs/claude-design.md Normal file
View file

@ -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 — <theme name>
# ──────────────────────────────────
# 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`