@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.
- package/CHANGELOG.md +56 -0
- 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 +47 -45
- package/build/web/core/css-utils/a2-color.css +443 -227
- package/build/web/core/css-utils/a2-font.css +0 -2
- package/build/web/core/css-utils/a2-spacing.css +476 -478
- package/build/web/core/css-utils/a2-utils.css +992 -772
- package/build/web/core/css-utils/border.css +47 -45
- package/build/web/core/css-utils/color.css +443 -227
- package/build/web/core/css-utils/font.css +0 -2
- package/build/web/core/css-utils/spacing.css +476 -478
- package/build/web/core/css-utils/utils.css +992 -772
- 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 -239
- package/build/web/core/semantic.d.ts +221 -0
- package/build/web/core/semantic.js +1613 -347
- package/build/web/core/semantic.scss +219 -137
- package/build/web/index.d.ts +3 -4
- package/build/web/index.js +0 -1
- package/build/web/types.d.ts +17 -0
- package/config.js +121 -497
- 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 +74 -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 +950 -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 +965 -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 -229
- 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 -439
- 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 -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
|
+
});
|