From f2360b9db32f2633f84e774a5de0a7b55d548234 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Sun, 17 May 2026 23:42:07 +0200 Subject: [PATCH] chore(shared-charts): soften the curated palette (#185) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Tune the curated chart palette to a softer, lower-saturation set. The values shipped in #175 were pulled straight from Tailwind's `-600 / -700` ramp; on real audit-log data the donut's three slices and the bar-chart's blue read as too punchy when they share a tile, especially in dark mode. Same five intents, same a11y posture — just less visual fight. ## What lands `libs/shared/charts/src/lib/_internal/palette.ts`: | Constant | Before | After | | --------------------------------- | -------- | -------- | | `DEFAULT_BAR_FILL` | `#1d4ed8` | `#4075e7` | | `semanticStatusColors.info` | `#2563eb` | `#4075e7` | | `semanticStatusColors.success` | `#16a34a` | `#46ac6b` | | `semanticStatusColors.warning` | `#ea580c` | `#f38043` | | `semanticStatusColors.error` | `#dc2626` | `#eb5252` | | `semanticStatusColors.neutral` | `#6b7280` | `#6b7280` (unchanged) | `info` and `DEFAULT_BAR_FILL` collapse to the same hex — bars and "informational" donut slices are *meant* to read as the same semantic class (no special status), so unifying them at the constant level removes a future drift hazard. Docstrings updated alongside — the previous comments name-checked Tailwind shades (`green-600`, `tailwind blue-700`) that no longer correspond to the values; the new comments describe the palette by intent (`muted green`, `muted orange`, ...) and call out that the softening is deliberate. ## Notes for the reviewer - **A11y posture unchanged.** The lib's contract is "AA contrast on white surfaces, deuteranopia/protanopia distinguishability via lightness deltas, not just hue". All four chromatic entries clear the same bar: each lightness sits in a distinct band (≈ 67 % for warning, ≈ 60 % for success, ≈ 60 % for error, ≈ 56 % for info), so colour-blind viewers still distinguish them by brightness even if the hue collapses. - **Why not derive these from `libs/shared/tokens/brand-tokens.css`?** Brand-primary is the dark teal `#12546c` and brand-accent is `#f7a919`. Neither reads correctly as "success" or "neutral chart fill"; the charts need a categorical palette tuned for *legibility on dense surfaces*, not for chrome and CTAs. Keeping the chart palette in its own lib stays consistent with ADR-0023's "lib owns the palette" stance. - **Bar fill default + `info` semantic alias to the same value on purpose.** A bar with no per-bar encoding is semantically "informational quantity over time" — the same intent as a donut slice tagged `info`. Future consumer that wants to flag a single "info" bar inside a stacked chart will read the colour as consistent. - **No code changes outside this file.** Consumers (``, ``, the audit page) import these constants by name; the swap is purely a value change. ## Test plan - [x] `pnpm nx test shared-charts` — 15 specs pass (the donut `colorMap` spec asserts the *consumer-provided* hexes, not the lib defaults, so the change is transparent there; the bar single-fill spec checks uniqueness, not the specific value). - [x] `pnpm nx test portal-admin` — 62 specs pass. - [x] `pnpm nx run-many -t test build lint -p shared-charts,portal-admin` — clean (same three pre-existing lint warnings unrelated to this PR). - [ ] **Manual smoke** — `pnpm nx serve portal-admin`, sign in with `Portal.Admin`, navigate to `/admin/audit`, switch to Charts: - Daily-volume bars render in the new muted blue. - Outcome donut slices: green (success), red (failure), orange (denied) — softer than before, semantic mapping intact. - Dark-mode toggle — palette still legible against the dark surface. - Side-by-side comparison vs `main` — the new shades feel calmer, especially when multiple charts share the viewport. ## What's next Nothing pending on the palette front. If a future chart needs a sixth intent (e.g. `pending` for in-flight states), add it here with a contrast / colour-blind check and update the typed `SemanticStatus` union in the same PR. --------- Co-authored-by: Julien Gautier Reviewed-on: https://git.unespace.com/julien/apf_portal/pulls/185 --- .../charts/src/lib/_internal/palette.ts | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/libs/shared/charts/src/lib/_internal/palette.ts b/libs/shared/charts/src/lib/_internal/palette.ts index 9a30b3e..cb26769 100644 --- a/libs/shared/charts/src/lib/_internal/palette.ts +++ b/libs/shared/charts/src/lib/_internal/palette.ts @@ -42,10 +42,12 @@ export type ColorScheme = 'sequential' | 'categorical'; /** * Default fill colour for bars when no per-bar encoding applies. - * `#1d4ed8` ≈ tailwind blue-700 — high contrast against white, - * still readable on a dark surface. + * Softer brand-aligned blue — AA contrast against white surfaces, + * still readable on a dark surface. Curated alongside + * {@link semanticStatusColors} so the chart palette reads as one + * coherent set rather than a Tailwind grab-bag. */ -export const DEFAULT_BAR_FILL = '#1d4ed8'; +export const DEFAULT_BAR_FILL = '#4075e7'; /** * Intent-bearing colours for the small set of statuses APF charts @@ -53,14 +55,16 @@ export const DEFAULT_BAR_FILL = '#1d4ed8'; * the lib (per ADR-0023), but they can pick from this curated map * by name — keeps the colour-blind-safety guarantee while letting * the donut chart encode meaning (success = green, denied = - * orange). + * orange). Values are softer than the Tailwind defaults — chart + * surfaces benefit from a lower-saturation palette so adjacent + * slices/bars don't visually fight when they share a tile. */ export const semanticStatusColors = { - success: '#16a34a', // green-600 - warning: '#ea580c', // orange-600 - error: '#dc2626', // red-600 - info: '#2563eb', // blue-600 - neutral: '#6b7280', // gray-500 + success: '#46ac6b', // muted green + warning: '#f38043', // muted orange + error: '#eb5252', // muted red + info: '#4075e7', // muted blue (same as DEFAULT_BAR_FILL) + neutral: '#6b7280', // gray } as const; export type SemanticStatus = keyof typeof semanticStatusColors;