@transitrix/diagrams 1.8.3 → 1.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -60,7 +60,7 @@ export function renderCapabilityTreeSvg(map, opts = {}) {
60
60
  <text class="tree-collapse-lbl" x="${btnCx}" y="${btnCy}" text-anchor="middle">${n.isCollapsed ? '+' : '−'}</text>`
61
61
  : '';
62
62
  return `<g${nameTitle ? ` title="${nameTitle}"` : ''}>
63
- <rect class="tree-level-${band}" x="${x}" y="${y}" width="${n.width}" height="${n.height}" rx="11" stroke="var(--ts-node-stroke,#94a3b8)" stroke-width="3"/>
63
+ <rect class="tree-level-${band}" x="${x}" y="${y}" width="${n.width}" height="${n.height}" rx="11" stroke="var(--ts-node-stroke,#004d67)" stroke-width="3"/>
64
64
  <rect class="tree-maturity-${mat}" x="${badgeX}" y="${badgeY}" width="${badgeW}" height="${badgeH}" rx="4"/>
65
65
  <text class="text-pill" x="${badgeX + badgeW / 2}" y="${badgeY + badgeH / 2}" text-anchor="middle" fill="white">L${mat}</text>
66
66
  <text class="text-primary" x="${textX}" y="${nameY}" dominant-baseline="central">${escXml(nameText)}</text>
@@ -80,7 +80,7 @@ export function renderCapabilityTreeSvg(map, opts = {}) {
80
80
  legendX += bw;
81
81
  if (count === 0)
82
82
  return '';
83
- return `<rect class="tree-level-${i}" x="${bx}" y="${legendY}" width="${bw}" height="24" rx="4" stroke="var(--ts-node-stroke,#94a3b8)" stroke-width="1"/>
83
+ return `<rect class="tree-level-${i}" x="${bx}" y="${legendY}" width="${bw}" height="24" rx="4" stroke="var(--ts-node-stroke,#004d67)" stroke-width="1"/>
84
84
  <text class="text-id" x="${bx + bw / 2}" y="${legendY + 12}" text-anchor="middle" dominant-baseline="central">${escXml(BAND_LABELS[i])}</text>`;
85
85
  }).filter(Boolean).join('\n');
86
86
  return `<svg xmlns="http://www.w3.org/2000/svg" width="${svgW}" height="${svgH}" viewBox="0 0 ${svgW} ${svgH}">
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Brand default-theme regression tests (hub issue #548).
3
+ *
4
+ * Locks in the three-role brand mapping from `brand/transitrix_brand.md`:
5
+ * - Petrol = structure (node/edge stroke) and hierarchy depth (fill tint).
6
+ * - A depth ramp never switches hue — only lightness/saturation move.
7
+ * - Text stays WCAG-AA readable against every fill it can be drawn on.
8
+ * `hc` (VS Code high-contrast) is intentionally excluded — it's an
9
+ * accessibility override, not one of the three brand-contract themes.
10
+ */
11
+ import { describe, expect, it } from 'vitest';
12
+ import { BRAND, BRAND_EMPHASIS, LAYER_COLORS, LEVEL_COLORS, MATURITY_COLORS, TREE_MATURITY_COLORS, STRUCTURAL, TREE_LEVEL_COLORS, } from '../tokens.js';
13
+ function hexToRgb(hex) {
14
+ const n = hex.replace('#', '');
15
+ return [0, 2, 4].map((i) => parseInt(n.slice(i, i + 2), 16));
16
+ }
17
+ function relativeLuminance([r, g, b]) {
18
+ const f = (v) => {
19
+ const c = v / 255;
20
+ return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
21
+ };
22
+ const [R, G, B] = [f(r), f(g), f(b)];
23
+ return 0.2126 * R + 0.7152 * G + 0.0722 * B;
24
+ }
25
+ /** WCAG 2.x contrast ratio between two hex colors (1..21). */
26
+ function contrast(a, b) {
27
+ const la = relativeLuminance(hexToRgb(a));
28
+ const lb = relativeLuminance(hexToRgb(b));
29
+ const [hi, lo] = la > lb ? [la, lb] : [lb, la];
30
+ return (hi + 0.05) / (lo + 0.05);
31
+ }
32
+ /** Hue in degrees (0-360) from a hex color; undefined (grey) hues return -1. */
33
+ function hue(hex) {
34
+ const [r, g, b] = hexToRgb(hex).map((v) => v / 255);
35
+ const max = Math.max(r, g, b), min = Math.min(r, g, b);
36
+ const delta = max - min;
37
+ if (delta < 0.001)
38
+ return -1; // achromatic
39
+ let h;
40
+ if (max === r)
41
+ h = ((g - b) / delta) % 6;
42
+ else if (max === g)
43
+ h = (b - r) / delta + 2;
44
+ else
45
+ h = (r - g) / delta + 4;
46
+ h *= 60;
47
+ return h < 0 ? h + 360 : h;
48
+ }
49
+ const PETROL_HUE = hue(BRAND.petrol);
50
+ const HUE_TOLERANCE = 6; // degrees — rounding slop from hand-tuned hex values
51
+ function expectSameHueFamily(hexes, label) {
52
+ for (const hex of hexes) {
53
+ const h = hue(hex);
54
+ const diff = Math.min(Math.abs(h - PETROL_HUE), 360 - Math.abs(h - PETROL_HUE));
55
+ expect(diff, `${label} ${hex} should stay in the petrol hue family (got hue ${h.toFixed(1)}, petrol is ${PETROL_HUE.toFixed(1)})`).toBeLessThanOrEqual(HUE_TOLERANCE);
56
+ }
57
+ }
58
+ // Minimum AA text-contrast ratios this design targets. Primary/secondary text
59
+ // is small (10-12px) so we hold the full 4.5:1 normal-text bar for the name
60
+ // label, and the calibrated ~4.0 bar the pre-brand palette already shipped
61
+ // for the muted secondary/id label (see brand-ramp calibration in PR #548).
62
+ const MIN_PRIMARY_CONTRAST = 4.5;
63
+ const MIN_SECONDARY_CONTRAST = 4.0;
64
+ describe('brand default theme (#548) — structure is petrol', () => {
65
+ it('nodeStroke and edgeStroke are brand petrol in the light theme', () => {
66
+ expect(STRUCTURAL.nodeStroke.light).toBe(BRAND.petrol);
67
+ expect(STRUCTURAL.edgeStroke.light).toBe(BRAND.petrol);
68
+ });
69
+ it('nodeStroke/edgeStroke stay in the petrol hue family in the dark theme', () => {
70
+ expectSameHueFamily([STRUCTURAL.nodeStroke.dark, STRUCTURAL.edgeStroke.dark], 'dark stroke');
71
+ });
72
+ });
73
+ describe('brand default theme (#548) — tint depth never switches hue', () => {
74
+ it('LEVEL_COLORS light ramp stays in the petrol hue family', () => {
75
+ expectSameHueFamily([...LEVEL_COLORS.light], 'LEVEL light');
76
+ });
77
+ it('LEVEL_COLORS dark ramp stays in the petrol hue family', () => {
78
+ expectSameHueFamily([...LEVEL_COLORS.dark], 'LEVEL dark');
79
+ });
80
+ it('LAYER_COLORS (DGCA/DGA columns) stay in the petrol hue family, both themes', () => {
81
+ const keys = ['driver', 'factor', 'goal', 'change', 'activity'];
82
+ expectSameHueFamily(keys.map((k) => LAYER_COLORS[k].light), 'LAYER light');
83
+ expectSameHueFamily(keys.map((k) => LAYER_COLORS[k].dark), 'LAYER dark');
84
+ });
85
+ it('TREE_LEVEL_COLORS (capability map bands) stay in the petrol hue family, both themes', () => {
86
+ const keys = ['band0', 'band1', 'band2'];
87
+ expectSameHueFamily(keys.map((k) => TREE_LEVEL_COLORS[k].light), 'BAND light');
88
+ expectSameHueFamily(keys.map((k) => TREE_LEVEL_COLORS[k].dark), 'BAND dark');
89
+ });
90
+ });
91
+ describe('brand default theme (#548) — WCAG-AA text-on-fill contrast', () => {
92
+ function checkFills(label, fills, textPrimary, textSecondary) {
93
+ for (const fill of fills) {
94
+ expect(contrast(fill, textPrimary), `${label} ${fill} vs text-primary`).toBeGreaterThanOrEqual(MIN_PRIMARY_CONTRAST);
95
+ expect(contrast(fill, textSecondary), `${label} ${fill} vs text-secondary`).toBeGreaterThanOrEqual(MIN_SECONDARY_CONTRAST);
96
+ }
97
+ }
98
+ it('LEVEL_COLORS — every slot, both themes', () => {
99
+ checkFills('LEVEL light', [...LEVEL_COLORS.light], STRUCTURAL.textPrimary.light, STRUCTURAL.textSecondary.light);
100
+ checkFills('LEVEL dark', [...LEVEL_COLORS.dark], STRUCTURAL.textPrimary.dark, STRUCTURAL.textSecondary.dark);
101
+ });
102
+ it('LAYER_COLORS — every column, both themes', () => {
103
+ const keys = ['driver', 'factor', 'goal', 'change', 'activity'];
104
+ checkFills('LAYER light', keys.map((k) => LAYER_COLORS[k].light), STRUCTURAL.textPrimary.light, STRUCTURAL.textSecondary.light);
105
+ checkFills('LAYER dark', keys.map((k) => LAYER_COLORS[k].dark), STRUCTURAL.textPrimary.dark, STRUCTURAL.textSecondary.dark);
106
+ });
107
+ it('TREE_LEVEL_COLORS — every band, both themes', () => {
108
+ const keys = ['band0', 'band1', 'band2'];
109
+ checkFills('BAND light', keys.map((k) => TREE_LEVEL_COLORS[k].light), STRUCTURAL.textPrimary.light, STRUCTURAL.textSecondary.light);
110
+ checkFills('BAND dark', keys.map((k) => TREE_LEVEL_COLORS[k].dark), STRUCTURAL.textPrimary.dark, STRUCTURAL.textSecondary.dark);
111
+ });
112
+ it('textPrimary/textSecondary stay readable against the shell background', () => {
113
+ expect(contrast(STRUCTURAL.textPrimary.light, '#ffffff')).toBeGreaterThanOrEqual(MIN_PRIMARY_CONTRAST);
114
+ expect(contrast(STRUCTURAL.textPrimary.dark, '#0a1628')).toBeGreaterThanOrEqual(MIN_PRIMARY_CONTRAST);
115
+ });
116
+ });
117
+ describe('brand default theme (#548) — amber/orange emphasis stays distinct from functional colors', () => {
118
+ it('does not reuse brand amber/orange for functional status', () => {
119
+ // FUNCTIONAL warning/error must remain visually distinct from the brand
120
+ // accents so a red error is never confused with brand orange, per
121
+ // brand/transitrix_brand.md "Don't reuse brand orange as semantic error".
122
+ expect(BRAND_EMPHASIS.orange.light).toBe(BRAND.orange);
123
+ expect(BRAND_EMPHASIS.amber.light).toBe(BRAND.amber);
124
+ });
125
+ it('critical-path emphasis tint pairs with the orange stroke, both themes', () => {
126
+ expect(contrast(BRAND_EMPHASIS.orange.light, BRAND_EMPHASIS.orangeTint.light)).toBeGreaterThan(1.5);
127
+ expect(contrast(BRAND_EMPHASIS.orange.dark, BRAND_EMPHASIS.orangeTint.dark)).toBeGreaterThan(1.5);
128
+ });
129
+ });
130
+ describe('maturity Likert ramp — one harmonious scale, white-label contrast', () => {
131
+ // .maturity-pill / tree-maturity badges hardcode white label text
132
+ // (capability-map-preview.ts, render-capability-tree.ts) regardless of
133
+ // theme, so every step needs AA (light/dark) or AAA (hc, since VS Code
134
+ // high-contrast mode exists specifically to demand stronger contrast)
135
+ // against white — not against the page's own text-primary/secondary.
136
+ const MIN_WHITE_CONTRAST_AA = 4.5;
137
+ const MIN_WHITE_CONTRAST_AAA = 7;
138
+ it('MATURITY_COLORS — every step reads white text at AA, light + dark', () => {
139
+ for (const hex of MATURITY_COLORS.light) {
140
+ expect(contrast(hex, '#ffffff'), `MATURITY light ${hex} vs white`).toBeGreaterThanOrEqual(MIN_WHITE_CONTRAST_AA);
141
+ }
142
+ for (const hex of MATURITY_COLORS.dark) {
143
+ expect(contrast(hex, '#ffffff'), `MATURITY dark ${hex} vs white`).toBeGreaterThanOrEqual(MIN_WHITE_CONTRAST_AA);
144
+ }
145
+ });
146
+ it('MATURITY_COLORS hc — every step reads white text at AAA', () => {
147
+ for (const hex of MATURITY_COLORS.hc) {
148
+ expect(contrast(hex, '#ffffff'), `MATURITY hc ${hex} vs white`).toBeGreaterThanOrEqual(MIN_WHITE_CONTRAST_AAA);
149
+ }
150
+ });
151
+ it('TREE_MATURITY_COLORS is the same harmonious ramp as MATURITY_COLORS', () => {
152
+ // Previously a divergent DSM-matching pink/yellow/blue set (plus a grey
153
+ // L1, an odd "no signal" hue for the worst rating) — "maturity" now
154
+ // means the same colors everywhere it's shown.
155
+ expect(TREE_MATURITY_COLORS.light).toEqual(MATURITY_COLORS.light);
156
+ expect(TREE_MATURITY_COLORS.dark).toEqual(MATURITY_COLORS.dark);
157
+ expect(TREE_MATURITY_COLORS.hc).toEqual(MATURITY_COLORS.hc);
158
+ });
159
+ });
@@ -1,4 +1,4 @@
1
1
  export type { AdaptiveTokens } from './tokens.js';
2
- export { BRAND, FUNCTIONAL, LAYER_COLORS, LEVEL_COLORS, MATURITY_COLORS, TREE_LEVEL_COLORS, TREE_MATURITY_COLORS, STRUCTURAL, TYPOGRAPHY, CSS_VAR, getBaseResetCss } from './tokens.js';
2
+ export { BRAND, BRAND_EMPHASIS, FUNCTIONAL, LAYER_COLORS, LEVEL_COLORS, MATURITY_COLORS, TREE_LEVEL_COLORS, TREE_MATURITY_COLORS, STRUCTURAL, TYPOGRAPHY, CSS_VAR, getBaseResetCss } from './tokens.js';
3
3
  export type { ThemeId } from './themes.js';
4
4
  export { generateWebviewCss, generateSvgEmbedCss } from './themes.js';
@@ -1,2 +1,2 @@
1
- export { BRAND, FUNCTIONAL, LAYER_COLORS, LEVEL_COLORS, MATURITY_COLORS, TREE_LEVEL_COLORS, TREE_MATURITY_COLORS, STRUCTURAL, TYPOGRAPHY, CSS_VAR, getBaseResetCss } from './tokens.js';
1
+ export { BRAND, BRAND_EMPHASIS, FUNCTIONAL, LAYER_COLORS, LEVEL_COLORS, MATURITY_COLORS, TREE_LEVEL_COLORS, TREE_MATURITY_COLORS, STRUCTURAL, TYPOGRAPHY, CSS_VAR, getBaseResetCss } from './tokens.js';
2
2
  export { generateWebviewCss, generateSvgEmbedCss } from './themes.js';
@@ -1,4 +1,4 @@
1
- import { LAYER_COLORS, LEVEL_COLORS, MATURITY_COLORS, TREE_LEVEL_COLORS, TREE_MATURITY_COLORS, STRUCTURAL, TYPOGRAPHY, CSS_VAR, getBaseResetCss, } from './tokens.js';
1
+ import { LAYER_COLORS, LEVEL_COLORS, MATURITY_COLORS, TREE_LEVEL_COLORS, TREE_MATURITY_COLORS, STRUCTURAL, BRAND_EMPHASIS, TYPOGRAPHY, CSS_VAR, getBaseResetCss, } from './tokens.js';
2
2
  const TRANSITRIX_LIGHT = {
3
3
  bg: '#ffffff',
4
4
  bgSurface: '#f8fafc',
@@ -89,6 +89,9 @@ function diagramVars(variant) {
89
89
  `${CSS_VAR.maturity3}:${mat[2]}`,
90
90
  `${CSS_VAR.maturity4}:${mat[3]}`,
91
91
  `${CSS_VAR.maturity5}:${mat[4]}`,
92
+ `${CSS_VAR.brandAmber}:${BRAND_EMPHASIS.amber[variant]}`,
93
+ `${CSS_VAR.brandOrange}:${BRAND_EMPHASIS.orange[variant]}`,
94
+ `${CSS_VAR.brandOrangeTint}:${BRAND_EMPHASIS.orangeTint[variant]}`,
92
95
  ].join(';') + ';' + levelVars + treeVars;
93
96
  }
94
97
  /** CSS for the webview shell (toolbar, canvas). */
@@ -1,4 +1,4 @@
1
- /** Transitrix brand palette. */
1
+ /** Transitrix brand palette (`brand/transitrix_brand.md` — canonical). */
2
2
  export declare const BRAND: {
3
3
  readonly petrol: "#004d67";
4
4
  readonly amber: "#ffaf00";
@@ -11,108 +11,167 @@ export declare const FUNCTIONAL: {
11
11
  readonly error: "#b91c1c";
12
12
  readonly info: "#1d6fa8";
13
13
  };
14
- /** Fill colors for FGCA/DGCA notation columns. */
14
+ /**
15
+ * Amber/orange "author emphasis" accents, theme-adjusted for contrast against
16
+ * each theme's background (dark theme gets the brand hues lightened per
17
+ * `brand/transitrix_brand.md` §Themes: "brand colours slightly desaturated
18
+ * for contrast on dark surfaces"). `orangeTint` is the light emphasis-fill
19
+ * paired with an `orange` stroke (e.g. Activities critical-path nodes).
20
+ * `hc` intentionally falls back to the plain BRAND hex — VS Code
21
+ * high-contrast mode prioritises accessibility overrides over brand fidelity.
22
+ */
23
+ export declare const BRAND_EMPHASIS: {
24
+ readonly amber: {
25
+ readonly light: "#ffaf00";
26
+ readonly dark: "#ffc83d";
27
+ readonly hc: "#ffaf00";
28
+ };
29
+ readonly orange: {
30
+ readonly light: "#ff4d00";
31
+ readonly dark: "#ff7542";
32
+ readonly hc: "#ff4d00";
33
+ };
34
+ readonly orangeTint: {
35
+ readonly light: "#ffeee5";
36
+ readonly dark: "#432114";
37
+ readonly hc: "#ffeee5";
38
+ };
39
+ };
40
+ /**
41
+ * Fill colors for FGCA/DGCA notation columns — a petrol tint per stage
42
+ * (Driver → Goal → Change → Activity), deepening left to right like the
43
+ * Goals-tree level ladder below. Petrol tint = structural position, never a
44
+ * hue switch, per the brand's "structure via petrol" role mapping.
45
+ */
15
46
  export declare const LAYER_COLORS: {
16
47
  readonly driver: {
17
- readonly light: "#fef3c7";
18
- readonly dark: "#292108";
48
+ readonly light: "#eaf3f6";
49
+ readonly dark: "#17262c";
19
50
  readonly hc: "#f59e0b";
20
51
  };
21
52
  readonly factor: {
22
- readonly light: "#fef3c7";
23
- readonly dark: "#292108";
53
+ readonly light: "#eaf3f6";
54
+ readonly dark: "#17262c";
24
55
  readonly hc: "#f59e0b";
25
56
  };
26
57
  readonly goal: {
27
- readonly light: "#e0e7ff";
28
- readonly dark: "#1e1b4b";
58
+ readonly light: "#d9ecf2";
59
+ readonly dark: "#192f37";
29
60
  readonly hc: "#818cf8";
30
61
  };
31
62
  readonly change: {
32
- readonly light: "#dbeafe";
33
- readonly dark: "#0c2042";
63
+ readonly light: "#c7e5ef";
64
+ readonly dark: "#1a3842";
34
65
  readonly hc: "#60a5fa";
35
66
  };
36
67
  readonly activity: {
37
- readonly light: "#d4edda";
38
- readonly dark: "#0a2414";
68
+ readonly light: "#b4e0ee";
69
+ readonly dark: "#1a414e";
39
70
  readonly hc: "#4ade80";
40
71
  };
41
72
  };
42
- /** Level fill colors for Goals tree nodes — 8 slots, cycled by level index. */
73
+ /**
74
+ * Level fill colors for Goals tree nodes — 8 slots, cycled by level index.
75
+ * A single-hue petrol tint ramp (level-0 lightest/root → level-7 deepest);
76
+ * hue never switches with depth. Saturation/lightness tuned so text-primary
77
+ * and text-secondary keep at least their pre-brand contrast ratio against
78
+ * every slot — see `theme/__tests__/tokens.test.ts`.
79
+ */
43
80
  export declare const LEVEL_COLORS: {
44
- readonly light: readonly ["#dbeafe", "#e0e7ff", "#d4edda", "#fef3c7", "#fce7f3", "#e0f2fe", "#f3e8ff", "#fef9c3"];
45
- readonly dark: readonly ["#0c2042", "#1e1b4b", "#0a2414", "#292108", "#2a0c1f", "#071e2e", "#1a0c33", "#2a2107"];
81
+ readonly light: readonly ["#eef4f7", "#e6f1f4", "#dfedf1", "#d7e9ef", "#cee6ed", "#c6e2ec", "#bfe0eb", "#b7deeb"];
82
+ readonly dark: readonly ["#152328", "#16292f", "#172e36", "#18333d", "#183944", "#183f4c", "#184554", "#174b5c"];
46
83
  readonly hc: readonly ["#60a5fa", "#818cf8", "#4ade80", "#f59e0b", "#f472b6", "#38bdf8", "#a78bfa", "#facc15"];
47
84
  };
48
- /** Structural diagram colors (nodes, edges, text). */
85
+ /**
86
+ * Structural diagram colors (nodes, edges, text). nodeStroke/edgeStroke are
87
+ * brand petrol — "structure via petrol" per the brand's role mapping.
88
+ * textPrimary is a dark petrol ink (light theme) / near-white petrol-tinted
89
+ * (dark theme); textSecondary stays a muted grey with a faint petrol lean.
90
+ * `hc` is untouched — VS Code high-contrast accessibility overrides win over
91
+ * brand fidelity there.
92
+ */
49
93
  export declare const STRUCTURAL: {
50
94
  readonly nodeStroke: {
51
- readonly light: "#94a3b8";
52
- readonly dark: "#475569";
95
+ readonly light: "#004d67";
96
+ readonly dark: "#53a9c6";
53
97
  readonly hc: "#e2e8f0";
54
98
  };
55
99
  readonly edgeStroke: {
56
- readonly light: "#94a3b8";
57
- readonly dark: "#475569";
100
+ readonly light: "#004d67";
101
+ readonly dark: "#53a9c6";
58
102
  readonly hc: "#e2e8f0";
59
103
  };
60
104
  readonly textPrimary: {
61
- readonly light: "#1e293b";
62
- readonly dark: "#f1f5f9";
105
+ readonly light: "#0d2b35";
106
+ readonly dark: "#edf5f8";
63
107
  readonly hc: "#ffffff";
64
108
  };
65
109
  readonly textSecondary: {
66
- readonly light: "#64748b";
67
- readonly dark: "#94a3b8";
110
+ readonly light: "#516970";
111
+ readonly dark: "#adc8d1";
68
112
  readonly hc: "#cbd5e1";
69
113
  };
70
114
  readonly headerText: {
71
- readonly light: "#374151";
72
- readonly dark: "#e5e7eb";
115
+ readonly light: "#0d2b35";
116
+ readonly dark: "#edf5f8";
73
117
  readonly hc: "#ffffff";
74
118
  };
75
119
  };
76
120
  /**
77
- * Maturity scale fill colors — Likert 1..5, danger→success.
121
+ * Maturity scale fill colors — Likert 1..5, danger→success. A single
122
+ * harmonious ramp: consistent saturation curve, hue stepping smoothly
123
+ * red→orange→gold→yellow-green→green (never jumping registers the way the
124
+ * previous per-theme picks did), tuned so the badge's hardcoded white label
125
+ * text (`.maturity-pill`, capability-map-preview.ts) stays ≥4.5:1 AA at
126
+ * every step. Same ramp for light/dark — these are small solid badges, not
127
+ * background tints, so theme brightness doesn't change the contrast need.
128
+ * `hc` pushes the same hues darker/more saturated for AAA (≥7:1), since VS
129
+ * Code high-contrast mode exists specifically to demand stronger contrast.
78
130
  * Used by Capability Map and any future maturity-based notation.
79
131
  */
80
132
  export declare const MATURITY_COLORS: {
81
- readonly light: readonly ["#b91c1c", "#d97706", "#ca8a04", "#65a30d", "#15803d"];
82
- readonly dark: readonly ["#7f1d1d", "#92400e", "#854d0e", "#3f6212", "#14532d"];
83
- readonly hc: readonly ["#f87171", "#fb923c", "#facc15", "#a3e635", "#4ade80"];
133
+ readonly light: readonly ["#bf2518", "#b05911", "#8d7011", "#407d21", "#237b48"];
134
+ readonly dark: readonly ["#bf2518", "#b05911", "#8d7011", "#407d21", "#237b48"];
135
+ readonly hc: readonly ["#a0190d", "#864109", "#5a4607", "#316317", "#155630"];
84
136
  };
85
137
  /**
86
- * DSM-matching capability tree node fill colors, grouped by depth band.
87
- * band0: depth 0–2 (pink)
88
- * band1: depth 3–4 (yellow)
89
- * band2: depth 5+ (blue)
138
+ * Capability tree node fill colors, grouped by depth band — a 3-step petrol
139
+ * tint (shallow → deep), same "petrol tint = structural depth" rule as
140
+ * LEVEL_COLORS above. Previously a DSM-matching pink/yellow/blue scheme;
141
+ * brand/transitrix_brand.md ("Components") anticipates DSM adopting the same
142
+ * shared color tokens, so Studio moving first is convergence, not drift.
143
+ * band0: depth 0–2 (shallowest)
144
+ * band1: depth 3–4
145
+ * band2: depth 5+ (deepest)
90
146
  */
91
147
  export declare const TREE_LEVEL_COLORS: {
92
148
  readonly band0: {
93
- readonly light: "#fee0e0";
94
- readonly dark: "#3d0f0f";
149
+ readonly light: "#e6f1f4";
150
+ readonly dark: "#152b32";
95
151
  readonly hc: "#ff9999";
96
152
  };
97
153
  readonly band1: {
98
- readonly light: "#ffffcb";
99
- readonly dark: "#3a3500";
154
+ readonly light: "#d0e9f1";
155
+ readonly dark: "#153b47";
100
156
  readonly hc: "#ffff88";
101
157
  };
102
158
  readonly band2: {
103
- readonly light: "#c2f0ff";
104
- readonly dark: "#003d4d";
159
+ readonly light: "#b5e3f2";
160
+ readonly dark: "#114c5f";
105
161
  readonly hc: "#66ccff";
106
162
  };
107
163
  };
108
164
  /**
109
- * DSM-matching maturity badge fills for the capability tree (L1–L5).
110
- * Distinct from MATURITY_COLORS which drives the cards / compliance views.
165
+ * Maturity badge fills for the capability tree (L1–L5). Same ramp as
166
+ * MATURITY_COLORS — previously a separate DSM-matching pink/yellow/blue set
167
+ * that didn't even share L1's meaning (grey = lowest maturity, an odd
168
+ * "no signal" hue for the worst rating) and had white-label contrast as low
169
+ * as 1.07:1 in `hc`. One harmonious Likert ramp for "maturity" everywhere.
111
170
  */
112
171
  export declare const TREE_MATURITY_COLORS: {
113
- readonly light: readonly ["#c1c1c1", "#ff3d67", "#ff9a59", "#fdef59", "#83cca3"];
114
- readonly dark: readonly ["#555555", "#cc1040", "#d06020", "#c8c000", "#4d9970"];
115
- readonly hc: readonly ["#cccccc", "#ff6699", "#ffbb77", "#ffff44", "#88ddaa"];
172
+ readonly light: readonly ["#bf2518", "#b05911", "#8d7011", "#407d21", "#237b48"];
173
+ readonly dark: readonly ["#bf2518", "#b05911", "#8d7011", "#407d21", "#237b48"];
174
+ readonly hc: readonly ["#a0190d", "#864109", "#5a4607", "#316317", "#155630"];
116
175
  };
117
176
  /**
118
177
  * Typography hierarchy. All previews resolve text styling from these roles
@@ -200,5 +259,8 @@ export declare const CSS_VAR: {
200
259
  readonly treeMaturity3: "--ts-tree-maturity-3";
201
260
  readonly treeMaturity4: "--ts-tree-maturity-4";
202
261
  readonly treeMaturity5: "--ts-tree-maturity-5";
262
+ readonly brandAmber: "--ts-brand-amber";
263
+ readonly brandOrange: "--ts-brand-orange";
264
+ readonly brandOrangeTint: "--ts-brand-orange-tint";
203
265
  };
204
266
  export declare function getBaseResetCss(): string;
@@ -1,4 +1,4 @@
1
- /** Transitrix brand palette. */
1
+ /** Transitrix brand palette (`brand/transitrix_brand.md` — canonical). */
2
2
  export const BRAND = {
3
3
  petrol: '#004d67',
4
4
  amber: '#ffaf00',
@@ -11,56 +11,103 @@ export const FUNCTIONAL = {
11
11
  error: '#b91c1c',
12
12
  info: '#1d6fa8',
13
13
  };
14
- /** Fill colors for FGCA/DGCA notation columns. */
14
+ /**
15
+ * Amber/orange "author emphasis" accents, theme-adjusted for contrast against
16
+ * each theme's background (dark theme gets the brand hues lightened per
17
+ * `brand/transitrix_brand.md` §Themes: "brand colours slightly desaturated
18
+ * for contrast on dark surfaces"). `orangeTint` is the light emphasis-fill
19
+ * paired with an `orange` stroke (e.g. Activities critical-path nodes).
20
+ * `hc` intentionally falls back to the plain BRAND hex — VS Code
21
+ * high-contrast mode prioritises accessibility overrides over brand fidelity.
22
+ */
23
+ export const BRAND_EMPHASIS = {
24
+ amber: { light: BRAND.amber, dark: '#ffc83d', hc: BRAND.amber },
25
+ orange: { light: BRAND.orange, dark: '#ff7542', hc: BRAND.orange },
26
+ orangeTint: { light: '#ffeee5', dark: '#432114', hc: '#ffeee5' },
27
+ };
28
+ /**
29
+ * Fill colors for FGCA/DGCA notation columns — a petrol tint per stage
30
+ * (Driver → Goal → Change → Activity), deepening left to right like the
31
+ * Goals-tree level ladder below. Petrol tint = structural position, never a
32
+ * hue switch, per the brand's "structure via petrol" role mapping.
33
+ */
15
34
  export const LAYER_COLORS = {
16
- driver: { light: '#fef3c7', dark: '#292108', hc: '#f59e0b' },
17
- factor: { light: '#fef3c7', dark: '#292108', hc: '#f59e0b' },
18
- goal: { light: '#e0e7ff', dark: '#1e1b4b', hc: '#818cf8' },
19
- change: { light: '#dbeafe', dark: '#0c2042', hc: '#60a5fa' },
20
- activity: { light: '#d4edda', dark: '#0a2414', hc: '#4ade80' },
35
+ driver: { light: '#eaf3f6', dark: '#17262c', hc: '#f59e0b' },
36
+ factor: { light: '#eaf3f6', dark: '#17262c', hc: '#f59e0b' },
37
+ goal: { light: '#d9ecf2', dark: '#192f37', hc: '#818cf8' },
38
+ change: { light: '#c7e5ef', dark: '#1a3842', hc: '#60a5fa' },
39
+ activity: { light: '#b4e0ee', dark: '#1a414e', hc: '#4ade80' },
21
40
  };
22
- /** Level fill colors for Goals tree nodes — 8 slots, cycled by level index. */
41
+ /**
42
+ * Level fill colors for Goals tree nodes — 8 slots, cycled by level index.
43
+ * A single-hue petrol tint ramp (level-0 lightest/root → level-7 deepest);
44
+ * hue never switches with depth. Saturation/lightness tuned so text-primary
45
+ * and text-secondary keep at least their pre-brand contrast ratio against
46
+ * every slot — see `theme/__tests__/tokens.test.ts`.
47
+ */
23
48
  export const LEVEL_COLORS = {
24
- light: ['#dbeafe', '#e0e7ff', '#d4edda', '#fef3c7', '#fce7f3', '#e0f2fe', '#f3e8ff', '#fef9c3'],
25
- dark: ['#0c2042', '#1e1b4b', '#0a2414', '#292108', '#2a0c1f', '#071e2e', '#1a0c33', '#2a2107'],
49
+ light: ['#eef4f7', '#e6f1f4', '#dfedf1', '#d7e9ef', '#cee6ed', '#c6e2ec', '#bfe0eb', '#b7deeb'],
50
+ dark: ['#152328', '#16292f', '#172e36', '#18333d', '#183944', '#183f4c', '#184554', '#174b5c'],
26
51
  hc: ['#60a5fa', '#818cf8', '#4ade80', '#f59e0b', '#f472b6', '#38bdf8', '#a78bfa', '#facc15'],
27
52
  };
28
- /** Structural diagram colors (nodes, edges, text). */
53
+ /**
54
+ * Structural diagram colors (nodes, edges, text). nodeStroke/edgeStroke are
55
+ * brand petrol — "structure via petrol" per the brand's role mapping.
56
+ * textPrimary is a dark petrol ink (light theme) / near-white petrol-tinted
57
+ * (dark theme); textSecondary stays a muted grey with a faint petrol lean.
58
+ * `hc` is untouched — VS Code high-contrast accessibility overrides win over
59
+ * brand fidelity there.
60
+ */
29
61
  export const STRUCTURAL = {
30
- nodeStroke: { light: '#94a3b8', dark: '#475569', hc: '#e2e8f0' },
31
- edgeStroke: { light: '#94a3b8', dark: '#475569', hc: '#e2e8f0' },
32
- textPrimary: { light: '#1e293b', dark: '#f1f5f9', hc: '#ffffff' },
33
- textSecondary: { light: '#64748b', dark: '#94a3b8', hc: '#cbd5e1' },
34
- headerText: { light: '#374151', dark: '#e5e7eb', hc: '#ffffff' },
62
+ nodeStroke: { light: BRAND.petrol, dark: '#53a9c6', hc: '#e2e8f0' },
63
+ edgeStroke: { light: BRAND.petrol, dark: '#53a9c6', hc: '#e2e8f0' },
64
+ textPrimary: { light: '#0d2b35', dark: '#edf5f8', hc: '#ffffff' },
65
+ textSecondary: { light: '#516970', dark: '#adc8d1', hc: '#cbd5e1' },
66
+ headerText: { light: '#0d2b35', dark: '#edf5f8', hc: '#ffffff' },
35
67
  };
36
68
  /**
37
- * Maturity scale fill colors — Likert 1..5, danger→success.
69
+ * Maturity scale fill colors — Likert 1..5, danger→success. A single
70
+ * harmonious ramp: consistent saturation curve, hue stepping smoothly
71
+ * red→orange→gold→yellow-green→green (never jumping registers the way the
72
+ * previous per-theme picks did), tuned so the badge's hardcoded white label
73
+ * text (`.maturity-pill`, capability-map-preview.ts) stays ≥4.5:1 AA at
74
+ * every step. Same ramp for light/dark — these are small solid badges, not
75
+ * background tints, so theme brightness doesn't change the contrast need.
76
+ * `hc` pushes the same hues darker/more saturated for AAA (≥7:1), since VS
77
+ * Code high-contrast mode exists specifically to demand stronger contrast.
38
78
  * Used by Capability Map and any future maturity-based notation.
39
79
  */
40
80
  export const MATURITY_COLORS = {
41
- light: ['#b91c1c', '#d97706', '#ca8a04', '#65a30d', '#15803d'],
42
- dark: ['#7f1d1d', '#92400e', '#854d0e', '#3f6212', '#14532d'],
43
- hc: ['#f87171', '#fb923c', '#facc15', '#a3e635', '#4ade80'],
81
+ light: ['#bf2518', '#b05911', '#8d7011', '#407d21', '#237b48'],
82
+ dark: ['#bf2518', '#b05911', '#8d7011', '#407d21', '#237b48'],
83
+ hc: ['#a0190d', '#864109', '#5a4607', '#316317', '#155630'],
44
84
  };
45
85
  /**
46
- * DSM-matching capability tree node fill colors, grouped by depth band.
47
- * band0: depth 0–2 (pink)
48
- * band1: depth 3–4 (yellow)
49
- * band2: depth 5+ (blue)
86
+ * Capability tree node fill colors, grouped by depth band — a 3-step petrol
87
+ * tint (shallow → deep), same "petrol tint = structural depth" rule as
88
+ * LEVEL_COLORS above. Previously a DSM-matching pink/yellow/blue scheme;
89
+ * brand/transitrix_brand.md ("Components") anticipates DSM adopting the same
90
+ * shared color tokens, so Studio moving first is convergence, not drift.
91
+ * band0: depth 0–2 (shallowest)
92
+ * band1: depth 3–4
93
+ * band2: depth 5+ (deepest)
50
94
  */
51
95
  export const TREE_LEVEL_COLORS = {
52
- band0: { light: '#fee0e0', dark: '#3d0f0f', hc: '#ff9999' },
53
- band1: { light: '#ffffcb', dark: '#3a3500', hc: '#ffff88' },
54
- band2: { light: '#c2f0ff', dark: '#003d4d', hc: '#66ccff' },
96
+ band0: { light: '#e6f1f4', dark: '#152b32', hc: '#ff9999' },
97
+ band1: { light: '#d0e9f1', dark: '#153b47', hc: '#ffff88' },
98
+ band2: { light: '#b5e3f2', dark: '#114c5f', hc: '#66ccff' },
55
99
  };
56
100
  /**
57
- * DSM-matching maturity badge fills for the capability tree (L1–L5).
58
- * Distinct from MATURITY_COLORS which drives the cards / compliance views.
101
+ * Maturity badge fills for the capability tree (L1–L5). Same ramp as
102
+ * MATURITY_COLORS — previously a separate DSM-matching pink/yellow/blue set
103
+ * that didn't even share L1's meaning (grey = lowest maturity, an odd
104
+ * "no signal" hue for the worst rating) and had white-label contrast as low
105
+ * as 1.07:1 in `hc`. One harmonious Likert ramp for "maturity" everywhere.
59
106
  */
60
107
  export const TREE_MATURITY_COLORS = {
61
- light: ['#c1c1c1', '#ff3d67', '#ff9a59', '#fdef59', '#83cca3'],
62
- dark: ['#555555', '#cc1040', '#d06020', '#c8c000', '#4d9970'],
63
- hc: ['#cccccc', '#ff6699', '#ffbb77', '#ffff44', '#88ddaa'],
108
+ light: MATURITY_COLORS.light,
109
+ dark: MATURITY_COLORS.dark,
110
+ hc: MATURITY_COLORS.hc,
64
111
  };
65
112
  /**
66
113
  * Typography hierarchy. All previews resolve text styling from these roles
@@ -129,6 +176,9 @@ export const CSS_VAR = {
129
176
  treeMaturity3: '--ts-tree-maturity-3',
130
177
  treeMaturity4: '--ts-tree-maturity-4',
131
178
  treeMaturity5: '--ts-tree-maturity-5',
179
+ brandAmber: '--ts-brand-amber',
180
+ brandOrange: '--ts-brand-orange',
181
+ brandOrangeTint: '--ts-brand-orange-tint',
132
182
  };
133
183
  export function getBaseResetCss() {
134
184
  return `*,*:before,*:after{box-sizing:border-box;}html,body{margin:0;padding:0;}`;
@@ -7,7 +7,7 @@ import { type NodeSizePreset } from '../node-size-presets.js';
7
7
  * network subset. Embedded alongside the shared theme CSS so the SVG is
8
8
  * self-contained for the JCEF host.
9
9
  */
10
- export declare const ACTIVITIES_NETWORK_CSS = "\n .act-node { fill: var(--ts-layer-activity, #d4edda); stroke: var(--ts-node-stroke, #94a3b8); stroke-width: 1; }\n .critical-node { fill: #fff7ed; stroke: var(--ts-brand-orange, #ff4d00); stroke-width: 2.5; }\n .milestone-node { fill: #ecfeff; stroke: var(--ts-text-muted, #64748b); stroke-dasharray: 4 2; }\n .critical-edge { stroke: var(--ts-brand-orange, #ff4d00); stroke-width: 2; }\n .arrow-fill-critical { fill: var(--ts-brand-orange, #ff4d00); }\n";
10
+ export declare const ACTIVITIES_NETWORK_CSS = "\n .act-node { fill: var(--ts-layer-activity, #d4edda); stroke: var(--ts-node-stroke, #004d67); stroke-width: 1; }\n .critical-node { fill: var(--ts-brand-orange-tint, #ffeee5); stroke: var(--ts-brand-orange, #ff4d00); stroke-width: 2.5; }\n .milestone-node { fill: #ecfeff; stroke: var(--ts-text-muted, #64748b); stroke-dasharray: 4 2; }\n .critical-edge { stroke: var(--ts-brand-orange, #ff4d00); stroke-width: 2; }\n .arrow-fill-critical { fill: var(--ts-brand-orange, #ff4d00); }\n";
11
11
  /** Arrowhead marker defs shared by both hosts for the network view. */
12
12
  export declare const ACTIVITIES_NETWORK_DEFS = "<defs>\n <marker id=\"arrow\" markerWidth=\"8\" markerHeight=\"8\" refX=\"8\" refY=\"3\" orient=\"auto\">\n <path d=\"M0,0 L0,6 L8,3 z\" class=\"arrow-fill\"/>\n </marker>\n <marker id=\"arrow-crit\" markerWidth=\"8\" markerHeight=\"8\" refX=\"8\" refY=\"3\" orient=\"auto\">\n <path d=\"M0,0 L0,6 L8,3 z\" class=\"arrow-fill-critical\"/>\n </marker>\n</defs>";
13
13
  /**
@@ -31,8 +31,8 @@ const N_PAD = 24;
31
31
  * self-contained for the JCEF host.
32
32
  */
33
33
  export const ACTIVITIES_NETWORK_CSS = `
34
- .act-node { fill: var(--ts-layer-activity, #d4edda); stroke: var(--ts-node-stroke, #94a3b8); stroke-width: 1; }
35
- .critical-node { fill: #fff7ed; stroke: var(--ts-brand-orange, #ff4d00); stroke-width: 2.5; }
34
+ .act-node { fill: var(--ts-layer-activity, #d4edda); stroke: var(--ts-node-stroke, #004d67); stroke-width: 1; }
35
+ .critical-node { fill: var(--ts-brand-orange-tint, #ffeee5); stroke: var(--ts-brand-orange, #ff4d00); stroke-width: 2.5; }
36
36
  .milestone-node { fill: #ecfeff; stroke: var(--ts-text-muted, #64748b); stroke-dasharray: 4 2; }
37
37
  .critical-edge { stroke: var(--ts-brand-orange, #ff4d00); stroke-width: 2; }
38
38
  .arrow-fill-critical { fill: var(--ts-brand-orange, #ff4d00); }
@@ -70,7 +70,7 @@ export interface RenderProcessOptions {
70
70
  /** Extra vertical space (px) reserved above the pool for a title block. */
71
71
  topInset?: number;
72
72
  }
73
- export declare const BPMN_PROCESS_CSS = "\n.bpmn-pool { fill: var(--ts-bg-surface,#f8fafc); stroke: var(--ts-node-stroke,#94a3b8); stroke-width: 1.5; }\n.bpmn-pool-name { fill: var(--ts-bg-elevated,#f1f5f9); stroke: var(--ts-node-stroke,#94a3b8); stroke-width: 0.75; }\n.bpmn-lane { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#94a3b8); stroke-width: 0.75; }\n.bpmn-lane-header { fill: var(--ts-bg-elevated,#f1f5f9); stroke: var(--ts-node-stroke,#94a3b8); stroke-width: 0.75; }\n.bpmn-pool-label { fill: var(--ts-text-primary,#1e293b); font-size: 11px; font-weight: 700; }\n.bpmn-lane-label { fill: var(--ts-text-primary,#1e293b); font-size: 11px; font-weight: 600; }\n.bpmn-task { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#94a3b8); stroke-width: 1.5; }\n.bpmn-task-name { fill: var(--ts-text-primary,#1e293b); font-size: 11px; text-anchor: middle; }\n.bpmn-event { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#94a3b8); }\n.bpmn-event-start { stroke-width: 1.5; }\n.bpmn-event-end { stroke-width: 4; }\n.bpmn-event-label { fill: var(--ts-text-secondary,#64748b); font-size: 10px; text-anchor: middle; }\n.bpmn-gateway { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#94a3b8); stroke-width: 1.5; }\n.bpmn-gateway-marker { stroke: var(--ts-node-stroke,#94a3b8); stroke-width: 2.5; stroke-linecap: round; fill: none; }\n.bpmn-gateway-label { fill: var(--ts-text-secondary,#64748b); font-size: 10px; text-anchor: middle; }\n.bpmn-seq-flow { fill: none; stroke: var(--ts-edge-stroke,#64748b); stroke-width: 1.5; }\n.bpmn-default-mark { stroke: var(--ts-edge-stroke,#64748b); stroke-width: 1.5; stroke-linecap: round; }\n.bpmn-assoc { fill: none; stroke: var(--ts-edge-stroke,#64748b); stroke-width: 1; stroke-dasharray: 4 2; }\n.bpmn-data-obj { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#94a3b8); stroke-width: 1; }\n.bpmn-data-obj-label { fill: var(--ts-text-secondary,#64748b); font-size: 10px; text-anchor: middle; }\n.bpmn-event-intermediate { fill: none; stroke: var(--ts-node-stroke,#94a3b8); stroke-width: 1; }\n.bpmn-event-icon { fill: none; stroke: var(--ts-node-stroke,#94a3b8); stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }\n";
73
+ export declare const BPMN_PROCESS_CSS = "\n.bpmn-pool { fill: var(--ts-bg-surface,#f8fafc); stroke: var(--ts-node-stroke,#004d67); stroke-width: 1.5; }\n.bpmn-pool-name { fill: var(--ts-bg-elevated,#f1f5f9); stroke: var(--ts-node-stroke,#004d67); stroke-width: 0.75; }\n.bpmn-lane { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); stroke-width: 0.75; }\n.bpmn-lane-header { fill: var(--ts-bg-elevated,#f1f5f9); stroke: var(--ts-node-stroke,#004d67); stroke-width: 0.75; }\n.bpmn-pool-label { fill: var(--ts-text-primary,#0d2b35); font-size: 11px; font-weight: 700; }\n.bpmn-lane-label { fill: var(--ts-text-primary,#0d2b35); font-size: 11px; font-weight: 600; }\n.bpmn-task { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); stroke-width: 1.5; }\n.bpmn-task-name { fill: var(--ts-text-primary,#0d2b35); font-size: 11px; text-anchor: middle; }\n.bpmn-event { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); }\n.bpmn-event-start { stroke-width: 1.5; }\n.bpmn-event-end { stroke-width: 4; }\n.bpmn-event-label { fill: var(--ts-text-secondary,#516970); font-size: 10px; text-anchor: middle; }\n.bpmn-gateway { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); stroke-width: 1.5; }\n.bpmn-gateway-marker { stroke: var(--ts-node-stroke,#004d67); stroke-width: 2.5; stroke-linecap: round; fill: none; }\n.bpmn-gateway-label { fill: var(--ts-text-secondary,#516970); font-size: 10px; text-anchor: middle; }\n.bpmn-seq-flow { fill: none; stroke: var(--ts-edge-stroke,#004d67); stroke-width: 1.5; }\n.bpmn-default-mark { stroke: var(--ts-edge-stroke,#004d67); stroke-width: 1.5; stroke-linecap: round; }\n.bpmn-assoc { fill: none; stroke: var(--ts-edge-stroke,#004d67); stroke-width: 1; stroke-dasharray: 4 2; }\n.bpmn-data-obj { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); stroke-width: 1; }\n.bpmn-data-obj-label { fill: var(--ts-text-secondary,#516970); font-size: 10px; text-anchor: middle; }\n.bpmn-event-intermediate { fill: none; stroke: var(--ts-node-stroke,#004d67); stroke-width: 1; }\n.bpmn-event-icon { fill: none; stroke: var(--ts-node-stroke,#004d67); stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }\n";
74
74
  /**
75
75
  * Canonical body emitter, shared by every host (VS Code, JCEF, static export).
76
76
  *
@@ -35,28 +35,28 @@ const LABEL_LINE_H = 13;
35
35
  // BPMN-specific CSS — no font-style:italic per CLAUDE.md design rule
36
36
  // ---------------------------------------------------------------------------
37
37
  export const BPMN_PROCESS_CSS = `
38
- .bpmn-pool { fill: var(--ts-bg-surface,#f8fafc); stroke: var(--ts-node-stroke,#94a3b8); stroke-width: 1.5; }
39
- .bpmn-pool-name { fill: var(--ts-bg-elevated,#f1f5f9); stroke: var(--ts-node-stroke,#94a3b8); stroke-width: 0.75; }
40
- .bpmn-lane { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#94a3b8); stroke-width: 0.75; }
41
- .bpmn-lane-header { fill: var(--ts-bg-elevated,#f1f5f9); stroke: var(--ts-node-stroke,#94a3b8); stroke-width: 0.75; }
42
- .bpmn-pool-label { fill: var(--ts-text-primary,#1e293b); font-size: 11px; font-weight: 700; }
43
- .bpmn-lane-label { fill: var(--ts-text-primary,#1e293b); font-size: 11px; font-weight: 600; }
44
- .bpmn-task { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#94a3b8); stroke-width: 1.5; }
45
- .bpmn-task-name { fill: var(--ts-text-primary,#1e293b); font-size: 11px; text-anchor: middle; }
46
- .bpmn-event { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#94a3b8); }
38
+ .bpmn-pool { fill: var(--ts-bg-surface,#f8fafc); stroke: var(--ts-node-stroke,#004d67); stroke-width: 1.5; }
39
+ .bpmn-pool-name { fill: var(--ts-bg-elevated,#f1f5f9); stroke: var(--ts-node-stroke,#004d67); stroke-width: 0.75; }
40
+ .bpmn-lane { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); stroke-width: 0.75; }
41
+ .bpmn-lane-header { fill: var(--ts-bg-elevated,#f1f5f9); stroke: var(--ts-node-stroke,#004d67); stroke-width: 0.75; }
42
+ .bpmn-pool-label { fill: var(--ts-text-primary,#0d2b35); font-size: 11px; font-weight: 700; }
43
+ .bpmn-lane-label { fill: var(--ts-text-primary,#0d2b35); font-size: 11px; font-weight: 600; }
44
+ .bpmn-task { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); stroke-width: 1.5; }
45
+ .bpmn-task-name { fill: var(--ts-text-primary,#0d2b35); font-size: 11px; text-anchor: middle; }
46
+ .bpmn-event { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); }
47
47
  .bpmn-event-start { stroke-width: 1.5; }
48
48
  .bpmn-event-end { stroke-width: 4; }
49
- .bpmn-event-label { fill: var(--ts-text-secondary,#64748b); font-size: 10px; text-anchor: middle; }
50
- .bpmn-gateway { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#94a3b8); stroke-width: 1.5; }
51
- .bpmn-gateway-marker { stroke: var(--ts-node-stroke,#94a3b8); stroke-width: 2.5; stroke-linecap: round; fill: none; }
52
- .bpmn-gateway-label { fill: var(--ts-text-secondary,#64748b); font-size: 10px; text-anchor: middle; }
53
- .bpmn-seq-flow { fill: none; stroke: var(--ts-edge-stroke,#64748b); stroke-width: 1.5; }
54
- .bpmn-default-mark { stroke: var(--ts-edge-stroke,#64748b); stroke-width: 1.5; stroke-linecap: round; }
55
- .bpmn-assoc { fill: none; stroke: var(--ts-edge-stroke,#64748b); stroke-width: 1; stroke-dasharray: 4 2; }
56
- .bpmn-data-obj { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#94a3b8); stroke-width: 1; }
57
- .bpmn-data-obj-label { fill: var(--ts-text-secondary,#64748b); font-size: 10px; text-anchor: middle; }
58
- .bpmn-event-intermediate { fill: none; stroke: var(--ts-node-stroke,#94a3b8); stroke-width: 1; }
59
- .bpmn-event-icon { fill: none; stroke: var(--ts-node-stroke,#94a3b8); stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
49
+ .bpmn-event-label { fill: var(--ts-text-secondary,#516970); font-size: 10px; text-anchor: middle; }
50
+ .bpmn-gateway { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); stroke-width: 1.5; }
51
+ .bpmn-gateway-marker { stroke: var(--ts-node-stroke,#004d67); stroke-width: 2.5; stroke-linecap: round; fill: none; }
52
+ .bpmn-gateway-label { fill: var(--ts-text-secondary,#516970); font-size: 10px; text-anchor: middle; }
53
+ .bpmn-seq-flow { fill: none; stroke: var(--ts-edge-stroke,#004d67); stroke-width: 1.5; }
54
+ .bpmn-default-mark { stroke: var(--ts-edge-stroke,#004d67); stroke-width: 1.5; stroke-linecap: round; }
55
+ .bpmn-assoc { fill: none; stroke: var(--ts-edge-stroke,#004d67); stroke-width: 1; stroke-dasharray: 4 2; }
56
+ .bpmn-data-obj { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); stroke-width: 1; }
57
+ .bpmn-data-obj-label { fill: var(--ts-text-secondary,#516970); font-size: 10px; text-anchor: middle; }
58
+ .bpmn-event-intermediate { fill: none; stroke: var(--ts-node-stroke,#004d67); stroke-width: 1; }
59
+ .bpmn-event-icon { fill: none; stroke: var(--ts-node-stroke,#004d67); stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
60
60
  `;
61
61
  // ---------------------------------------------------------------------------
62
62
  // Internal helpers
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@transitrix/diagrams",
3
- "version": "1.8.3",
3
+ "version": "1.8.4",
4
4
  "type": "module",
5
5
  "description": "Shared rendering, validation, and pure mutations for Transitrix custom diagram formats.",
6
6
  "license": "MIT",
@@ -82,7 +82,7 @@ export function renderCapabilityTreeSvg(
82
82
  : '';
83
83
 
84
84
  return `<g${nameTitle ? ` title="${nameTitle}"` : ''}>
85
- <rect class="tree-level-${band}" x="${x}" y="${y}" width="${n.width}" height="${n.height}" rx="11" stroke="var(--ts-node-stroke,#94a3b8)" stroke-width="3"/>
85
+ <rect class="tree-level-${band}" x="${x}" y="${y}" width="${n.width}" height="${n.height}" rx="11" stroke="var(--ts-node-stroke,#004d67)" stroke-width="3"/>
86
86
  <rect class="tree-maturity-${mat}" x="${badgeX}" y="${badgeY}" width="${badgeW}" height="${badgeH}" rx="4"/>
87
87
  <text class="text-pill" x="${badgeX + badgeW / 2}" y="${badgeY + badgeH / 2}" text-anchor="middle" fill="white">L${mat}</text>
88
88
  <text class="text-primary" x="${textX}" y="${nameY}" dominant-baseline="central">${escXml(nameText)}</text>
@@ -102,7 +102,7 @@ export function renderCapabilityTreeSvg(
102
102
  const bx = legendX;
103
103
  legendX += bw;
104
104
  if (count === 0) return '';
105
- return `<rect class="tree-level-${i}" x="${bx}" y="${legendY}" width="${bw}" height="24" rx="4" stroke="var(--ts-node-stroke,#94a3b8)" stroke-width="1"/>
105
+ return `<rect class="tree-level-${i}" x="${bx}" y="${legendY}" width="${bw}" height="24" rx="4" stroke="var(--ts-node-stroke,#004d67)" stroke-width="1"/>
106
106
  <text class="text-id" x="${bx + bw / 2}" y="${legendY + 12}" text-anchor="middle" dominant-baseline="central">${escXml(BAND_LABELS[i])}</text>`;
107
107
  }).filter(Boolean).join('\n');
108
108
 
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Brand default-theme regression tests (hub issue #548).
3
+ *
4
+ * Locks in the three-role brand mapping from `brand/transitrix_brand.md`:
5
+ * - Petrol = structure (node/edge stroke) and hierarchy depth (fill tint).
6
+ * - A depth ramp never switches hue — only lightness/saturation move.
7
+ * - Text stays WCAG-AA readable against every fill it can be drawn on.
8
+ * `hc` (VS Code high-contrast) is intentionally excluded — it's an
9
+ * accessibility override, not one of the three brand-contract themes.
10
+ */
11
+ import { describe, expect, it } from 'vitest';
12
+ import {
13
+ BRAND,
14
+ BRAND_EMPHASIS,
15
+ LAYER_COLORS,
16
+ LEVEL_COLORS,
17
+ MATURITY_COLORS,
18
+ TREE_MATURITY_COLORS,
19
+ STRUCTURAL,
20
+ TREE_LEVEL_COLORS,
21
+ } from '../tokens.js';
22
+
23
+ function hexToRgb(hex: string): [number, number, number] {
24
+ const n = hex.replace('#', '');
25
+ return [0, 2, 4].map((i) => parseInt(n.slice(i, i + 2), 16)) as [number, number, number];
26
+ }
27
+
28
+ function relativeLuminance([r, g, b]: [number, number, number]): number {
29
+ const f = (v: number): number => {
30
+ const c = v / 255;
31
+ return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
32
+ };
33
+ const [R, G, B] = [f(r), f(g), f(b)];
34
+ return 0.2126 * R + 0.7152 * G + 0.0722 * B;
35
+ }
36
+
37
+ /** WCAG 2.x contrast ratio between two hex colors (1..21). */
38
+ function contrast(a: string, b: string): number {
39
+ const la = relativeLuminance(hexToRgb(a));
40
+ const lb = relativeLuminance(hexToRgb(b));
41
+ const [hi, lo] = la > lb ? [la, lb] : [lb, la];
42
+ return (hi + 0.05) / (lo + 0.05);
43
+ }
44
+
45
+ /** Hue in degrees (0-360) from a hex color; undefined (grey) hues return -1. */
46
+ function hue(hex: string): number {
47
+ const [r, g, b] = hexToRgb(hex).map((v) => v / 255);
48
+ const max = Math.max(r, g, b), min = Math.min(r, g, b);
49
+ const delta = max - min;
50
+ if (delta < 0.001) return -1; // achromatic
51
+ let h: number;
52
+ if (max === r) h = ((g - b) / delta) % 6;
53
+ else if (max === g) h = (b - r) / delta + 2;
54
+ else h = (r - g) / delta + 4;
55
+ h *= 60;
56
+ return h < 0 ? h + 360 : h;
57
+ }
58
+
59
+ const PETROL_HUE = hue(BRAND.petrol);
60
+ const HUE_TOLERANCE = 6; // degrees — rounding slop from hand-tuned hex values
61
+
62
+ function expectSameHueFamily(hexes: string[], label: string): void {
63
+ for (const hex of hexes) {
64
+ const h = hue(hex);
65
+ const diff = Math.min(Math.abs(h - PETROL_HUE), 360 - Math.abs(h - PETROL_HUE));
66
+ expect(diff, `${label} ${hex} should stay in the petrol hue family (got hue ${h.toFixed(1)}, petrol is ${PETROL_HUE.toFixed(1)})`).toBeLessThanOrEqual(HUE_TOLERANCE);
67
+ }
68
+ }
69
+
70
+ // Minimum AA text-contrast ratios this design targets. Primary/secondary text
71
+ // is small (10-12px) so we hold the full 4.5:1 normal-text bar for the name
72
+ // label, and the calibrated ~4.0 bar the pre-brand palette already shipped
73
+ // for the muted secondary/id label (see brand-ramp calibration in PR #548).
74
+ const MIN_PRIMARY_CONTRAST = 4.5;
75
+ const MIN_SECONDARY_CONTRAST = 4.0;
76
+
77
+ describe('brand default theme (#548) — structure is petrol', () => {
78
+ it('nodeStroke and edgeStroke are brand petrol in the light theme', () => {
79
+ expect(STRUCTURAL.nodeStroke.light).toBe(BRAND.petrol);
80
+ expect(STRUCTURAL.edgeStroke.light).toBe(BRAND.petrol);
81
+ });
82
+
83
+ it('nodeStroke/edgeStroke stay in the petrol hue family in the dark theme', () => {
84
+ expectSameHueFamily([STRUCTURAL.nodeStroke.dark, STRUCTURAL.edgeStroke.dark], 'dark stroke');
85
+ });
86
+ });
87
+
88
+ describe('brand default theme (#548) — tint depth never switches hue', () => {
89
+ it('LEVEL_COLORS light ramp stays in the petrol hue family', () => {
90
+ expectSameHueFamily([...LEVEL_COLORS.light], 'LEVEL light');
91
+ });
92
+ it('LEVEL_COLORS dark ramp stays in the petrol hue family', () => {
93
+ expectSameHueFamily([...LEVEL_COLORS.dark], 'LEVEL dark');
94
+ });
95
+ it('LAYER_COLORS (DGCA/DGA columns) stay in the petrol hue family, both themes', () => {
96
+ const keys = ['driver', 'factor', 'goal', 'change', 'activity'] as const;
97
+ expectSameHueFamily(keys.map((k) => LAYER_COLORS[k].light), 'LAYER light');
98
+ expectSameHueFamily(keys.map((k) => LAYER_COLORS[k].dark), 'LAYER dark');
99
+ });
100
+ it('TREE_LEVEL_COLORS (capability map bands) stay in the petrol hue family, both themes', () => {
101
+ const keys = ['band0', 'band1', 'band2'] as const;
102
+ expectSameHueFamily(keys.map((k) => TREE_LEVEL_COLORS[k].light), 'BAND light');
103
+ expectSameHueFamily(keys.map((k) => TREE_LEVEL_COLORS[k].dark), 'BAND dark');
104
+ });
105
+ });
106
+
107
+ describe('brand default theme (#548) — WCAG-AA text-on-fill contrast', () => {
108
+ function checkFills(label: string, fills: string[], textPrimary: string, textSecondary: string): void {
109
+ for (const fill of fills) {
110
+ expect(contrast(fill, textPrimary), `${label} ${fill} vs text-primary`).toBeGreaterThanOrEqual(MIN_PRIMARY_CONTRAST);
111
+ expect(contrast(fill, textSecondary), `${label} ${fill} vs text-secondary`).toBeGreaterThanOrEqual(MIN_SECONDARY_CONTRAST);
112
+ }
113
+ }
114
+
115
+ it('LEVEL_COLORS — every slot, both themes', () => {
116
+ checkFills('LEVEL light', [...LEVEL_COLORS.light], STRUCTURAL.textPrimary.light, STRUCTURAL.textSecondary.light);
117
+ checkFills('LEVEL dark', [...LEVEL_COLORS.dark], STRUCTURAL.textPrimary.dark, STRUCTURAL.textSecondary.dark);
118
+ });
119
+
120
+ it('LAYER_COLORS — every column, both themes', () => {
121
+ const keys = ['driver', 'factor', 'goal', 'change', 'activity'] as const;
122
+ checkFills('LAYER light', keys.map((k) => LAYER_COLORS[k].light), STRUCTURAL.textPrimary.light, STRUCTURAL.textSecondary.light);
123
+ checkFills('LAYER dark', keys.map((k) => LAYER_COLORS[k].dark), STRUCTURAL.textPrimary.dark, STRUCTURAL.textSecondary.dark);
124
+ });
125
+
126
+ it('TREE_LEVEL_COLORS — every band, both themes', () => {
127
+ const keys = ['band0', 'band1', 'band2'] as const;
128
+ checkFills('BAND light', keys.map((k) => TREE_LEVEL_COLORS[k].light), STRUCTURAL.textPrimary.light, STRUCTURAL.textSecondary.light);
129
+ checkFills('BAND dark', keys.map((k) => TREE_LEVEL_COLORS[k].dark), STRUCTURAL.textPrimary.dark, STRUCTURAL.textSecondary.dark);
130
+ });
131
+
132
+ it('textPrimary/textSecondary stay readable against the shell background', () => {
133
+ expect(contrast(STRUCTURAL.textPrimary.light, '#ffffff')).toBeGreaterThanOrEqual(MIN_PRIMARY_CONTRAST);
134
+ expect(contrast(STRUCTURAL.textPrimary.dark, '#0a1628')).toBeGreaterThanOrEqual(MIN_PRIMARY_CONTRAST);
135
+ });
136
+ });
137
+
138
+ describe('brand default theme (#548) — amber/orange emphasis stays distinct from functional colors', () => {
139
+ it('does not reuse brand amber/orange for functional status', () => {
140
+ // FUNCTIONAL warning/error must remain visually distinct from the brand
141
+ // accents so a red error is never confused with brand orange, per
142
+ // brand/transitrix_brand.md "Don't reuse brand orange as semantic error".
143
+ expect(BRAND_EMPHASIS.orange.light).toBe(BRAND.orange);
144
+ expect(BRAND_EMPHASIS.amber.light).toBe(BRAND.amber);
145
+ });
146
+
147
+ it('critical-path emphasis tint pairs with the orange stroke, both themes', () => {
148
+ expect(contrast(BRAND_EMPHASIS.orange.light, BRAND_EMPHASIS.orangeTint.light)).toBeGreaterThan(1.5);
149
+ expect(contrast(BRAND_EMPHASIS.orange.dark, BRAND_EMPHASIS.orangeTint.dark)).toBeGreaterThan(1.5);
150
+ });
151
+ });
152
+
153
+ describe('maturity Likert ramp — one harmonious scale, white-label contrast', () => {
154
+ // .maturity-pill / tree-maturity badges hardcode white label text
155
+ // (capability-map-preview.ts, render-capability-tree.ts) regardless of
156
+ // theme, so every step needs AA (light/dark) or AAA (hc, since VS Code
157
+ // high-contrast mode exists specifically to demand stronger contrast)
158
+ // against white — not against the page's own text-primary/secondary.
159
+ const MIN_WHITE_CONTRAST_AA = 4.5;
160
+ const MIN_WHITE_CONTRAST_AAA = 7;
161
+
162
+ it('MATURITY_COLORS — every step reads white text at AA, light + dark', () => {
163
+ for (const hex of MATURITY_COLORS.light) {
164
+ expect(contrast(hex, '#ffffff'), `MATURITY light ${hex} vs white`).toBeGreaterThanOrEqual(MIN_WHITE_CONTRAST_AA);
165
+ }
166
+ for (const hex of MATURITY_COLORS.dark) {
167
+ expect(contrast(hex, '#ffffff'), `MATURITY dark ${hex} vs white`).toBeGreaterThanOrEqual(MIN_WHITE_CONTRAST_AA);
168
+ }
169
+ });
170
+
171
+ it('MATURITY_COLORS hc — every step reads white text at AAA', () => {
172
+ for (const hex of MATURITY_COLORS.hc) {
173
+ expect(contrast(hex, '#ffffff'), `MATURITY hc ${hex} vs white`).toBeGreaterThanOrEqual(MIN_WHITE_CONTRAST_AAA);
174
+ }
175
+ });
176
+
177
+ it('TREE_MATURITY_COLORS is the same harmonious ramp as MATURITY_COLORS', () => {
178
+ // Previously a divergent DSM-matching pink/yellow/blue set (plus a grey
179
+ // L1, an odd "no signal" hue for the worst rating) — "maturity" now
180
+ // means the same colors everywhere it's shown.
181
+ expect(TREE_MATURITY_COLORS.light).toEqual(MATURITY_COLORS.light);
182
+ expect(TREE_MATURITY_COLORS.dark).toEqual(MATURITY_COLORS.dark);
183
+ expect(TREE_MATURITY_COLORS.hc).toEqual(MATURITY_COLORS.hc);
184
+ });
185
+ });
@@ -1,4 +1,4 @@
1
1
  export type { AdaptiveTokens } from './tokens.js';
2
- export { BRAND, FUNCTIONAL, LAYER_COLORS, LEVEL_COLORS, MATURITY_COLORS, TREE_LEVEL_COLORS, TREE_MATURITY_COLORS, STRUCTURAL, TYPOGRAPHY, CSS_VAR, getBaseResetCss } from './tokens.js';
2
+ export { BRAND, BRAND_EMPHASIS, FUNCTIONAL, LAYER_COLORS, LEVEL_COLORS, MATURITY_COLORS, TREE_LEVEL_COLORS, TREE_MATURITY_COLORS, STRUCTURAL, TYPOGRAPHY, CSS_VAR, getBaseResetCss } from './tokens.js';
3
3
  export type { ThemeId } from './themes.js';
4
4
  export { generateWebviewCss, generateSvgEmbedCss } from './themes.js';
@@ -5,6 +5,7 @@ import {
5
5
  TREE_LEVEL_COLORS,
6
6
  TREE_MATURITY_COLORS,
7
7
  STRUCTURAL,
8
+ BRAND_EMPHASIS,
8
9
  TYPOGRAPHY,
9
10
  CSS_VAR,
10
11
  getBaseResetCss,
@@ -106,6 +107,9 @@ function diagramVars(variant: 'light' | 'dark' | 'hc'): string {
106
107
  `${CSS_VAR.maturity3}:${mat[2]}`,
107
108
  `${CSS_VAR.maturity4}:${mat[3]}`,
108
109
  `${CSS_VAR.maturity5}:${mat[4]}`,
110
+ `${CSS_VAR.brandAmber}:${BRAND_EMPHASIS.amber[variant]}`,
111
+ `${CSS_VAR.brandOrange}:${BRAND_EMPHASIS.orange[variant]}`,
112
+ `${CSS_VAR.brandOrangeTint}:${BRAND_EMPHASIS.orangeTint[variant]}`,
109
113
  ].join(';') + ';' + levelVars + treeVars;
110
114
  }
111
115
 
@@ -1,4 +1,4 @@
1
- /** Transitrix brand palette. */
1
+ /** Transitrix brand palette (`brand/transitrix_brand.md` — canonical). */
2
2
  export const BRAND = {
3
3
  petrol: '#004d67',
4
4
  amber: '#ffaf00',
@@ -13,61 +13,109 @@ export const FUNCTIONAL = {
13
13
  info: '#1d6fa8',
14
14
  } as const;
15
15
 
16
- /** Fill colors for FGCA/DGCA notation columns. */
16
+ /**
17
+ * Amber/orange "author emphasis" accents, theme-adjusted for contrast against
18
+ * each theme's background (dark theme gets the brand hues lightened per
19
+ * `brand/transitrix_brand.md` §Themes: "brand colours slightly desaturated
20
+ * for contrast on dark surfaces"). `orangeTint` is the light emphasis-fill
21
+ * paired with an `orange` stroke (e.g. Activities critical-path nodes).
22
+ * `hc` intentionally falls back to the plain BRAND hex — VS Code
23
+ * high-contrast mode prioritises accessibility overrides over brand fidelity.
24
+ */
25
+ export const BRAND_EMPHASIS = {
26
+ amber: { light: BRAND.amber, dark: '#ffc83d', hc: BRAND.amber },
27
+ orange: { light: BRAND.orange, dark: '#ff7542', hc: BRAND.orange },
28
+ orangeTint: { light: '#ffeee5', dark: '#432114', hc: '#ffeee5' },
29
+ } as const;
30
+
31
+ /**
32
+ * Fill colors for FGCA/DGCA notation columns — a petrol tint per stage
33
+ * (Driver → Goal → Change → Activity), deepening left to right like the
34
+ * Goals-tree level ladder below. Petrol tint = structural position, never a
35
+ * hue switch, per the brand's "structure via petrol" role mapping.
36
+ */
17
37
  export const LAYER_COLORS = {
18
- driver: { light: '#fef3c7', dark: '#292108', hc: '#f59e0b' },
19
- factor: { light: '#fef3c7', dark: '#292108', hc: '#f59e0b' },
20
- goal: { light: '#e0e7ff', dark: '#1e1b4b', hc: '#818cf8' },
21
- change: { light: '#dbeafe', dark: '#0c2042', hc: '#60a5fa' },
22
- activity: { light: '#d4edda', dark: '#0a2414', hc: '#4ade80' },
38
+ driver: { light: '#eaf3f6', dark: '#17262c', hc: '#f59e0b' },
39
+ factor: { light: '#eaf3f6', dark: '#17262c', hc: '#f59e0b' },
40
+ goal: { light: '#d9ecf2', dark: '#192f37', hc: '#818cf8' },
41
+ change: { light: '#c7e5ef', dark: '#1a3842', hc: '#60a5fa' },
42
+ activity: { light: '#b4e0ee', dark: '#1a414e', hc: '#4ade80' },
23
43
  } as const;
24
44
 
25
- /** Level fill colors for Goals tree nodes — 8 slots, cycled by level index. */
45
+ /**
46
+ * Level fill colors for Goals tree nodes — 8 slots, cycled by level index.
47
+ * A single-hue petrol tint ramp (level-0 lightest/root → level-7 deepest);
48
+ * hue never switches with depth. Saturation/lightness tuned so text-primary
49
+ * and text-secondary keep at least their pre-brand contrast ratio against
50
+ * every slot — see `theme/__tests__/tokens.test.ts`.
51
+ */
26
52
  export const LEVEL_COLORS = {
27
- light: ['#dbeafe', '#e0e7ff', '#d4edda', '#fef3c7', '#fce7f3', '#e0f2fe', '#f3e8ff', '#fef9c3'],
28
- dark: ['#0c2042', '#1e1b4b', '#0a2414', '#292108', '#2a0c1f', '#071e2e', '#1a0c33', '#2a2107'],
53
+ light: ['#eef4f7', '#e6f1f4', '#dfedf1', '#d7e9ef', '#cee6ed', '#c6e2ec', '#bfe0eb', '#b7deeb'],
54
+ dark: ['#152328', '#16292f', '#172e36', '#18333d', '#183944', '#183f4c', '#184554', '#174b5c'],
29
55
  hc: ['#60a5fa', '#818cf8', '#4ade80', '#f59e0b', '#f472b6', '#38bdf8', '#a78bfa', '#facc15'],
30
56
  } as const;
31
57
 
32
- /** Structural diagram colors (nodes, edges, text). */
58
+ /**
59
+ * Structural diagram colors (nodes, edges, text). nodeStroke/edgeStroke are
60
+ * brand petrol — "structure via petrol" per the brand's role mapping.
61
+ * textPrimary is a dark petrol ink (light theme) / near-white petrol-tinted
62
+ * (dark theme); textSecondary stays a muted grey with a faint petrol lean.
63
+ * `hc` is untouched — VS Code high-contrast accessibility overrides win over
64
+ * brand fidelity there.
65
+ */
33
66
  export const STRUCTURAL = {
34
- nodeStroke: { light: '#94a3b8', dark: '#475569', hc: '#e2e8f0' },
35
- edgeStroke: { light: '#94a3b8', dark: '#475569', hc: '#e2e8f0' },
36
- textPrimary: { light: '#1e293b', dark: '#f1f5f9', hc: '#ffffff' },
37
- textSecondary: { light: '#64748b', dark: '#94a3b8', hc: '#cbd5e1' },
38
- headerText: { light: '#374151', dark: '#e5e7eb', hc: '#ffffff' },
67
+ nodeStroke: { light: BRAND.petrol, dark: '#53a9c6', hc: '#e2e8f0' },
68
+ edgeStroke: { light: BRAND.petrol, dark: '#53a9c6', hc: '#e2e8f0' },
69
+ textPrimary: { light: '#0d2b35', dark: '#edf5f8', hc: '#ffffff' },
70
+ textSecondary: { light: '#516970', dark: '#adc8d1', hc: '#cbd5e1' },
71
+ headerText: { light: '#0d2b35', dark: '#edf5f8', hc: '#ffffff' },
39
72
  } as const;
40
73
 
41
74
  /**
42
- * Maturity scale fill colors — Likert 1..5, danger→success.
75
+ * Maturity scale fill colors — Likert 1..5, danger→success. A single
76
+ * harmonious ramp: consistent saturation curve, hue stepping smoothly
77
+ * red→orange→gold→yellow-green→green (never jumping registers the way the
78
+ * previous per-theme picks did), tuned so the badge's hardcoded white label
79
+ * text (`.maturity-pill`, capability-map-preview.ts) stays ≥4.5:1 AA at
80
+ * every step. Same ramp for light/dark — these are small solid badges, not
81
+ * background tints, so theme brightness doesn't change the contrast need.
82
+ * `hc` pushes the same hues darker/more saturated for AAA (≥7:1), since VS
83
+ * Code high-contrast mode exists specifically to demand stronger contrast.
43
84
  * Used by Capability Map and any future maturity-based notation.
44
85
  */
45
86
  export const MATURITY_COLORS = {
46
- light: ['#b91c1c', '#d97706', '#ca8a04', '#65a30d', '#15803d'],
47
- dark: ['#7f1d1d', '#92400e', '#854d0e', '#3f6212', '#14532d'],
48
- hc: ['#f87171', '#fb923c', '#facc15', '#a3e635', '#4ade80'],
87
+ light: ['#bf2518', '#b05911', '#8d7011', '#407d21', '#237b48'],
88
+ dark: ['#bf2518', '#b05911', '#8d7011', '#407d21', '#237b48'],
89
+ hc: ['#a0190d', '#864109', '#5a4607', '#316317', '#155630'],
49
90
  } as const;
50
91
 
51
92
  /**
52
- * DSM-matching capability tree node fill colors, grouped by depth band.
53
- * band0: depth 0–2 (pink)
54
- * band1: depth 3–4 (yellow)
55
- * band2: depth 5+ (blue)
93
+ * Capability tree node fill colors, grouped by depth band — a 3-step petrol
94
+ * tint (shallow → deep), same "petrol tint = structural depth" rule as
95
+ * LEVEL_COLORS above. Previously a DSM-matching pink/yellow/blue scheme;
96
+ * brand/transitrix_brand.md ("Components") anticipates DSM adopting the same
97
+ * shared color tokens, so Studio moving first is convergence, not drift.
98
+ * band0: depth 0–2 (shallowest)
99
+ * band1: depth 3–4
100
+ * band2: depth 5+ (deepest)
56
101
  */
57
102
  export const TREE_LEVEL_COLORS = {
58
- band0: { light: '#fee0e0', dark: '#3d0f0f', hc: '#ff9999' },
59
- band1: { light: '#ffffcb', dark: '#3a3500', hc: '#ffff88' },
60
- band2: { light: '#c2f0ff', dark: '#003d4d', hc: '#66ccff' },
103
+ band0: { light: '#e6f1f4', dark: '#152b32', hc: '#ff9999' },
104
+ band1: { light: '#d0e9f1', dark: '#153b47', hc: '#ffff88' },
105
+ band2: { light: '#b5e3f2', dark: '#114c5f', hc: '#66ccff' },
61
106
  } as const;
62
107
 
63
108
  /**
64
- * DSM-matching maturity badge fills for the capability tree (L1–L5).
65
- * Distinct from MATURITY_COLORS which drives the cards / compliance views.
109
+ * Maturity badge fills for the capability tree (L1–L5). Same ramp as
110
+ * MATURITY_COLORS — previously a separate DSM-matching pink/yellow/blue set
111
+ * that didn't even share L1's meaning (grey = lowest maturity, an odd
112
+ * "no signal" hue for the worst rating) and had white-label contrast as low
113
+ * as 1.07:1 in `hc`. One harmonious Likert ramp for "maturity" everywhere.
66
114
  */
67
115
  export const TREE_MATURITY_COLORS = {
68
- light: ['#c1c1c1', '#ff3d67', '#ff9a59', '#fdef59', '#83cca3'],
69
- dark: ['#555555', '#cc1040', '#d06020', '#c8c000', '#4d9970'],
70
- hc: ['#cccccc', '#ff6699', '#ffbb77', '#ffff44', '#88ddaa'],
116
+ light: MATURITY_COLORS.light,
117
+ dark: MATURITY_COLORS.dark,
118
+ hc: MATURITY_COLORS.hc,
71
119
  } as const;
72
120
 
73
121
  /**
@@ -158,6 +206,9 @@ export const CSS_VAR = {
158
206
  treeMaturity3: '--ts-tree-maturity-3',
159
207
  treeMaturity4: '--ts-tree-maturity-4',
160
208
  treeMaturity5: '--ts-tree-maturity-5',
209
+ brandAmber: '--ts-brand-amber',
210
+ brandOrange: '--ts-brand-orange',
211
+ brandOrangeTint: '--ts-brand-orange-tint',
161
212
  } as const;
162
213
 
163
214
  export function getBaseResetCss(): string {
@@ -38,8 +38,8 @@ const N_PAD = 24;
38
38
  * self-contained for the JCEF host.
39
39
  */
40
40
  export const ACTIVITIES_NETWORK_CSS = `
41
- .act-node { fill: var(--ts-layer-activity, #d4edda); stroke: var(--ts-node-stroke, #94a3b8); stroke-width: 1; }
42
- .critical-node { fill: #fff7ed; stroke: var(--ts-brand-orange, #ff4d00); stroke-width: 2.5; }
41
+ .act-node { fill: var(--ts-layer-activity, #d4edda); stroke: var(--ts-node-stroke, #004d67); stroke-width: 1; }
42
+ .critical-node { fill: var(--ts-brand-orange-tint, #ffeee5); stroke: var(--ts-brand-orange, #ff4d00); stroke-width: 2.5; }
43
43
  .milestone-node { fill: #ecfeff; stroke: var(--ts-text-muted, #64748b); stroke-dasharray: 4 2; }
44
44
  .critical-edge { stroke: var(--ts-brand-orange, #ff4d00); stroke-width: 2; }
45
45
  .arrow-fill-critical { fill: var(--ts-brand-orange, #ff4d00); }
@@ -112,28 +112,28 @@ const LABEL_LINE_H = 13;
112
112
  // ---------------------------------------------------------------------------
113
113
 
114
114
  export const BPMN_PROCESS_CSS = `
115
- .bpmn-pool { fill: var(--ts-bg-surface,#f8fafc); stroke: var(--ts-node-stroke,#94a3b8); stroke-width: 1.5; }
116
- .bpmn-pool-name { fill: var(--ts-bg-elevated,#f1f5f9); stroke: var(--ts-node-stroke,#94a3b8); stroke-width: 0.75; }
117
- .bpmn-lane { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#94a3b8); stroke-width: 0.75; }
118
- .bpmn-lane-header { fill: var(--ts-bg-elevated,#f1f5f9); stroke: var(--ts-node-stroke,#94a3b8); stroke-width: 0.75; }
119
- .bpmn-pool-label { fill: var(--ts-text-primary,#1e293b); font-size: 11px; font-weight: 700; }
120
- .bpmn-lane-label { fill: var(--ts-text-primary,#1e293b); font-size: 11px; font-weight: 600; }
121
- .bpmn-task { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#94a3b8); stroke-width: 1.5; }
122
- .bpmn-task-name { fill: var(--ts-text-primary,#1e293b); font-size: 11px; text-anchor: middle; }
123
- .bpmn-event { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#94a3b8); }
115
+ .bpmn-pool { fill: var(--ts-bg-surface,#f8fafc); stroke: var(--ts-node-stroke,#004d67); stroke-width: 1.5; }
116
+ .bpmn-pool-name { fill: var(--ts-bg-elevated,#f1f5f9); stroke: var(--ts-node-stroke,#004d67); stroke-width: 0.75; }
117
+ .bpmn-lane { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); stroke-width: 0.75; }
118
+ .bpmn-lane-header { fill: var(--ts-bg-elevated,#f1f5f9); stroke: var(--ts-node-stroke,#004d67); stroke-width: 0.75; }
119
+ .bpmn-pool-label { fill: var(--ts-text-primary,#0d2b35); font-size: 11px; font-weight: 700; }
120
+ .bpmn-lane-label { fill: var(--ts-text-primary,#0d2b35); font-size: 11px; font-weight: 600; }
121
+ .bpmn-task { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); stroke-width: 1.5; }
122
+ .bpmn-task-name { fill: var(--ts-text-primary,#0d2b35); font-size: 11px; text-anchor: middle; }
123
+ .bpmn-event { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); }
124
124
  .bpmn-event-start { stroke-width: 1.5; }
125
125
  .bpmn-event-end { stroke-width: 4; }
126
- .bpmn-event-label { fill: var(--ts-text-secondary,#64748b); font-size: 10px; text-anchor: middle; }
127
- .bpmn-gateway { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#94a3b8); stroke-width: 1.5; }
128
- .bpmn-gateway-marker { stroke: var(--ts-node-stroke,#94a3b8); stroke-width: 2.5; stroke-linecap: round; fill: none; }
129
- .bpmn-gateway-label { fill: var(--ts-text-secondary,#64748b); font-size: 10px; text-anchor: middle; }
130
- .bpmn-seq-flow { fill: none; stroke: var(--ts-edge-stroke,#64748b); stroke-width: 1.5; }
131
- .bpmn-default-mark { stroke: var(--ts-edge-stroke,#64748b); stroke-width: 1.5; stroke-linecap: round; }
132
- .bpmn-assoc { fill: none; stroke: var(--ts-edge-stroke,#64748b); stroke-width: 1; stroke-dasharray: 4 2; }
133
- .bpmn-data-obj { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#94a3b8); stroke-width: 1; }
134
- .bpmn-data-obj-label { fill: var(--ts-text-secondary,#64748b); font-size: 10px; text-anchor: middle; }
135
- .bpmn-event-intermediate { fill: none; stroke: var(--ts-node-stroke,#94a3b8); stroke-width: 1; }
136
- .bpmn-event-icon { fill: none; stroke: var(--ts-node-stroke,#94a3b8); stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
126
+ .bpmn-event-label { fill: var(--ts-text-secondary,#516970); font-size: 10px; text-anchor: middle; }
127
+ .bpmn-gateway { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); stroke-width: 1.5; }
128
+ .bpmn-gateway-marker { stroke: var(--ts-node-stroke,#004d67); stroke-width: 2.5; stroke-linecap: round; fill: none; }
129
+ .bpmn-gateway-label { fill: var(--ts-text-secondary,#516970); font-size: 10px; text-anchor: middle; }
130
+ .bpmn-seq-flow { fill: none; stroke: var(--ts-edge-stroke,#004d67); stroke-width: 1.5; }
131
+ .bpmn-default-mark { stroke: var(--ts-edge-stroke,#004d67); stroke-width: 1.5; stroke-linecap: round; }
132
+ .bpmn-assoc { fill: none; stroke: var(--ts-edge-stroke,#004d67); stroke-width: 1; stroke-dasharray: 4 2; }
133
+ .bpmn-data-obj { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); stroke-width: 1; }
134
+ .bpmn-data-obj-label { fill: var(--ts-text-secondary,#516970); font-size: 10px; text-anchor: middle; }
135
+ .bpmn-event-intermediate { fill: none; stroke: var(--ts-node-stroke,#004d67); stroke-width: 1; }
136
+ .bpmn-event-icon { fill: none; stroke: var(--ts-node-stroke,#004d67); stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
137
137
  `;
138
138
 
139
139
  // ---------------------------------------------------------------------------