@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.
Files changed (146) hide show
  1. package/CHANGELOG.md +52 -2
  2. package/README.md +332 -0
  3. package/build/web/core/component-variables.scss +1088 -131
  4. package/build/web/core/component.d.ts +558 -0
  5. package/build/web/core/component.js +6685 -249
  6. package/build/web/core/component.scss +557 -69
  7. package/build/web/core/css-utils/a2-border.css +23 -51
  8. package/build/web/core/css-utils/a2-color.css +221 -233
  9. package/build/web/core/css-utils/a2-font.css +1 -29
  10. package/build/web/core/css-utils/a2-spacing.css +238 -483
  11. package/build/web/core/css-utils/a2-utils.css +496 -781
  12. package/build/web/core/css-utils/border.css +23 -51
  13. package/build/web/core/css-utils/color.css +221 -233
  14. package/build/web/core/css-utils/font.css +1 -29
  15. package/build/web/core/css-utils/spacing.css +238 -483
  16. package/build/web/core/css-utils/utils.css +496 -781
  17. package/build/web/core/index.d.ts +6 -0
  18. package/build/web/core/index.js +1 -1
  19. package/build/web/core/primitive-variables.scss +148 -65
  20. package/build/web/core/primitive.d.ts +209 -0
  21. package/build/web/core/primitive.js +779 -61
  22. package/build/web/core/primitive.scss +207 -124
  23. package/build/web/core/semantic-variables.scss +363 -245
  24. package/build/web/core/semantic.d.ts +221 -0
  25. package/build/web/core/semantic.js +1592 -347
  26. package/build/web/core/semantic.scss +219 -140
  27. package/build/web/index.d.ts +3 -4
  28. package/build/web/types.d.ts +17 -0
  29. package/config.js +121 -496
  30. package/eslint.config.mjs +11 -1
  31. package/package.json +15 -5
  32. package/src/global/primitive/breakpoint.tokens.json +54 -0
  33. package/src/global/primitive/color.tokens.json +1092 -0
  34. package/src/global/primitive/duration.tokens.json +44 -0
  35. package/src/global/primitive/font.tokens.json +151 -0
  36. package/src/global/primitive/radius.tokens.json +94 -0
  37. package/src/global/primitive/size.tokens.json +174 -0
  38. package/src/global/primitive/transition.tokens.json +32 -0
  39. package/src/theme/core/background.tokens.json +1312 -0
  40. package/src/theme/core/border.tokens.json +192 -0
  41. package/src/theme/core/chart.tokens.json +982 -0
  42. package/src/theme/core/component/ai-mark.tokens.json +20 -0
  43. package/src/theme/core/component/alert.tokens.json +261 -0
  44. package/src/theme/core/component/announcement.tokens.json +460 -0
  45. package/src/theme/core/component/avatar.tokens.json +137 -0
  46. package/src/theme/core/component/badge.tokens.json +42 -0
  47. package/src/theme/core/component/breadcrumb.tokens.json +42 -0
  48. package/src/theme/core/component/button-toggle.tokens.json +428 -0
  49. package/src/theme/core/component/button.tokens.json +941 -0
  50. package/src/theme/core/component/calendar.tokens.json +391 -0
  51. package/src/theme/core/component/card.tokens.json +107 -0
  52. package/src/theme/core/component/checkbox.tokens.json +631 -0
  53. package/src/theme/core/component/chip.tokens.json +169 -0
  54. package/src/theme/core/component/combobox.tokens.json +269 -0
  55. package/src/theme/core/component/details.tokens.json +152 -0
  56. package/src/theme/core/component/dialog.tokens.json +87 -0
  57. package/src/theme/core/component/divider.tokens.json +23 -0
  58. package/src/theme/core/component/dnd.tokens.json +208 -0
  59. package/src/theme/core/component/drawer.tokens.json +61 -0
  60. package/src/theme/core/component/drilldown.tokens.json +61 -0
  61. package/src/theme/core/component/edit-card.tokens.json +381 -0
  62. package/src/theme/core/component/field-label.tokens.json +42 -0
  63. package/src/theme/core/component/field-message.tokens.json +65 -0
  64. package/src/theme/core/component/icon.tokens.json +42 -0
  65. package/src/theme/core/component/link.tokens.json +108 -0
  66. package/src/theme/core/component/list-view.tokens.json +82 -0
  67. package/src/theme/core/component/listbox.tokens.json +283 -0
  68. package/src/theme/core/component/menu.tokens.json +230 -0
  69. package/src/theme/core/component/overflow.tokens.json +84 -0
  70. package/src/theme/core/component/page.tokens.json +377 -0
  71. package/src/theme/core/component/pagination.tokens.json +63 -0
  72. package/src/theme/core/component/popover.tokens.json +122 -0
  73. package/src/theme/core/component/progress-bar.tokens.json +133 -0
  74. package/src/theme/core/component/radio.tokens.json +631 -0
  75. package/src/theme/core/component/segmented-control.tokens.json +175 -0
  76. package/src/theme/core/component/select-card.tokens.json +943 -0
  77. package/src/theme/core/component/side-nav.tokens.json +349 -0
  78. package/src/theme/core/component/skeleton.tokens.json +42 -0
  79. package/src/theme/core/component/spinner.tokens.json +96 -0
  80. package/src/theme/core/component/status-icon.tokens.json +164 -0
  81. package/src/theme/core/component/stepper.tokens.json +484 -0
  82. package/src/theme/core/component/switch.tokens.json +285 -0
  83. package/src/theme/core/component/tab.tokens.json +192 -0
  84. package/src/theme/core/component/text-field.tokens.json +160 -0
  85. package/src/theme/core/component/text.tokens.json +59 -0
  86. package/src/theme/core/component/toast.tokens.json +343 -0
  87. package/src/theme/core/component/toolbar.tokens.json +114 -0
  88. package/src/theme/core/component/tooltip.tokens.json +61 -0
  89. package/src/theme/core/focus.tokens.json +56 -0
  90. package/src/theme/core/foreground.tokens.json +416 -0
  91. package/src/theme/core/gradient.tokens.json +41 -0
  92. package/src/theme/core/opacity.tokens.json +25 -0
  93. package/src/theme/core/shadow.tokens.json +81 -0
  94. package/src/theme/core/status.tokens.json +74 -0
  95. package/src/theme/core/typography.tokens.json +163 -0
  96. package/src/utils/__tests__/css-utils-format-utils.test.js +312 -0
  97. package/src/utils/__tests__/sd-build-configs.test.js +306 -0
  98. package/src/utils/__tests__/sd-formats.test.js +942 -0
  99. package/src/utils/__tests__/sd-transforms.test.js +336 -0
  100. package/src/utils/__tests__/token-helpers.test.js +1160 -0
  101. package/src/utils/copy-css-utils-cli.js +13 -1
  102. package/src/utils/css-utils-format-utils.js +105 -176
  103. package/src/utils/figma/__tests__/sync-gradient.test.js +561 -0
  104. package/src/utils/figma/__tests__/token-conversion.test.js +117 -0
  105. package/src/utils/figma/__tests__/token-resolution.test.js +231 -0
  106. package/src/utils/figma/auth.js +355 -0
  107. package/src/utils/figma/constants.js +22 -0
  108. package/src/utils/figma/errors.js +80 -0
  109. package/src/utils/figma/figma-api.js +1069 -0
  110. package/src/utils/figma/get-token.js +348 -0
  111. package/src/utils/figma/sync-components.js +909 -0
  112. package/src/utils/figma/sync-main.js +692 -0
  113. package/src/utils/figma/sync-orchestration.js +683 -0
  114. package/src/utils/figma/sync-primitives.js +230 -0
  115. package/src/utils/figma/sync-semantic.js +1056 -0
  116. package/src/utils/figma/token-conversion.js +340 -0
  117. package/src/utils/figma/token-parsing.js +186 -0
  118. package/src/utils/figma/token-resolution.js +569 -0
  119. package/src/utils/figma/utils.js +199 -0
  120. package/src/utils/sd-build-configs.js +305 -0
  121. package/src/utils/sd-formats.js +948 -0
  122. package/src/utils/sd-transforms.js +165 -0
  123. package/src/utils/token-helpers.js +848 -0
  124. package/tsconfig.json +18 -0
  125. package/vitest.config.js +17 -0
  126. package/.turbo/turbo-build.log +0 -37
  127. package/build/web/core/raw.js +0 -234
  128. package/src/global/primitive/breakpoint.js +0 -19
  129. package/src/global/primitive/color.js +0 -231
  130. package/src/global/primitive/duration.js +0 -16
  131. package/src/global/primitive/font.js +0 -60
  132. package/src/global/primitive/radius.js +0 -31
  133. package/src/global/primitive/size.js +0 -55
  134. package/src/global/primitive/transition.js +0 -16
  135. package/src/theme/core/background.js +0 -170
  136. package/src/theme/core/border.js +0 -103
  137. package/src/theme/core/charts.js +0 -464
  138. package/src/theme/core/component/button.js +0 -708
  139. package/src/theme/core/component/checkbox.js +0 -405
  140. package/src/theme/core/focus.js +0 -35
  141. package/src/theme/core/foreground.js +0 -148
  142. package/src/theme/core/overlay.js +0 -137
  143. package/src/theme/core/shadow.js +0 -29
  144. package/src/theme/core/status.js +0 -49
  145. package/src/theme/core/typography.js +0 -82
  146. 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
+ });