Skip to content

Commit 8bb844b

Browse files
fix(landing): derive readable text from author-chosen block colours (#569) (#576)
* fix(landing): derive readable text from author-chosen block colours (#569) A landing-page block that sets its own colour paired it with a hardcoded foreground, in both directions: the preview widget painted the accent and assumed white on top, and used the accent as text on a card whose surface it never checked — so the published fixture's Stats block (#0f172a) rendered dark-on-dark and its numbers vanished. The same gap is live in the app, and worse. `accentVars()` emitted `--block-accent` with no paired foreground, so every block painting that surface hardcoded `text-primary-foreground`/`text-white`; `Section` went further and dropped `themeMap[theme]` — the class carrying the text colour — whenever an author set a background, leaving a dark section rendering the page's dark inherited `--foreground` on the public `/` and `/p/[slug]` routes. Tenant theming overrode `--primary` but never recomputed `--primary-foreground`, so a pale brand colour was white-on-pale everywhere. One helper now derives one colour from the other: `readableOn()` picks the ink by comparing real WCAG contrast ratios against both candidates (a luminance cut sends green-600 to white at 3.3:1 when near-black scores 5.8:1), escalating to pure black/white for mid-tones where neither branded ink clears AA; `accentTextOn()` handles the inverse case, keeping the author's colour when it already passes and otherwise mixing it toward white/black until it does. - `accentVars()` emits `--block-accent-foreground`; the six blocks that paint an accent surface consume it instead of a hardcoded near-white - `Section` rebinds the `--foreground` token, not just `color` — the Heading and Text blocks carry an explicit `text-foreground` that beats inheritance - tenant CSS vars derive `--primary-foreground` from the tenant's brand colour - the widget's stats value computes both inks and lets the same `dark:` variant that paints the card choose, since `resources/styles.css` is not part of the widget build and `dark:` resolves to `prefers-color-scheme`, not the `.dark` class the widget sets The app copy under `lib/color/` and the widget copy under `mcp-server/` are deliberate mirrors: mcp-server is not an npm workspace of the root package and cannot import from `lib/`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ax5eoJNqvCPk8zqsmv9V3x * docs(qa): before/after contrast evidence for #569 Screenshots referenced from the pull request: the landing-page-preview widget in light and dark for both demo fixtures, and a real Code Academy landing page carrying a dark section, a pale section, a pale-accent hero and CTA. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ax5eoJNqvCPk8zqsmv9V3x --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent b1134e7 commit 8bb844b

24 files changed

Lines changed: 945 additions & 29 deletions

components/tenant/tenant-css-vars-server.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { readableOn } from '@/lib/color/contrast'
12
import { getPresetById, FONT_OPTIONS, type StoredPreset, type CSSVariableMap } from '@/lib/themes/presets'
23

34
/**
@@ -63,6 +64,17 @@ export function TenantCssVarsServer({ themePreset, primaryColor, secondaryColor
6364
brandOverrides.push(`--primary: ${primaryColor};`)
6465
brandOverrides.push(`--sidebar-primary: ${primaryColor};`)
6566
brandOverrides.push(`--ring: ${primaryColor};`)
67+
68+
// `--primary-foreground` is the text drawn on top of `--primary`, and the
69+
// stock value is a near-white. Overriding the brand colour without it left
70+
// every primary surface — buttons, the Puck hero/CTA/banner blocks, the
71+
// sidebar — white-on-pale for any school with a light brand colour (#569).
72+
// Derived only when the colour parses; an exotic value keeps the default.
73+
const primaryInk = readableOn(primaryColor, '')
74+
if (primaryInk) {
75+
brandOverrides.push(`--primary-foreground: ${primaryInk};`)
76+
brandOverrides.push(`--sidebar-primary-foreground: ${primaryInk};`)
77+
}
6678
}
6779
if (secondaryColor) {
6880
brandOverrides.push(`--secondary-brand: ${secondaryColor};`)
52.4 KB
Loading
52.2 KB
Loading
109 KB
Loading
108 KB
Loading

docs/qa/569/app-after.png

52.9 KB
Loading

docs/qa/569/app-before.png

51.1 KB
Loading
52.4 KB
Loading
52.2 KB
Loading
108 KB
Loading

0 commit comments

Comments
 (0)