@servicetitan/hammer-token 2.5.2 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/CHANGELOG.md +52 -2
  2. package/README.md +332 -0
  3. package/build/web/core/component-variables.scss +1088 -131
  4. package/build/web/core/component.d.ts +558 -0
  5. package/build/web/core/component.js +6685 -249
  6. package/build/web/core/component.scss +557 -69
  7. package/build/web/core/css-utils/a2-border.css +23 -51
  8. package/build/web/core/css-utils/a2-color.css +221 -233
  9. package/build/web/core/css-utils/a2-font.css +1 -29
  10. package/build/web/core/css-utils/a2-spacing.css +238 -483
  11. package/build/web/core/css-utils/a2-utils.css +496 -781
  12. package/build/web/core/css-utils/border.css +23 -51
  13. package/build/web/core/css-utils/color.css +221 -233
  14. package/build/web/core/css-utils/font.css +1 -29
  15. package/build/web/core/css-utils/spacing.css +238 -483
  16. package/build/web/core/css-utils/utils.css +496 -781
  17. package/build/web/core/index.d.ts +6 -0
  18. package/build/web/core/index.js +1 -1
  19. package/build/web/core/primitive-variables.scss +148 -65
  20. package/build/web/core/primitive.d.ts +209 -0
  21. package/build/web/core/primitive.js +779 -61
  22. package/build/web/core/primitive.scss +207 -124
  23. package/build/web/core/semantic-variables.scss +363 -245
  24. package/build/web/core/semantic.d.ts +221 -0
  25. package/build/web/core/semantic.js +1592 -347
  26. package/build/web/core/semantic.scss +219 -140
  27. package/build/web/index.d.ts +3 -4
  28. package/build/web/types.d.ts +17 -0
  29. package/config.js +121 -496
  30. package/eslint.config.mjs +11 -1
  31. package/package.json +15 -5
  32. package/src/global/primitive/breakpoint.tokens.json +54 -0
  33. package/src/global/primitive/color.tokens.json +1092 -0
  34. package/src/global/primitive/duration.tokens.json +44 -0
  35. package/src/global/primitive/font.tokens.json +151 -0
  36. package/src/global/primitive/radius.tokens.json +94 -0
  37. package/src/global/primitive/size.tokens.json +174 -0
  38. package/src/global/primitive/transition.tokens.json +32 -0
  39. package/src/theme/core/background.tokens.json +1312 -0
  40. package/src/theme/core/border.tokens.json +192 -0
  41. package/src/theme/core/chart.tokens.json +982 -0
  42. package/src/theme/core/component/ai-mark.tokens.json +20 -0
  43. package/src/theme/core/component/alert.tokens.json +261 -0
  44. package/src/theme/core/component/announcement.tokens.json +460 -0
  45. package/src/theme/core/component/avatar.tokens.json +137 -0
  46. package/src/theme/core/component/badge.tokens.json +42 -0
  47. package/src/theme/core/component/breadcrumb.tokens.json +42 -0
  48. package/src/theme/core/component/button-toggle.tokens.json +428 -0
  49. package/src/theme/core/component/button.tokens.json +941 -0
  50. package/src/theme/core/component/calendar.tokens.json +391 -0
  51. package/src/theme/core/component/card.tokens.json +107 -0
  52. package/src/theme/core/component/checkbox.tokens.json +631 -0
  53. package/src/theme/core/component/chip.tokens.json +169 -0
  54. package/src/theme/core/component/combobox.tokens.json +269 -0
  55. package/src/theme/core/component/details.tokens.json +152 -0
  56. package/src/theme/core/component/dialog.tokens.json +87 -0
  57. package/src/theme/core/component/divider.tokens.json +23 -0
  58. package/src/theme/core/component/dnd.tokens.json +208 -0
  59. package/src/theme/core/component/drawer.tokens.json +61 -0
  60. package/src/theme/core/component/drilldown.tokens.json +61 -0
  61. package/src/theme/core/component/edit-card.tokens.json +381 -0
  62. package/src/theme/core/component/field-label.tokens.json +42 -0
  63. package/src/theme/core/component/field-message.tokens.json +65 -0
  64. package/src/theme/core/component/icon.tokens.json +42 -0
  65. package/src/theme/core/component/link.tokens.json +108 -0
  66. package/src/theme/core/component/list-view.tokens.json +82 -0
  67. package/src/theme/core/component/listbox.tokens.json +283 -0
  68. package/src/theme/core/component/menu.tokens.json +230 -0
  69. package/src/theme/core/component/overflow.tokens.json +84 -0
  70. package/src/theme/core/component/page.tokens.json +377 -0
  71. package/src/theme/core/component/pagination.tokens.json +63 -0
  72. package/src/theme/core/component/popover.tokens.json +122 -0
  73. package/src/theme/core/component/progress-bar.tokens.json +133 -0
  74. package/src/theme/core/component/radio.tokens.json +631 -0
  75. package/src/theme/core/component/segmented-control.tokens.json +175 -0
  76. package/src/theme/core/component/select-card.tokens.json +943 -0
  77. package/src/theme/core/component/side-nav.tokens.json +349 -0
  78. package/src/theme/core/component/skeleton.tokens.json +42 -0
  79. package/src/theme/core/component/spinner.tokens.json +96 -0
  80. package/src/theme/core/component/status-icon.tokens.json +164 -0
  81. package/src/theme/core/component/stepper.tokens.json +484 -0
  82. package/src/theme/core/component/switch.tokens.json +285 -0
  83. package/src/theme/core/component/tab.tokens.json +192 -0
  84. package/src/theme/core/component/text-field.tokens.json +160 -0
  85. package/src/theme/core/component/text.tokens.json +59 -0
  86. package/src/theme/core/component/toast.tokens.json +343 -0
  87. package/src/theme/core/component/toolbar.tokens.json +114 -0
  88. package/src/theme/core/component/tooltip.tokens.json +61 -0
  89. package/src/theme/core/focus.tokens.json +56 -0
  90. package/src/theme/core/foreground.tokens.json +416 -0
  91. package/src/theme/core/gradient.tokens.json +41 -0
  92. package/src/theme/core/opacity.tokens.json +25 -0
  93. package/src/theme/core/shadow.tokens.json +81 -0
  94. package/src/theme/core/status.tokens.json +74 -0
  95. package/src/theme/core/typography.tokens.json +163 -0
  96. package/src/utils/__tests__/css-utils-format-utils.test.js +312 -0
  97. package/src/utils/__tests__/sd-build-configs.test.js +306 -0
  98. package/src/utils/__tests__/sd-formats.test.js +942 -0
  99. package/src/utils/__tests__/sd-transforms.test.js +336 -0
  100. package/src/utils/__tests__/token-helpers.test.js +1160 -0
  101. package/src/utils/copy-css-utils-cli.js +13 -1
  102. package/src/utils/css-utils-format-utils.js +105 -176
  103. package/src/utils/figma/__tests__/sync-gradient.test.js +561 -0
  104. package/src/utils/figma/__tests__/token-conversion.test.js +117 -0
  105. package/src/utils/figma/__tests__/token-resolution.test.js +231 -0
  106. package/src/utils/figma/auth.js +355 -0
  107. package/src/utils/figma/constants.js +22 -0
  108. package/src/utils/figma/errors.js +80 -0
  109. package/src/utils/figma/figma-api.js +1069 -0
  110. package/src/utils/figma/get-token.js +348 -0
  111. package/src/utils/figma/sync-components.js +909 -0
  112. package/src/utils/figma/sync-main.js +692 -0
  113. package/src/utils/figma/sync-orchestration.js +683 -0
  114. package/src/utils/figma/sync-primitives.js +230 -0
  115. package/src/utils/figma/sync-semantic.js +1056 -0
  116. package/src/utils/figma/token-conversion.js +340 -0
  117. package/src/utils/figma/token-parsing.js +186 -0
  118. package/src/utils/figma/token-resolution.js +569 -0
  119. package/src/utils/figma/utils.js +199 -0
  120. package/src/utils/sd-build-configs.js +305 -0
  121. package/src/utils/sd-formats.js +948 -0
  122. package/src/utils/sd-transforms.js +165 -0
  123. package/src/utils/token-helpers.js +848 -0
  124. package/tsconfig.json +18 -0
  125. package/vitest.config.js +17 -0
  126. package/.turbo/turbo-build.log +0 -37
  127. package/build/web/core/raw.js +0 -234
  128. package/src/global/primitive/breakpoint.js +0 -19
  129. package/src/global/primitive/color.js +0 -231
  130. package/src/global/primitive/duration.js +0 -16
  131. package/src/global/primitive/font.js +0 -60
  132. package/src/global/primitive/radius.js +0 -31
  133. package/src/global/primitive/size.js +0 -55
  134. package/src/global/primitive/transition.js +0 -16
  135. package/src/theme/core/background.js +0 -170
  136. package/src/theme/core/border.js +0 -103
  137. package/src/theme/core/charts.js +0 -464
  138. package/src/theme/core/component/button.js +0 -708
  139. package/src/theme/core/component/checkbox.js +0 -405
  140. package/src/theme/core/focus.js +0 -35
  141. package/src/theme/core/foreground.js +0 -148
  142. package/src/theme/core/overlay.js +0 -137
  143. package/src/theme/core/shadow.js +0 -29
  144. package/src/theme/core/status.js +0 -49
  145. package/src/theme/core/typography.js +0 -82
  146. package/type/types.ts +0 -344
@@ -0,0 +1,848 @@
1
+ /**
2
+ * Token helper functions for Style Dictionary DTCG format
3
+ * These functions handle token value extraction, resolution, and CSS variable chain building
4
+ */
5
+
6
+ /**
7
+ * Extracts the DTCG value from a token
8
+ * @param {Object} token - The token object
9
+ * @returns {*} The token's $value property
10
+ */
11
+ const getDtcgValue = (token) => token.$value;
12
+
13
+ /**
14
+ * Extracts the DTCG token type
15
+ * @param {Object} token - The token object
16
+ * @returns {string|undefined} The token's $type property
17
+ */
18
+ const getTokenType = (token) => token.$type;
19
+
20
+ /**
21
+ * Extracts the DTCG description from a token
22
+ * @param {Object} token - The token object
23
+ * @returns {string|undefined} The token's $description property
24
+ */
25
+ const getDtcgDescription = (token) => token.$description;
26
+
27
+ /**
28
+ * Checks if a token has a dark mode value
29
+ * @param {Object} token - The token object
30
+ * @returns {boolean} True if the token has a dark mode value defined
31
+ */
32
+ const hasDarkValue = (token) =>
33
+ token.$extensions?.appearance?.dark?.$value !== undefined;
34
+
35
+ /**
36
+ * Gets the dark mode value from a token
37
+ * @param {Object} token - The token object
38
+ * @returns {*} The dark mode value, or undefined if not present
39
+ */
40
+ const getDarkValue = (token) => token.$extensions?.appearance?.dark?.$value;
41
+
42
+ /**
43
+ * Checks if a value is a composite color object (contains color and alpha properties)
44
+ * @param {*} value - The value to check
45
+ * @returns {boolean} True if the value is a composite color object
46
+ * @example
47
+ * isCompositeColor({ color: '#fff', alpha: 0.5 }) // true
48
+ * isCompositeColor('#fff') // false
49
+ */
50
+ const isCompositeColor = (value) =>
51
+ value &&
52
+ typeof value === "object" &&
53
+ !Array.isArray(value) &&
54
+ value.color !== undefined &&
55
+ value.alpha !== undefined;
56
+
57
+ /**
58
+ * Checks if a value is a dimension object (contains value and unit properties)
59
+ * @param {*} value - The value to check
60
+ * @returns {boolean} True if the value is a dimension object
61
+ * @example
62
+ * isDimensionValue({ value: 16, unit: 'px' }) // true
63
+ * isDimensionValue('16px') // false
64
+ */
65
+ const isDimensionValue = (value) =>
66
+ value &&
67
+ typeof value === "object" &&
68
+ !Array.isArray(value) &&
69
+ value.value !== undefined &&
70
+ value.unit !== undefined;
71
+
72
+ /**
73
+ * Checks if a value is a DTCG gradient object ({ type, angle, stops })
74
+ * @param {*} value - The value to check
75
+ * @returns {boolean} True if the value is a gradient object
76
+ * @example
77
+ * isGradientValue({ type: 'linear', angle: 90, stops: [...] }) // true
78
+ * isGradientValue('#fff') // false
79
+ */
80
+ const isGradientValue = (value) =>
81
+ value &&
82
+ typeof value === "object" &&
83
+ !Array.isArray(value) &&
84
+ typeof value.type === "string" &&
85
+ typeof value.angle === "number" &&
86
+ Array.isArray(value.stops);
87
+
88
+ /**
89
+ * Resolves a gradient token's $value to the actual gradient object ({ type, angle, stops }),
90
+ * following reference strings (e.g. "{gradient.primary}") through the tokenMap if needed.
91
+ * @param {Object} token - The token object
92
+ * @param {Map<string, Object>} tokenMap - Map of token names to token objects
93
+ * @returns {Object|null} The resolved gradient value object, or null if not resolvable
94
+ */
95
+ const resolveGradientValue = (token, tokenMap) => {
96
+ const originalValue = token.original?.$value;
97
+ if (isGradientValue(originalValue)) return originalValue;
98
+ if (typeof originalValue === "string" && originalValue.includes("{")) {
99
+ const refPath = originalValue.match(/\{([^{}]+)\}/)?.[1];
100
+ if (refPath) {
101
+ const refToken = tokenMap.get(refPath.replace(/\./g, "-"));
102
+ if (refToken) return resolveGradientValue(refToken, tokenMap);
103
+ }
104
+ }
105
+ return null;
106
+ };
107
+
108
+ /**
109
+ * Builds a CSS gradient string from a DTCG gradient value object, preserving CSS custom
110
+ * property references for each color stop. Uses the per-stop appearance extension to
111
+ * resolve light/dark color values.
112
+ * @param {Object} gradientValue - The gradient value object ({ type, angle, stops })
113
+ * @param {boolean} isDark - Whether to use dark mode stop colors
114
+ * @param {Map<string, Object>} tokenMap - Map of token names to token objects
115
+ * @param {Object} dictionary - The Style Dictionary dictionary object
116
+ * @param {string} cssVarPrefix - Prefix for CSS variable names (e.g., 'a2-')
117
+ * @param {Function} usesReferences - The SD5 usesReferences function
118
+ * @param {Function} resolveReferences - The SD5 resolveReferences function
119
+ * @returns {string} CSS gradient string with var() chains for stop colors
120
+ * @example
121
+ * buildGradientValue({ type: 'linear', angle: 90, stops }, false, tokenMap, dictionary, 'a2-', usesReferences, resolveReferences)
122
+ * // → 'linear-gradient(90deg, var(--a2-color-blue-600, #0265dc) 0%, var(--a2-color-cyan-300, #45d8f2) 100%)'
123
+ */
124
+ const buildGradientValue = (
125
+ gradientValue,
126
+ isDark,
127
+ tokenMap,
128
+ dictionary,
129
+ cssVarPrefix,
130
+ usesReferences,
131
+ resolveReferences,
132
+ ) => {
133
+ const { type, angle, stops } = gradientValue;
134
+ const stopStrings = stops.map((stop) => {
135
+ const pct =
136
+ typeof stop.position === "number"
137
+ ? `${stop.position * 100}%`
138
+ : stop.position.replace(/\{([^{}]+)\}/g, (match) =>
139
+ buildRecursiveVarChainWithLightDark(
140
+ match,
141
+ tokenMap,
142
+ dictionary,
143
+ false,
144
+ usesReferences,
145
+ resolveReferences,
146
+ cssVarPrefix,
147
+ ),
148
+ );
149
+ const colorRef = isDark
150
+ ? (stop.color.$extensions?.appearance?.dark?.$value ?? stop.color.$value)
151
+ : (stop.color.$extensions?.appearance?.light?.$value ??
152
+ stop.color.$value);
153
+ if (typeof colorRef === "string" && usesReferences(colorRef)) {
154
+ const varChain = buildRecursiveVarChainWithLightDark(
155
+ colorRef,
156
+ tokenMap,
157
+ dictionary,
158
+ false,
159
+ usesReferences,
160
+ resolveReferences,
161
+ cssVarPrefix,
162
+ );
163
+ return `${varChain} ${pct}`;
164
+ }
165
+ return `${colorRef} ${pct}`;
166
+ });
167
+ return `${type}-gradient(${angle}deg, ${stopStrings.join(", ")})`;
168
+ };
169
+
170
+ /**
171
+ * Wraps values containing commas in double quotes for SCSS map compatibility.
172
+ * SCSS maps use commas as entry separators, so values with commas need quoting.
173
+ * @param {*} value - The value to potentially wrap
174
+ * @returns {*} The wrapped value if it contains commas, otherwise the original value
175
+ * @example
176
+ * wrapScssValue('Arial, sans-serif') // '"Arial, sans-serif"'
177
+ * wrapScssValue('Arial') // 'Arial'
178
+ */
179
+ const wrapScssValue = (value) => {
180
+ if (typeof value !== "string") return value;
181
+ if (value.includes(",")) {
182
+ return `"${value}"`;
183
+ }
184
+ return value;
185
+ };
186
+
187
+ /**
188
+ * Gets the token name from its path, filtering out $schema and other $-prefixed segments
189
+ * @param {Object} token - The token object
190
+ * @returns {string} The token name as a hyphen-separated string
191
+ * @example
192
+ * getTokenName({ path: ['color', 'primary', '500'] }) // 'color-primary-500'
193
+ */
194
+ const getTokenName = (token) => {
195
+ const tokenPath = token.path;
196
+ if (Array.isArray(tokenPath) && tokenPath.length > 0) {
197
+ const filteredPath = tokenPath.filter(
198
+ (segment) => segment !== "$schema" && !String(segment).startsWith("$"),
199
+ );
200
+ if (filteredPath.length > 0) {
201
+ return filteredPath.join("-");
202
+ }
203
+ }
204
+ return "unknown";
205
+ };
206
+
207
+ /**
208
+ * Builds a Map of tokens keyed by their names for O(1) lookups.
209
+ * Result is memoized per dictionary instance to avoid redundant construction
210
+ * across multiple format calls that share the same dictionary.
211
+ * @param {Object} dictionary - The Style Dictionary dictionary object
212
+ * @returns {Map<string, Object>} A Map where keys are token names and values are token objects
213
+ */
214
+ const _tokenMapCache = new WeakMap();
215
+ const buildTokenMap = (dictionary) => {
216
+ if (_tokenMapCache.has(dictionary)) return _tokenMapCache.get(dictionary);
217
+ const map = new Map(
218
+ (dictionary.unfilteredAllTokens || dictionary.allTokens).map((t) => [
219
+ getTokenName(t),
220
+ t,
221
+ ]),
222
+ );
223
+ _tokenMapCache.set(dictionary, map);
224
+ return map;
225
+ };
226
+
227
+ /**
228
+ * Converts a hex color to hex8 format (with alpha channel)
229
+ * @param {string} hexColor - The hex color to convert (e.g., '#ff0000' or '#f00')
230
+ * @param {number} alpha - The alpha value between 0 and 1
231
+ * @returns {string} The hex8 color string (e.g., '#ff000080')
232
+ * @example
233
+ * convertToHex8('#ff0000', 0.5) // '#ff000080'
234
+ * convertToHex8('#f00', 1) // '#ff0000FF'
235
+ */
236
+ const convertToHex8 = (hexColor, alpha) => {
237
+ if (!hexColor || typeof hexColor !== "string" || !hexColor.startsWith("#")) {
238
+ return hexColor;
239
+ }
240
+
241
+ // Remove # and normalize to 6-digit hex
242
+ let hex = hexColor.replace("#", "");
243
+ if (hex.length === 3) {
244
+ // Expand shorthand: #RGB -> #RRGGBB
245
+ hex = hex
246
+ .split("")
247
+ .map((char) => char + char)
248
+ .join("");
249
+ } else if (hex.length !== 6) {
250
+ return hexColor; // Invalid hex format
251
+ }
252
+
253
+ // Convert alpha (0-1) to hex (00-FF)
254
+ const alphaHex = Math.round(alpha * 255)
255
+ .toString(16)
256
+ .padStart(2, "0")
257
+ .toUpperCase();
258
+
259
+ return `#${hex}${alphaHex}`;
260
+ };
261
+
262
+ /**
263
+ * Converts a composite color { color, alpha } to a CSS color-mix() expression,
264
+ * preserving the primitive CSS custom property reference.
265
+ *
266
+ * Example output:
267
+ * color-mix(in srgb, var(--a2-color-neutral-0, #ffffff) 8%, transparent)
268
+ *
269
+ * This keeps the live link to the primitive token so runtime theme overrides
270
+ * (e.g. changing --a2-color-neutral-0) automatically update the alpha color too.
271
+ *
272
+ * Browser support: Chrome 111+, Firefox 113+, Safari 16.2+ (as of 2024).
273
+ *
274
+ * ---
275
+ * FUTURE: CSS Relative Color Syntax (Option 3)
276
+ * When browser support matures (Chrome 119+, Safari 17.5+, Firefox 128+),
277
+ * this can be simplified to:
278
+ * rgb(from var(--a2-color-neutral-0, #ffffff) r g b / 0.08)
279
+ * Change the return statement below to that template and remove the alpha→percentage
280
+ * conversion. The `from` keyword extracts r/g/b channels from the referenced color,
281
+ * so no color-space conversion is needed. Switch when your minimum browser targets
282
+ * include the versions listed above.
283
+ * ---
284
+ *
285
+ * @param {string} refTokenName - The token name (hyphen-separated, e.g. 'color-neutral-0')
286
+ * @param {string} hexFallback - The resolved hex color as a fallback (e.g. '#ffffff')
287
+ * @param {number} alpha - The alpha value between 0 and 1
288
+ * @param {string} [cssVarPrefix=''] - Prefix for CSS variable names (e.g. 'a2-')
289
+ * @returns {string} CSS color-mix() expression
290
+ * @example
291
+ * compositeColorToCssMix('color-neutral-0', '#ffffff', 0.08, 'a2-')
292
+ * // → 'color-mix(in srgb, var(--a2-color-neutral-0, #ffffff) 8%, transparent)'
293
+ */
294
+ const compositeColorToCssMix = (
295
+ refTokenName,
296
+ hexFallback,
297
+ alpha,
298
+ cssVarPrefix = "",
299
+ ) => {
300
+ const pct = Math.round(alpha * 10000) / 100; // avoid floating point drift, e.g. 0.06 → 6
301
+ return `color-mix(in srgb, var(--${cssVarPrefix}${refTokenName}, ${hexFallback}) ${pct}%, transparent)`;
302
+ };
303
+
304
+ /**
305
+ * Resolves a reference string using Style Dictionary utilities.
306
+ * @param {string} refString - The reference string to resolve (e.g., '{color.primary.500}')
307
+ * @param {Object} dictionary - The Style Dictionary dictionary object
308
+ * @param {Function} resolveReferences - The SD5 resolveReferences function
309
+ * @returns {*} The resolved value, or the original string if resolution fails
310
+ */
311
+ const resolveReference = (refString, dictionary, resolveReferences) => {
312
+ if (typeof refString !== "string") {
313
+ return refString;
314
+ }
315
+ // Use unfiltered tokens when available so references to theme/primitive tokens
316
+ // resolve correctly in filtered builds (e.g. component.js only has component tokens).
317
+ const tokensForResolution = dictionary.unfilteredTokens ?? dictionary.tokens;
318
+ try {
319
+ return resolveReferences(refString, tokensForResolution, {
320
+ usesDtcg: true,
321
+ });
322
+ } catch {
323
+ return refString;
324
+ }
325
+ };
326
+
327
+ /**
328
+ * Resolves a composite color object (with color and alpha properties) to a CSS value.
329
+ *
330
+ * When a tokenMap and cssVarPrefix are provided and the color references a known primitive
331
+ * token, emits a color-mix() expression that preserves the CSS custom property reference:
332
+ * color-mix(in srgb, var(--a2-color-neutral-0, #ffffff) 8%, transparent)
333
+ *
334
+ * Falls back to a hex8 string (#rrggbbAA) when the primitive token is not in tokenMap
335
+ * or when tokenMap is not provided (e.g. in SCSS map / JS export contexts where a
336
+ * static value is needed).
337
+ *
338
+ * @param {Object} colorObj - The composite color object with color and alpha properties
339
+ * @param {Object} dictionary - The Style Dictionary dictionary object
340
+ * @param {Function} resolveReferences - The SD5 resolveReferences function
341
+ * @param {Map<string, Object>} [tokenMap] - Optional token map for CSS var name lookup
342
+ * @param {string} [cssVarPrefix=''] - Optional prefix for CSS variable names (e.g. 'a2-')
343
+ * @returns {string|null} CSS color value string, or null if not a composite color
344
+ */
345
+ const resolveCompositeColor = (
346
+ colorObj,
347
+ dictionary,
348
+ resolveReferences,
349
+ tokenMap,
350
+ cssVarPrefix = "",
351
+ ) => {
352
+ if (!isCompositeColor(colorObj)) {
353
+ return null;
354
+ }
355
+ const colorValue = resolveReference(
356
+ colorObj.color,
357
+ dictionary,
358
+ resolveReferences,
359
+ );
360
+ if (typeof colorValue === "string" && colorValue.startsWith("#")) {
361
+ // When a tokenMap is available, emit color-mix() to preserve the primitive var reference.
362
+ if (tokenMap && typeof colorObj.color === "string") {
363
+ const refMatch = colorObj.color.match(/\{([^{}]+)\}/);
364
+ if (refMatch) {
365
+ const refTokenName = refMatch[1].replace(/\./g, "-");
366
+ if (tokenMap.has(refTokenName)) {
367
+ return compositeColorToCssMix(
368
+ refTokenName,
369
+ colorValue,
370
+ colorObj.alpha,
371
+ cssVarPrefix,
372
+ );
373
+ }
374
+ }
375
+ }
376
+ return convertToHex8(colorValue, colorObj.alpha);
377
+ }
378
+ return colorValue;
379
+ };
380
+
381
+ /**
382
+ * Gets the resolved token value, handling dark mode, composite colors, and references
383
+ * @param {Object} token - The token object
384
+ * @param {Object} dictionary - The Style Dictionary dictionary object
385
+ * @param {Object} [options] - Options for value resolution
386
+ * @param {boolean} [options.isDark=false] - Whether to get the dark mode value
387
+ * @param {boolean} [options.forScssMap=false] - Whether to format for SCSS map (quotes commas)
388
+ * @param {Function} resolveReferences - The SD5 resolveReferences function
389
+ * @returns {string} The resolved token value as a string
390
+ */
391
+ const getTokenValue = (
392
+ token,
393
+ dictionary,
394
+ { isDark = false, forScssMap = false } = {},
395
+ resolveReferences,
396
+ ) => {
397
+ let value = getDtcgValue(token);
398
+
399
+ // Handle dark mode
400
+ if (isDark && hasDarkValue(token)) {
401
+ value = getDarkValue(token);
402
+ }
403
+
404
+ // Handle composite colors
405
+ if (isCompositeColor(value)) {
406
+ const resolved = resolveCompositeColor(
407
+ value,
408
+ dictionary,
409
+ resolveReferences,
410
+ );
411
+ if (resolved) {
412
+ value = resolved;
413
+ }
414
+ }
415
+
416
+ // Handle dimension values { value, unit }
417
+ if (isDimensionValue(value)) {
418
+ value = `${value.value}${value.unit}`;
419
+ }
420
+
421
+ // Resolve references; SD5 resolveReferences may return a token object, so extract value and loop until primitive
422
+ for (;;) {
423
+ if (
424
+ typeof value === "string" &&
425
+ value.includes("{") &&
426
+ value.includes("}")
427
+ ) {
428
+ value = resolveReference(value, dictionary, resolveReferences);
429
+ }
430
+ // SD5 can return the full token object; extract value
431
+ if (value && typeof value === "object" && !Array.isArray(value)) {
432
+ if (value.$value !== undefined || value.value !== undefined) {
433
+ value =
434
+ isDark && value.$extensions?.appearance?.dark?.$value !== undefined
435
+ ? value.$extensions.appearance.dark.$value
436
+ : (value.$value ?? value.value);
437
+ continue;
438
+ }
439
+ }
440
+ if (isCompositeColor(value)) {
441
+ const resolved = resolveCompositeColor(
442
+ value,
443
+ dictionary,
444
+ resolveReferences,
445
+ );
446
+ if (resolved) {
447
+ value = resolved;
448
+ }
449
+ }
450
+ if (isDimensionValue(value)) {
451
+ value = `${value.value}${value.unit}`;
452
+ }
453
+ break;
454
+ }
455
+
456
+ // Convert to string and clean up
457
+ if (value === undefined || value === null) {
458
+ return "";
459
+ }
460
+ const valueStr = typeof value === "string" ? value : JSON.stringify(value);
461
+
462
+ // Normalize transparent to rgba for consistency
463
+ const cleaned = valueStr.replaceAll('"', "");
464
+ if (cleaned === "transparent") {
465
+ return "rgba(0, 0, 0, 0)";
466
+ }
467
+
468
+ // Wrap values with commas in quotes for SCSS map compatibility
469
+ if (forScssMap) {
470
+ return wrapScssValue(cleaned);
471
+ }
472
+ return cleaned;
473
+ };
474
+
475
+ /**
476
+ * Checks if a value is a reference string using the usesReferences function
477
+ * @param {*} value - The value to check
478
+ * @param {Function} usesReferences - The SD5 usesReferences function
479
+ * @returns {boolean} True if the value is a reference string
480
+ */
481
+ const isReferenceString = (value, usesReferences) =>
482
+ typeof value === "string" && usesReferences(value);
483
+
484
+ /**
485
+ * Builds a recursive CSS var chain with light-dark() at the level where light/dark values differ.
486
+ * Creates nested var() fallbacks that ultimately resolve to light-dark() at the primitive level.
487
+ * @param {string} originalValue - The original reference string (e.g., '{status.color.danger}')
488
+ * @param {Map<string, Object>} tokenMap - Map of token names to token objects
489
+ * @param {Object} dictionary - The Style Dictionary dictionary object
490
+ * @param {boolean} [useLightDark=false] - Whether to include light-dark() for color variants
491
+ * @param {Function} usesReferences - The SD5 usesReferences function
492
+ * @param {Function} resolveReferences - The SD5 resolveReferences function
493
+ * @param {string} [cssVarPrefix=''] - Prefix for CSS variable names (e.g., 'a2-')
494
+ * @param {boolean} [forScssMap=false] - Whether to format for SCSS map
495
+ * @returns {string} The CSS var chain string
496
+ * @example
497
+ * // Returns: 'var(--a2-status-color-danger, light-dark(var(--a2-color-red-500, #e13212), var(--a2-color-red-100, #ff745f)))'
498
+ * buildRecursiveVarChainWithLightDark('{status.color.danger}', tokenMap, dictionary, true, usesReferences, resolveReferences, 'a2-')
499
+ */
500
+ const buildRecursiveVarChainWithLightDark = (
501
+ originalValue,
502
+ tokenMap,
503
+ dictionary,
504
+ useLightDark = false,
505
+ usesReferences,
506
+ resolveReferences,
507
+ cssVarPrefix = "",
508
+ forScssMap = false,
509
+ ) => {
510
+ // If not a reference string, return the value as-is
511
+ if (
512
+ !originalValue ||
513
+ typeof originalValue !== "string" ||
514
+ !usesReferences(originalValue)
515
+ ) {
516
+ return originalValue;
517
+ }
518
+
519
+ // Replace each {reference} with var(--name, recursiveResult)
520
+ return originalValue.replace(/\{([^{}]+)\}/g, (_, refPath) => {
521
+ const refTokenName = refPath.replace(/\./g, "-");
522
+ const refToken = tokenMap.get(refTokenName);
523
+ if (!refToken) return `var(--${cssVarPrefix}${refTokenName})`;
524
+
525
+ const refOriginalLight = refToken.original?.$value;
526
+ const refOriginalDark =
527
+ refToken.original?.$extensions?.appearance?.dark?.$value;
528
+ const refHasDarkVariant = refOriginalDark !== undefined;
529
+
530
+ // Check if light and dark values are DIFFERENT - this is where light-dark() should go
531
+ const lightDarkDiffer =
532
+ refHasDarkVariant && refOriginalLight !== refOriginalDark;
533
+
534
+ if (useLightDark && lightDarkDiffer) {
535
+ // Light and dark values differ - insert light-dark() here
536
+ const lightResult = buildRecursiveVarChainWithLightDark(
537
+ refOriginalLight,
538
+ tokenMap,
539
+ dictionary,
540
+ false,
541
+ usesReferences,
542
+ resolveReferences,
543
+ cssVarPrefix,
544
+ forScssMap,
545
+ );
546
+ // Same fix as darkFinal: ensure lightResult is a string before using it
547
+ const lightFinal =
548
+ typeof lightResult === "string" && usesReferences(refOriginalLight)
549
+ ? lightResult
550
+ : getTokenValue(
551
+ refToken,
552
+ dictionary,
553
+ { isDark: false, forScssMap },
554
+ resolveReferences,
555
+ );
556
+
557
+ const darkResult = buildRecursiveVarChainWithLightDark(
558
+ refOriginalDark,
559
+ tokenMap,
560
+ dictionary,
561
+ false,
562
+ usesReferences,
563
+ resolveReferences,
564
+ cssVarPrefix,
565
+ forScssMap,
566
+ );
567
+ // Ensure darkResult is a string (resolved var chain), not just truthy.
568
+ // Composite color objects pass through buildRecursiveVarChainWithLightDark unchanged,
569
+ // but usesReferences() returns true for them (checks nested properties).
570
+ const darkFinal =
571
+ typeof darkResult === "string" && usesReferences(refOriginalDark)
572
+ ? darkResult
573
+ : getTokenValue(
574
+ refToken,
575
+ dictionary,
576
+ { isDark: true, forScssMap },
577
+ resolveReferences,
578
+ );
579
+
580
+ return `var(--${cssVarPrefix}${refTokenName}, light-dark(${lightFinal}, ${darkFinal}))`;
581
+ }
582
+
583
+ // Light and dark are same (or no dark) - continue recursing, propagate useLightDark
584
+ if (
585
+ refOriginalLight &&
586
+ typeof refOriginalLight === "string" &&
587
+ usesReferences(refOriginalLight)
588
+ ) {
589
+ const nestedResult = buildRecursiveVarChainWithLightDark(
590
+ refOriginalLight,
591
+ tokenMap,
592
+ dictionary,
593
+ useLightDark,
594
+ usesReferences,
595
+ resolveReferences,
596
+ cssVarPrefix,
597
+ forScssMap,
598
+ );
599
+ return `var(--${cssVarPrefix}${refTokenName}, ${nestedResult})`;
600
+ }
601
+
602
+ // Final value (primitive) - get resolved value
603
+ const finalValue = getTokenValue(
604
+ refToken,
605
+ dictionary,
606
+ { isDark: false, forScssMap },
607
+ resolveReferences,
608
+ );
609
+ return `var(--${cssVarPrefix}${refTokenName}, ${finalValue})`;
610
+ });
611
+ };
612
+
613
+ /**
614
+ * Builds a fallback value with recursive refs and light-dark() at the appropriate level.
615
+ * This is the main entry point for generating CSS fallback values with dark mode support.
616
+ * @param {Object} token - The token object
617
+ * @param {Object} dictionary - The Style Dictionary dictionary object
618
+ * @param {Map<string, Object>} tokenMap - Map of token names to token objects
619
+ * @param {Object} [options] - Options for fallback building
620
+ * @param {boolean} [options.isDark=false] - Whether to get the dark mode value
621
+ * @param {boolean} [options.useLightDark=false] - Whether to include light-dark() for color variants
622
+ * @param {string} [options.cssVarPrefix=''] - Prefix for CSS variable names
623
+ * @param {boolean} [options.forScssMap=false] - Whether to format for SCSS map
624
+ * @param {Function} usesReferences - The SD5 usesReferences function
625
+ * @param {Function} resolveReferences - The SD5 resolveReferences function
626
+ * @returns {string} The fallback value string with CSS var chains and/or light-dark()
627
+ */
628
+ const buildFallbackWithRefs = (
629
+ token,
630
+ dictionary,
631
+ tokenMap,
632
+ {
633
+ isDark = false,
634
+ useLightDark = false,
635
+ cssVarPrefix = "",
636
+ forScssMap = false,
637
+ } = {},
638
+ usesReferences,
639
+ resolveReferences,
640
+ ) => {
641
+ const originalValue = token.original?.$value;
642
+ const originalDark = token.original?.$extensions?.appearance?.dark?.$value;
643
+ const hasDark = originalDark !== undefined;
644
+
645
+ // Check if this token's light/dark values are different at top level
646
+ // Use JSON.stringify for object comparison (composite colors)
647
+ const lightDarkDifferAtTop =
648
+ hasDark && JSON.stringify(originalValue) !== JSON.stringify(originalDark);
649
+
650
+ // If useLightDark and light/dark differ at this level, insert light-dark() here
651
+ if (useLightDark && lightDarkDifferAtTop) {
652
+ // Handle light value - could be a string reference or composite object
653
+ let lightFinal;
654
+ if (isReferenceString(originalValue, usesReferences)) {
655
+ lightFinal = buildRecursiveVarChainWithLightDark(
656
+ originalValue,
657
+ tokenMap,
658
+ dictionary,
659
+ false,
660
+ usesReferences,
661
+ resolveReferences,
662
+ cssVarPrefix,
663
+ forScssMap,
664
+ );
665
+ } else {
666
+ // Composite color or plain value - resolve directly.
667
+ // Pass tokenMap + cssVarPrefix so composite colors emit color-mix() in CSS contexts.
668
+ if (isCompositeColor(originalValue)) {
669
+ lightFinal =
670
+ resolveCompositeColor(
671
+ originalValue,
672
+ dictionary,
673
+ resolveReferences,
674
+ tokenMap,
675
+ cssVarPrefix,
676
+ ) ??
677
+ getTokenValue(
678
+ token,
679
+ dictionary,
680
+ { isDark: false, forScssMap },
681
+ resolveReferences,
682
+ );
683
+ } else {
684
+ lightFinal = getTokenValue(
685
+ token,
686
+ dictionary,
687
+ { isDark: false, forScssMap },
688
+ resolveReferences,
689
+ );
690
+ }
691
+ }
692
+
693
+ // Handle dark value - could be a string reference or composite object
694
+ let darkFinal;
695
+ if (isReferenceString(originalDark, usesReferences)) {
696
+ darkFinal = buildRecursiveVarChainWithLightDark(
697
+ originalDark,
698
+ tokenMap,
699
+ dictionary,
700
+ false,
701
+ usesReferences,
702
+ resolveReferences,
703
+ cssVarPrefix,
704
+ forScssMap,
705
+ );
706
+ } else {
707
+ // Composite color or plain value - resolve directly.
708
+ // Pass tokenMap + cssVarPrefix so composite colors emit color-mix() in CSS contexts.
709
+ if (isCompositeColor(originalDark)) {
710
+ darkFinal =
711
+ resolveCompositeColor(
712
+ originalDark,
713
+ dictionary,
714
+ resolveReferences,
715
+ tokenMap,
716
+ cssVarPrefix,
717
+ ) ??
718
+ getTokenValue(
719
+ token,
720
+ dictionary,
721
+ { isDark: true, forScssMap },
722
+ resolveReferences,
723
+ );
724
+ } else {
725
+ darkFinal = getTokenValue(
726
+ token,
727
+ dictionary,
728
+ { isDark: true, forScssMap },
729
+ resolveReferences,
730
+ );
731
+ }
732
+ }
733
+
734
+ return `light-dark(${lightFinal}, ${darkFinal})`;
735
+ }
736
+
737
+ // Light/dark same or no dark - recurse and propagate useLightDark to find where they differ
738
+ // When isDark is true and we have a dark value, use originalDark instead of originalValue
739
+ const valueToProcess = isDark && hasDark ? originalDark : originalValue;
740
+
741
+ if (isReferenceString(valueToProcess, usesReferences)) {
742
+ const result = buildRecursiveVarChainWithLightDark(
743
+ valueToProcess,
744
+ tokenMap,
745
+ dictionary,
746
+ useLightDark,
747
+ usesReferences,
748
+ resolveReferences,
749
+ cssVarPrefix,
750
+ forScssMap,
751
+ );
752
+ if (result && result !== valueToProcess) {
753
+ return result;
754
+ }
755
+ }
756
+
757
+ // Composite color with inner reference and same light/dark value: resolve referenced token
758
+ // to get a light-dark() pair. Uses color-mix() to preserve the primitive CSS var reference.
759
+ if (
760
+ useLightDark &&
761
+ isCompositeColor(originalValue) &&
762
+ isReferenceString(originalValue.color, usesReferences)
763
+ ) {
764
+ const refMatch = originalValue.color.match(/\{([^{}]+)\}/);
765
+ if (refMatch) {
766
+ const refTokenName = refMatch[1].replace(/\./g, "-");
767
+ const refToken = tokenMap.get(refTokenName);
768
+ if (refToken && hasDarkValue(refToken)) {
769
+ const lightResolved = getTokenValue(
770
+ refToken,
771
+ dictionary,
772
+ { isDark: false, forScssMap },
773
+ resolveReferences,
774
+ );
775
+ const darkResolved = getTokenValue(
776
+ refToken,
777
+ dictionary,
778
+ { isDark: true, forScssMap },
779
+ resolveReferences,
780
+ );
781
+ if (
782
+ typeof lightResolved === "string" &&
783
+ typeof darkResolved === "string" &&
784
+ lightResolved.startsWith("#")
785
+ ) {
786
+ const lightMix = compositeColorToCssMix(
787
+ refTokenName,
788
+ lightResolved,
789
+ originalValue.alpha,
790
+ cssVarPrefix,
791
+ );
792
+ const darkMix = compositeColorToCssMix(
793
+ refTokenName,
794
+ darkResolved,
795
+ originalValue.alpha,
796
+ cssVarPrefix,
797
+ );
798
+ return `light-dark(${lightMix}, ${darkMix})`;
799
+ }
800
+ return `light-dark(${lightResolved}, ${darkResolved})`;
801
+ }
802
+ }
803
+ }
804
+
805
+ // No references or composite value - return the resolved final value.
806
+ // For composite colors, pass tokenMap + cssVarPrefix to emit color-mix() in CSS contexts.
807
+ const valueToCheck = isDark && hasDark ? originalDark : originalValue;
808
+ if (isCompositeColor(valueToCheck)) {
809
+ const mixed = resolveCompositeColor(
810
+ valueToCheck,
811
+ dictionary,
812
+ resolveReferences,
813
+ tokenMap,
814
+ cssVarPrefix,
815
+ );
816
+ if (mixed) return mixed;
817
+ }
818
+ return getTokenValue(
819
+ token,
820
+ dictionary,
821
+ { isDark, forScssMap },
822
+ resolveReferences,
823
+ );
824
+ };
825
+
826
+ module.exports = {
827
+ getDtcgValue,
828
+ getTokenType,
829
+ getDtcgDescription,
830
+ hasDarkValue,
831
+ getDarkValue,
832
+ isCompositeColor,
833
+ isDimensionValue,
834
+ isGradientValue,
835
+ wrapScssValue,
836
+ getTokenName,
837
+ buildTokenMap,
838
+ convertToHex8,
839
+ compositeColorToCssMix,
840
+ resolveReference,
841
+ resolveCompositeColor,
842
+ getTokenValue,
843
+ isReferenceString,
844
+ buildRecursiveVarChainWithLightDark,
845
+ buildFallbackWithRefs,
846
+ buildGradientValue,
847
+ resolveGradientValue,
848
+ };