From a899edf21244b6cbae84293c300733adc8784cd2 Mon Sep 17 00:00:00 2001 From: Mao Nakamoto <41178744+maonakamoto@users.noreply.github.com> Date: Fri, 28 Aug 2026 12:02:45 +0200 Subject: [PATCH] fix(design): the homepage had a fifth colour system, and the guard missed it #148 shipped with the guard passing and the homepage still emitting bg-blue-100, text-indigo-600, bg-purple-100. Both were true, because the guard only walked app/ and components/. The palette was in data/professionals.ts -- its own getAccentColorClasses, giving each of the six first-party professionals a colour (blue, green, indigo, red, amber, purple). Same mistake as the bots in #144, in the one directory the scanner did not read. That is the whole lesson: a source scanner is blind in exactly the shapes you forget to look at, so its first clean run proves nothing. - the six professionals now use the brand, like the six bots - the guard walks data/ and lib/ too - data/menuItems.ts lost a blue/purple gradient Two files are now DOCUMENTED exceptions rather than debt, because their colour is categorical and has to stay mutually distinguishable: lib/constants.ts document status -- pending yellow, processing blue, ready green, error red. Recolouring "processing" to ochre would make state read as brand chrome. lib/infrastructure/providers.ts per-provider identity, so Ollama and OpenAI are tellable apart at a glance. Both sit alongside Callout.tsx's info/warning/error triad, each with its reason written next to it. verify: format, lint, typecheck, 256 tests, build -- all green. Homepage confirmed to emit no raw palette class at all. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01HVwg8DKHQktxJuHeLM3xpG --- data/menuItems.ts | 2 +- data/professionals.ts | 77 +++++------------------ tests/__tests__/lib/design-tokens.test.ts | 25 +++++++- 3 files changed, 40 insertions(+), 64 deletions(-) diff --git a/data/menuItems.ts b/data/menuItems.ts index a5f228ee..5ad18e5e 100644 --- a/data/menuItems.ts +++ b/data/menuItems.ts @@ -64,7 +64,7 @@ export const menuItems: MenuItem[] = [ header: { title: 'AI Professionals', subtitle: 'Expert advisors for every need', - gradient: 'bg-gradient-to-r from-blue-50 to-purple-50', + gradient: 'bg-action-tint', }, }, }, diff --git a/data/professionals.ts b/data/professionals.ts index 58155130..5d125be1 100644 --- a/data/professionals.ts +++ b/data/professionals.ts @@ -334,64 +334,21 @@ export const professionalCategories = { } as const; /** - * Get accent color class for Tailwind + * Accent classes for a first-party professional. + * + * There is one scheme, because there is one brand. These six ARE Botsmann, so + * they wear the identity defined in app/globals.css rather than a per-person + * colour -- a blue Lex beside an ochre CTA was the whole problem. + * + * The `color` argument is kept so callers and the data shape stay unchanged; + * it no longer selects a palette. */ -export const getAccentColorClasses = (color: ProfessionalAccentColor) => { - const colors = { - blue: { - bg: 'bg-blue-500', - bgLight: 'bg-blue-100', - bgGradient: 'from-blue-500 to-blue-600', - text: 'text-blue-600', - border: 'border-blue-500', - hover: 'hover:bg-blue-600', - groupHoverText: 'group-hover:text-blue-600', - }, - green: { - bg: 'bg-green-500', - bgLight: 'bg-green-100', - bgGradient: 'from-green-500 to-green-600', - text: 'text-green-600', - border: 'border-green-500', - hover: 'hover:bg-green-600', - groupHoverText: 'group-hover:text-green-600', - }, - indigo: { - bg: 'bg-indigo-500', - bgLight: 'bg-indigo-100', - bgGradient: 'from-indigo-500 to-indigo-600', - text: 'text-indigo-600', - border: 'border-indigo-500', - hover: 'hover:bg-indigo-600', - groupHoverText: 'group-hover:text-indigo-600', - }, - red: { - bg: 'bg-red-500', - bgLight: 'bg-red-100', - bgGradient: 'from-red-500 to-red-600', - text: 'text-red-600', - border: 'border-red-500', - hover: 'hover:bg-red-600', - groupHoverText: 'group-hover:text-red-600', - }, - amber: { - bg: 'bg-amber-500', - bgLight: 'bg-amber-100', - bgGradient: 'from-amber-500 to-amber-600', - text: 'text-amber-600', - border: 'border-amber-500', - hover: 'hover:bg-amber-600', - groupHoverText: 'group-hover:text-amber-600', - }, - purple: { - bg: 'bg-purple-500', - bgLight: 'bg-purple-100', - bgGradient: 'from-purple-500 to-purple-600', - text: 'text-purple-600', - border: 'border-purple-500', - hover: 'hover:bg-purple-600', - groupHoverText: 'group-hover:text-purple-600', - }, - }; - return colors[color]; -}; +export const getAccentColorClasses = (_color?: ProfessionalAccentColor) => ({ + bg: 'bg-action', + bgLight: 'bg-action-tint', + bgGradient: 'from-action to-action-hover', + text: 'text-action', + border: 'border-action', + hover: 'hover:bg-action-hover', + groupHoverText: 'group-hover:text-action', +}); diff --git a/tests/__tests__/lib/design-tokens.test.ts b/tests/__tests__/lib/design-tokens.test.ts index e0be795f..b2b539c7 100644 --- a/tests/__tests__/lib/design-tokens.test.ts +++ b/tests/__tests__/lib/design-tokens.test.ts @@ -37,8 +37,19 @@ const CUSTOM_BOT_SURFACES = [ join('lib', 'config', 'colors.ts'), ]; -/** Semantic status colours are a separate scale from the brand accent. */ -const SEMANTIC_FILES = [join('components', 'knowledge', 'Callout.tsx')]; +/** + * Categorical and semantic scales, which are NOT the brand accent. + * + * A status scale has to stay mutually distinguishable: recolouring + * "processing" to ochre would make it read as brand chrome rather than state. + * Same for provider identity, which exists so you can tell Ollama from OpenAI + * at a glance. Each entry is an exception with a reason, not a to-do. + */ +const SEMANTIC_FILES = [ + join('components', 'knowledge', 'Callout.tsx'), // info / warning / error + join('lib', 'constants.ts'), // document status: pending / processing / ready / error + join('lib', 'infrastructure', 'providers.ts'), // per-provider identity colours +]; function tsxFiles(dir: string): string[] { const out: string[] = []; @@ -59,7 +70,15 @@ function isAllowed(rel: string): boolean { } describe('design tokens', () => { - const files = [...tsxFiles(join(ROOT, 'app')), ...tsxFiles(join(ROOT, 'components'))]; + // data/ and lib/ too: the palette hid in data/professionals.ts for a whole + // sweep because the first version of this guard only looked at app/ and + // components/. A scanner is blind in exactly the shapes it forgets to read. + const files = [ + ...tsxFiles(join(ROOT, 'app')), + ...tsxFiles(join(ROOT, 'components')), + ...tsxFiles(join(ROOT, 'data')), + ...tsxFiles(join(ROOT, 'lib')), + ]; it('scans a meaningful number of files', () => { expect(files.length).toBeGreaterThan(100);