@servicetitan/hammer-token 2.5.0 → 3.0.0

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 (147) hide show
  1. package/CHANGELOG.md +56 -0
  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 +47 -45
  8. package/build/web/core/css-utils/a2-color.css +443 -227
  9. package/build/web/core/css-utils/a2-font.css +0 -2
  10. package/build/web/core/css-utils/a2-spacing.css +476 -478
  11. package/build/web/core/css-utils/a2-utils.css +992 -772
  12. package/build/web/core/css-utils/border.css +47 -45
  13. package/build/web/core/css-utils/color.css +443 -227
  14. package/build/web/core/css-utils/font.css +0 -2
  15. package/build/web/core/css-utils/spacing.css +476 -478
  16. package/build/web/core/css-utils/utils.css +992 -772
  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 -239
  24. package/build/web/core/semantic.d.ts +221 -0
  25. package/build/web/core/semantic.js +1613 -347
  26. package/build/web/core/semantic.scss +219 -137
  27. package/build/web/index.d.ts +3 -4
  28. package/build/web/index.js +0 -1
  29. package/build/web/types.d.ts +17 -0
  30. package/config.js +121 -497
  31. package/eslint.config.mjs +11 -1
  32. package/package.json +15 -5
  33. package/src/global/primitive/breakpoint.tokens.json +54 -0
  34. package/src/global/primitive/color.tokens.json +1092 -0
  35. package/src/global/primitive/duration.tokens.json +44 -0
  36. package/src/global/primitive/font.tokens.json +151 -0
  37. package/src/global/primitive/radius.tokens.json +94 -0
  38. package/src/global/primitive/size.tokens.json +174 -0
  39. package/src/global/primitive/transition.tokens.json +32 -0
  40. package/src/theme/core/background.tokens.json +1312 -0
  41. package/src/theme/core/border.tokens.json +192 -0
  42. package/src/theme/core/chart.tokens.json +982 -0
  43. package/src/theme/core/component/ai-mark.tokens.json +20 -0
  44. package/src/theme/core/component/alert.tokens.json +261 -0
  45. package/src/theme/core/component/announcement.tokens.json +460 -0
  46. package/src/theme/core/component/avatar.tokens.json +137 -0
  47. package/src/theme/core/component/badge.tokens.json +42 -0
  48. package/src/theme/core/component/breadcrumb.tokens.json +42 -0
  49. package/src/theme/core/component/button-toggle.tokens.json +428 -0
  50. package/src/theme/core/component/button.tokens.json +941 -0
  51. package/src/theme/core/component/calendar.tokens.json +391 -0
  52. package/src/theme/core/component/card.tokens.json +107 -0
  53. package/src/theme/core/component/checkbox.tokens.json +631 -0
  54. package/src/theme/core/component/chip.tokens.json +169 -0
  55. package/src/theme/core/component/combobox.tokens.json +269 -0
  56. package/src/theme/core/component/details.tokens.json +152 -0
  57. package/src/theme/core/component/dialog.tokens.json +87 -0
  58. package/src/theme/core/component/divider.tokens.json +23 -0
  59. package/src/theme/core/component/dnd.tokens.json +208 -0
  60. package/src/theme/core/component/drawer.tokens.json +61 -0
  61. package/src/theme/core/component/drilldown.tokens.json +61 -0
  62. package/src/theme/core/component/edit-card.tokens.json +381 -0
  63. package/src/theme/core/component/field-label.tokens.json +42 -0
  64. package/src/theme/core/component/field-message.tokens.json +74 -0
  65. package/src/theme/core/component/icon.tokens.json +42 -0
  66. package/src/theme/core/component/link.tokens.json +108 -0
  67. package/src/theme/core/component/list-view.tokens.json +82 -0
  68. package/src/theme/core/component/listbox.tokens.json +283 -0
  69. package/src/theme/core/component/menu.tokens.json +230 -0
  70. package/src/theme/core/component/overflow.tokens.json +84 -0
  71. package/src/theme/core/component/page.tokens.json +377 -0
  72. package/src/theme/core/component/pagination.tokens.json +63 -0
  73. package/src/theme/core/component/popover.tokens.json +122 -0
  74. package/src/theme/core/component/progress-bar.tokens.json +133 -0
  75. package/src/theme/core/component/radio.tokens.json +631 -0
  76. package/src/theme/core/component/segmented-control.tokens.json +175 -0
  77. package/src/theme/core/component/select-card.tokens.json +943 -0
  78. package/src/theme/core/component/side-nav.tokens.json +349 -0
  79. package/src/theme/core/component/skeleton.tokens.json +42 -0
  80. package/src/theme/core/component/spinner.tokens.json +96 -0
  81. package/src/theme/core/component/status-icon.tokens.json +164 -0
  82. package/src/theme/core/component/stepper.tokens.json +484 -0
  83. package/src/theme/core/component/switch.tokens.json +285 -0
  84. package/src/theme/core/component/tab.tokens.json +192 -0
  85. package/src/theme/core/component/text-field.tokens.json +160 -0
  86. package/src/theme/core/component/text.tokens.json +59 -0
  87. package/src/theme/core/component/toast.tokens.json +343 -0
  88. package/src/theme/core/component/toolbar.tokens.json +114 -0
  89. package/src/theme/core/component/tooltip.tokens.json +61 -0
  90. package/src/theme/core/focus.tokens.json +56 -0
  91. package/src/theme/core/foreground.tokens.json +416 -0
  92. package/src/theme/core/gradient.tokens.json +41 -0
  93. package/src/theme/core/opacity.tokens.json +25 -0
  94. package/src/theme/core/shadow.tokens.json +81 -0
  95. package/src/theme/core/status.tokens.json +74 -0
  96. package/src/theme/core/typography.tokens.json +163 -0
  97. package/src/utils/__tests__/css-utils-format-utils.test.js +312 -0
  98. package/src/utils/__tests__/sd-build-configs.test.js +306 -0
  99. package/src/utils/__tests__/sd-formats.test.js +950 -0
  100. package/src/utils/__tests__/sd-transforms.test.js +336 -0
  101. package/src/utils/__tests__/token-helpers.test.js +1160 -0
  102. package/src/utils/copy-css-utils-cli.js +13 -1
  103. package/src/utils/css-utils-format-utils.js +105 -176
  104. package/src/utils/figma/__tests__/sync-gradient.test.js +561 -0
  105. package/src/utils/figma/__tests__/token-conversion.test.js +117 -0
  106. package/src/utils/figma/__tests__/token-resolution.test.js +231 -0
  107. package/src/utils/figma/auth.js +355 -0
  108. package/src/utils/figma/constants.js +22 -0
  109. package/src/utils/figma/errors.js +80 -0
  110. package/src/utils/figma/figma-api.js +1069 -0
  111. package/src/utils/figma/get-token.js +348 -0
  112. package/src/utils/figma/sync-components.js +909 -0
  113. package/src/utils/figma/sync-main.js +692 -0
  114. package/src/utils/figma/sync-orchestration.js +683 -0
  115. package/src/utils/figma/sync-primitives.js +230 -0
  116. package/src/utils/figma/sync-semantic.js +1056 -0
  117. package/src/utils/figma/token-conversion.js +340 -0
  118. package/src/utils/figma/token-parsing.js +186 -0
  119. package/src/utils/figma/token-resolution.js +569 -0
  120. package/src/utils/figma/utils.js +199 -0
  121. package/src/utils/sd-build-configs.js +305 -0
  122. package/src/utils/sd-formats.js +965 -0
  123. package/src/utils/sd-transforms.js +165 -0
  124. package/src/utils/token-helpers.js +848 -0
  125. package/tsconfig.json +18 -0
  126. package/vitest.config.js +17 -0
  127. package/.turbo/turbo-build.log +0 -37
  128. package/build/web/core/raw.js +0 -229
  129. package/src/global/primitive/breakpoint.js +0 -19
  130. package/src/global/primitive/color.js +0 -231
  131. package/src/global/primitive/duration.js +0 -16
  132. package/src/global/primitive/font.js +0 -60
  133. package/src/global/primitive/radius.js +0 -31
  134. package/src/global/primitive/size.js +0 -55
  135. package/src/global/primitive/transition.js +0 -16
  136. package/src/theme/core/background.js +0 -170
  137. package/src/theme/core/border.js +0 -103
  138. package/src/theme/core/charts.js +0 -439
  139. package/src/theme/core/component/button.js +0 -708
  140. package/src/theme/core/component/checkbox.js +0 -405
  141. package/src/theme/core/focus.js +0 -35
  142. package/src/theme/core/foreground.js +0 -148
  143. package/src/theme/core/overlay.js +0 -137
  144. package/src/theme/core/shadow.js +0 -29
  145. package/src/theme/core/status.js +0 -49
  146. package/src/theme/core/typography.js +0 -82
  147. package/type/types.ts +0 -341
@@ -0,0 +1,1160 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import {
3
+ getDtcgValue,
4
+ getTokenType,
5
+ getDtcgDescription,
6
+ hasDarkValue,
7
+ getDarkValue,
8
+ isCompositeColor,
9
+ isDimensionValue,
10
+ wrapScssValue,
11
+ getTokenName,
12
+ buildTokenMap,
13
+ convertToHex8,
14
+ compositeColorToCssMix,
15
+ resolveReference,
16
+ resolveCompositeColor,
17
+ getTokenValue,
18
+ isReferenceString,
19
+ buildRecursiveVarChainWithLightDark,
20
+ buildFallbackWithRefs,
21
+ isGradientValue,
22
+ resolveGradientValue,
23
+ buildGradientValue,
24
+ } from "../token-helpers.js";
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Helpers
28
+ // ---------------------------------------------------------------------------
29
+
30
+ const mockDictionary = (tokens = []) => ({
31
+ allTokens: tokens,
32
+ unfilteredAllTokens: tokens,
33
+ tokens: {},
34
+ unfilteredTokens: {},
35
+ });
36
+
37
+ const mockUsesReferences = (value) =>
38
+ typeof value === "string" && value.includes("{");
39
+
40
+ const mockResolveReferences = (refString) => {
41
+ // Minimal stub: returns the refString unchanged (caller handles fallback)
42
+ return refString;
43
+ };
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // getDtcgValue
47
+ // ---------------------------------------------------------------------------
48
+
49
+ describe("getDtcgValue", () => {
50
+ it("returns the $value property", () => {
51
+ expect(getDtcgValue({ $value: "#abc" })).toBe("#abc");
52
+ });
53
+
54
+ it("returns undefined when $value is absent", () => {
55
+ expect(getDtcgValue({})).toBeUndefined();
56
+ });
57
+
58
+ it("handles object values", () => {
59
+ const val = { color: "#fff", alpha: 0.5 };
60
+ expect(getDtcgValue({ $value: val })).toBe(val);
61
+ });
62
+ });
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // getTokenType
66
+ // ---------------------------------------------------------------------------
67
+
68
+ describe("getTokenType", () => {
69
+ it("returns the $type property", () => {
70
+ expect(getTokenType({ $type: "color" })).toBe("color");
71
+ });
72
+
73
+ it("returns undefined when $type is absent", () => {
74
+ expect(getTokenType({})).toBeUndefined();
75
+ });
76
+ });
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // getDtcgDescription
80
+ // ---------------------------------------------------------------------------
81
+
82
+ describe("getDtcgDescription", () => {
83
+ it("returns the $description property", () => {
84
+ expect(getDtcgDescription({ $description: "A primary color" })).toBe(
85
+ "A primary color",
86
+ );
87
+ });
88
+
89
+ it("returns undefined when $description is absent", () => {
90
+ expect(getDtcgDescription({})).toBeUndefined();
91
+ });
92
+
93
+ it("returns empty string when $description is empty string", () => {
94
+ expect(getDtcgDescription({ $description: "" })).toBe("");
95
+ });
96
+ });
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // hasDarkValue
100
+ // ---------------------------------------------------------------------------
101
+
102
+ describe("hasDarkValue", () => {
103
+ it("returns true when dark $value is present", () => {
104
+ const token = {
105
+ $extensions: { appearance: { dark: { $value: "#000" } } },
106
+ };
107
+ expect(hasDarkValue(token)).toBe(true);
108
+ });
109
+
110
+ it("returns false when dark $value is absent", () => {
111
+ expect(hasDarkValue({})).toBe(false);
112
+ expect(hasDarkValue({ $extensions: {} })).toBe(false);
113
+ expect(hasDarkValue({ $extensions: { appearance: {} } })).toBe(false);
114
+ expect(hasDarkValue({ $extensions: { appearance: { dark: {} } } })).toBe(
115
+ false,
116
+ );
117
+ });
118
+ });
119
+
120
+ // ---------------------------------------------------------------------------
121
+ // getDarkValue
122
+ // ---------------------------------------------------------------------------
123
+
124
+ describe("getDarkValue", () => {
125
+ it("returns the dark $value", () => {
126
+ const token = {
127
+ $extensions: { appearance: { dark: { $value: "#111" } } },
128
+ };
129
+ expect(getDarkValue(token)).toBe("#111");
130
+ });
131
+
132
+ it("returns undefined when not present", () => {
133
+ expect(getDarkValue({})).toBeUndefined();
134
+ });
135
+ });
136
+
137
+ // ---------------------------------------------------------------------------
138
+ // isCompositeColor
139
+ // ---------------------------------------------------------------------------
140
+
141
+ describe("isCompositeColor", () => {
142
+ it("returns true for { color, alpha } objects", () => {
143
+ expect(isCompositeColor({ color: "#fff", alpha: 0.5 })).toBe(true);
144
+ });
145
+
146
+ it("returns false for strings", () => {
147
+ expect(isCompositeColor("#fff")).toBe(false);
148
+ });
149
+
150
+ it("returns false for arrays", () => {
151
+ expect(isCompositeColor([{ color: "#fff", alpha: 0 }])).toBe(false);
152
+ });
153
+
154
+ it("returns falsy for null/undefined", () => {
155
+ expect(isCompositeColor(null)).toBeFalsy();
156
+ expect(isCompositeColor(undefined)).toBeFalsy();
157
+ });
158
+
159
+ it("returns false when color property is missing", () => {
160
+ expect(isCompositeColor({ alpha: 0.5 })).toBe(false);
161
+ });
162
+
163
+ it("returns false when alpha property is missing", () => {
164
+ expect(isCompositeColor({ color: "#fff" })).toBe(false);
165
+ });
166
+ });
167
+
168
+ // ---------------------------------------------------------------------------
169
+ // isDimensionValue
170
+ // ---------------------------------------------------------------------------
171
+
172
+ describe("isDimensionValue", () => {
173
+ it("returns true for { value, unit } objects", () => {
174
+ expect(isDimensionValue({ value: 16, unit: "px" })).toBe(true);
175
+ });
176
+
177
+ it("returns false for strings", () => {
178
+ expect(isDimensionValue("16px")).toBe(false);
179
+ });
180
+
181
+ it("returns false for arrays", () => {
182
+ expect(isDimensionValue([16, "px"])).toBe(false);
183
+ });
184
+
185
+ it("returns falsy for null/undefined", () => {
186
+ expect(isDimensionValue(null)).toBeFalsy();
187
+ expect(isDimensionValue(undefined)).toBeFalsy();
188
+ });
189
+
190
+ it("returns false when value property is missing", () => {
191
+ expect(isDimensionValue({ unit: "px" })).toBe(false);
192
+ });
193
+
194
+ it("returns false when unit property is missing", () => {
195
+ expect(isDimensionValue({ value: 16 })).toBe(false);
196
+ });
197
+ });
198
+
199
+ // ---------------------------------------------------------------------------
200
+ // wrapScssValue
201
+ // ---------------------------------------------------------------------------
202
+
203
+ describe("wrapScssValue", () => {
204
+ it("wraps strings containing commas in double quotes", () => {
205
+ expect(wrapScssValue("Arial, sans-serif")).toBe('"Arial, sans-serif"');
206
+ });
207
+
208
+ it("returns strings without commas unchanged", () => {
209
+ expect(wrapScssValue("Arial")).toBe("Arial");
210
+ });
211
+
212
+ it("returns non-string values unchanged", () => {
213
+ expect(wrapScssValue(42)).toBe(42);
214
+ expect(wrapScssValue(null)).toBe(null);
215
+ expect(wrapScssValue(undefined)).toBe(undefined);
216
+ });
217
+
218
+ it("wraps even when comma is at the start or end", () => {
219
+ expect(wrapScssValue(",value")).toBe('",value"');
220
+ expect(wrapScssValue("value,")).toBe('"value,"');
221
+ });
222
+ });
223
+
224
+ // ---------------------------------------------------------------------------
225
+ // getTokenName
226
+ // ---------------------------------------------------------------------------
227
+
228
+ describe("getTokenName", () => {
229
+ it("joins path segments with hyphens", () => {
230
+ expect(getTokenName({ path: ["color", "primary", "500"] })).toBe(
231
+ "color-primary-500",
232
+ );
233
+ });
234
+
235
+ it("filters out $schema segment", () => {
236
+ expect(getTokenName({ path: ["$schema", "color", "neutral"] })).toBe(
237
+ "color-neutral",
238
+ );
239
+ });
240
+
241
+ it("filters out any $-prefixed segments", () => {
242
+ expect(getTokenName({ path: ["$meta", "size", "4"] })).toBe("size-4");
243
+ });
244
+
245
+ it("returns 'unknown' for empty path", () => {
246
+ expect(getTokenName({ path: [] })).toBe("unknown");
247
+ });
248
+
249
+ it("returns 'unknown' when path is absent", () => {
250
+ expect(getTokenName({})).toBe("unknown");
251
+ });
252
+
253
+ it("returns 'unknown' when all segments are filtered out", () => {
254
+ expect(getTokenName({ path: ["$schema", "$meta"] })).toBe("unknown");
255
+ });
256
+
257
+ it("handles numeric path segments", () => {
258
+ expect(getTokenName({ path: ["size", 4] })).toBe("size-4");
259
+ });
260
+ });
261
+
262
+ // ---------------------------------------------------------------------------
263
+ // buildTokenMap
264
+ // ---------------------------------------------------------------------------
265
+
266
+ describe("buildTokenMap", () => {
267
+ it("creates a Map keyed by token name", () => {
268
+ const token = { path: ["color", "primary"], $value: "#fff" };
269
+ const dict = mockDictionary([token]);
270
+ const map = buildTokenMap(dict);
271
+ expect(map).toBeInstanceOf(Map);
272
+ expect(map.get("color-primary")).toBe(token);
273
+ });
274
+
275
+ it("caches result via WeakMap (same reference returned)", () => {
276
+ const dict = mockDictionary([{ path: ["a"], $value: "1" }]);
277
+ const first = buildTokenMap(dict);
278
+ const second = buildTokenMap(dict);
279
+ expect(first).toBe(second);
280
+ });
281
+
282
+ it("falls back to allTokens when unfilteredAllTokens is absent", () => {
283
+ const token = { path: ["size", "4"], $value: "1rem" };
284
+ const dict = { allTokens: [token], tokens: {} };
285
+ const map = buildTokenMap(dict);
286
+ expect(map.get("size-4")).toBe(token);
287
+ });
288
+
289
+ it("handles empty token list", () => {
290
+ const dict = mockDictionary([]);
291
+ const map = buildTokenMap(dict);
292
+ expect(map.size).toBe(0);
293
+ });
294
+ });
295
+
296
+ // ---------------------------------------------------------------------------
297
+ // convertToHex8
298
+ // ---------------------------------------------------------------------------
299
+
300
+ describe("convertToHex8", () => {
301
+ it("converts 6-digit hex with alpha 0.5 to hex8", () => {
302
+ expect(convertToHex8("#ff0000", 0.5)).toBe("#ff000080");
303
+ });
304
+
305
+ it("expands 3-digit shorthand hex", () => {
306
+ expect(convertToHex8("#f00", 1)).toBe("#ff0000FF");
307
+ });
308
+
309
+ it("uses alpha 0 → '00'", () => {
310
+ expect(convertToHex8("#ffffff", 0)).toBe("#ffffff00");
311
+ });
312
+
313
+ it("uses alpha 1 → 'FF'", () => {
314
+ expect(convertToHex8("#000000", 1)).toBe("#000000FF");
315
+ });
316
+
317
+ it("returns input unchanged for invalid hex (no #)", () => {
318
+ expect(convertToHex8("ff0000", 0.5)).toBe("ff0000");
319
+ });
320
+
321
+ it("returns input unchanged when not a string", () => {
322
+ expect(convertToHex8(null, 0.5)).toBe(null);
323
+ expect(convertToHex8(undefined, 0.5)).toBe(undefined);
324
+ });
325
+
326
+ it("returns input unchanged for wrong hex length (5 digits)", () => {
327
+ expect(convertToHex8("#12345", 0.5)).toBe("#12345");
328
+ });
329
+
330
+ it("handles floating point alpha without drift (0.06 → 6%)", () => {
331
+ // 0.06 * 255 = 15.3 → rounds to 15 → 0x0F
332
+ const result = convertToHex8("#ffffff", 0.06);
333
+ expect(result).toBe("#ffffff0F");
334
+ });
335
+ });
336
+
337
+ // ---------------------------------------------------------------------------
338
+ // compositeColorToCssMix
339
+ // ---------------------------------------------------------------------------
340
+
341
+ describe("compositeColorToCssMix", () => {
342
+ it("generates correct color-mix() with prefix", () => {
343
+ const result = compositeColorToCssMix(
344
+ "color-neutral-0",
345
+ "#ffffff",
346
+ 0.08,
347
+ "a2-",
348
+ );
349
+ expect(result).toBe(
350
+ "color-mix(in srgb, var(--a2-color-neutral-0, #ffffff) 8%, transparent)",
351
+ );
352
+ });
353
+
354
+ it("generates color-mix() without prefix", () => {
355
+ const result = compositeColorToCssMix("color-neutral-0", "#ffffff", 0.08);
356
+ expect(result).toBe(
357
+ "color-mix(in srgb, var(--color-neutral-0, #ffffff) 8%, transparent)",
358
+ );
359
+ });
360
+
361
+ it("avoids floating point drift (0.06 → 6%)", () => {
362
+ const result = compositeColorToCssMix("color-neutral-0", "#fff", 0.06, "");
363
+ expect(result).toContain("6%");
364
+ expect(result).not.toContain("5.999");
365
+ });
366
+
367
+ it("handles alpha 1 → 100%", () => {
368
+ const result = compositeColorToCssMix("color-neutral-0", "#fff", 1, "");
369
+ expect(result).toContain("100%");
370
+ });
371
+
372
+ it("handles alpha 0 → 0%", () => {
373
+ const result = compositeColorToCssMix("color-neutral-0", "#fff", 0, "");
374
+ expect(result).toContain("0%");
375
+ });
376
+ });
377
+
378
+ // ---------------------------------------------------------------------------
379
+ // resolveReference
380
+ // ---------------------------------------------------------------------------
381
+
382
+ describe("resolveReference", () => {
383
+ it("returns non-strings unchanged", () => {
384
+ const obj = { $value: "#fff" };
385
+ expect(resolveReference(obj, {}, () => obj)).toBe(obj);
386
+ });
387
+
388
+ it("calls resolveReferences with the string and dictionary tokens", () => {
389
+ const resolveFn = vi.fn().mockReturnValue("#resolved");
390
+ const dict = { tokens: { color: {} }, unfilteredTokens: { color: {} } };
391
+ const result = resolveReference("{color.primary}", dict, resolveFn);
392
+ expect(result).toBe("#resolved");
393
+ expect(resolveFn).toHaveBeenCalledWith(
394
+ "{color.primary}",
395
+ dict.unfilteredTokens,
396
+ { usesDtcg: true },
397
+ );
398
+ });
399
+
400
+ it("falls back to original string if resolveReferences throws", () => {
401
+ const resolveFn = () => {
402
+ throw new Error("unresolvable");
403
+ };
404
+ const dict = { tokens: {}, unfilteredTokens: {} };
405
+ const result = resolveReference("{bad.ref}", dict, resolveFn);
406
+ expect(result).toBe("{bad.ref}");
407
+ });
408
+
409
+ it("uses unfilteredTokens when available over tokens", () => {
410
+ const resolveFn = vi.fn().mockReturnValue("#ok");
411
+ const dict = {
412
+ tokens: { a: 1 },
413
+ unfilteredTokens: { a: 1, b: 2 },
414
+ };
415
+ resolveReference("{a}", dict, resolveFn);
416
+ expect(resolveFn).toHaveBeenCalledWith("{a}", dict.unfilteredTokens, {
417
+ usesDtcg: true,
418
+ });
419
+ });
420
+ });
421
+
422
+ // ---------------------------------------------------------------------------
423
+ // resolveCompositeColor
424
+ // ---------------------------------------------------------------------------
425
+
426
+ describe("resolveCompositeColor", () => {
427
+ it("returns null for non-composite-color input", () => {
428
+ const dict = mockDictionary([]);
429
+ expect(resolveCompositeColor("#fff", dict, mockResolveReferences)).toBe(
430
+ null,
431
+ );
432
+ });
433
+
434
+ it("returns hex8 when no tokenMap provided", () => {
435
+ const colorObj = { color: "#ffffff", alpha: 0.5 };
436
+ const dict = mockDictionary([]);
437
+ const resolveFn = vi.fn().mockReturnValue("#ffffff");
438
+ const result = resolveCompositeColor(colorObj, dict, resolveFn);
439
+ expect(result).toBe("#ffffff80");
440
+ });
441
+
442
+ it("returns color-mix() when tokenMap has the referenced token", () => {
443
+ const refToken = { path: ["color", "neutral", "0"], $value: "#ffffff" };
444
+ const dict = mockDictionary([refToken]);
445
+ const tokenMap = buildTokenMap(dict);
446
+ const colorObj = { color: "{color.neutral.0}", alpha: 0.08 };
447
+ const resolveFn = vi.fn().mockReturnValue("#ffffff");
448
+ const result = resolveCompositeColor(
449
+ colorObj,
450
+ dict,
451
+ resolveFn,
452
+ tokenMap,
453
+ "a2-",
454
+ );
455
+ expect(result).toBe(
456
+ "color-mix(in srgb, var(--a2-color-neutral-0, #ffffff) 8%, transparent)",
457
+ );
458
+ });
459
+
460
+ it("returns hex8 when tokenMap does not contain the referenced token", () => {
461
+ const dict = mockDictionary([]);
462
+ const tokenMap = new Map();
463
+ const colorObj = { color: "{color.missing}", alpha: 0.5 };
464
+ const resolveFn = vi.fn().mockReturnValue("#aabbcc");
465
+ const result = resolveCompositeColor(
466
+ colorObj,
467
+ dict,
468
+ resolveFn,
469
+ tokenMap,
470
+ "a2-",
471
+ );
472
+ expect(result).toBe("#aabbcc80");
473
+ });
474
+
475
+ it("returns resolved color value when color doesn't start with #", () => {
476
+ const dict = mockDictionary([]);
477
+ const colorObj = { color: "{some.ref}", alpha: 0.5 };
478
+ const resolveFn = vi.fn().mockReturnValue("rgb(255,255,255)");
479
+ const result = resolveCompositeColor(colorObj, dict, resolveFn);
480
+ expect(result).toBe("rgb(255,255,255)");
481
+ });
482
+ });
483
+
484
+ // ---------------------------------------------------------------------------
485
+ // getTokenValue
486
+ // ---------------------------------------------------------------------------
487
+
488
+ describe("getTokenValue", () => {
489
+ const resolveFn = (val) => val; // identity
490
+
491
+ it("returns plain string value", () => {
492
+ const token = { $value: "#abc123" };
493
+ expect(getTokenValue(token, mockDictionary(), {}, resolveFn)).toBe(
494
+ "#abc123",
495
+ );
496
+ });
497
+
498
+ it("returns dark value when isDark=true and dark value exists", () => {
499
+ const token = {
500
+ $value: "#ffffff",
501
+ $extensions: { appearance: { dark: { $value: "#000000" } } },
502
+ };
503
+ expect(
504
+ getTokenValue(token, mockDictionary(), { isDark: true }, resolveFn),
505
+ ).toBe("#000000");
506
+ });
507
+
508
+ it("returns light value when isDark=true but no dark value", () => {
509
+ const token = { $value: "#ffffff" };
510
+ expect(
511
+ getTokenValue(token, mockDictionary(), { isDark: true }, resolveFn),
512
+ ).toBe("#ffffff");
513
+ });
514
+
515
+ it("unpacks dimension objects { value, unit }", () => {
516
+ const token = { $value: { value: 16, unit: "px" } };
517
+ expect(getTokenValue(token, mockDictionary(), {}, resolveFn)).toBe("16px");
518
+ });
519
+
520
+ it("normalizes 'transparent' to rgba(0, 0, 0, 0)", () => {
521
+ const token = { $value: "transparent" };
522
+ expect(getTokenValue(token, mockDictionary(), {}, resolveFn)).toBe(
523
+ "rgba(0, 0, 0, 0)",
524
+ );
525
+ });
526
+
527
+ it("returns empty string for undefined value", () => {
528
+ const token = { $value: undefined };
529
+ expect(getTokenValue(token, mockDictionary(), {}, resolveFn)).toBe("");
530
+ });
531
+
532
+ it("returns empty string for null value", () => {
533
+ const token = { $value: null };
534
+ expect(getTokenValue(token, mockDictionary(), {}, resolveFn)).toBe("");
535
+ });
536
+
537
+ it("wraps comma-containing values when forScssMap=true", () => {
538
+ const token = { $value: "Arial, sans-serif" };
539
+ expect(
540
+ getTokenValue(token, mockDictionary(), { forScssMap: true }, resolveFn),
541
+ ).toBe('"Arial, sans-serif"');
542
+ });
543
+
544
+ it("does NOT wrap comma values when forScssMap=false", () => {
545
+ const token = { $value: "Arial, sans-serif" };
546
+ expect(getTokenValue(token, mockDictionary(), {}, resolveFn)).toBe(
547
+ "Arial, sans-serif",
548
+ );
549
+ });
550
+
551
+ it("resolves composite color value", () => {
552
+ const token = { $value: { color: "#ffffff", alpha: 0.5 } };
553
+ const resolveFnHex = vi.fn().mockReturnValue("#ffffff");
554
+ const result = getTokenValue(token, mockDictionary(), {}, resolveFnHex);
555
+ expect(result).toBe("#ffffff80");
556
+ });
557
+
558
+ it("stringifies non-string non-object values", () => {
559
+ const token = { $value: 42 };
560
+ expect(getTokenValue(token, mockDictionary(), {}, resolveFn)).toBe("42");
561
+ });
562
+
563
+ it("extracts value from token-object returned by resolveReferences", () => {
564
+ // resolveReferences returns a full token object
565
+ const resolveFnObj = () => ({ $value: "#resolved" });
566
+ const token = { $value: "{some.ref}" };
567
+ const dict = { allTokens: [], tokens: {}, unfilteredTokens: {} };
568
+ const result = getTokenValue(token, dict, {}, resolveFnObj);
569
+ expect(result).toBe("#resolved");
570
+ });
571
+ });
572
+
573
+ // ---------------------------------------------------------------------------
574
+ // isReferenceString
575
+ // ---------------------------------------------------------------------------
576
+
577
+ describe("isReferenceString", () => {
578
+ it("returns true for strings usesReferences considers a reference", () => {
579
+ expect(isReferenceString("{color.primary}", mockUsesReferences)).toBe(true);
580
+ });
581
+
582
+ it("returns false for plain strings", () => {
583
+ expect(isReferenceString("#ffffff", mockUsesReferences)).toBe(false);
584
+ });
585
+
586
+ it("returns false for non-strings", () => {
587
+ expect(isReferenceString(42, mockUsesReferences)).toBe(false);
588
+ expect(isReferenceString(null, mockUsesReferences)).toBe(false);
589
+ expect(isReferenceString(undefined, mockUsesReferences)).toBe(false);
590
+ });
591
+ });
592
+
593
+ // ---------------------------------------------------------------------------
594
+ // buildRecursiveVarChainWithLightDark
595
+ // ---------------------------------------------------------------------------
596
+
597
+ describe("buildRecursiveVarChainWithLightDark", () => {
598
+ const resolveRefsFn = (val) => val;
599
+
600
+ it("returns non-reference values as-is", () => {
601
+ const tokenMap = new Map();
602
+ const dict = mockDictionary([]);
603
+ const result = buildRecursiveVarChainWithLightDark(
604
+ "#ffffff",
605
+ tokenMap,
606
+ dict,
607
+ false,
608
+ mockUsesReferences,
609
+ resolveRefsFn,
610
+ "a2-",
611
+ );
612
+ expect(result).toBe("#ffffff");
613
+ });
614
+
615
+ it("returns var(--name) for unknown token references", () => {
616
+ const tokenMap = new Map();
617
+ const dict = mockDictionary([]);
618
+ const result = buildRecursiveVarChainWithLightDark(
619
+ "{unknown.token}",
620
+ tokenMap,
621
+ dict,
622
+ false,
623
+ mockUsesReferences,
624
+ resolveRefsFn,
625
+ "a2-",
626
+ );
627
+ expect(result).toBe("var(--a2-unknown-token)");
628
+ });
629
+
630
+ it("builds var chain for primitive token (no dark variant)", () => {
631
+ const primToken = {
632
+ path: ["color", "neutral", "0"],
633
+ $value: "#ffffff",
634
+ original: { $value: "#ffffff" },
635
+ };
636
+ const tokenMap = new Map([["color-neutral-0", primToken]]);
637
+ const dict = mockDictionary([primToken]);
638
+ const result = buildRecursiveVarChainWithLightDark(
639
+ "{color.neutral.0}",
640
+ tokenMap,
641
+ dict,
642
+ true,
643
+ mockUsesReferences,
644
+ resolveRefsFn,
645
+ "a2-",
646
+ );
647
+ expect(result).toBe("var(--a2-color-neutral-0, #ffffff)");
648
+ });
649
+
650
+ it("inserts light-dark() when light and dark values differ", () => {
651
+ const primToken = {
652
+ path: ["color", "neutral", "0"],
653
+ $value: "#ffffff",
654
+ original: {
655
+ $value: "#ffffff",
656
+ $extensions: { appearance: { dark: { $value: "#000000" } } },
657
+ },
658
+ $extensions: { appearance: { dark: { $value: "#000000" } } },
659
+ };
660
+ const dict = mockDictionary([primToken]);
661
+ const tokenMap = buildTokenMap(dict);
662
+ const resolveRefs = vi.fn().mockReturnValue("#ffffff");
663
+
664
+ const result = buildRecursiveVarChainWithLightDark(
665
+ "{color.neutral.0}",
666
+ tokenMap,
667
+ dict,
668
+ true,
669
+ mockUsesReferences,
670
+ resolveRefs,
671
+ "a2-",
672
+ );
673
+ expect(result).toContain("light-dark(");
674
+ expect(result).toContain("var(--a2-color-neutral-0,");
675
+ });
676
+
677
+ it("propagates recursion through intermediate references", () => {
678
+ const primToken = {
679
+ path: ["color", "red", "500"],
680
+ $value: "#e13212",
681
+ original: { $value: "#e13212" },
682
+ };
683
+ const semanticToken = {
684
+ path: ["status", "color", "danger"],
685
+ $value: "{color.red.500}",
686
+ original: { $value: "{color.red.500}" },
687
+ };
688
+ const dict = mockDictionary([primToken, semanticToken]);
689
+ const tokenMap = buildTokenMap(dict);
690
+
691
+ const result = buildRecursiveVarChainWithLightDark(
692
+ "{status.color.danger}",
693
+ tokenMap,
694
+ dict,
695
+ false,
696
+ mockUsesReferences,
697
+ resolveRefsFn,
698
+ "a2-",
699
+ );
700
+ // Should chain through to the primitive
701
+ expect(result).toContain("var(--a2-status-color-danger,");
702
+ expect(result).toContain("var(--a2-color-red-500,");
703
+ });
704
+ });
705
+
706
+ // ---------------------------------------------------------------------------
707
+ // buildFallbackWithRefs
708
+ // ---------------------------------------------------------------------------
709
+
710
+ describe("buildFallbackWithRefs", () => {
711
+ const resolveRefsFn = (val) => val;
712
+
713
+ it("returns resolved plain value when no references", () => {
714
+ const token = { $value: "#ffffff", original: { $value: "#ffffff" } };
715
+ const dict = mockDictionary([token]);
716
+ const tokenMap = buildTokenMap(dict);
717
+ const result = buildFallbackWithRefs(
718
+ token,
719
+ dict,
720
+ tokenMap,
721
+ {},
722
+ mockUsesReferences,
723
+ resolveRefsFn,
724
+ );
725
+ expect(result).toBe("#ffffff");
726
+ });
727
+
728
+ it("returns light-dark() when top-level light and dark values differ", () => {
729
+ const token = {
730
+ $value: "#ffffff",
731
+ original: {
732
+ $value: "#ffffff",
733
+ $extensions: { appearance: { dark: { $value: "#000000" } } },
734
+ },
735
+ $extensions: { appearance: { dark: { $value: "#000000" } } },
736
+ };
737
+ const dict = mockDictionary([token]);
738
+ const tokenMap = buildTokenMap(dict);
739
+ const result = buildFallbackWithRefs(
740
+ token,
741
+ dict,
742
+ tokenMap,
743
+ { useLightDark: true },
744
+ mockUsesReferences,
745
+ resolveRefsFn,
746
+ );
747
+ expect(result).toBe("light-dark(#ffffff, #000000)");
748
+ });
749
+
750
+ it("returns plain value when light and dark are the same", () => {
751
+ const token = {
752
+ $value: "#ffffff",
753
+ original: {
754
+ $value: "#ffffff",
755
+ $extensions: { appearance: { dark: { $value: "#ffffff" } } },
756
+ },
757
+ $extensions: { appearance: { dark: { $value: "#ffffff" } } },
758
+ };
759
+ const dict = mockDictionary([token]);
760
+ const tokenMap = buildTokenMap(dict);
761
+ const result = buildFallbackWithRefs(
762
+ token,
763
+ dict,
764
+ tokenMap,
765
+ { useLightDark: true },
766
+ mockUsesReferences,
767
+ resolveRefsFn,
768
+ );
769
+ expect(result).toBe("#ffffff");
770
+ });
771
+
772
+ it("returns dark value when isDark=true and no references", () => {
773
+ const token = {
774
+ $value: "#ffffff",
775
+ original: {
776
+ $value: "#ffffff",
777
+ $extensions: { appearance: { dark: { $value: "#111111" } } },
778
+ },
779
+ $extensions: { appearance: { dark: { $value: "#111111" } } },
780
+ };
781
+ const dict = mockDictionary([token]);
782
+ const tokenMap = buildTokenMap(dict);
783
+ const result = buildFallbackWithRefs(
784
+ token,
785
+ dict,
786
+ tokenMap,
787
+ { isDark: true },
788
+ mockUsesReferences,
789
+ resolveRefsFn,
790
+ );
791
+ expect(result).toBe("#111111");
792
+ });
793
+
794
+ it("handles reference values (light=dark=ref) without light-dark()", () => {
795
+ const primToken = {
796
+ path: ["color", "neutral", "0"],
797
+ $value: "#ffffff",
798
+ original: { $value: "#ffffff" },
799
+ };
800
+ const token = {
801
+ $value: "{color.neutral.0}",
802
+ original: { $value: "{color.neutral.0}" },
803
+ path: ["semantic", "bg"],
804
+ };
805
+ const dict = mockDictionary([primToken, token]);
806
+ const tokenMap = buildTokenMap(dict);
807
+ const result = buildFallbackWithRefs(
808
+ token,
809
+ dict,
810
+ tokenMap,
811
+ { useLightDark: false },
812
+ mockUsesReferences,
813
+ resolveRefsFn,
814
+ );
815
+ expect(result).toContain("var(--color-neutral-0,");
816
+ });
817
+
818
+ it("handles composite color with light-dark() when referenced token has dark", () => {
819
+ const refToken = {
820
+ path: ["color", "neutral", "0"],
821
+ $value: "#ffffff",
822
+ original: {
823
+ $value: "#ffffff",
824
+ $extensions: { appearance: { dark: { $value: "#000000" } } },
825
+ },
826
+ $extensions: { appearance: { dark: { $value: "#000000" } } },
827
+ };
828
+ const token = {
829
+ $value: { color: "{color.neutral.0}", alpha: 0.08 },
830
+ original: { $value: { color: "{color.neutral.0}", alpha: 0.08 } },
831
+ };
832
+ const resolveRefs = vi.fn().mockReturnValue("#ffffff");
833
+ const dict = mockDictionary([refToken]);
834
+ const tokenMap = buildTokenMap(dict);
835
+ const result = buildFallbackWithRefs(
836
+ token,
837
+ dict,
838
+ tokenMap,
839
+ { useLightDark: true, cssVarPrefix: "a2-" },
840
+ mockUsesReferences,
841
+ resolveRefs,
842
+ );
843
+ expect(result).toContain("light-dark(");
844
+ expect(result).toContain("color-mix(");
845
+ });
846
+
847
+ it("handles composite color (same light/dark) falling through to color-mix()", () => {
848
+ const refToken = {
849
+ path: ["color", "neutral", "0"],
850
+ $value: "#ffffff",
851
+ original: { $value: "#ffffff" },
852
+ };
853
+ const token = {
854
+ $value: { color: "{color.neutral.0}", alpha: 0.5 },
855
+ original: { $value: { color: "{color.neutral.0}", alpha: 0.5 } },
856
+ };
857
+ const resolveRefs = vi.fn().mockReturnValue("#ffffff");
858
+ const dict = mockDictionary([refToken]);
859
+ const tokenMap = buildTokenMap(dict);
860
+ const result = buildFallbackWithRefs(
861
+ token,
862
+ dict,
863
+ tokenMap,
864
+ { useLightDark: false, cssVarPrefix: "a2-" },
865
+ mockUsesReferences,
866
+ resolveRefs,
867
+ );
868
+ expect(result).toContain("color-mix(");
869
+ expect(result).toContain("#ffffff");
870
+ });
871
+ });
872
+
873
+ // ---------------------------------------------------------------------------
874
+ // isGradientValue
875
+ // ---------------------------------------------------------------------------
876
+
877
+ describe("isGradientValue", () => {
878
+ it("returns true for a valid gradient object", () => {
879
+ expect(isGradientValue({ type: "linear", angle: 90, stops: [] })).toBe(
880
+ true,
881
+ );
882
+ });
883
+
884
+ it("returns false for strings", () => {
885
+ expect(isGradientValue("#fff")).toBe(false);
886
+ });
887
+
888
+ it("returns false for arrays", () => {
889
+ expect(isGradientValue([{ type: "linear" }])).toBe(false);
890
+ });
891
+
892
+ it("returns falsy for null/undefined", () => {
893
+ expect(isGradientValue(null)).toBeFalsy();
894
+ expect(isGradientValue(undefined)).toBeFalsy();
895
+ });
896
+
897
+ it("returns false when type is not a string", () => {
898
+ expect(isGradientValue({ type: 42, angle: 90, stops: [] })).toBe(false);
899
+ });
900
+
901
+ it("returns false when angle is not a number", () => {
902
+ expect(isGradientValue({ type: "linear", angle: "90deg", stops: [] })).toBe(
903
+ false,
904
+ );
905
+ });
906
+
907
+ it("returns false when stops is not an array", () => {
908
+ expect(isGradientValue({ type: "linear", angle: 90, stops: null })).toBe(
909
+ false,
910
+ );
911
+ });
912
+ });
913
+
914
+ // ---------------------------------------------------------------------------
915
+ // resolveGradientValue
916
+ // ---------------------------------------------------------------------------
917
+
918
+ const makeGradientValue = (overrides = {}) => ({
919
+ type: "linear",
920
+ angle: 90,
921
+ stops: [],
922
+ ...overrides,
923
+ });
924
+
925
+ describe("resolveGradientValue", () => {
926
+ it("returns gradient value directly from token.original.$value", () => {
927
+ const gradient = makeGradientValue({ stops: [] });
928
+ const token = { original: { $value: gradient } };
929
+ expect(resolveGradientValue(token, new Map())).toBe(gradient);
930
+ });
931
+
932
+ it("follows a reference string to find the gradient in tokenMap", () => {
933
+ const gradient = makeGradientValue();
934
+ const refToken = { original: { $value: gradient } };
935
+ const tokenMap = new Map([["gradient-primary", refToken]]);
936
+ const token = { original: { $value: "{gradient.primary}" } };
937
+ expect(resolveGradientValue(token, tokenMap)).toBe(gradient);
938
+ });
939
+
940
+ it("returns null when reference cannot be found in tokenMap", () => {
941
+ const token = { original: { $value: "{gradient.missing}" } };
942
+ expect(resolveGradientValue(token, new Map())).toBeNull();
943
+ });
944
+
945
+ it("returns null when original.$value is a plain string (not a reference)", () => {
946
+ const token = { original: { $value: "some-string" } };
947
+ expect(resolveGradientValue(token, new Map())).toBeNull();
948
+ });
949
+
950
+ it("returns null when original is absent", () => {
951
+ expect(resolveGradientValue({}, new Map())).toBeNull();
952
+ });
953
+
954
+ it("resolves nested references (reference pointing to another reference)", () => {
955
+ const gradient = makeGradientValue();
956
+ const leafToken = { original: { $value: gradient } };
957
+ const midToken = { original: { $value: "{gradient.primary}" } };
958
+ const tokenMap = new Map([
959
+ ["gradient-primary", leafToken],
960
+ ["alias-gradient", midToken],
961
+ ]);
962
+ const token = { original: { $value: "{alias.gradient}" } };
963
+ expect(resolveGradientValue(token, tokenMap)).toBe(gradient);
964
+ });
965
+ });
966
+
967
+ // ---------------------------------------------------------------------------
968
+ // buildGradientValue
969
+ // ---------------------------------------------------------------------------
970
+
971
+ describe("buildGradientValue", () => {
972
+ const usesRefs = (v) => typeof v === "string" && v.includes("{");
973
+ const resolveRefs = (v) => v;
974
+
975
+ const makeStop = (lightRef, darkRef, position) => ({
976
+ position,
977
+ color: {
978
+ $type: "color",
979
+ $value: lightRef,
980
+ $extensions: {
981
+ appearance: {
982
+ light: { $type: "color", $value: lightRef },
983
+ dark: { $type: "color", $value: darkRef },
984
+ },
985
+ },
986
+ },
987
+ });
988
+
989
+ it("produces linear-gradient() with correct angle", () => {
990
+ const gradient = {
991
+ type: "linear",
992
+ angle: 90,
993
+ stops: [
994
+ makeStop("#0265dc", "#45d8f2", 0),
995
+ makeStop("#45d8f2", "#cdf4fb", 1),
996
+ ],
997
+ };
998
+ const result = buildGradientValue(
999
+ gradient,
1000
+ false,
1001
+ new Map(),
1002
+ mockDictionary(),
1003
+ "",
1004
+ usesRefs,
1005
+ resolveRefs,
1006
+ );
1007
+ expect(result).toMatch(/^linear-gradient\(90deg,/);
1008
+ });
1009
+
1010
+ it("uses light stop colors when isDark=false", () => {
1011
+ const gradient = {
1012
+ type: "linear",
1013
+ angle: 45,
1014
+ stops: [makeStop("#aaaaaa", "#111111", 0)],
1015
+ };
1016
+ const result = buildGradientValue(
1017
+ gradient,
1018
+ false,
1019
+ new Map(),
1020
+ mockDictionary(),
1021
+ "",
1022
+ usesRefs,
1023
+ resolveRefs,
1024
+ );
1025
+ expect(result).toContain("#aaaaaa");
1026
+ expect(result).not.toContain("#111111");
1027
+ });
1028
+
1029
+ it("uses dark stop colors when isDark=true", () => {
1030
+ const gradient = {
1031
+ type: "linear",
1032
+ angle: 45,
1033
+ stops: [makeStop("#aaaaaa", "#111111", 0)],
1034
+ };
1035
+ const result = buildGradientValue(
1036
+ gradient,
1037
+ true,
1038
+ new Map(),
1039
+ mockDictionary(),
1040
+ "",
1041
+ usesRefs,
1042
+ resolveRefs,
1043
+ );
1044
+ expect(result).toContain("#111111");
1045
+ expect(result).not.toContain("#aaaaaa");
1046
+ });
1047
+
1048
+ it("passes string positions through as-is", () => {
1049
+ const gradient = {
1050
+ type: "linear",
1051
+ angle: 90,
1052
+ stops: [
1053
+ makeStop("#fff", "#000", "10px"),
1054
+ makeStop("#000", "#fff", "50px"),
1055
+ ],
1056
+ };
1057
+ const result = buildGradientValue(
1058
+ gradient,
1059
+ false,
1060
+ new Map(),
1061
+ mockDictionary(),
1062
+ "",
1063
+ usesRefs,
1064
+ resolveRefs,
1065
+ );
1066
+ expect(result).toContain("10px");
1067
+ expect(result).toContain("50px");
1068
+ expect(result).not.toContain("1000%");
1069
+ });
1070
+
1071
+ it("resolves token references embedded in string positions", () => {
1072
+ const sizeToken = {
1073
+ path: ["size", "4"],
1074
+ $value: "1rem",
1075
+ original: { $value: "1rem" },
1076
+ };
1077
+ const tokenMap = new Map([["size-4", sizeToken]]);
1078
+ const dict = mockDictionary([sizeToken]);
1079
+
1080
+ const gradient = {
1081
+ type: "linear",
1082
+ angle: 90,
1083
+ stops: [makeStop("#fff", "#000", "calc(100% - {size.4})")],
1084
+ };
1085
+ const result = buildGradientValue(
1086
+ gradient,
1087
+ false,
1088
+ tokenMap,
1089
+ dict,
1090
+ "a2-",
1091
+ usesRefs,
1092
+ resolveRefs,
1093
+ );
1094
+ expect(result).toContain("calc(100% - var(--a2-size-4,");
1095
+ expect(result).not.toContain("{size.4}");
1096
+ });
1097
+
1098
+ it("converts position 0→'0%' and 1→'100%'", () => {
1099
+ const gradient = {
1100
+ type: "linear",
1101
+ angle: 90,
1102
+ stops: [makeStop("#fff", "#000", 0), makeStop("#000", "#fff", 1)],
1103
+ };
1104
+ const result = buildGradientValue(
1105
+ gradient,
1106
+ false,
1107
+ new Map(),
1108
+ mockDictionary(),
1109
+ "",
1110
+ usesRefs,
1111
+ resolveRefs,
1112
+ );
1113
+ expect(result).toContain("0%");
1114
+ expect(result).toContain("100%");
1115
+ });
1116
+
1117
+ it("builds var() chain for reference color stops", () => {
1118
+ const blueToken = {
1119
+ path: ["color", "blue", "600"],
1120
+ $value: "#0265dc",
1121
+ original: { $value: "#0265dc" },
1122
+ };
1123
+ const tokenMap = new Map([["color-blue-600", blueToken]]);
1124
+ const dict = mockDictionary([blueToken]);
1125
+
1126
+ const gradient = {
1127
+ type: "linear",
1128
+ angle: 90,
1129
+ stops: [makeStop("{color.blue.600}", "{color.blue.600}", 0)],
1130
+ };
1131
+ const result = buildGradientValue(
1132
+ gradient,
1133
+ false,
1134
+ tokenMap,
1135
+ dict,
1136
+ "a2-",
1137
+ usesRefs,
1138
+ resolveRefs,
1139
+ );
1140
+ expect(result).toContain("var(--a2-color-blue-600,");
1141
+ });
1142
+
1143
+ it("falls back to stop.$value when appearance light/dark is absent", () => {
1144
+ const stop = {
1145
+ position: 0,
1146
+ color: { $type: "color", $value: "#fallback" },
1147
+ };
1148
+ const gradient = { type: "linear", angle: 90, stops: [stop] };
1149
+ const result = buildGradientValue(
1150
+ gradient,
1151
+ false,
1152
+ new Map(),
1153
+ mockDictionary(),
1154
+ "",
1155
+ usesRefs,
1156
+ resolveRefs,
1157
+ );
1158
+ expect(result).toContain("#fallback");
1159
+ });
1160
+ });