Skip to content

Commit 30a0aec

Browse files
committed
feat(grounding): get_variable_defs emits hex for COLOR values
A tool-layer eval (3 clean subagents answering grounding questions over a live design file) surfaced a systematic friction: get_variable_defs returned COLOR values as 0–1 float RGBA only, while get_design_context's globalVars already emit hex — so every runner had to hand-convert RGBA and reconcile two color formats. All three hit it; n=1 had mis-located the fix at get_design_context. Add a `hex` (#RRGGBB / #RRGGBBAA, alpha only when < 1) alongside the RGBA channels on COLOR variable values, mirroring globalVars exactly (shared toHex). Pure addition: RGBA channels stay, so token_map and other consumers are untouched. Scoped via SerializedRGBASchema.extend so fills/strokes RGBA is not affected. Verified live against a real design file's color variables.
1 parent 92de535 commit 30a0aec

3 files changed

Lines changed: 74 additions & 8 deletions

File tree

packages/plugin/src/handlers/get-variable-defs.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import type { GetVariableDefsResult, SerializedVariableValue } from '@figma-mcp-relay/shared';
1+
import {
2+
type GetVariableDefsResult,
3+
type SerializedVariableValue,
4+
toHex,
5+
} from '@figma-mcp-relay/shared';
26

37
import type { SandboxToolHandler } from '../dispatcher.js';
48

@@ -8,7 +12,10 @@ const serializeVariableValue = (value: VariableValue): SerializedVariableValue =
812
return { type: 'VARIABLE_ALIAS', id: value.id };
913
}
1014
const color = value as RGB | RGBA;
11-
return { r: color.r, g: color.g, b: color.b, a: 'a' in color ? color.a : 1 };
15+
const a = 'a' in color ? color.a : 1;
16+
// hex mirrors get_design_context's globalVars (#RRGGBB / #RRGGBBAA) so a bound color resolves in
17+
// one tool, without hand-converting normalised RGBA. RGBA channels stay for back-compat.
18+
return { r: color.r, g: color.g, b: color.b, a, hex: toHex(color, a) };
1219
}
1320
return value;
1421
};

packages/plugin/test/handlers/get-variable-defs.test.ts

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,62 @@ describe('get_variable_defs handler', () => {
5959
variableIds: ['V:1', 'V:2'],
6060
});
6161
expect(result.collections[0]?.modes).toHaveLength(2);
62-
// RGB (no alpha) is normalised to a=1
63-
expect(result.variables[0]?.valuesByMode.m2).toEqual({ r: 0, g: 0, b: 0, a: 1 });
64-
expect(result.variables[0]?.valuesByMode.m1).toEqual({ r: 1, g: 1, b: 1, a: 1 });
62+
// RGB (no alpha) is normalised to a=1, with a hex mirror
63+
expect(result.variables[0]?.valuesByMode.m2).toEqual({
64+
r: 0,
65+
g: 0,
66+
b: 0,
67+
a: 1,
68+
hex: '#000000',
69+
});
70+
expect(result.variables[0]?.valuesByMode.m1).toEqual({
71+
r: 1,
72+
g: 1,
73+
b: 1,
74+
a: 1,
75+
hex: '#FFFFFF',
76+
});
6577
// alias passthrough
6678
expect(result.variables[1]?.valuesByMode.m1).toEqual({ type: 'VARIABLE_ALIAS', id: 'V:1' });
6779
});
6880

81+
it('adds a hex mirror to COLOR values, 8-digit when alpha < 1', async () => {
82+
const handler = createGetVariableDefsHandler(
83+
fakeFigma(
84+
[],
85+
[
86+
{
87+
id: 'V:4',
88+
name: 'color/scrim',
89+
key: 'vk4',
90+
resolvedType: 'COLOR',
91+
variableCollectionId: 'VC:1',
92+
valuesByMode: {
93+
opaque: { r: 1, g: 1, b: 1, a: 1 },
94+
scrim: { r: 1, g: 1, b: 1, a: 0.6 },
95+
},
96+
},
97+
],
98+
),
99+
);
100+
const result = (await handler(undefined)) as GetVariableDefsResult;
101+
expect(result.variables[0]?.valuesByMode.opaque).toEqual({
102+
r: 1,
103+
g: 1,
104+
b: 1,
105+
a: 1,
106+
hex: '#FFFFFF',
107+
});
108+
// alpha < 1 → 8-digit hex (0.6 × 255 = 153 = 0x99), matching globalVars
109+
expect(result.variables[0]?.valuesByMode.scrim).toEqual({
110+
r: 1,
111+
g: 1,
112+
b: 1,
113+
a: 0.6,
114+
hex: '#FFFFFF99',
115+
});
116+
});
117+
69118
it('passes primitive values through unchanged', async () => {
70119
const handler = createGetVariableDefsHandler(
71120
fakeFigma(

packages/shared/src/variables.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,24 @@ export const SerializedVariableAliasSchema = z.object({
99
export type SerializedVariableAlias = z.infer<typeof SerializedVariableAliasSchema>;
1010

1111
/**
12-
* A resolved value for one mode: primitive, color (RGB normalised to RGBA), or an alias to another
13-
* variable.
12+
* A COLOR variable value: RGBA plus a convenience `hex` (#RRGGBB / #RRGGBBAA, alpha only when < 1)
13+
* mirroring the hex that get_design_context's globalVars already emits — so an agent reading a
14+
* bound variable's color needn't convert normalised RGBA by hand or cross to a second tool. RGBA
15+
* stays for back-compat (token_map and other consumers read the channels directly).
16+
*/
17+
export const SerializedVariableColorSchema = SerializedRGBASchema.extend({
18+
hex: z.string().optional(),
19+
});
20+
21+
/**
22+
* A resolved value for one mode: primitive, color (RGB normalised to RGBA + hex), or an alias to
23+
* another variable.
1424
*/
1525
export const SerializedVariableValueSchema = z.union([
1626
z.boolean(),
1727
z.number(),
1828
z.string(),
19-
SerializedRGBASchema,
29+
SerializedVariableColorSchema,
2030
SerializedVariableAliasSchema,
2131
]);
2232
export type SerializedVariableValue = z.infer<typeof SerializedVariableValueSchema>;

0 commit comments

Comments
 (0)