@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.
- package/CHANGELOG.md +50 -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 -245
- package/build/web/core/semantic.d.ts +221 -0
- package/build/web/core/semantic.js +1592 -347
- package/build/web/core/semantic.scss +219 -140
- 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 -234
- 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 -464
- 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 -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
|
+
};
|