@servicetitan/hammer-token 2.5.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +332 -0
  3. package/build/web/core/component-variables.scss +1088 -131
  4. package/build/web/core/component.d.ts +558 -0
  5. package/build/web/core/component.js +6685 -249
  6. package/build/web/core/component.scss +557 -69
  7. package/build/web/core/css-utils/a2-border.css +47 -45
  8. package/build/web/core/css-utils/a2-color.css +443 -227
  9. package/build/web/core/css-utils/a2-font.css +0 -2
  10. package/build/web/core/css-utils/a2-spacing.css +476 -478
  11. package/build/web/core/css-utils/a2-utils.css +992 -772
  12. package/build/web/core/css-utils/border.css +47 -45
  13. package/build/web/core/css-utils/color.css +443 -227
  14. package/build/web/core/css-utils/font.css +0 -2
  15. package/build/web/core/css-utils/spacing.css +476 -478
  16. package/build/web/core/css-utils/utils.css +992 -772
  17. package/build/web/core/index.d.ts +6 -0
  18. package/build/web/core/index.js +1 -1
  19. package/build/web/core/primitive-variables.scss +148 -65
  20. package/build/web/core/primitive.d.ts +209 -0
  21. package/build/web/core/primitive.js +779 -61
  22. package/build/web/core/primitive.scss +207 -124
  23. package/build/web/core/semantic-variables.scss +363 -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/index.js +0 -1
  29. package/build/web/types.d.ts +17 -0
  30. package/config.js +121 -497
  31. package/eslint.config.mjs +11 -1
  32. package/package.json +15 -5
  33. package/src/global/primitive/breakpoint.tokens.json +54 -0
  34. package/src/global/primitive/color.tokens.json +1092 -0
  35. package/src/global/primitive/duration.tokens.json +44 -0
  36. package/src/global/primitive/font.tokens.json +151 -0
  37. package/src/global/primitive/radius.tokens.json +94 -0
  38. package/src/global/primitive/size.tokens.json +174 -0
  39. package/src/global/primitive/transition.tokens.json +32 -0
  40. package/src/theme/core/background.tokens.json +1312 -0
  41. package/src/theme/core/border.tokens.json +192 -0
  42. package/src/theme/core/chart.tokens.json +982 -0
  43. package/src/theme/core/component/ai-mark.tokens.json +20 -0
  44. package/src/theme/core/component/alert.tokens.json +261 -0
  45. package/src/theme/core/component/announcement.tokens.json +460 -0
  46. package/src/theme/core/component/avatar.tokens.json +137 -0
  47. package/src/theme/core/component/badge.tokens.json +42 -0
  48. package/src/theme/core/component/breadcrumb.tokens.json +42 -0
  49. package/src/theme/core/component/button-toggle.tokens.json +428 -0
  50. package/src/theme/core/component/button.tokens.json +941 -0
  51. package/src/theme/core/component/calendar.tokens.json +391 -0
  52. package/src/theme/core/component/card.tokens.json +107 -0
  53. package/src/theme/core/component/checkbox.tokens.json +631 -0
  54. package/src/theme/core/component/chip.tokens.json +169 -0
  55. package/src/theme/core/component/combobox.tokens.json +269 -0
  56. package/src/theme/core/component/details.tokens.json +152 -0
  57. package/src/theme/core/component/dialog.tokens.json +87 -0
  58. package/src/theme/core/component/divider.tokens.json +23 -0
  59. package/src/theme/core/component/dnd.tokens.json +208 -0
  60. package/src/theme/core/component/drawer.tokens.json +61 -0
  61. package/src/theme/core/component/drilldown.tokens.json +61 -0
  62. package/src/theme/core/component/edit-card.tokens.json +381 -0
  63. package/src/theme/core/component/field-label.tokens.json +42 -0
  64. package/src/theme/core/component/field-message.tokens.json +74 -0
  65. package/src/theme/core/component/icon.tokens.json +42 -0
  66. package/src/theme/core/component/link.tokens.json +108 -0
  67. package/src/theme/core/component/list-view.tokens.json +82 -0
  68. package/src/theme/core/component/listbox.tokens.json +283 -0
  69. package/src/theme/core/component/menu.tokens.json +230 -0
  70. package/src/theme/core/component/overflow.tokens.json +84 -0
  71. package/src/theme/core/component/page.tokens.json +377 -0
  72. package/src/theme/core/component/pagination.tokens.json +63 -0
  73. package/src/theme/core/component/popover.tokens.json +122 -0
  74. package/src/theme/core/component/progress-bar.tokens.json +133 -0
  75. package/src/theme/core/component/radio.tokens.json +631 -0
  76. package/src/theme/core/component/segmented-control.tokens.json +175 -0
  77. package/src/theme/core/component/select-card.tokens.json +943 -0
  78. package/src/theme/core/component/side-nav.tokens.json +349 -0
  79. package/src/theme/core/component/skeleton.tokens.json +42 -0
  80. package/src/theme/core/component/spinner.tokens.json +96 -0
  81. package/src/theme/core/component/status-icon.tokens.json +164 -0
  82. package/src/theme/core/component/stepper.tokens.json +484 -0
  83. package/src/theme/core/component/switch.tokens.json +285 -0
  84. package/src/theme/core/component/tab.tokens.json +192 -0
  85. package/src/theme/core/component/text-field.tokens.json +160 -0
  86. package/src/theme/core/component/text.tokens.json +59 -0
  87. package/src/theme/core/component/toast.tokens.json +343 -0
  88. package/src/theme/core/component/toolbar.tokens.json +114 -0
  89. package/src/theme/core/component/tooltip.tokens.json +61 -0
  90. package/src/theme/core/focus.tokens.json +56 -0
  91. package/src/theme/core/foreground.tokens.json +416 -0
  92. package/src/theme/core/gradient.tokens.json +41 -0
  93. package/src/theme/core/opacity.tokens.json +25 -0
  94. package/src/theme/core/shadow.tokens.json +81 -0
  95. package/src/theme/core/status.tokens.json +74 -0
  96. package/src/theme/core/typography.tokens.json +163 -0
  97. package/src/utils/__tests__/css-utils-format-utils.test.js +312 -0
  98. package/src/utils/__tests__/sd-build-configs.test.js +306 -0
  99. package/src/utils/__tests__/sd-formats.test.js +950 -0
  100. package/src/utils/__tests__/sd-transforms.test.js +336 -0
  101. package/src/utils/__tests__/token-helpers.test.js +1160 -0
  102. package/src/utils/copy-css-utils-cli.js +13 -1
  103. package/src/utils/css-utils-format-utils.js +105 -176
  104. package/src/utils/figma/__tests__/sync-gradient.test.js +561 -0
  105. package/src/utils/figma/__tests__/token-conversion.test.js +117 -0
  106. package/src/utils/figma/__tests__/token-resolution.test.js +231 -0
  107. package/src/utils/figma/auth.js +355 -0
  108. package/src/utils/figma/constants.js +22 -0
  109. package/src/utils/figma/errors.js +80 -0
  110. package/src/utils/figma/figma-api.js +1069 -0
  111. package/src/utils/figma/get-token.js +348 -0
  112. package/src/utils/figma/sync-components.js +909 -0
  113. package/src/utils/figma/sync-main.js +692 -0
  114. package/src/utils/figma/sync-orchestration.js +683 -0
  115. package/src/utils/figma/sync-primitives.js +230 -0
  116. package/src/utils/figma/sync-semantic.js +1056 -0
  117. package/src/utils/figma/token-conversion.js +340 -0
  118. package/src/utils/figma/token-parsing.js +186 -0
  119. package/src/utils/figma/token-resolution.js +569 -0
  120. package/src/utils/figma/utils.js +199 -0
  121. package/src/utils/sd-build-configs.js +305 -0
  122. package/src/utils/sd-formats.js +965 -0
  123. package/src/utils/sd-transforms.js +165 -0
  124. package/src/utils/token-helpers.js +848 -0
  125. package/tsconfig.json +18 -0
  126. package/vitest.config.js +17 -0
  127. package/.turbo/turbo-build.log +0 -37
  128. package/build/web/core/raw.js +0 -234
  129. package/src/global/primitive/breakpoint.js +0 -19
  130. package/src/global/primitive/color.js +0 -231
  131. package/src/global/primitive/duration.js +0 -16
  132. package/src/global/primitive/font.js +0 -60
  133. package/src/global/primitive/radius.js +0 -31
  134. package/src/global/primitive/size.js +0 -55
  135. package/src/global/primitive/transition.js +0 -16
  136. package/src/theme/core/background.js +0 -170
  137. package/src/theme/core/border.js +0 -103
  138. package/src/theme/core/charts.js +0 -464
  139. package/src/theme/core/component/button.js +0 -708
  140. package/src/theme/core/component/checkbox.js +0 -405
  141. package/src/theme/core/focus.js +0 -35
  142. package/src/theme/core/foreground.js +0 -148
  143. package/src/theme/core/overlay.js +0 -137
  144. package/src/theme/core/shadow.js +0 -29
  145. package/src/theme/core/status.js +0 -49
  146. package/src/theme/core/typography.js +0 -82
  147. package/type/types.ts +0 -344
@@ -0,0 +1,569 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Token Resolution Utilities
5
+ *
6
+ * Functions for resolving token references and dependencies.
7
+ */
8
+
9
+ const { isPrimitiveToken } = require("./token-parsing");
10
+
11
+ /**
12
+ * Check if a value is a reference string
13
+ * A valid reference is a string that contains exactly one reference: "{token.path}"
14
+ * Strings with multiple references or other content are not considered references
15
+ * @param {any} value - Value to check
16
+ * @returns {boolean} True if value is a reference
17
+ */
18
+ function isReference(value) {
19
+ if (typeof value !== "string") return false;
20
+ // Must start with { and end with }
21
+ if (!value.startsWith("{") || !value.endsWith("}")) return false;
22
+ // Must contain exactly one reference (no spaces or other content outside the braces)
23
+ // This excludes strings like "{size.0} {size.0} {size.0}" which have multiple references
24
+ const trimmed = value.trim();
25
+ if (trimmed.length < 3) return false; // At least "{x}"
26
+ // Check that there's only one set of braces and no content outside them
27
+ const innerContent = trimmed.slice(1, -1);
28
+ // If there are any braces or spaces in the middle, it's not a single reference
29
+ if (
30
+ innerContent.includes("{") ||
31
+ innerContent.includes("}") ||
32
+ trimmed !== value
33
+ ) {
34
+ return false;
35
+ }
36
+ return true;
37
+ }
38
+
39
+ /**
40
+ * Check if a value is a composite color with a reference in the color property
41
+ * @param {any} value - Value to check
42
+ * @returns {boolean} True if value is a composite color with reference
43
+ */
44
+ function isCompositeColorWithReference(value) {
45
+ return (
46
+ value &&
47
+ typeof value === "object" &&
48
+ !Array.isArray(value) &&
49
+ value.color &&
50
+ typeof value.color === "string" &&
51
+ isReference(value.color) &&
52
+ (value.alpha !== undefined || value.a !== undefined)
53
+ );
54
+ }
55
+
56
+ /**
57
+ * Extract reference path from reference string
58
+ * @param {string} refString - Reference string like "{token.path}"
59
+ * @returns {string|null} Reference path or null if not a valid reference
60
+ */
61
+ function extractReferencePath(refString) {
62
+ if (!isReference(refString)) return null;
63
+ return refString.slice(1, -1); // Remove { and }
64
+ }
65
+
66
+ /**
67
+ * Resolve all references in a string that contains multiple space-separated references
68
+ * Example: "{size.0} {size.0} {size.0}" -> "0rem 0rem 0rem"
69
+ * @param {string} value - String that may contain multiple references like "{token.path} {other.token}"
70
+ * @param {Map} resolvedTokens - Map of resolved tokens
71
+ * @param {boolean} isDark - Whether to use dark mode values
72
+ * @returns {string} String with all references resolved to their actual values
73
+ */
74
+ function escapeForRegExp(str) {
75
+ // Escape all characters that have special meaning in regular expressions
76
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
77
+ }
78
+
79
+ function resolveMultipleReferences(value, resolvedTokens, isDark = false) {
80
+ if (typeof value !== "string") return value;
81
+
82
+ // Find all references in the string using regex
83
+ const referencePattern = /\{([^}]+)\}/g;
84
+ let result = value;
85
+ const seenReferences = new Map(); // Cache resolved values to avoid duplicate work
86
+
87
+ // Process all matches
88
+ let match;
89
+ while ((match = referencePattern.exec(value)) !== null) {
90
+ const fullMatch = match[0]; // e.g., "{size.0}"
91
+ const refPath = match[1]; // e.g., "size.0"
92
+
93
+ // Skip if we've already processed this reference
94
+ if (seenReferences.has(fullMatch)) {
95
+ continue;
96
+ }
97
+
98
+ // Resolve the reference path to a token path
99
+ const resolvedPath = resolveReferencePath(refPath, resolvedTokens);
100
+ const tokenData = resolvedTokens.get(resolvedPath);
101
+
102
+ if (tokenData) {
103
+ // Get the resolved value (use dark if specified, otherwise light)
104
+ let resolvedValue = isDark
105
+ ? tokenData.resolvedDark
106
+ : tokenData.resolvedLight;
107
+
108
+ // If it's a reference string, try to resolve it further (recursive)
109
+ if (typeof resolvedValue === "string" && isReference(resolvedValue)) {
110
+ resolvedValue = resolveValue(resolvedValue, resolvedTokens, isDark);
111
+ }
112
+
113
+ // If the resolved value is an object (dimension or other), convert to string
114
+ if (
115
+ resolvedValue &&
116
+ typeof resolvedValue === "object" &&
117
+ !Array.isArray(resolvedValue)
118
+ ) {
119
+ if (resolvedValue.value !== undefined) {
120
+ // Dimension object: {value: 0, unit: "rem"}
121
+ resolvedValue = `${resolvedValue.value}${resolvedValue.unit || ""}`;
122
+ } else {
123
+ // Other object types - convert to string representation
124
+ resolvedValue = String(resolvedValue);
125
+ }
126
+ }
127
+
128
+ // Convert to string if needed and cache it
129
+ if (resolvedValue !== null && resolvedValue !== undefined) {
130
+ const stringValue = String(resolvedValue);
131
+ seenReferences.set(fullMatch, stringValue);
132
+ }
133
+ }
134
+ }
135
+
136
+ // Replace all references with their resolved values
137
+ for (const [reference, resolvedValue] of seenReferences.entries()) {
138
+ const escapedReference = escapeForRegExp(reference);
139
+ result = result.replace(new RegExp(escapedReference, "g"), resolvedValue);
140
+ }
141
+
142
+ return result;
143
+ }
144
+
145
+ // Cache for resolved reference paths to avoid redundant lookups
146
+ // Key: refPath, Value: resolved token path
147
+ const referencePathCache = new Map();
148
+
149
+ /**
150
+ * Resolve a reference to a token path
151
+ * Optimized with caching to avoid redundant path resolution
152
+ * @param {string} refPath - Reference path (e.g., "color.blue.500")
153
+ * @param {Map} tokenRegistry - Token registry map
154
+ * @returns {string} Resolved token path
155
+ */
156
+ function resolveReferencePath(refPath, tokenRegistry) {
157
+ // Check cache first (use registry size as cache key to invalidate when registry changes)
158
+ const cacheKey = `${refPath}:${tokenRegistry.size}`;
159
+ if (referencePathCache.has(cacheKey)) {
160
+ const cached = referencePathCache.get(cacheKey);
161
+ // Verify the cached path still exists in registry
162
+ if (tokenRegistry.has(cached)) {
163
+ return cached;
164
+ }
165
+ // Cache invalid, remove it
166
+ referencePathCache.delete(cacheKey);
167
+ }
168
+
169
+ // Convert reference path (e.g., "color.blue.500") to token path
170
+ // First try exact match by converting dots to slashes
171
+ const pathWithSlashes = refPath.split(".").join("/");
172
+
173
+ if (tokenRegistry.has(pathWithSlashes)) {
174
+ referencePathCache.set(cacheKey, pathWithSlashes);
175
+ return pathWithSlashes;
176
+ }
177
+
178
+ // Try with $root appended (for group references)
179
+ const withRoot = `${pathWithSlashes}/$root`;
180
+ if (tokenRegistry.has(withRoot)) {
181
+ referencePathCache.set(cacheKey, withRoot);
182
+ return withRoot;
183
+ }
184
+
185
+ // Try finding by matching path segments
186
+ // Optimize: Build a lookup map for segment-based matching if registry is large
187
+ let resolvedPath = null;
188
+ if (tokenRegistry.size > 100) {
189
+ // For large registries, build a segment-based index
190
+ const segmentIndex = new Map();
191
+ for (const [tokenPath] of tokenRegistry.entries()) {
192
+ const segments = tokenPath.split("/");
193
+ const key = `${segments.length}:${segments.join(":")}`;
194
+ if (!segmentIndex.has(key)) {
195
+ segmentIndex.set(key, tokenPath);
196
+ }
197
+ }
198
+
199
+ const refSegments = refPath.split(".");
200
+ const searchKey = `${refSegments.length}:${refSegments.join(":")}`;
201
+ resolvedPath = segmentIndex.get(searchKey) || null;
202
+ } else {
203
+ // For small registries, linear search is fine
204
+ for (const [tokenPath] of tokenRegistry.entries()) {
205
+ const pathArray = tokenPath.split("/");
206
+ const refSegments = refPath.split(".");
207
+
208
+ if (pathArray.length === refSegments.length) {
209
+ let matches = true;
210
+ for (let i = 0; i < refSegments.length; i++) {
211
+ if (pathArray[i] !== refSegments[i]) {
212
+ matches = false;
213
+ break;
214
+ }
215
+ }
216
+ if (matches) {
217
+ resolvedPath = tokenPath;
218
+ break;
219
+ }
220
+ }
221
+ }
222
+ }
223
+
224
+ // Use resolved path or fallback
225
+ const result = resolvedPath || pathWithSlashes;
226
+ referencePathCache.set(cacheKey, result);
227
+ return result;
228
+ }
229
+
230
+ /**
231
+ * Clear the reference path cache (useful for testing or when token registry changes significantly)
232
+ */
233
+ function clearReferencePathCache() {
234
+ referencePathCache.clear();
235
+ }
236
+
237
+ /**
238
+ * Resolve token references recursively
239
+ * @param {any} value - Value that may be a reference
240
+ * @param {Map} tokenRegistry - Token registry map
241
+ * @param {boolean} isDark - Whether to use dark mode
242
+ * @param {Set} visited - Set of visited paths to prevent cycles
243
+ * @returns {any} Resolved value
244
+ */
245
+ function resolveValue(
246
+ value,
247
+ tokenRegistry,
248
+ isDark = false,
249
+ visited = new Set(),
250
+ ) {
251
+ if (!isReference(value)) {
252
+ return value;
253
+ }
254
+
255
+ const refPath = extractReferencePath(value);
256
+ const resolvedPath = resolveReferencePath(refPath, tokenRegistry);
257
+
258
+ // Prevent infinite loops
259
+ if (visited.has(resolvedPath)) {
260
+ return value;
261
+ }
262
+ visited.add(resolvedPath);
263
+
264
+ const refTokenData = tokenRegistry.get(resolvedPath);
265
+ if (!refTokenData) {
266
+ return value; // Can't resolve, return original
267
+ }
268
+
269
+ const { token } = refTokenData;
270
+ let refValue = token.$value;
271
+
272
+ // Handle appearance variants
273
+ if (isDark && token.$extensions?.appearance?.dark?.$value !== undefined) {
274
+ refValue = token.$extensions.appearance.dark.$value;
275
+ } else if (
276
+ !isDark &&
277
+ token.$extensions?.appearance?.light?.$value !== undefined
278
+ ) {
279
+ refValue = token.$extensions.appearance.light.$value;
280
+ }
281
+
282
+ // If the referenced value is also a reference, resolve it
283
+ if (isReference(refValue)) {
284
+ return resolveValue(refValue, tokenRegistry, isDark, visited);
285
+ }
286
+
287
+ return refValue;
288
+ }
289
+
290
+ /**
291
+ * Resolve token references to actual values
292
+ * @param {Map} tokenRegistry - Token registry map
293
+ * @returns {Map} Map of resolved tokens
294
+ */
295
+ function resolvePrimitiveReferences(tokenRegistry) {
296
+ const resolved = new Map();
297
+
298
+ for (const [tokenPath, tokenData] of tokenRegistry.entries()) {
299
+ const { token, sourceFilePath } = tokenData;
300
+
301
+ // Get light value
302
+ let lightValue = token.$value;
303
+ if (token.$extensions?.appearance?.light?.$value !== undefined) {
304
+ lightValue = token.$extensions.appearance.light.$value;
305
+ }
306
+
307
+ // Get dark value
308
+ let darkValue = token.$extensions?.appearance?.dark?.$value;
309
+ if (darkValue === undefined) {
310
+ darkValue = lightValue; // Fallback to light value
311
+ }
312
+
313
+ // Determine if this is a primitive token dynamically based on source file path
314
+ const isPrimitive = isPrimitiveToken(sourceFilePath);
315
+
316
+ // Only resolve references for primitive tokens, keep references for semantic/component tokens
317
+ if (isPrimitive && isReference(lightValue)) {
318
+ lightValue = resolveValue(lightValue, tokenRegistry, false);
319
+ }
320
+
321
+ if (isPrimitive && isReference(darkValue)) {
322
+ darkValue = resolveValue(darkValue, tokenRegistry, true);
323
+ }
324
+
325
+ resolved.set(tokenPath, {
326
+ ...tokenData,
327
+ resolvedLight: lightValue,
328
+ resolvedDark: darkValue,
329
+ isPrimitive,
330
+ });
331
+ }
332
+
333
+ return resolved;
334
+ }
335
+
336
+ /**
337
+ * Topologically sort semantic tokens based on their dependencies
338
+ * Returns tokens in dependency order (dependencies first)
339
+ * @param {Array} semanticTokens - Array of [tokenPath, tokenData] tuples
340
+ * @param {Map} resolvedTokens - Map of resolved tokens
341
+ * @returns {Array} Sorted array of [tokenPath, tokenData] tuples
342
+ */
343
+ function topologicalSortSemanticTokens(semanticTokens, resolvedTokens) {
344
+ // Build dependency graph
345
+ const dependencies = new Map(); // tokenPath -> Set of tokenPaths it depends on
346
+ const dependents = new Map(); // tokenPath -> Set of tokenPaths that depend on it
347
+
348
+ for (const [tokenPath, tokenData] of semanticTokens) {
349
+ dependencies.set(tokenPath, new Set());
350
+ dependents.set(tokenPath, new Set());
351
+
352
+ const { resolvedLight, resolvedDark } = tokenData;
353
+
354
+ // Check light value dependencies
355
+ if (isReference(resolvedLight)) {
356
+ const refPath = extractReferencePath(resolvedLight);
357
+ const targetPath = resolveReferencePath(refPath, resolvedTokens);
358
+ // Only track dependencies on other semantic tokens (not primitives)
359
+ if (
360
+ resolvedTokens.has(targetPath) &&
361
+ !resolvedTokens.get(targetPath).isPrimitive
362
+ ) {
363
+ dependencies.get(tokenPath).add(targetPath);
364
+ if (!dependents.has(targetPath)) {
365
+ dependents.set(targetPath, new Set());
366
+ }
367
+ dependents.get(targetPath).add(tokenPath);
368
+ }
369
+ }
370
+
371
+ // Check dark value dependencies
372
+ if (isReference(resolvedDark)) {
373
+ const refPath = extractReferencePath(resolvedDark);
374
+ const targetPath = resolveReferencePath(refPath, resolvedTokens);
375
+ // Only track dependencies on other semantic tokens (not primitives)
376
+ if (
377
+ resolvedTokens.has(targetPath) &&
378
+ !resolvedTokens.get(targetPath).isPrimitive
379
+ ) {
380
+ dependencies.get(tokenPath).add(targetPath);
381
+ if (!dependents.has(targetPath)) {
382
+ dependents.set(targetPath, new Set());
383
+ }
384
+ dependents.get(targetPath).add(tokenPath);
385
+ }
386
+ }
387
+ }
388
+
389
+ // Topological sort using Kahn's algorithm
390
+ const sorted = [];
391
+ const queue = [];
392
+ const inDegree = new Map();
393
+
394
+ // Initialize in-degree count
395
+ for (const [tokenPath] of semanticTokens) {
396
+ inDegree.set(tokenPath, dependencies.get(tokenPath).size);
397
+ if (inDegree.get(tokenPath) === 0) {
398
+ queue.push(tokenPath);
399
+ }
400
+ }
401
+
402
+ // Process nodes with no dependencies
403
+ while (queue.length > 0) {
404
+ const tokenPath = queue.shift();
405
+ sorted.push(tokenPath);
406
+
407
+ // Reduce in-degree for dependents
408
+ for (const dependent of dependents.get(tokenPath) || []) {
409
+ inDegree.set(dependent, inDegree.get(dependent) - 1);
410
+ if (inDegree.get(dependent) === 0) {
411
+ queue.push(dependent);
412
+ }
413
+ }
414
+ }
415
+
416
+ // If we couldn't sort all tokens, there might be a cycle
417
+ // In that case, return the original order for the unsorted ones
418
+ if (sorted.length < semanticTokens.length) {
419
+ const sortedSet = new Set(sorted);
420
+ for (const [tokenPath] of semanticTokens) {
421
+ if (!sortedSet.has(tokenPath)) {
422
+ sorted.push(tokenPath);
423
+ }
424
+ }
425
+ }
426
+
427
+ // Return tokens in sorted order
428
+ const tokenMap = new Map(semanticTokens);
429
+ return sorted.map((tokenPath) => [tokenPath, tokenMap.get(tokenPath)]);
430
+ }
431
+
432
+ /**
433
+ * Formats an alpha value (0–1) as a percentage string, e.g. 0.08 → "8%".
434
+ * @param {number} alpha
435
+ * @returns {string}
436
+ */
437
+ function formatAlpha(alpha) {
438
+ return `${Math.round((alpha ?? 1) * 100)}%`;
439
+ }
440
+
441
+ /**
442
+ * Builds a reference chain string by following token references through the registry
443
+ * until reaching a terminal (non-reference) value.
444
+ * Composite colors (with alpha) are rendered as "chain (N% opacity)".
445
+ * Example: "{semantic.color.primary}" → "semantic/color/primary → color/blue/500 → #0066cc"
446
+ * Example: "{shadow.color.default}" → "shadow/color/default → color/neutral/900 → #1a1a1a (8% opacity)"
447
+ * @param {string} refValue - Reference string like "{token.path}"
448
+ * @param {Map} tokenRegistry - Map of resolved tokens
449
+ * @param {Set<string>} [visited] - Visited paths to prevent cycles
450
+ * @returns {string|null} Chain string or null if path cannot be resolved
451
+ */
452
+ function buildReferenceChain(refValue, tokenRegistry, visited = new Set()) {
453
+ const refPath = extractReferencePath(refValue);
454
+ if (!refPath) return null;
455
+
456
+ const varPath = refPath.replace(/\./g, "/");
457
+ if (visited.has(varPath)) return varPath;
458
+ visited.add(varPath);
459
+
460
+ const tokenData = tokenRegistry?.get(varPath);
461
+ if (!tokenData) return varPath;
462
+
463
+ const nextValue = tokenData.token?.$value;
464
+
465
+ // Follow plain reference chain
466
+ if (nextValue && isReference(nextValue)) {
467
+ const rest = buildReferenceChain(nextValue, tokenRegistry, visited);
468
+ return rest ? `${varPath} → ${rest}` : varPath;
469
+ }
470
+
471
+ // Terminal is a composite color with a reference — show ref at opacity → hex at opacity
472
+ if (nextValue && isCompositeColorWithReference(nextValue)) {
473
+ const refPath = extractReferencePath(nextValue.color);
474
+ const colorPath = refPath ? refPath.replace(/\./g, "/") : null;
475
+ const opacity = formatAlpha(nextValue.alpha ?? nextValue.a);
476
+ const baseHex = colorPath && tokenRegistry?.get(colorPath)?.resolvedLight;
477
+ const hexStr =
478
+ baseHex && typeof baseHex === "string" ? baseHex.toUpperCase() : null;
479
+ const tail = hexStr
480
+ ? `${colorPath} at ${opacity} opacity → ${hexStr} at ${opacity} opacity`
481
+ : colorPath
482
+ ? `${colorPath} at ${opacity} opacity`
483
+ : null;
484
+ return tail ? `${varPath} → ${tail}` : varPath;
485
+ }
486
+
487
+ // Terminal node: append the final resolved value (string or dimension object)
488
+ const finalValue = tokenData.resolvedLight;
489
+ if (finalValue && typeof finalValue === "string") {
490
+ return `${varPath} → ${finalValue}`;
491
+ }
492
+ if (
493
+ finalValue &&
494
+ typeof finalValue === "object" &&
495
+ finalValue.value !== undefined &&
496
+ finalValue.unit !== undefined
497
+ ) {
498
+ const { value: dimVal, unit } = finalValue;
499
+ const formatted =
500
+ unit === "rem" && dimVal !== 0
501
+ ? `${dimVal}rem (${dimVal * 16}px)`
502
+ : `${dimVal}${unit}`;
503
+ return `${varPath} → ${formatted}`;
504
+ }
505
+
506
+ return varPath;
507
+ }
508
+
509
+ /**
510
+ * Returns a default description for a token derived from its reference value.
511
+ * Follows the full reference chain to the terminal value (e.g. hex for colors).
512
+ * Used when a token has no explicit $description.
513
+ * @param {Object} token - The token object
514
+ * @param {Map} [tokenRegistry] - Map of resolved tokens for chain following
515
+ * @returns {string} Default description or empty string
516
+ */
517
+ function getDefaultDescription(token, tokenRegistry) {
518
+ const value = token.$value;
519
+ if (!value) return "";
520
+
521
+ if (isReference(value)) {
522
+ return buildReferenceChain(value, tokenRegistry) || "";
523
+ }
524
+
525
+ if (isCompositeColorWithReference(value)) {
526
+ const refPath = extractReferencePath(value.color);
527
+ const colorPath = refPath ? refPath.replace(/\./g, "/") : null;
528
+ const opacity = formatAlpha(value.alpha ?? value.a);
529
+ const baseHex = colorPath && tokenRegistry?.get(colorPath)?.resolvedLight;
530
+ const hexStr =
531
+ baseHex && typeof baseHex === "string" ? baseHex.toUpperCase() : null;
532
+ if (!colorPath) return "";
533
+ return hexStr
534
+ ? `${colorPath} at ${opacity} opacity → ${hexStr} at ${opacity} opacity`
535
+ : `${colorPath} at ${opacity} opacity`;
536
+ }
537
+
538
+ return "";
539
+ }
540
+
541
+ /**
542
+ * Returns the token's description with the reference chain always appended.
543
+ * If the token has an explicit $description, the chain is appended on a new line.
544
+ * If there is no $description, only the chain is returned.
545
+ * @param {Object} token - The token object
546
+ * @param {Map} [tokenRegistry] - Map of resolved tokens for chain following
547
+ * @returns {string} Description string
548
+ */
549
+ function getTokenDescription(token, tokenRegistry) {
550
+ const description = token.$description || "";
551
+ const chain = getDefaultDescription(token, tokenRegistry);
552
+ if (description && chain) return `${description}\n${chain}`;
553
+ return description || chain;
554
+ }
555
+
556
+ module.exports = {
557
+ isReference,
558
+ isCompositeColorWithReference,
559
+ extractReferencePath,
560
+ resolveMultipleReferences,
561
+ resolveReferencePath,
562
+ resolveValue,
563
+ resolvePrimitiveReferences,
564
+ topologicalSortSemanticTokens,
565
+ clearReferencePathCache,
566
+ buildReferenceChain,
567
+ getDefaultDescription,
568
+ getTokenDescription,
569
+ };