Skip to content

Commit 9478503

Browse files
committed
feat(web): align environment colors to the canon palette
Matches cmds-app/platform, whose ramp changed on 2026-08-12. Green left test because admins read it as a traffic light - green means "passing" everywhere else in the app - and the green/red pair it made with live gave a red-green color blind operator no signal. Live drops red for a hueless token that inverts by theme, since red read as "stop" and competed with the danger color on destructive buttons. Category keys now use the canonical roster tokens (work, test, stage, live, special). The old keys were all aliases, which infra's rule against writing aliases forbids in new code.
1 parent 5f5dfe7 commit 9478503

2 files changed

Lines changed: 44 additions & 15 deletions

File tree

web/src/lib/environmentColors.ts

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,55 @@
11
// Canonical color legend for environment categories. Mapped by handle or alias
22
// so admin pages, status boards, and service rows all paint the same swatch
33
// for the same environment.
4+
//
5+
// The palette is canon in daniel-miller/infra/README.md ("Environment
6+
// colors"); cmds-app/platform/web/src/lib/environments.ts carries the same
7+
// values. Keep this file in step with those, not ahead of them.
8+
//
9+
// The ramp says how far a mistake travels, not whether a thing is healthy.
10+
// Green left it on 2026-08-12 because admins read it as a traffic light: green
11+
// means "passing" and "healthy" everywhere else in the app, and the green/red
12+
// pair it made with live gave a red-green color blind operator no signal.
13+
// Violet work, blue test, amber stage, then live with no hue. Amber sits at
14+
// the loud end because stage is the one that looks like production without
15+
// being it.
416

517
export const ENVIRONMENT_COLORS = {
6-
local: "#4A90D9",
7-
development: "#2ECC71",
8-
sandbox: "#F5A623",
9-
production: "#E74C3C",
10-
other: "#5D6472",
18+
work: "rgb(150, 122, 214)",
19+
test: "rgb(74, 144, 217)",
20+
stage: "rgb(245, 166, 35)",
21+
// Theme-aware token rather than a literal: a near-black swatch disappears
22+
// against a dark card. See --color-env-live in styles/globals.css. Red left
23+
// here on 2026-08-12 - it read as "stop", and it competed with the danger
24+
// color marking destructive actions on the same screen.
25+
live: "var(--color-env-live)",
26+
// Cold, echo, and demo share one gray. Unmapped handles land here too: a
27+
// token nobody has categorized should look unremarkable rather than borrow a
28+
// lifecycle color.
29+
special: "rgb(93, 100, 114)",
1130
} as const;
1231

1332
export type EnvironmentCategory = keyof typeof ENVIRONMENT_COLORS;
1433

15-
const LOCAL_TAGS = new Set(["local", "work"]);
16-
const DEVELOPMENT_TAGS = new Set(["development", "dev", "test", "qa", "uat"]);
34+
const WORK_TAGS = new Set(["work", "local"]);
35+
const TEST_TAGS = new Set(["test", "dev", "development", "qa", "uat"]);
1736
// "demo" is deliberately absent: since the 2026-07-31 roster rename it names
18-
// the demonstration environment (gray "other"), not the pre-production gate.
19-
const SANDBOX_TAGS = new Set(["sandbox", "stage", "staging"]);
20-
const PRODUCTION_TAGS = new Set(["production", "prod", "live"]);
37+
// the demonstration environment (gray "special"), not the pre-production gate.
38+
const STAGE_TAGS = new Set(["stage", "sandbox", "staging"]);
39+
const LIVE_TAGS = new Set(["live", "prod", "production"]);
2140

2241
export function categorizeEnvironment(
2342
handle: string,
2443
aliases: readonly string[] = [],
2544
): EnvironmentCategory {
2645
const tags = [handle, ...aliases].map((t) => t.toLowerCase());
2746
for (const t of tags) {
28-
if (LOCAL_TAGS.has(t)) return "local";
29-
if (DEVELOPMENT_TAGS.has(t)) return "development";
30-
if (SANDBOX_TAGS.has(t)) return "sandbox";
31-
if (PRODUCTION_TAGS.has(t)) return "production";
47+
if (WORK_TAGS.has(t)) return "work";
48+
if (TEST_TAGS.has(t)) return "test";
49+
if (STAGE_TAGS.has(t)) return "stage";
50+
if (LIVE_TAGS.has(t)) return "live";
3251
}
33-
return "other";
52+
return "special";
3453
}
3554

3655
export function environmentColor(handle: string, aliases: readonly string[] = []): string {

web/src/styles/globals.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@
2929

3030
--radius-card: 0.75rem;
3131
--radius-badge: 0.375rem;
32+
33+
/* Live's roster color, hueless on purpose: production is the ordinary place
34+
to be, and the lifecycle ramp spends its color saying "you are not in
35+
production". A fixed near-black would disappear against a dark card, so the
36+
token inverts by theme instead. Lighter than the gray the derived and
37+
independent environments share, so live still reads apart from them.
38+
Canon: daniel-miller/infra/README.md, "Environment colors". */
39+
--color-env-live: oklch(0.32 0.02 265);
3240
}
3341

3442
.dark {
@@ -50,6 +58,8 @@
5058
--color-success: oklch(0.75 0.17 152);
5159
--color-warning: oklch(0.83 0.17 82);
5260
--color-danger: oklch(0.68 0.22 27);
61+
62+
--color-env-live: oklch(0.75 0.015 265);
5363
}
5464

5565
html,

0 commit comments

Comments
 (0)