@servicetitan/hammer-token 2.5.2 → 3.0.1
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.
- package/CHANGELOG.md +52 -2
- package/README.md +332 -0
- package/build/web/core/component-variables.scss +1088 -131
- package/build/web/core/component.d.ts +558 -0
- package/build/web/core/component.js +6685 -249
- package/build/web/core/component.scss +557 -69
- package/build/web/core/css-utils/a2-border.css +23 -51
- package/build/web/core/css-utils/a2-color.css +221 -233
- package/build/web/core/css-utils/a2-font.css +1 -29
- package/build/web/core/css-utils/a2-spacing.css +238 -483
- package/build/web/core/css-utils/a2-utils.css +496 -781
- package/build/web/core/css-utils/border.css +23 -51
- package/build/web/core/css-utils/color.css +221 -233
- package/build/web/core/css-utils/font.css +1 -29
- package/build/web/core/css-utils/spacing.css +238 -483
- package/build/web/core/css-utils/utils.css +496 -781
- package/build/web/core/index.d.ts +6 -0
- package/build/web/core/index.js +1 -1
- package/build/web/core/primitive-variables.scss +148 -65
- package/build/web/core/primitive.d.ts +209 -0
- package/build/web/core/primitive.js +779 -61
- package/build/web/core/primitive.scss +207 -124
- package/build/web/core/semantic-variables.scss +363 -245
- package/build/web/core/semantic.d.ts +221 -0
- package/build/web/core/semantic.js +1592 -347
- package/build/web/core/semantic.scss +219 -140
- package/build/web/index.d.ts +3 -4
- package/build/web/types.d.ts +17 -0
- package/config.js +121 -496
- package/eslint.config.mjs +11 -1
- package/package.json +15 -5
- package/src/global/primitive/breakpoint.tokens.json +54 -0
- package/src/global/primitive/color.tokens.json +1092 -0
- package/src/global/primitive/duration.tokens.json +44 -0
- package/src/global/primitive/font.tokens.json +151 -0
- package/src/global/primitive/radius.tokens.json +94 -0
- package/src/global/primitive/size.tokens.json +174 -0
- package/src/global/primitive/transition.tokens.json +32 -0
- package/src/theme/core/background.tokens.json +1312 -0
- package/src/theme/core/border.tokens.json +192 -0
- package/src/theme/core/chart.tokens.json +982 -0
- package/src/theme/core/component/ai-mark.tokens.json +20 -0
- package/src/theme/core/component/alert.tokens.json +261 -0
- package/src/theme/core/component/announcement.tokens.json +460 -0
- package/src/theme/core/component/avatar.tokens.json +137 -0
- package/src/theme/core/component/badge.tokens.json +42 -0
- package/src/theme/core/component/breadcrumb.tokens.json +42 -0
- package/src/theme/core/component/button-toggle.tokens.json +428 -0
- package/src/theme/core/component/button.tokens.json +941 -0
- package/src/theme/core/component/calendar.tokens.json +391 -0
- package/src/theme/core/component/card.tokens.json +107 -0
- package/src/theme/core/component/checkbox.tokens.json +631 -0
- package/src/theme/core/component/chip.tokens.json +169 -0
- package/src/theme/core/component/combobox.tokens.json +269 -0
- package/src/theme/core/component/details.tokens.json +152 -0
- package/src/theme/core/component/dialog.tokens.json +87 -0
- package/src/theme/core/component/divider.tokens.json +23 -0
- package/src/theme/core/component/dnd.tokens.json +208 -0
- package/src/theme/core/component/drawer.tokens.json +61 -0
- package/src/theme/core/component/drilldown.tokens.json +61 -0
- package/src/theme/core/component/edit-card.tokens.json +381 -0
- package/src/theme/core/component/field-label.tokens.json +42 -0
- package/src/theme/core/component/field-message.tokens.json +65 -0
- package/src/theme/core/component/icon.tokens.json +42 -0
- package/src/theme/core/component/link.tokens.json +108 -0
- package/src/theme/core/component/list-view.tokens.json +82 -0
- package/src/theme/core/component/listbox.tokens.json +283 -0
- package/src/theme/core/component/menu.tokens.json +230 -0
- package/src/theme/core/component/overflow.tokens.json +84 -0
- package/src/theme/core/component/page.tokens.json +377 -0
- package/src/theme/core/component/pagination.tokens.json +63 -0
- package/src/theme/core/component/popover.tokens.json +122 -0
- package/src/theme/core/component/progress-bar.tokens.json +133 -0
- package/src/theme/core/component/radio.tokens.json +631 -0
- package/src/theme/core/component/segmented-control.tokens.json +175 -0
- package/src/theme/core/component/select-card.tokens.json +943 -0
- package/src/theme/core/component/side-nav.tokens.json +349 -0
- package/src/theme/core/component/skeleton.tokens.json +42 -0
- package/src/theme/core/component/spinner.tokens.json +96 -0
- package/src/theme/core/component/status-icon.tokens.json +164 -0
- package/src/theme/core/component/stepper.tokens.json +484 -0
- package/src/theme/core/component/switch.tokens.json +285 -0
- package/src/theme/core/component/tab.tokens.json +192 -0
- package/src/theme/core/component/text-field.tokens.json +160 -0
- package/src/theme/core/component/text.tokens.json +59 -0
- package/src/theme/core/component/toast.tokens.json +343 -0
- package/src/theme/core/component/toolbar.tokens.json +114 -0
- package/src/theme/core/component/tooltip.tokens.json +61 -0
- package/src/theme/core/focus.tokens.json +56 -0
- package/src/theme/core/foreground.tokens.json +416 -0
- package/src/theme/core/gradient.tokens.json +41 -0
- package/src/theme/core/opacity.tokens.json +25 -0
- package/src/theme/core/shadow.tokens.json +81 -0
- package/src/theme/core/status.tokens.json +74 -0
- package/src/theme/core/typography.tokens.json +163 -0
- package/src/utils/__tests__/css-utils-format-utils.test.js +312 -0
- package/src/utils/__tests__/sd-build-configs.test.js +306 -0
- package/src/utils/__tests__/sd-formats.test.js +942 -0
- package/src/utils/__tests__/sd-transforms.test.js +336 -0
- package/src/utils/__tests__/token-helpers.test.js +1160 -0
- package/src/utils/copy-css-utils-cli.js +13 -1
- package/src/utils/css-utils-format-utils.js +105 -176
- package/src/utils/figma/__tests__/sync-gradient.test.js +561 -0
- package/src/utils/figma/__tests__/token-conversion.test.js +117 -0
- package/src/utils/figma/__tests__/token-resolution.test.js +231 -0
- package/src/utils/figma/auth.js +355 -0
- package/src/utils/figma/constants.js +22 -0
- package/src/utils/figma/errors.js +80 -0
- package/src/utils/figma/figma-api.js +1069 -0
- package/src/utils/figma/get-token.js +348 -0
- package/src/utils/figma/sync-components.js +909 -0
- package/src/utils/figma/sync-main.js +692 -0
- package/src/utils/figma/sync-orchestration.js +683 -0
- package/src/utils/figma/sync-primitives.js +230 -0
- package/src/utils/figma/sync-semantic.js +1056 -0
- package/src/utils/figma/token-conversion.js +340 -0
- package/src/utils/figma/token-parsing.js +186 -0
- package/src/utils/figma/token-resolution.js +569 -0
- package/src/utils/figma/utils.js +199 -0
- package/src/utils/sd-build-configs.js +305 -0
- package/src/utils/sd-formats.js +948 -0
- package/src/utils/sd-transforms.js +165 -0
- package/src/utils/token-helpers.js +848 -0
- package/tsconfig.json +18 -0
- package/vitest.config.js +17 -0
- package/.turbo/turbo-build.log +0 -37
- package/build/web/core/raw.js +0 -234
- package/src/global/primitive/breakpoint.js +0 -19
- package/src/global/primitive/color.js +0 -231
- package/src/global/primitive/duration.js +0 -16
- package/src/global/primitive/font.js +0 -60
- package/src/global/primitive/radius.js +0 -31
- package/src/global/primitive/size.js +0 -55
- package/src/global/primitive/transition.js +0 -16
- package/src/theme/core/background.js +0 -170
- package/src/theme/core/border.js +0 -103
- package/src/theme/core/charts.js +0 -464
- package/src/theme/core/component/button.js +0 -708
- package/src/theme/core/component/checkbox.js +0 -405
- package/src/theme/core/focus.js +0 -35
- package/src/theme/core/foreground.js +0 -148
- package/src/theme/core/overlay.js +0 -137
- package/src/theme/core/shadow.js +0 -29
- package/src/theme/core/status.js +0 -49
- package/src/theme/core/typography.js +0 -82
- package/type/types.ts +0 -344
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { buildSemanticVariableRequest } from "../sync-semantic.js";
|
|
3
|
+
import { buildComponentVariableRequest } from "../sync-components.js";
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Helpers
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
const MODES = {
|
|
10
|
+
light: { id: "mode-light" },
|
|
11
|
+
dark: { id: "mode-dark" },
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const COLLECTION_ID = "col-1";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Build the resolved token map that sync-semantic and sync-components expect.
|
|
18
|
+
* Each entry mirrors what resolvePrimitiveReferences produces.
|
|
19
|
+
*/
|
|
20
|
+
function makeTokenMap(entries) {
|
|
21
|
+
return new Map(entries);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Minimal primitive color token data
|
|
26
|
+
*/
|
|
27
|
+
function makePrimitiveTokenData(tokenPath, hexValue) {
|
|
28
|
+
return [
|
|
29
|
+
tokenPath,
|
|
30
|
+
{
|
|
31
|
+
token: { $type: "color", $value: hexValue },
|
|
32
|
+
resolvedLight: hexValue,
|
|
33
|
+
resolvedDark: hexValue,
|
|
34
|
+
isPrimitive: true,
|
|
35
|
+
sourceFilePath: "/src/global/primitive/color.tokens.json",
|
|
36
|
+
path: tokenPath.split("/"),
|
|
37
|
+
variableName: tokenPath,
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Gradient token data for the semantic tier (gradient.primary)
|
|
44
|
+
*/
|
|
45
|
+
function makeSemanticGradientTokenData() {
|
|
46
|
+
const gradientValue = {
|
|
47
|
+
type: "linear",
|
|
48
|
+
angle: 90,
|
|
49
|
+
stops: [
|
|
50
|
+
{
|
|
51
|
+
position: 0,
|
|
52
|
+
color: {
|
|
53
|
+
$type: "color",
|
|
54
|
+
$value: "{color.blue.600}",
|
|
55
|
+
$extensions: {
|
|
56
|
+
appearance: {
|
|
57
|
+
light: { $type: "color", $value: "{color.blue.600}" },
|
|
58
|
+
dark: { $type: "color", $value: "{color.cyan.300}" },
|
|
59
|
+
},
|
|
60
|
+
"com.figma.scopes": ["SHAPE_FILL", "FRAME_FILL", "STROKE_COLOR"],
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
position: 1,
|
|
66
|
+
color: {
|
|
67
|
+
$type: "color",
|
|
68
|
+
$value: "{color.cyan.300}",
|
|
69
|
+
$extensions: {
|
|
70
|
+
appearance: {
|
|
71
|
+
light: { $type: "color", $value: "{color.cyan.300}" },
|
|
72
|
+
dark: { $type: "color", $value: "{color.cyan.50}" },
|
|
73
|
+
},
|
|
74
|
+
"com.figma.scopes": ["SHAPE_FILL", "FRAME_FILL", "STROKE_COLOR"],
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return [
|
|
82
|
+
"gradient/primary",
|
|
83
|
+
{
|
|
84
|
+
token: { $type: "gradient", $value: gradientValue },
|
|
85
|
+
resolvedLight: gradientValue,
|
|
86
|
+
resolvedDark: gradientValue,
|
|
87
|
+
isPrimitive: false,
|
|
88
|
+
sourceFilePath: "/src/theme/core/gradient.tokens.json",
|
|
89
|
+
path: ["gradient", "primary"],
|
|
90
|
+
variableName: "gradient/primary",
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Component gradient token data for ai-mark.primary.background.gradient
|
|
97
|
+
*/
|
|
98
|
+
function makeComponentGradientTokenData() {
|
|
99
|
+
return [
|
|
100
|
+
"ai-mark/primary/background/gradient",
|
|
101
|
+
{
|
|
102
|
+
token: {
|
|
103
|
+
$type: "gradient",
|
|
104
|
+
$value: "{gradient.primary}",
|
|
105
|
+
$extensions: {
|
|
106
|
+
appearance: {
|
|
107
|
+
light: { $type: "gradient", $value: "{gradient.primary}" },
|
|
108
|
+
dark: { $type: "gradient", $value: "{gradient.primary}" },
|
|
109
|
+
},
|
|
110
|
+
"com.figma.scopes": ["FRAME_FILL", "SHAPE_FILL"],
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
resolvedLight: "{gradient.primary}",
|
|
114
|
+
resolvedDark: "{gradient.primary}",
|
|
115
|
+
isPrimitive: false,
|
|
116
|
+
sourceFilePath: "/src/theme/core/component/ai-mark.tokens.json",
|
|
117
|
+
path: ["ai-mark", "primary", "background", "gradient"],
|
|
118
|
+
variableName: "ai-mark/primary/background/gradient",
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Shared token map with all required entries
|
|
124
|
+
function makeFullTokenMap() {
|
|
125
|
+
return makeTokenMap([
|
|
126
|
+
makePrimitiveTokenData("color/blue/600", "#0265dc"),
|
|
127
|
+
makePrimitiveTokenData("color/cyan/300", "#45d8f2"),
|
|
128
|
+
makePrimitiveTokenData("color/cyan/50", "#cdf4fb"),
|
|
129
|
+
makeSemanticGradientTokenData(),
|
|
130
|
+
makeComponentGradientTokenData(),
|
|
131
|
+
]);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Existing primitive variables with IDs (simulates what Figma already has)
|
|
135
|
+
const existingPrimitiveVariables = [
|
|
136
|
+
{
|
|
137
|
+
id: "var-blue-600",
|
|
138
|
+
name: "primitive/color/blue/600",
|
|
139
|
+
variableCollectionId: COLLECTION_ID,
|
|
140
|
+
scopes: ["ALL_SCOPES"],
|
|
141
|
+
valuesByMode: {
|
|
142
|
+
"mode-light": { r: 0.01, g: 0.4, b: 0.86, a: 1 },
|
|
143
|
+
"mode-dark": { r: 0.01, g: 0.4, b: 0.86, a: 1 },
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: "var-cyan-300",
|
|
148
|
+
name: "primitive/color/cyan/300",
|
|
149
|
+
variableCollectionId: COLLECTION_ID,
|
|
150
|
+
scopes: ["ALL_SCOPES"],
|
|
151
|
+
valuesByMode: {
|
|
152
|
+
"mode-light": { r: 0.27, g: 0.85, b: 0.95, a: 1 },
|
|
153
|
+
"mode-dark": { r: 0.27, g: 0.85, b: 0.95, a: 1 },
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
id: "var-cyan-50",
|
|
158
|
+
name: "primitive/color/cyan/50",
|
|
159
|
+
variableCollectionId: COLLECTION_ID,
|
|
160
|
+
scopes: ["ALL_SCOPES"],
|
|
161
|
+
valuesByMode: {
|
|
162
|
+
"mode-light": { r: 0.8, g: 0.96, b: 0.98, a: 1 },
|
|
163
|
+
"mode-dark": { r: 0.8, g: 0.96, b: 0.98, a: 1 },
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
// buildSemanticVariableRequest — gradient unpacking
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
describe("buildSemanticVariableRequest — gradient tokens", () => {
|
|
173
|
+
it("creates two COLOR stop variables for gradient/primary", () => {
|
|
174
|
+
const tokensToSync = makeFullTokenMap();
|
|
175
|
+
const result = buildSemanticVariableRequest(
|
|
176
|
+
tokensToSync,
|
|
177
|
+
existingPrimitiveVariables,
|
|
178
|
+
COLLECTION_ID,
|
|
179
|
+
MODES,
|
|
180
|
+
new Map(),
|
|
181
|
+
tokensToSync,
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const gradientVars = result.variableChanges.filter((v) =>
|
|
185
|
+
v.name?.startsWith("semantic/gradient/primary/stop-"),
|
|
186
|
+
);
|
|
187
|
+
expect(gradientVars).toHaveLength(2);
|
|
188
|
+
expect(gradientVars[0].name).toBe("semantic/gradient/primary/stop-0");
|
|
189
|
+
expect(gradientVars[1].name).toBe("semantic/gradient/primary/stop-1");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("creates stop variables as COLOR type", () => {
|
|
193
|
+
const tokensToSync = makeFullTokenMap();
|
|
194
|
+
const result = buildSemanticVariableRequest(
|
|
195
|
+
tokensToSync,
|
|
196
|
+
existingPrimitiveVariables,
|
|
197
|
+
COLLECTION_ID,
|
|
198
|
+
MODES,
|
|
199
|
+
new Map(),
|
|
200
|
+
tokensToSync,
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const stopVars = result.variableChanges.filter((v) =>
|
|
204
|
+
v.name?.startsWith("semantic/gradient/primary/stop-"),
|
|
205
|
+
);
|
|
206
|
+
for (const v of stopVars) {
|
|
207
|
+
expect(v.resolvedType).toBe("COLOR");
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("stop-0 light mode aliases primitive/color/blue/600", () => {
|
|
212
|
+
const tokensToSync = makeFullTokenMap();
|
|
213
|
+
const result = buildSemanticVariableRequest(
|
|
214
|
+
tokensToSync,
|
|
215
|
+
existingPrimitiveVariables,
|
|
216
|
+
COLLECTION_ID,
|
|
217
|
+
MODES,
|
|
218
|
+
new Map(),
|
|
219
|
+
tokensToSync,
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const stop0ModeValues = result.variableModeValues.filter((mv) => {
|
|
223
|
+
const varChange = result.variableChanges.find(
|
|
224
|
+
(v) => v.id === mv.variableId,
|
|
225
|
+
);
|
|
226
|
+
return varChange?.name === "semantic/gradient/primary/stop-0";
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const lightModeValue = stop0ModeValues.find(
|
|
230
|
+
(mv) => mv.modeId === "mode-light",
|
|
231
|
+
);
|
|
232
|
+
expect(lightModeValue?.value).toEqual({
|
|
233
|
+
type: "VARIABLE_ALIAS",
|
|
234
|
+
id: "var-blue-600",
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("stop-0 dark mode aliases primitive/color/cyan/300", () => {
|
|
239
|
+
const tokensToSync = makeFullTokenMap();
|
|
240
|
+
const result = buildSemanticVariableRequest(
|
|
241
|
+
tokensToSync,
|
|
242
|
+
existingPrimitiveVariables,
|
|
243
|
+
COLLECTION_ID,
|
|
244
|
+
MODES,
|
|
245
|
+
new Map(),
|
|
246
|
+
tokensToSync,
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
const stop0ModeValues = result.variableModeValues.filter((mv) => {
|
|
250
|
+
const varChange = result.variableChanges.find(
|
|
251
|
+
(v) => v.id === mv.variableId,
|
|
252
|
+
);
|
|
253
|
+
return varChange?.name === "semantic/gradient/primary/stop-0";
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const darkModeValue = stop0ModeValues.find(
|
|
257
|
+
(mv) => mv.modeId === "mode-dark",
|
|
258
|
+
);
|
|
259
|
+
expect(darkModeValue?.value).toEqual({
|
|
260
|
+
type: "VARIABLE_ALIAS",
|
|
261
|
+
id: "var-cyan-300",
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("stop-1 light mode aliases primitive/color/cyan/300", () => {
|
|
266
|
+
const tokensToSync = makeFullTokenMap();
|
|
267
|
+
const result = buildSemanticVariableRequest(
|
|
268
|
+
tokensToSync,
|
|
269
|
+
existingPrimitiveVariables,
|
|
270
|
+
COLLECTION_ID,
|
|
271
|
+
MODES,
|
|
272
|
+
new Map(),
|
|
273
|
+
tokensToSync,
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
const stop1ModeValues = result.variableModeValues.filter((mv) => {
|
|
277
|
+
const varChange = result.variableChanges.find(
|
|
278
|
+
(v) => v.id === mv.variableId,
|
|
279
|
+
);
|
|
280
|
+
return varChange?.name === "semantic/gradient/primary/stop-1";
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const lightModeValue = stop1ModeValues.find(
|
|
284
|
+
(mv) => mv.modeId === "mode-light",
|
|
285
|
+
);
|
|
286
|
+
expect(lightModeValue?.value).toEqual({
|
|
287
|
+
type: "VARIABLE_ALIAS",
|
|
288
|
+
id: "var-cyan-300",
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("stop-1 dark mode aliases primitive/color/cyan/50", () => {
|
|
293
|
+
const tokensToSync = makeFullTokenMap();
|
|
294
|
+
const result = buildSemanticVariableRequest(
|
|
295
|
+
tokensToSync,
|
|
296
|
+
existingPrimitiveVariables,
|
|
297
|
+
COLLECTION_ID,
|
|
298
|
+
MODES,
|
|
299
|
+
new Map(),
|
|
300
|
+
tokensToSync,
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
const stop1ModeValues = result.variableModeValues.filter((mv) => {
|
|
304
|
+
const varChange = result.variableChanges.find(
|
|
305
|
+
(v) => v.id === mv.variableId,
|
|
306
|
+
);
|
|
307
|
+
return varChange?.name === "semantic/gradient/primary/stop-1";
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
const darkModeValue = stop1ModeValues.find(
|
|
311
|
+
(mv) => mv.modeId === "mode-dark",
|
|
312
|
+
);
|
|
313
|
+
expect(darkModeValue?.value).toEqual({
|
|
314
|
+
type: "VARIABLE_ALIAS",
|
|
315
|
+
id: "var-cyan-50",
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("assigns correct figma scopes to stop variables", () => {
|
|
320
|
+
const tokensToSync = makeFullTokenMap();
|
|
321
|
+
const result = buildSemanticVariableRequest(
|
|
322
|
+
tokensToSync,
|
|
323
|
+
existingPrimitiveVariables,
|
|
324
|
+
COLLECTION_ID,
|
|
325
|
+
MODES,
|
|
326
|
+
new Map(),
|
|
327
|
+
tokensToSync,
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
const stopVars = result.variableChanges.filter((v) =>
|
|
331
|
+
v.name?.startsWith("semantic/gradient/primary/stop-"),
|
|
332
|
+
);
|
|
333
|
+
for (const v of stopVars) {
|
|
334
|
+
expect(v.scopes).toEqual(
|
|
335
|
+
expect.arrayContaining(["SHAPE_FILL", "FRAME_FILL", "STROKE_COLOR"]),
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("does NOT create a variable for the gradient/primary composite token itself", () => {
|
|
341
|
+
const tokensToSync = makeFullTokenMap();
|
|
342
|
+
const result = buildSemanticVariableRequest(
|
|
343
|
+
tokensToSync,
|
|
344
|
+
existingPrimitiveVariables,
|
|
345
|
+
COLLECTION_ID,
|
|
346
|
+
MODES,
|
|
347
|
+
new Map(),
|
|
348
|
+
tokensToSync,
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
const gradientComposite = result.variableChanges.find(
|
|
352
|
+
(v) => v.name === "semantic/gradient/primary",
|
|
353
|
+
);
|
|
354
|
+
expect(gradientComposite).toBeUndefined();
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("produces no errors when primitives exist", () => {
|
|
358
|
+
const tokensToSync = makeFullTokenMap();
|
|
359
|
+
const result = buildSemanticVariableRequest(
|
|
360
|
+
tokensToSync,
|
|
361
|
+
existingPrimitiveVariables,
|
|
362
|
+
COLLECTION_ID,
|
|
363
|
+
MODES,
|
|
364
|
+
new Map(),
|
|
365
|
+
tokensToSync,
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
const gradientErrors = result.errors.filter((e) => e.includes("gradient"));
|
|
369
|
+
expect(gradientErrors).toHaveLength(0);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it("skips stop variables that already exist with matching values", () => {
|
|
373
|
+
const tokensToSync = makeFullTokenMap();
|
|
374
|
+
|
|
375
|
+
// Pre-populate existing variables with the stop vars
|
|
376
|
+
const existingWithStops = [
|
|
377
|
+
...existingPrimitiveVariables,
|
|
378
|
+
{
|
|
379
|
+
id: "existing-stop-0",
|
|
380
|
+
name: "semantic/gradient/primary/stop-0",
|
|
381
|
+
variableCollectionId: COLLECTION_ID,
|
|
382
|
+
scopes: ["SHAPE_FILL", "FRAME_FILL", "STROKE_COLOR"],
|
|
383
|
+
description:
|
|
384
|
+
"stop 0 at 0%: light: color/blue/600 → #0265dc, dark: color/cyan/300 → #45d8f2",
|
|
385
|
+
valuesByMode: {
|
|
386
|
+
"mode-light": { type: "VARIABLE_ALIAS", id: "var-blue-600" },
|
|
387
|
+
"mode-dark": { type: "VARIABLE_ALIAS", id: "var-cyan-300" },
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
id: "existing-stop-1",
|
|
392
|
+
name: "semantic/gradient/primary/stop-1",
|
|
393
|
+
variableCollectionId: COLLECTION_ID,
|
|
394
|
+
scopes: ["SHAPE_FILL", "FRAME_FILL", "STROKE_COLOR"],
|
|
395
|
+
description:
|
|
396
|
+
"stop 1 at 100%: light: color/cyan/300 → #45d8f2, dark: color/cyan/50 → #cdf4fb",
|
|
397
|
+
valuesByMode: {
|
|
398
|
+
"mode-light": { type: "VARIABLE_ALIAS", id: "var-cyan-300" },
|
|
399
|
+
"mode-dark": { type: "VARIABLE_ALIAS", id: "var-cyan-50" },
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
];
|
|
403
|
+
|
|
404
|
+
const result = buildSemanticVariableRequest(
|
|
405
|
+
tokensToSync,
|
|
406
|
+
existingWithStops,
|
|
407
|
+
COLLECTION_ID,
|
|
408
|
+
MODES,
|
|
409
|
+
new Map(),
|
|
410
|
+
tokensToSync,
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
const gradientCreates = result.variableChanges.filter(
|
|
414
|
+
(v) =>
|
|
415
|
+
v.action === "CREATE" &&
|
|
416
|
+
v.name?.startsWith("semantic/gradient/primary/stop-"),
|
|
417
|
+
);
|
|
418
|
+
expect(gradientCreates).toHaveLength(0);
|
|
419
|
+
expect(result.stats.skipped).toBeGreaterThanOrEqual(2);
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
// ---------------------------------------------------------------------------
|
|
424
|
+
// buildComponentVariableRequest — gradient stop aliasing
|
|
425
|
+
// ---------------------------------------------------------------------------
|
|
426
|
+
|
|
427
|
+
describe("buildComponentVariableRequest — gradient tokens", () => {
|
|
428
|
+
// Build semantic tempIdMap with stop variable IDs already resolved
|
|
429
|
+
function makeSemanticTempIdMap() {
|
|
430
|
+
const tempIdMap = new Map();
|
|
431
|
+
tempIdMap.set("gradient/primary/stop-0", "sem-stop-0-id");
|
|
432
|
+
tempIdMap.set("semantic/gradient/primary/stop-0", "sem-stop-0-id");
|
|
433
|
+
tempIdMap.set("gradient/primary/stop-1", "sem-stop-1-id");
|
|
434
|
+
tempIdMap.set("semantic/gradient/primary/stop-1", "sem-stop-1-id");
|
|
435
|
+
return tempIdMap;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
it("creates two stop variables for ai-mark gradient component token", () => {
|
|
439
|
+
const tokensToSync = makeFullTokenMap();
|
|
440
|
+
const result = buildComponentVariableRequest(
|
|
441
|
+
tokensToSync,
|
|
442
|
+
existingPrimitiveVariables,
|
|
443
|
+
COLLECTION_ID,
|
|
444
|
+
MODES,
|
|
445
|
+
makeSemanticTempIdMap(),
|
|
446
|
+
tokensToSync,
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
const compStopVars = result.variableChanges.filter((v) =>
|
|
450
|
+
v.name?.startsWith("component/ai-mark/primary/background/gradient/stop-"),
|
|
451
|
+
);
|
|
452
|
+
expect(compStopVars).toHaveLength(2);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it("component stop variables alias the corresponding semantic stop variables", () => {
|
|
456
|
+
const tokensToSync = makeFullTokenMap();
|
|
457
|
+
const result = buildComponentVariableRequest(
|
|
458
|
+
tokensToSync,
|
|
459
|
+
existingPrimitiveVariables,
|
|
460
|
+
COLLECTION_ID,
|
|
461
|
+
MODES,
|
|
462
|
+
makeSemanticTempIdMap(),
|
|
463
|
+
tokensToSync,
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
const stop0ModeValues = result.variableModeValues.filter((mv) => {
|
|
467
|
+
const varChange = result.variableChanges.find(
|
|
468
|
+
(v) => v.id === mv.variableId,
|
|
469
|
+
);
|
|
470
|
+
return (
|
|
471
|
+
varChange?.name ===
|
|
472
|
+
"component/ai-mark/primary/background/gradient/stop-0"
|
|
473
|
+
);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
expect(stop0ModeValues[0]?.value).toEqual({
|
|
477
|
+
type: "VARIABLE_ALIAS",
|
|
478
|
+
id: "sem-stop-0-id",
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it("component stop-1 aliases semantic/gradient/primary/stop-1", () => {
|
|
483
|
+
const tokensToSync = makeFullTokenMap();
|
|
484
|
+
const result = buildComponentVariableRequest(
|
|
485
|
+
tokensToSync,
|
|
486
|
+
existingPrimitiveVariables,
|
|
487
|
+
COLLECTION_ID,
|
|
488
|
+
MODES,
|
|
489
|
+
makeSemanticTempIdMap(),
|
|
490
|
+
tokensToSync,
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
const stop1ModeValues = result.variableModeValues.filter((mv) => {
|
|
494
|
+
const varChange = result.variableChanges.find(
|
|
495
|
+
(v) => v.id === mv.variableId,
|
|
496
|
+
);
|
|
497
|
+
return (
|
|
498
|
+
varChange?.name ===
|
|
499
|
+
"component/ai-mark/primary/background/gradient/stop-1"
|
|
500
|
+
);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
expect(stop1ModeValues[0]?.value).toEqual({
|
|
504
|
+
type: "VARIABLE_ALIAS",
|
|
505
|
+
id: "sem-stop-1-id",
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it("does NOT create a variable for the ai-mark gradient composite token itself", () => {
|
|
510
|
+
const tokensToSync = makeFullTokenMap();
|
|
511
|
+
const result = buildComponentVariableRequest(
|
|
512
|
+
tokensToSync,
|
|
513
|
+
existingPrimitiveVariables,
|
|
514
|
+
COLLECTION_ID,
|
|
515
|
+
MODES,
|
|
516
|
+
makeSemanticTempIdMap(),
|
|
517
|
+
tokensToSync,
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
const compositeVar = result.variableChanges.find(
|
|
521
|
+
(v) => v.name === "component/ai-mark/primary/background/gradient",
|
|
522
|
+
);
|
|
523
|
+
expect(compositeVar).toBeUndefined();
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it("produces no errors when semantic stop IDs are in tempIdMap", () => {
|
|
527
|
+
const tokensToSync = makeFullTokenMap();
|
|
528
|
+
const result = buildComponentVariableRequest(
|
|
529
|
+
tokensToSync,
|
|
530
|
+
existingPrimitiveVariables,
|
|
531
|
+
COLLECTION_ID,
|
|
532
|
+
MODES,
|
|
533
|
+
makeSemanticTempIdMap(),
|
|
534
|
+
tokensToSync,
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
const gradientErrors = result.errors.filter(
|
|
538
|
+
(e) => e.includes("gradient") || e.includes("ai-mark"),
|
|
539
|
+
);
|
|
540
|
+
expect(gradientErrors).toHaveLength(0);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it("component stop variables are COLOR type", () => {
|
|
544
|
+
const tokensToSync = makeFullTokenMap();
|
|
545
|
+
const result = buildComponentVariableRequest(
|
|
546
|
+
tokensToSync,
|
|
547
|
+
existingPrimitiveVariables,
|
|
548
|
+
COLLECTION_ID,
|
|
549
|
+
MODES,
|
|
550
|
+
makeSemanticTempIdMap(),
|
|
551
|
+
tokensToSync,
|
|
552
|
+
);
|
|
553
|
+
|
|
554
|
+
const compStopVars = result.variableChanges.filter((v) =>
|
|
555
|
+
v.name?.startsWith("component/ai-mark/primary/background/gradient/stop-"),
|
|
556
|
+
);
|
|
557
|
+
for (const v of compStopVars) {
|
|
558
|
+
expect(v.resolvedType).toBe("COLOR");
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { isGradientToken, parseGradientStops } from "../token-conversion.js";
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// isGradientToken
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
describe("isGradientToken", () => {
|
|
9
|
+
it("returns true for 'gradient' type", () => {
|
|
10
|
+
expect(isGradientToken("gradient")).toBe(true);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("returns false for 'color' type", () => {
|
|
14
|
+
expect(isGradientToken("color")).toBe(false);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("returns false for 'dimension' type", () => {
|
|
18
|
+
expect(isGradientToken("dimension")).toBe(false);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("returns false for undefined", () => {
|
|
22
|
+
expect(isGradientToken(undefined)).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("returns false for empty string", () => {
|
|
26
|
+
expect(isGradientToken("")).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// parseGradientStops
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
const makeStop = (lightRef, darkRef, position = 0) => ({
|
|
35
|
+
position,
|
|
36
|
+
color: {
|
|
37
|
+
$type: "color",
|
|
38
|
+
$value: lightRef,
|
|
39
|
+
$extensions: {
|
|
40
|
+
appearance: {
|
|
41
|
+
light: { $type: "color", $value: lightRef },
|
|
42
|
+
dark: { $type: "color", $value: darkRef },
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("parseGradientStops", () => {
|
|
49
|
+
it("returns an array with one entry per stop", () => {
|
|
50
|
+
const gradient = {
|
|
51
|
+
type: "linear",
|
|
52
|
+
angle: 90,
|
|
53
|
+
stops: [makeStop("{color.blue.600}", "{color.cyan.300}", 0)],
|
|
54
|
+
};
|
|
55
|
+
const result = parseGradientStops(gradient);
|
|
56
|
+
expect(result).toHaveLength(1);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("extracts lightRef and darkRef for each stop", () => {
|
|
60
|
+
const gradient = {
|
|
61
|
+
type: "linear",
|
|
62
|
+
angle: 90,
|
|
63
|
+
stops: [
|
|
64
|
+
makeStop("{color.blue.600}", "{color.cyan.300}", 0),
|
|
65
|
+
makeStop("{color.cyan.300}", "{color.cyan.50}", 1),
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
const result = parseGradientStops(gradient);
|
|
69
|
+
expect(result[0]).toEqual({
|
|
70
|
+
lightRef: "{color.blue.600}",
|
|
71
|
+
darkRef: "{color.cyan.300}",
|
|
72
|
+
});
|
|
73
|
+
expect(result[1]).toEqual({
|
|
74
|
+
lightRef: "{color.cyan.300}",
|
|
75
|
+
darkRef: "{color.cyan.50}",
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("falls back to stop.color.$value when appearance is absent", () => {
|
|
80
|
+
const gradient = {
|
|
81
|
+
type: "linear",
|
|
82
|
+
angle: 90,
|
|
83
|
+
stops: [
|
|
84
|
+
{
|
|
85
|
+
position: 0,
|
|
86
|
+
color: { $type: "color", $value: "#fallback" },
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
};
|
|
90
|
+
const result = parseGradientStops(gradient);
|
|
91
|
+
expect(result[0].lightRef).toBe("#fallback");
|
|
92
|
+
expect(result[0].darkRef).toBe("#fallback");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("returns null for null input", () => {
|
|
96
|
+
expect(parseGradientStops(null)).toBeNull();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("returns null for undefined input", () => {
|
|
100
|
+
expect(parseGradientStops(undefined)).toBeNull();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("returns null when stops is not an array", () => {
|
|
104
|
+
expect(
|
|
105
|
+
parseGradientStops({ type: "linear", angle: 90, stops: null }),
|
|
106
|
+
).toBeNull();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("returns null for a string (reference not yet resolved)", () => {
|
|
110
|
+
expect(parseGradientStops("{gradient.primary}")).toBeNull();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("returns empty array for gradient with no stops", () => {
|
|
114
|
+
const result = parseGradientStops({ type: "linear", angle: 90, stops: [] });
|
|
115
|
+
expect(result).toEqual([]);
|
|
116
|
+
});
|
|
117
|
+
});
|