@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,909 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Component Token Sync
|
|
5
|
+
*
|
|
6
|
+
* Functions for building variable requests for component tokens.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { isComponentToken } = require("./token-parsing");
|
|
10
|
+
const {
|
|
11
|
+
isReference,
|
|
12
|
+
isCompositeColorWithReference,
|
|
13
|
+
extractReferencePath,
|
|
14
|
+
resolveMultipleReferences,
|
|
15
|
+
resolveReferencePath,
|
|
16
|
+
resolveValue,
|
|
17
|
+
topologicalSortSemanticTokens,
|
|
18
|
+
getTokenDescription,
|
|
19
|
+
buildReferenceChain,
|
|
20
|
+
} = require("./token-resolution");
|
|
21
|
+
const {
|
|
22
|
+
convertColorValue,
|
|
23
|
+
convertTokenValueToFigma,
|
|
24
|
+
getFigmaVariableType,
|
|
25
|
+
isGradientToken,
|
|
26
|
+
parseGradientStops,
|
|
27
|
+
} = require("./token-conversion");
|
|
28
|
+
const {
|
|
29
|
+
valuesEqual,
|
|
30
|
+
getVariableIdByName,
|
|
31
|
+
determineVariableName: determineVariableNameHelper,
|
|
32
|
+
} = require("./figma-api");
|
|
33
|
+
/**
|
|
34
|
+
* Get Figma variable scopes from token $extensions["com.figma.scopes"]. Missing or empty = ["ALL_SCOPES"].
|
|
35
|
+
* @param {Object} [token] - Leaf token object
|
|
36
|
+
* @returns {string[]} Scopes array
|
|
37
|
+
*/
|
|
38
|
+
function getScopesForVariable(token) {
|
|
39
|
+
const fromToken = token?.$extensions?.["com.figma.scopes"];
|
|
40
|
+
return Array.isArray(fromToken) && fromToken.length > 0
|
|
41
|
+
? fromToken
|
|
42
|
+
: ["ALL_SCOPES"];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Determine the variable name for a token path (wrapper that includes isComponentToken)
|
|
47
|
+
* @param {string} targetPath - Token path (e.g., "color/neutral/0")
|
|
48
|
+
* @param {Map} resolvedTokensForRefs - Map of resolved tokens for reference resolution
|
|
49
|
+
* @param {Array} existingVariables - Array of existing Figma variables
|
|
50
|
+
* @param {Map} tempIdMap - Temporary ID map for tracking created variables
|
|
51
|
+
* @returns {string} Variable name with correct prefix (primitive/, semantic/, or component/)
|
|
52
|
+
*/
|
|
53
|
+
function determineVariableName(
|
|
54
|
+
targetPath,
|
|
55
|
+
resolvedTokensForRefs,
|
|
56
|
+
existingVariables,
|
|
57
|
+
tempIdMap,
|
|
58
|
+
) {
|
|
59
|
+
return determineVariableNameHelper(
|
|
60
|
+
targetPath,
|
|
61
|
+
resolvedTokensForRefs,
|
|
62
|
+
existingVariables,
|
|
63
|
+
tempIdMap,
|
|
64
|
+
isComponentToken,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Build bulk request for component variable changes only
|
|
70
|
+
* Similar to buildSemanticVariableRequest but for component tokens
|
|
71
|
+
* Returns an object with variables array and variableModeValues array
|
|
72
|
+
* @param {Map} tokensToSync - Map of tokens to sync (may be filtered for extended themes)
|
|
73
|
+
* @param {Array} existingVariables - Array of existing variables
|
|
74
|
+
* @param {string} collectionId - Collection ID
|
|
75
|
+
* @param {Object} modes - Modes object with light and dark
|
|
76
|
+
* @param {Map} tempIdMap - Temporary ID map for tracking created variables
|
|
77
|
+
* @param {Map} [allResolvedTokens] - Optional full map of all resolved tokens for reference resolution
|
|
78
|
+
* @returns {Object} Object with variableChanges, variableModeValues, stats, errors, and tempIdMap
|
|
79
|
+
*/
|
|
80
|
+
function buildComponentVariableRequest(
|
|
81
|
+
tokensToSync,
|
|
82
|
+
existingVariables,
|
|
83
|
+
collectionId,
|
|
84
|
+
modes,
|
|
85
|
+
tempIdMap = new Map(),
|
|
86
|
+
allResolvedTokens = null,
|
|
87
|
+
) {
|
|
88
|
+
const variableChanges = [];
|
|
89
|
+
const variableModeValues = [];
|
|
90
|
+
const stats = { created: 0, updated: 0, skipped: 0, deleted: 0, errors: 0 };
|
|
91
|
+
const errors = [];
|
|
92
|
+
|
|
93
|
+
// Use allResolvedTokens for reference resolution if provided, otherwise use tokensToSync
|
|
94
|
+
const resolvedTokensForRefs = allResolvedTokens || tokensToSync;
|
|
95
|
+
|
|
96
|
+
// Filter to only component tokens (exclude primitives and semantic tokens)
|
|
97
|
+
const componentTokens = [];
|
|
98
|
+
for (const [tokenPath, tokenData] of tokensToSync.entries()) {
|
|
99
|
+
if (!tokenData.isPrimitive && isComponentToken(tokenData.sourceFilePath)) {
|
|
100
|
+
componentTokens.push([tokenPath, tokenData]);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const keptComponentNames = new Set();
|
|
105
|
+
for (const [path, tokenData] of componentTokens) {
|
|
106
|
+
const tokenType = tokenData.token.$type || "string";
|
|
107
|
+
if (isGradientToken(tokenType)) {
|
|
108
|
+
// Gradient tokens expand to per-stop variables; don't keep the composite name
|
|
109
|
+
const gradRefValue = tokenData.token.$value;
|
|
110
|
+
const refPath =
|
|
111
|
+
typeof gradRefValue === "string" && gradRefValue.includes("{")
|
|
112
|
+
? gradRefValue.match(/\{([^{}]+)\}/)?.[1]?.replace(/\./g, "/")
|
|
113
|
+
: null;
|
|
114
|
+
const semToken = refPath ? resolvedTokensForRefs.get(refPath) : null;
|
|
115
|
+
const gradientValue =
|
|
116
|
+
semToken && typeof semToken.token.$value === "object"
|
|
117
|
+
? semToken.token.$value
|
|
118
|
+
: typeof gradRefValue === "object"
|
|
119
|
+
? gradRefValue
|
|
120
|
+
: null;
|
|
121
|
+
const stops = parseGradientStops(gradientValue);
|
|
122
|
+
if (stops) {
|
|
123
|
+
for (let i = 0; i < stops.length; i++) {
|
|
124
|
+
keptComponentNames.add(`component/${path}/stop-${i}`);
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
keptComponentNames.add(`component/${path}`);
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
keptComponentNames.add(`component/${path}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Topologically sort component tokens to process dependencies first
|
|
135
|
+
const sortedComponentTokens = topologicalSortSemanticTokens(
|
|
136
|
+
componentTokens,
|
|
137
|
+
resolvedTokensForRefs,
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// Validate modes before processing
|
|
141
|
+
if (!modes?.light || !modes?.dark) {
|
|
142
|
+
const errorMsg = `Modes are missing: light=${!!modes?.light}, dark=${!!modes?.dark}`;
|
|
143
|
+
errors.push(errorMsg);
|
|
144
|
+
stats.errors++;
|
|
145
|
+
}
|
|
146
|
+
if (!modes?.light?.id || !modes?.dark?.id) {
|
|
147
|
+
const errorMsg = `Mode IDs are missing: light.id=${modes?.light?.id}, dark.id=${modes?.dark?.id}`;
|
|
148
|
+
errors.push(errorMsg);
|
|
149
|
+
stats.errors++;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Process component tokens in dependency order
|
|
153
|
+
for (const [tokenPath, tokenData] of sortedComponentTokens) {
|
|
154
|
+
try {
|
|
155
|
+
const { token, resolvedLight, resolvedDark } = tokenData;
|
|
156
|
+
const tokenType = token.$type || "string";
|
|
157
|
+
|
|
158
|
+
// Add "component/" prefix to variable name for organization
|
|
159
|
+
const variableName = `component/${tokenPath}`;
|
|
160
|
+
|
|
161
|
+
// Special handling for gradient tokens: unpack each stop into a COLOR variable
|
|
162
|
+
// that aliases the corresponding semantic stop variable.
|
|
163
|
+
if (isGradientToken(tokenType)) {
|
|
164
|
+
const gradRefValue = token.$value;
|
|
165
|
+
const refPath =
|
|
166
|
+
typeof gradRefValue === "string" && gradRefValue.includes("{")
|
|
167
|
+
? gradRefValue.match(/\{([^{}]+)\}/)?.[1]?.replace(/\./g, "/")
|
|
168
|
+
: null;
|
|
169
|
+
|
|
170
|
+
// Case 1: reference to semantic gradient token (e.g., "{gradient.primary}")
|
|
171
|
+
// Case 2: inline gradient object (e.g., toast-specific gradients)
|
|
172
|
+
const isInlineGradient = !refPath && typeof gradRefValue === "object";
|
|
173
|
+
|
|
174
|
+
if (!refPath && !isInlineGradient) {
|
|
175
|
+
errors.push(`Could not extract gradient reference from ${tokenPath}`);
|
|
176
|
+
stats.errors++;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let gradientValue = null;
|
|
181
|
+
|
|
182
|
+
if (refPath) {
|
|
183
|
+
const semTokenData = resolvedTokensForRefs.get(refPath);
|
|
184
|
+
if (!semTokenData) {
|
|
185
|
+
errors.push(
|
|
186
|
+
`Could not find semantic gradient token ${refPath} for ${tokenPath}`,
|
|
187
|
+
);
|
|
188
|
+
stats.errors++;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
gradientValue =
|
|
192
|
+
typeof semTokenData.token.$value === "object"
|
|
193
|
+
? semTokenData.token.$value
|
|
194
|
+
: null;
|
|
195
|
+
} else {
|
|
196
|
+
gradientValue = gradRefValue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const stops = parseGradientStops(gradientValue);
|
|
200
|
+
|
|
201
|
+
if (!stops) {
|
|
202
|
+
errors.push(`Could not parse gradient stops for ${tokenPath}`);
|
|
203
|
+
stats.errors++;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const gradientDescription = getTokenDescription(
|
|
208
|
+
token,
|
|
209
|
+
resolvedTokensForRefs,
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
for (let i = 0; i < stops.length; i++) {
|
|
213
|
+
const stopVariableName = `${variableName}/stop-${i}`;
|
|
214
|
+
const stopTokenPath = `${tokenPath}/stop-${i}`;
|
|
215
|
+
const { lightRef: stopLightRef, darkRef: stopDarkRef } = stops[i];
|
|
216
|
+
const lightChain =
|
|
217
|
+
buildReferenceChain(stopLightRef, resolvedTokensForRefs) ||
|
|
218
|
+
stopLightRef;
|
|
219
|
+
const darkChain =
|
|
220
|
+
buildReferenceChain(stopDarkRef, resolvedTokensForRefs) ||
|
|
221
|
+
stopDarkRef;
|
|
222
|
+
const stopColorDetail =
|
|
223
|
+
stopLightRef === stopDarkRef
|
|
224
|
+
? lightChain
|
|
225
|
+
: `light: ${lightChain}, dark: ${darkChain}`;
|
|
226
|
+
const rawPosition = gradientValue.stops[i]?.position;
|
|
227
|
+
const stopPosition =
|
|
228
|
+
typeof rawPosition === "number"
|
|
229
|
+
? `${rawPosition * 100}%`
|
|
230
|
+
: String(rawPosition ?? i);
|
|
231
|
+
const stopDescription = gradientDescription
|
|
232
|
+
? `${gradientDescription}\nstop ${i} at ${stopPosition}: ${stopColorDetail}`
|
|
233
|
+
: `stop ${i} at ${stopPosition}: ${stopColorDetail}`;
|
|
234
|
+
|
|
235
|
+
let lightAliasId, darkAliasId;
|
|
236
|
+
|
|
237
|
+
if (refPath) {
|
|
238
|
+
// Reference case: alias both modes to the same semantic stop variable
|
|
239
|
+
// (the semantic stop variable already encodes light/dark per-mode)
|
|
240
|
+
const semanticStopVariableName = `semantic/${refPath}/stop-${i}`;
|
|
241
|
+
const semanticStopTokenPath = `${refPath}/stop-${i}`;
|
|
242
|
+
const aliasId =
|
|
243
|
+
tempIdMap.get(semanticStopTokenPath) ||
|
|
244
|
+
tempIdMap.get(semanticStopVariableName) ||
|
|
245
|
+
getVariableIdByName(
|
|
246
|
+
semanticStopVariableName,
|
|
247
|
+
existingVariables,
|
|
248
|
+
) ||
|
|
249
|
+
getVariableIdByName(semanticStopTokenPath, existingVariables);
|
|
250
|
+
|
|
251
|
+
if (!aliasId) {
|
|
252
|
+
errors.push(
|
|
253
|
+
`Could not resolve alias for ${stopTokenPath} -> ${semanticStopVariableName}`,
|
|
254
|
+
);
|
|
255
|
+
stats.errors++;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
lightAliasId = aliasId;
|
|
259
|
+
darkAliasId = aliasId;
|
|
260
|
+
} else {
|
|
261
|
+
// Inline case: resolve each stop's color reference to a semantic variable
|
|
262
|
+
const { lightRef, darkRef } = stops[i];
|
|
263
|
+
const lightRefPath = extractReferencePath(lightRef) ?? lightRef;
|
|
264
|
+
const lightTargetPath = resolveReferencePath(
|
|
265
|
+
lightRefPath,
|
|
266
|
+
resolvedTokensForRefs,
|
|
267
|
+
);
|
|
268
|
+
const lightTargetVariableName = determineVariableName(
|
|
269
|
+
lightTargetPath,
|
|
270
|
+
resolvedTokensForRefs,
|
|
271
|
+
existingVariables,
|
|
272
|
+
tempIdMap,
|
|
273
|
+
);
|
|
274
|
+
lightAliasId =
|
|
275
|
+
tempIdMap.get(lightTargetPath) ||
|
|
276
|
+
tempIdMap.get(lightTargetVariableName) ||
|
|
277
|
+
getVariableIdByName(lightTargetVariableName, existingVariables) ||
|
|
278
|
+
getVariableIdByName(lightTargetPath, existingVariables);
|
|
279
|
+
|
|
280
|
+
if (!lightAliasId) {
|
|
281
|
+
errors.push(
|
|
282
|
+
`Could not resolve light alias for ${stopTokenPath} -> ${lightTargetPath}`,
|
|
283
|
+
);
|
|
284
|
+
stats.errors++;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const darkRefPath = extractReferencePath(darkRef) ?? darkRef;
|
|
289
|
+
const darkTargetPath = resolveReferencePath(
|
|
290
|
+
darkRefPath,
|
|
291
|
+
resolvedTokensForRefs,
|
|
292
|
+
);
|
|
293
|
+
const darkTargetVariableName = determineVariableName(
|
|
294
|
+
darkTargetPath,
|
|
295
|
+
resolvedTokensForRefs,
|
|
296
|
+
existingVariables,
|
|
297
|
+
tempIdMap,
|
|
298
|
+
);
|
|
299
|
+
darkAliasId =
|
|
300
|
+
tempIdMap.get(darkTargetPath) ||
|
|
301
|
+
tempIdMap.get(darkTargetVariableName) ||
|
|
302
|
+
getVariableIdByName(darkTargetVariableName, existingVariables) ||
|
|
303
|
+
getVariableIdByName(darkTargetPath, existingVariables);
|
|
304
|
+
|
|
305
|
+
if (!darkAliasId) {
|
|
306
|
+
errors.push(
|
|
307
|
+
`Could not resolve dark alias for ${stopTokenPath} -> ${darkTargetPath}`,
|
|
308
|
+
);
|
|
309
|
+
stats.errors++;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const lightFigmaValue = { type: "VARIABLE_ALIAS", id: lightAliasId };
|
|
315
|
+
const darkFigmaValue = { type: "VARIABLE_ALIAS", id: darkAliasId };
|
|
316
|
+
|
|
317
|
+
// Get scopes from the stop color's extensions on the semantic token
|
|
318
|
+
const stopColor = gradientValue.stops[i]?.color;
|
|
319
|
+
const stopScopes =
|
|
320
|
+
Array.isArray(stopColor?.$extensions?.["com.figma.scopes"]) &&
|
|
321
|
+
stopColor.$extensions["com.figma.scopes"].length > 0
|
|
322
|
+
? stopColor.$extensions["com.figma.scopes"]
|
|
323
|
+
: ["ALL_SCOPES"];
|
|
324
|
+
|
|
325
|
+
const existingId = getVariableIdByName(
|
|
326
|
+
stopVariableName,
|
|
327
|
+
existingVariables,
|
|
328
|
+
);
|
|
329
|
+
let existing = null;
|
|
330
|
+
if (existingId) {
|
|
331
|
+
existing =
|
|
332
|
+
existingVariables.length > 100
|
|
333
|
+
? (() => {
|
|
334
|
+
const m = new Map(
|
|
335
|
+
existingVariables
|
|
336
|
+
.filter((v) => v.id)
|
|
337
|
+
.map((v) => [v.id, v]),
|
|
338
|
+
);
|
|
339
|
+
return m.get(existingId) || null;
|
|
340
|
+
})()
|
|
341
|
+
: existingVariables.find((v) => v.id === existingId) || null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (existing) {
|
|
345
|
+
const existingValues = existing.valuesByMode || {};
|
|
346
|
+
const lightSame = valuesEqual(
|
|
347
|
+
existingValues[modes.light.id],
|
|
348
|
+
lightFigmaValue,
|
|
349
|
+
);
|
|
350
|
+
const darkSame = valuesEqual(
|
|
351
|
+
existingValues[modes.dark.id],
|
|
352
|
+
darkFigmaValue,
|
|
353
|
+
);
|
|
354
|
+
const sortedUpdate = [...stopScopes].sort();
|
|
355
|
+
const sortedExisting = [...(existing.scopes ?? [])].sort();
|
|
356
|
+
const scopesEqual =
|
|
357
|
+
sortedUpdate.length === sortedExisting.length &&
|
|
358
|
+
sortedUpdate.every((s, idx) => s === sortedExisting[idx]);
|
|
359
|
+
|
|
360
|
+
const stopDescriptionSame =
|
|
361
|
+
(existing.description || "") === stopDescription;
|
|
362
|
+
if (lightSame && darkSame && scopesEqual && stopDescriptionSame) {
|
|
363
|
+
tempIdMap.set(stopTokenPath, existing.id);
|
|
364
|
+
tempIdMap.set(stopVariableName, existing.id);
|
|
365
|
+
stats.skipped++;
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
variableChanges.push({
|
|
370
|
+
action: "UPDATE",
|
|
371
|
+
id: existing.id,
|
|
372
|
+
name: stopVariableName,
|
|
373
|
+
scopes: stopScopes,
|
|
374
|
+
description: stopDescription,
|
|
375
|
+
});
|
|
376
|
+
variableModeValues.push(
|
|
377
|
+
{
|
|
378
|
+
variableId: existing.id,
|
|
379
|
+
modeId: modes.light.id,
|
|
380
|
+
value: lightFigmaValue,
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
variableId: existing.id,
|
|
384
|
+
modeId: modes.dark.id,
|
|
385
|
+
value: darkFigmaValue,
|
|
386
|
+
},
|
|
387
|
+
);
|
|
388
|
+
tempIdMap.set(stopTokenPath, existing.id);
|
|
389
|
+
tempIdMap.set(stopVariableName, existing.id);
|
|
390
|
+
stats.updated++;
|
|
391
|
+
} else {
|
|
392
|
+
const tempIdCounter = tempIdMap.size + variableChanges.length + 1;
|
|
393
|
+
const tempId = `temp_var_${tempIdCounter}_${stopVariableName.replace(/\//g, "_").replace(/[^a-zA-Z0-9_]/g, "_")}`;
|
|
394
|
+
tempIdMap.set(stopTokenPath, tempId);
|
|
395
|
+
tempIdMap.set(stopVariableName, tempId);
|
|
396
|
+
|
|
397
|
+
variableChanges.push({
|
|
398
|
+
action: "CREATE",
|
|
399
|
+
id: tempId,
|
|
400
|
+
name: stopVariableName,
|
|
401
|
+
variableCollectionId: collectionId,
|
|
402
|
+
resolvedType: "COLOR",
|
|
403
|
+
scopes: stopScopes,
|
|
404
|
+
description: stopDescription,
|
|
405
|
+
});
|
|
406
|
+
variableModeValues.push(
|
|
407
|
+
{
|
|
408
|
+
variableId: tempId,
|
|
409
|
+
modeId: modes.light.id,
|
|
410
|
+
value: lightFigmaValue,
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
variableId: tempId,
|
|
414
|
+
modeId: modes.dark.id,
|
|
415
|
+
value: darkFigmaValue,
|
|
416
|
+
},
|
|
417
|
+
);
|
|
418
|
+
stats.created++;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Skip normal processing — the gradient composite token has no Figma variable
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Check if this should be an alias (only for direct string references, not composite colors)
|
|
427
|
+
const isLightReference = isReference(resolvedLight);
|
|
428
|
+
const isDarkReference = isReference(resolvedDark);
|
|
429
|
+
const isLightCompositeWithRef =
|
|
430
|
+
isCompositeColorWithReference(resolvedLight);
|
|
431
|
+
const isDarkCompositeWithRef =
|
|
432
|
+
isCompositeColorWithReference(resolvedDark);
|
|
433
|
+
|
|
434
|
+
let lightFigmaValue;
|
|
435
|
+
let darkFigmaValue;
|
|
436
|
+
let lightAliasId = null;
|
|
437
|
+
let darkAliasId = null;
|
|
438
|
+
|
|
439
|
+
// Handle composite colors with references - resolve the color and apply alpha
|
|
440
|
+
if (isLightCompositeWithRef) {
|
|
441
|
+
const refString = resolvedLight.color;
|
|
442
|
+
const refPath = extractReferencePath(refString);
|
|
443
|
+
if (!refPath) {
|
|
444
|
+
errors.push(
|
|
445
|
+
`Could not extract reference path from ${refString} for ${tokenPath}`,
|
|
446
|
+
);
|
|
447
|
+
stats.errors++;
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const targetPath = resolveReferencePath(refPath, resolvedTokensForRefs);
|
|
452
|
+
const targetTokenData = resolvedTokensForRefs.get(targetPath);
|
|
453
|
+
if (!targetTokenData) {
|
|
454
|
+
errors.push(
|
|
455
|
+
`Could not find target token ${targetPath} for ${tokenPath}`,
|
|
456
|
+
);
|
|
457
|
+
stats.errors++;
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
let resolvedColorValue = targetTokenData.resolvedLight;
|
|
462
|
+
if (!resolvedColorValue) {
|
|
463
|
+
errors.push(
|
|
464
|
+
`Target token ${targetPath} has no resolved light value for ${tokenPath}`,
|
|
465
|
+
);
|
|
466
|
+
stats.errors++;
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Semantic tokens may have resolvedLight still as a reference (e.g. "{color.neutral.950}"); resolve to literal
|
|
471
|
+
if (isReference(resolvedColorValue)) {
|
|
472
|
+
resolvedColorValue = resolveValue(
|
|
473
|
+
resolvedColorValue,
|
|
474
|
+
resolvedTokensForRefs,
|
|
475
|
+
false,
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const baseColor = convertColorValue(resolvedColorValue);
|
|
480
|
+
if (baseColor === null) {
|
|
481
|
+
errors.push(
|
|
482
|
+
`Could not convert resolved color value for ${tokenPath}: ${JSON.stringify(resolvedColorValue)}`,
|
|
483
|
+
);
|
|
484
|
+
stats.errors++;
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
lightFigmaValue = {
|
|
489
|
+
...baseColor,
|
|
490
|
+
a: resolvedLight.alpha ?? resolvedLight.a ?? baseColor.a,
|
|
491
|
+
};
|
|
492
|
+
} else if (isLightReference) {
|
|
493
|
+
// Direct string reference - create alias
|
|
494
|
+
const refPath = extractReferencePath(resolvedLight);
|
|
495
|
+
if (!refPath) {
|
|
496
|
+
errors.push(
|
|
497
|
+
`Could not extract reference path from ${resolvedLight} for ${tokenPath}`,
|
|
498
|
+
);
|
|
499
|
+
stats.errors++;
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const targetPath = resolveReferencePath(refPath, resolvedTokensForRefs);
|
|
504
|
+
// Determine the correct variable name based on token type
|
|
505
|
+
const targetVariableName = determineVariableName(
|
|
506
|
+
targetPath,
|
|
507
|
+
resolvedTokensForRefs,
|
|
508
|
+
existingVariables,
|
|
509
|
+
tempIdMap,
|
|
510
|
+
);
|
|
511
|
+
lightAliasId =
|
|
512
|
+
tempIdMap.get(targetPath) ||
|
|
513
|
+
tempIdMap.get(targetVariableName) ||
|
|
514
|
+
getVariableIdByName(targetVariableName, existingVariables) ||
|
|
515
|
+
getVariableIdByName(targetPath, existingVariables);
|
|
516
|
+
|
|
517
|
+
if (!lightAliasId) {
|
|
518
|
+
const errorMsg = `Could not resolve light alias for ${tokenPath} -> ${targetPath}`;
|
|
519
|
+
errors.push(errorMsg);
|
|
520
|
+
stats.errors++;
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
lightFigmaValue = { type: "VARIABLE_ALIAS", id: lightAliasId };
|
|
525
|
+
} else {
|
|
526
|
+
// Check if the value is a string with multiple references
|
|
527
|
+
let valueToConvert = resolvedLight;
|
|
528
|
+
if (
|
|
529
|
+
typeof resolvedLight === "string" &&
|
|
530
|
+
resolvedLight.includes("{") &&
|
|
531
|
+
resolvedLight.includes("}") &&
|
|
532
|
+
!isReference(resolvedLight)
|
|
533
|
+
) {
|
|
534
|
+
valueToConvert = resolveMultipleReferences(
|
|
535
|
+
resolvedLight,
|
|
536
|
+
resolvedTokensForRefs,
|
|
537
|
+
false,
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// When value is currentColor, use com.figma.alternateValue.mode if present
|
|
542
|
+
if (valueToConvert === "currentColor") {
|
|
543
|
+
const altMode = token.$extensions?.["com.figma.alternateValue"]?.mode;
|
|
544
|
+
if (altMode?.light != null) {
|
|
545
|
+
let altLight = altMode.light;
|
|
546
|
+
if (isReference(altLight)) {
|
|
547
|
+
altLight = resolveValue(altLight, resolvedTokensForRefs, false);
|
|
548
|
+
}
|
|
549
|
+
const converted = convertColorValue(altLight);
|
|
550
|
+
if (converted !== null) {
|
|
551
|
+
lightFigmaValue = converted;
|
|
552
|
+
} else {
|
|
553
|
+
errors.push(
|
|
554
|
+
`Could not convert com.figma.alternateValue.mode.light for ${tokenPath}: ${JSON.stringify(altMode.light)}`,
|
|
555
|
+
);
|
|
556
|
+
stats.errors++;
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
} else {
|
|
560
|
+
errors.push(
|
|
561
|
+
`Token ${tokenPath} uses currentColor but has no com.figma.alternateValue.mode (required for Figma sync)`,
|
|
562
|
+
);
|
|
563
|
+
stats.errors++;
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
} else {
|
|
567
|
+
const converted = convertTokenValueToFigma(
|
|
568
|
+
tokenType,
|
|
569
|
+
valueToConvert,
|
|
570
|
+
{
|
|
571
|
+
tokenPath,
|
|
572
|
+
scopes: getScopesForVariable(token),
|
|
573
|
+
},
|
|
574
|
+
);
|
|
575
|
+
if (converted === null) {
|
|
576
|
+
errors.push(`Could not convert light value for ${tokenPath}`);
|
|
577
|
+
stats.errors++;
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
lightFigmaValue = converted;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Handle dark mode values (same logic as light)
|
|
585
|
+
if (isDarkCompositeWithRef) {
|
|
586
|
+
const refString = resolvedDark.color;
|
|
587
|
+
const refPath = extractReferencePath(refString);
|
|
588
|
+
if (!refPath) {
|
|
589
|
+
errors.push(
|
|
590
|
+
`Could not extract reference path from ${refString} for ${tokenPath}`,
|
|
591
|
+
);
|
|
592
|
+
stats.errors++;
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const targetPath = resolveReferencePath(refPath, resolvedTokensForRefs);
|
|
597
|
+
const targetTokenData = resolvedTokensForRefs.get(targetPath);
|
|
598
|
+
if (!targetTokenData) {
|
|
599
|
+
errors.push(
|
|
600
|
+
`Could not find target token ${targetPath} for ${tokenPath}`,
|
|
601
|
+
);
|
|
602
|
+
stats.errors++;
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
let resolvedColorValueDark = targetTokenData.resolvedDark;
|
|
607
|
+
if (!resolvedColorValueDark) {
|
|
608
|
+
errors.push(
|
|
609
|
+
`Target token ${targetPath} has no resolved dark value for ${tokenPath}`,
|
|
610
|
+
);
|
|
611
|
+
stats.errors++;
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (isReference(resolvedColorValueDark)) {
|
|
616
|
+
resolvedColorValueDark = resolveValue(
|
|
617
|
+
resolvedColorValueDark,
|
|
618
|
+
resolvedTokensForRefs,
|
|
619
|
+
true,
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const baseColorDark = convertColorValue(resolvedColorValueDark);
|
|
624
|
+
if (baseColorDark === null) {
|
|
625
|
+
errors.push(
|
|
626
|
+
`Could not convert resolved color value for ${tokenPath}: ${JSON.stringify(resolvedColorValueDark)}`,
|
|
627
|
+
);
|
|
628
|
+
stats.errors++;
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
darkFigmaValue = {
|
|
633
|
+
...baseColorDark,
|
|
634
|
+
a: resolvedDark.alpha ?? resolvedDark.a ?? baseColorDark.a,
|
|
635
|
+
};
|
|
636
|
+
} else if (isDarkReference) {
|
|
637
|
+
const refPath = extractReferencePath(resolvedDark);
|
|
638
|
+
if (!refPath) {
|
|
639
|
+
errors.push(
|
|
640
|
+
`Could not extract reference path from ${resolvedDark} for ${tokenPath}`,
|
|
641
|
+
);
|
|
642
|
+
stats.errors++;
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const targetPath = resolveReferencePath(refPath, resolvedTokensForRefs);
|
|
647
|
+
// Determine the correct variable name based on token type
|
|
648
|
+
const targetVariableName = determineVariableName(
|
|
649
|
+
targetPath,
|
|
650
|
+
resolvedTokensForRefs,
|
|
651
|
+
existingVariables,
|
|
652
|
+
tempIdMap,
|
|
653
|
+
);
|
|
654
|
+
darkAliasId =
|
|
655
|
+
tempIdMap.get(targetPath) ||
|
|
656
|
+
tempIdMap.get(targetVariableName) ||
|
|
657
|
+
getVariableIdByName(targetVariableName, existingVariables) ||
|
|
658
|
+
getVariableIdByName(targetPath, existingVariables);
|
|
659
|
+
|
|
660
|
+
if (!darkAliasId) {
|
|
661
|
+
const errorMsg = `Could not resolve dark alias for ${tokenPath} -> ${targetPath}`;
|
|
662
|
+
errors.push(errorMsg);
|
|
663
|
+
stats.errors++;
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
darkFigmaValue = { type: "VARIABLE_ALIAS", id: darkAliasId };
|
|
668
|
+
} else {
|
|
669
|
+
let valueToConvert = resolvedDark;
|
|
670
|
+
if (
|
|
671
|
+
typeof resolvedDark === "string" &&
|
|
672
|
+
resolvedDark.includes("{") &&
|
|
673
|
+
resolvedDark.includes("}") &&
|
|
674
|
+
!isReference(resolvedDark)
|
|
675
|
+
) {
|
|
676
|
+
valueToConvert = resolveMultipleReferences(
|
|
677
|
+
resolvedDark,
|
|
678
|
+
resolvedTokensForRefs,
|
|
679
|
+
true,
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// When value is currentColor, use com.figma.alternateValue.mode if present
|
|
684
|
+
if (valueToConvert === "currentColor") {
|
|
685
|
+
const altMode = token.$extensions?.["com.figma.alternateValue"]?.mode;
|
|
686
|
+
if (altMode?.dark != null) {
|
|
687
|
+
let altDark = altMode.dark;
|
|
688
|
+
if (isReference(altDark)) {
|
|
689
|
+
altDark = resolveValue(altDark, resolvedTokensForRefs, true);
|
|
690
|
+
}
|
|
691
|
+
const converted = convertColorValue(altDark);
|
|
692
|
+
if (converted !== null) {
|
|
693
|
+
darkFigmaValue = converted;
|
|
694
|
+
} else {
|
|
695
|
+
errors.push(
|
|
696
|
+
`Could not convert com.figma.alternateValue.mode.dark for ${tokenPath}: ${JSON.stringify(altMode.dark)}`,
|
|
697
|
+
);
|
|
698
|
+
stats.errors++;
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
} else if (lightFigmaValue != null) {
|
|
702
|
+
// No dark alternate; reuse light value so the variable has both modes
|
|
703
|
+
darkFigmaValue = lightFigmaValue;
|
|
704
|
+
} else {
|
|
705
|
+
errors.push(
|
|
706
|
+
`Token ${tokenPath} uses currentColor but has no com.figma.alternateValue.mode (required for Figma sync)`,
|
|
707
|
+
);
|
|
708
|
+
stats.errors++;
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
} else {
|
|
712
|
+
const converted = convertTokenValueToFigma(
|
|
713
|
+
tokenType,
|
|
714
|
+
valueToConvert,
|
|
715
|
+
{
|
|
716
|
+
tokenPath,
|
|
717
|
+
scopes: getScopesForVariable(token),
|
|
718
|
+
},
|
|
719
|
+
);
|
|
720
|
+
if (converted === null) {
|
|
721
|
+
errors.push(`Could not convert dark value for ${tokenPath}`);
|
|
722
|
+
stats.errors++;
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
darkFigmaValue = converted;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Get existing variable - optimize with getVariableIdByName
|
|
730
|
+
const existingId =
|
|
731
|
+
getVariableIdByName(variableName, existingVariables) ||
|
|
732
|
+
getVariableIdByName(tokenPath, existingVariables);
|
|
733
|
+
// Optimize: Use Map for O(1) lookup if we have many variables
|
|
734
|
+
let existing = null;
|
|
735
|
+
if (existingId) {
|
|
736
|
+
if (existingVariables.length > 100) {
|
|
737
|
+
const variableIdMap = new Map();
|
|
738
|
+
for (const v of existingVariables) {
|
|
739
|
+
if (v.id) variableIdMap.set(v.id, v);
|
|
740
|
+
}
|
|
741
|
+
existing = variableIdMap.get(existingId) || null;
|
|
742
|
+
} else {
|
|
743
|
+
existing = existingVariables.find((v) => v.id === existingId);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (existing) {
|
|
748
|
+
if (!existing.id) {
|
|
749
|
+
const errorMsg = `Existing variable for ${tokenPath} has no ID`;
|
|
750
|
+
errors.push(errorMsg);
|
|
751
|
+
stats.errors++;
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
if (!modes.light?.id || !modes.dark?.id) {
|
|
756
|
+
errors.push(
|
|
757
|
+
`Mode IDs are missing: light=${modes.light?.id}, dark=${modes.dark?.id}`,
|
|
758
|
+
);
|
|
759
|
+
stats.errors++;
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// Check if values are the same
|
|
764
|
+
const existingValues = existing.valuesByMode || {};
|
|
765
|
+
const existingLight = existingValues[modes.light.id];
|
|
766
|
+
const existingDark = existingValues[modes.dark.id];
|
|
767
|
+
|
|
768
|
+
const lightSame = valuesEqual(existingLight, lightFigmaValue);
|
|
769
|
+
const darkSame = valuesEqual(existingDark, darkFigmaValue);
|
|
770
|
+
|
|
771
|
+
const updateScopes = getScopesForVariable(tokenData.token);
|
|
772
|
+
const existingScopes = existing.scopes || [];
|
|
773
|
+
const sortedUpdate = [...updateScopes].sort();
|
|
774
|
+
const sortedExisting = [...existingScopes].sort();
|
|
775
|
+
const scopesEqual =
|
|
776
|
+
sortedUpdate.length === sortedExisting.length &&
|
|
777
|
+
sortedUpdate.every((s, i) => s === sortedExisting[i]);
|
|
778
|
+
|
|
779
|
+
const tokenDescription = getTokenDescription(
|
|
780
|
+
token,
|
|
781
|
+
resolvedTokensForRefs,
|
|
782
|
+
);
|
|
783
|
+
const descriptionSame =
|
|
784
|
+
(existing.description || "") === tokenDescription;
|
|
785
|
+
|
|
786
|
+
if (lightSame && darkSame) {
|
|
787
|
+
// Values unchanged: still send UPDATE if scopes or description need to be applied/updated
|
|
788
|
+
if (!scopesEqual || !descriptionSame) {
|
|
789
|
+
variableChanges.push({
|
|
790
|
+
action: "UPDATE",
|
|
791
|
+
id: existing.id,
|
|
792
|
+
name: variableName,
|
|
793
|
+
scopes: updateScopes,
|
|
794
|
+
description: tokenDescription,
|
|
795
|
+
});
|
|
796
|
+
stats.updated++;
|
|
797
|
+
} else {
|
|
798
|
+
stats.skipped++;
|
|
799
|
+
}
|
|
800
|
+
tempIdMap.set(tokenPath, existing.id);
|
|
801
|
+
tempIdMap.set(variableName, existing.id);
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// Update variable (values changed) — always send our scopes so Figma matches code
|
|
806
|
+
const updatePayload = {
|
|
807
|
+
action: "UPDATE",
|
|
808
|
+
id: existing.id,
|
|
809
|
+
name: variableName,
|
|
810
|
+
scopes: updateScopes,
|
|
811
|
+
description: tokenDescription,
|
|
812
|
+
};
|
|
813
|
+
variableChanges.push(updatePayload);
|
|
814
|
+
|
|
815
|
+
variableModeValues.push(
|
|
816
|
+
{
|
|
817
|
+
variableId: existing.id,
|
|
818
|
+
modeId: modes.light.id,
|
|
819
|
+
value: lightFigmaValue,
|
|
820
|
+
},
|
|
821
|
+
{
|
|
822
|
+
variableId: existing.id,
|
|
823
|
+
modeId: modes.dark.id,
|
|
824
|
+
value: darkFigmaValue,
|
|
825
|
+
},
|
|
826
|
+
);
|
|
827
|
+
|
|
828
|
+
tempIdMap.set(tokenPath, existing.id);
|
|
829
|
+
tempIdMap.set(variableName, existing.id);
|
|
830
|
+
stats.updated++;
|
|
831
|
+
} else {
|
|
832
|
+
// Validate mode IDs before creating new variable
|
|
833
|
+
if (!modes.light?.id || !modes.dark?.id) {
|
|
834
|
+
errors.push(
|
|
835
|
+
`Mode IDs are missing: light.id=${modes.light?.id}, dark.id=${modes.dark?.id}`,
|
|
836
|
+
);
|
|
837
|
+
stats.errors++;
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// Create new variable with temporary ID
|
|
842
|
+
const tempIdCounter = tempIdMap.size + variableChanges.length + 1;
|
|
843
|
+
const tempId = `temp_var_${tempIdCounter}_${variableName.replace(/\//g, "_").replace(/[^a-zA-Z0-9_]/g, "_")}`;
|
|
844
|
+
tempIdMap.set(tokenPath, tempId);
|
|
845
|
+
tempIdMap.set(variableName, tempId);
|
|
846
|
+
|
|
847
|
+
// Determine variable type based on the actual resolved value
|
|
848
|
+
let variableType = getFigmaVariableType(tokenType);
|
|
849
|
+
if (
|
|
850
|
+
variableType === "FLOAT" &&
|
|
851
|
+
typeof lightFigmaValue === "string" &&
|
|
852
|
+
lightFigmaValue.includes(" ")
|
|
853
|
+
) {
|
|
854
|
+
variableType = "STRING";
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
const createScopes = getScopesForVariable(tokenData.token);
|
|
858
|
+
const createPayload = {
|
|
859
|
+
action: "CREATE",
|
|
860
|
+
id: tempId,
|
|
861
|
+
name: variableName,
|
|
862
|
+
variableCollectionId: collectionId,
|
|
863
|
+
resolvedType: variableType,
|
|
864
|
+
description: getTokenDescription(token, resolvedTokensForRefs),
|
|
865
|
+
};
|
|
866
|
+
createPayload.scopes = createScopes;
|
|
867
|
+
variableChanges.push(createPayload);
|
|
868
|
+
|
|
869
|
+
variableModeValues.push(
|
|
870
|
+
{
|
|
871
|
+
variableId: tempId,
|
|
872
|
+
modeId: modes.light.id,
|
|
873
|
+
value: lightFigmaValue,
|
|
874
|
+
},
|
|
875
|
+
{
|
|
876
|
+
variableId: tempId,
|
|
877
|
+
modeId: modes.dark.id,
|
|
878
|
+
value: darkFigmaValue,
|
|
879
|
+
},
|
|
880
|
+
);
|
|
881
|
+
|
|
882
|
+
stats.created++;
|
|
883
|
+
}
|
|
884
|
+
} catch (error) {
|
|
885
|
+
errors.push(`${tokenPath}: ${error.message}`);
|
|
886
|
+
stats.errors++;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// Delete component variables that are no longer in token source
|
|
891
|
+
for (const v of existingVariables) {
|
|
892
|
+
if (v.name?.startsWith("component/") && !keptComponentNames.has(v.name)) {
|
|
893
|
+
variableChanges.push({ action: "DELETE", id: v.id });
|
|
894
|
+
stats.deleted++;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
return {
|
|
899
|
+
variableChanges,
|
|
900
|
+
variableModeValues,
|
|
901
|
+
stats,
|
|
902
|
+
errors,
|
|
903
|
+
tempIdMap,
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
module.exports = {
|
|
908
|
+
buildComponentVariableRequest,
|
|
909
|
+
};
|