mirror of
https://github.com/kbenestad/mdcms.git
synced 2026-06-18 15:24:32 +00:00
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:
commit
92af867da2
4 changed files with 253 additions and 13 deletions
22
CLAUDE.md
22
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).
|
||||
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
|
|
|
|||
|
|
@ -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
184
docs/claude-design.md
Normal 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`
|
||||
Loading…
Reference in a new issue