cloudflare/kumo
Publicmirrored fromhttps://github.com/cloudflare/kumoAvailable
packages/kumo-figma/scripts/figma-api.ts
723lines · modecode
| 1 | /** |
| 2 | * Figma Variables API client for syncing design tokens |
| 3 | * |
| 4 | * This module provides a unidirectional sync from code to Figma. |
| 5 | * It purges all existing variables and recreates them fresh, |
| 6 | * making the codebase the single source of truth. |
| 7 | */ |
| 8 | |
| 9 | /** Color with optional alpha (input format) */ |
| 10 | export type FigmaColorInput = { r: number; g: number; b: number; a?: number }; |
| 11 | |
| 12 | /** Color with required alpha (Figma API format) */ |
| 13 | type FigmaColor = { r: number; g: number; b: number; a: number }; |
| 14 | |
| 15 | export type ResolvedToken = { |
| 16 | name: string; |
| 17 | light: FigmaColorInput; |
| 18 | dark: FigmaColorInput; |
| 19 | }; |
| 20 | |
| 21 | /** |
| 22 | * A resolved typography token with numeric value |
| 23 | */ |
| 24 | export type ResolvedTypographyToken = { |
| 25 | name: string; |
| 26 | value: number; |
| 27 | }; |
| 28 | |
| 29 | export type FigmaConfig = { |
| 30 | fileKey: string; |
| 31 | token: string; |
| 32 | }; |
| 33 | |
| 34 | /** |
| 35 | * Extended theme mode configuration |
| 36 | */ |
| 37 | export type ExtendedMode = { |
| 38 | name: string; |
| 39 | /** Map of token name -> color value for this mode (overrides light values) */ |
| 40 | overrides: Record<string, FigmaColorInput>; |
| 41 | }; |
| 42 | |
| 43 | /** |
| 44 | * Full sync configuration |
| 45 | */ |
| 46 | export type SyncConfig = { |
| 47 | fileKey: string; |
| 48 | token: string; |
| 49 | collectionName: string; |
| 50 | /** Base tokens with Light/Dark values */ |
| 51 | tokens: ResolvedToken[]; |
| 52 | /** Additional modes (e.g., fedramp) that extend the base tokens */ |
| 53 | extendedModes?: ExtendedMode[]; |
| 54 | }; |
| 55 | |
| 56 | export type SyncResult = { |
| 57 | success: boolean; |
| 58 | error?: string; |
| 59 | tempIdToRealId?: Record<string, string>; |
| 60 | }; |
| 61 | |
| 62 | /** |
| 63 | * Normalize color to Figma format (0-1 range, always include alpha) |
| 64 | */ |
| 65 | function normalizeFigmaColor(color: FigmaColorInput): FigmaColor { |
| 66 | return { |
| 67 | r: color.r, |
| 68 | g: color.g, |
| 69 | b: color.b, |
| 70 | a: color.a ?? 1, |
| 71 | }; |
| 72 | } |
| 73 | |
| 74 | /** |
| 75 | * Generate a stable ID for a variable based on its name |
| 76 | */ |
| 77 | function generateVariableId(name: string): string { |
| 78 | return `var_${name.replace(/-/g, "_")}`; |
| 79 | } |
| 80 | |
| 81 | /** |
| 82 | * Generate a stable ID for a mode based on its name |
| 83 | */ |
| 84 | function generateModeId(name: string): string { |
| 85 | return `mode_${name.toLowerCase().replace(/\s+/g, "_")}`; |
| 86 | } |
| 87 | |
| 88 | type FigmaPayload = { |
| 89 | variableCollections?: Array<{ |
| 90 | action: "CREATE" | "UPDATE" | "DELETE"; |
| 91 | id: string; |
| 92 | name?: string; |
| 93 | /** Initial mode ID for base collections (not used for extensions) */ |
| 94 | initialModeId?: string; |
| 95 | /** For extension collections: the parent collection ID */ |
| 96 | parentVariableCollectionId?: string; |
| 97 | /** For extension collections: maps extension mode IDs to parent mode IDs */ |
| 98 | initialModeIdToInitialParentModeIdMap?: Record<string, string>; |
| 99 | }>; |
| 100 | variableModes?: Array<{ |
| 101 | action: "CREATE" | "UPDATE" | "DELETE"; |
| 102 | id: string; |
| 103 | name?: string; |
| 104 | variableCollectionId?: string; |
| 105 | }>; |
| 106 | variables?: Array<{ |
| 107 | action: "CREATE" | "UPDATE" | "DELETE"; |
| 108 | id: string; |
| 109 | name?: string; |
| 110 | variableCollectionId?: string; |
| 111 | resolvedType?: "COLOR" | "FLOAT" | "STRING"; |
| 112 | }>; |
| 113 | variableModeValues?: Array<{ |
| 114 | variableId: string; |
| 115 | modeId: string; |
| 116 | value: FigmaColor | number | string; |
| 117 | }>; |
| 118 | }; |
| 119 | |
| 120 | /** |
| 121 | * Get local variables from a Figma file |
| 122 | */ |
| 123 | export async function getLocalVariables( |
| 124 | fileKey: string, |
| 125 | token: string, |
| 126 | ): Promise<{ |
| 127 | success: boolean; |
| 128 | error?: string; |
| 129 | data?: { |
| 130 | variables: Record< |
| 131 | string, |
| 132 | { id: string; name: string; variableCollectionId: string } |
| 133 | >; |
| 134 | variableCollections: Record< |
| 135 | string, |
| 136 | { |
| 137 | id: string; |
| 138 | name: string; |
| 139 | modes: Array<{ modeId: string; name: string }>; |
| 140 | } |
| 141 | >; |
| 142 | }; |
| 143 | }> { |
| 144 | const url = `https://api.figma.com/v1/files/${fileKey}/variables/local`; |
| 145 | |
| 146 | try { |
| 147 | const response = await fetch(url, { |
| 148 | method: "GET", |
| 149 | headers: { "X-Figma-Token": token }, |
| 150 | }); |
| 151 | |
| 152 | const responseText = await response.text(); |
| 153 | let responseJson: unknown; |
| 154 | |
| 155 | try { |
| 156 | responseJson = JSON.parse(responseText); |
| 157 | } catch { |
| 158 | return { |
| 159 | success: false, |
| 160 | error: `Failed to parse response: ${responseText}`, |
| 161 | }; |
| 162 | } |
| 163 | |
| 164 | if (!response.ok) { |
| 165 | const errorMessage = |
| 166 | responseJson && |
| 167 | typeof responseJson === "object" && |
| 168 | responseJson !== null |
| 169 | ? (responseJson as Record<string, unknown>).message || |
| 170 | (responseJson as Record<string, unknown>).error || |
| 171 | responseText |
| 172 | : responseText; |
| 173 | return { |
| 174 | success: false, |
| 175 | error: `Figma API error (${response.status}): ${errorMessage}`, |
| 176 | }; |
| 177 | } |
| 178 | |
| 179 | const meta = |
| 180 | responseJson && |
| 181 | typeof responseJson === "object" && |
| 182 | responseJson !== null && |
| 183 | "meta" in responseJson |
| 184 | ? ( |
| 185 | responseJson as { |
| 186 | meta: { |
| 187 | variables: Record<string, unknown>; |
| 188 | variableCollections: Record<string, unknown>; |
| 189 | }; |
| 190 | } |
| 191 | ).meta |
| 192 | : null; |
| 193 | |
| 194 | return { |
| 195 | success: true, |
| 196 | data: (meta as { |
| 197 | variables: Record< |
| 198 | string, |
| 199 | { id: string; name: string; variableCollectionId: string } |
| 200 | >; |
| 201 | variableCollections: Record< |
| 202 | string, |
| 203 | { |
| 204 | id: string; |
| 205 | name: string; |
| 206 | modes: Array<{ modeId: string; name: string }>; |
| 207 | } |
| 208 | >; |
| 209 | }) ?? { variables: {}, variableCollections: {} }, |
| 210 | }; |
| 211 | } catch (error) { |
| 212 | const message = error instanceof Error ? error.message : String(error); |
| 213 | return { success: false, error: `Network error: ${message}` }; |
| 214 | } |
| 215 | } |
| 216 | |
| 217 | /** |
| 218 | * Send a payload to Figma Variables API |
| 219 | */ |
| 220 | async function sendFigmaPayload( |
| 221 | fileKey: string, |
| 222 | token: string, |
| 223 | payload: FigmaPayload, |
| 224 | ): Promise<SyncResult> { |
| 225 | const url = `https://api.figma.com/v1/files/${fileKey}/variables`; |
| 226 | |
| 227 | try { |
| 228 | const response = await fetch(url, { |
| 229 | method: "POST", |
| 230 | headers: { |
| 231 | "X-Figma-Token": token, |
| 232 | "Content-Type": "application/json", |
| 233 | }, |
| 234 | body: JSON.stringify(payload), |
| 235 | }); |
| 236 | |
| 237 | const responseText = await response.text(); |
| 238 | let responseJson: unknown; |
| 239 | |
| 240 | try { |
| 241 | responseJson = JSON.parse(responseText); |
| 242 | } catch { |
| 243 | responseJson = null; |
| 244 | } |
| 245 | |
| 246 | if (!response.ok) { |
| 247 | const errorMessage = |
| 248 | responseJson && |
| 249 | typeof responseJson === "object" && |
| 250 | responseJson !== null |
| 251 | ? (responseJson as Record<string, unknown>).message || |
| 252 | (responseJson as Record<string, unknown>).error || |
| 253 | responseText |
| 254 | : responseText; |
| 255 | return { |
| 256 | success: false, |
| 257 | error: `Figma API error (${response.status}): ${errorMessage}`, |
| 258 | }; |
| 259 | } |
| 260 | |
| 261 | const meta = |
| 262 | responseJson && |
| 263 | typeof responseJson === "object" && |
| 264 | responseJson !== null && |
| 265 | "meta" in responseJson |
| 266 | ? ( |
| 267 | responseJson as { |
| 268 | meta: { tempIdToRealId?: Record<string, string> }; |
| 269 | } |
| 270 | ).meta |
| 271 | : null; |
| 272 | |
| 273 | return { success: true, tempIdToRealId: meta?.tempIdToRealId }; |
| 274 | } catch (error) { |
| 275 | const message = error instanceof Error ? error.message : String(error); |
| 276 | return { success: false, error: `Network error: ${message}` }; |
| 277 | } |
| 278 | } |
| 279 | |
| 280 | /** |
| 281 | * Delete all variables and collections from a Figma file |
| 282 | */ |
| 283 | export async function purgeAllVariables( |
| 284 | fileKey: string, |
| 285 | token: string, |
| 286 | ): Promise<SyncResult> { |
| 287 | // First, get all existing variables |
| 288 | const existing = await getLocalVariables(fileKey, token); |
| 289 | if (!existing.success || !existing.data) { |
| 290 | return existing; |
| 291 | } |
| 292 | |
| 293 | const { variables, variableCollections } = existing.data; |
| 294 | const variableIds = Object.keys(variables); |
| 295 | const collectionIds = Object.keys(variableCollections); |
| 296 | |
| 297 | if (variableIds.length === 0 && collectionIds.length === 0) { |
| 298 | return { success: true }; |
| 299 | } |
| 300 | |
| 301 | // Delete all variables first, then collections |
| 302 | const payload: FigmaPayload = { |
| 303 | variables: variableIds.map((id) => ({ action: "DELETE", id })), |
| 304 | variableCollections: collectionIds.map((id) => ({ action: "DELETE", id })), |
| 305 | }; |
| 306 | |
| 307 | return sendFigmaPayload(fileKey, token, payload); |
| 308 | } |
| 309 | |
| 310 | /** |
| 311 | * Sync tokens to Figma (purge + create) |
| 312 | * |
| 313 | * This is a unidirectional sync that makes the codebase the single source of truth. |
| 314 | * It deletes all existing variables and recreates them from scratch. |
| 315 | * |
| 316 | * Creates: |
| 317 | * 1. Base collection with Light and Dark modes |
| 318 | * 2. Extension collections for each extended mode (e.g., "fedramp") that inherit |
| 319 | * from the base collection and can override specific token values. |
| 320 | * Each extension also has Light/Dark modes mapped to the parent's modes. |
| 321 | */ |
| 322 | export async function syncToFigma(config: SyncConfig): Promise<SyncResult> { |
| 323 | const { fileKey, token, collectionName, tokens, extendedModes = [] } = config; |
| 324 | |
| 325 | if (!tokens.length) { |
| 326 | return { success: false, error: "No tokens to sync" }; |
| 327 | } |
| 328 | |
| 329 | // Step 1: Purge all existing variables |
| 330 | const purgeResult = await purgeAllVariables(fileKey, token); |
| 331 | if (!purgeResult.success) { |
| 332 | return { success: false, error: `Failed to purge: ${purgeResult.error}` }; |
| 333 | } |
| 334 | |
| 335 | // Step 2: Build the base collection payload |
| 336 | const baseCollectionId = "kumo_collection"; |
| 337 | const lightModeId = generateModeId("Light"); |
| 338 | const darkModeId = generateModeId("Dark"); |
| 339 | |
| 340 | // Build base mode definitions (Light and Dark only) |
| 341 | const variableModes: FigmaPayload["variableModes"] = [ |
| 342 | { |
| 343 | action: "UPDATE", |
| 344 | id: lightModeId, |
| 345 | name: "Light", |
| 346 | variableCollectionId: baseCollectionId, |
| 347 | }, |
| 348 | { |
| 349 | action: "CREATE", |
| 350 | id: darkModeId, |
| 351 | name: "Dark", |
| 352 | variableCollectionId: baseCollectionId, |
| 353 | }, |
| 354 | ]; |
| 355 | |
| 356 | // Build variables |
| 357 | const variables: FigmaPayload["variables"] = tokens.map((t) => ({ |
| 358 | action: "CREATE", |
| 359 | id: generateVariableId(t.name), |
| 360 | name: t.name, |
| 361 | variableCollectionId: baseCollectionId, |
| 362 | resolvedType: "COLOR", |
| 363 | })); |
| 364 | |
| 365 | // Build mode values for base collection |
| 366 | const variableModeValues: FigmaPayload["variableModeValues"] = []; |
| 367 | |
| 368 | for (const t of tokens) { |
| 369 | const varId = generateVariableId(t.name); |
| 370 | |
| 371 | // Light mode |
| 372 | variableModeValues.push({ |
| 373 | variableId: varId, |
| 374 | modeId: lightModeId, |
| 375 | value: normalizeFigmaColor(t.light), |
| 376 | }); |
| 377 | |
| 378 | // Dark mode |
| 379 | variableModeValues.push({ |
| 380 | variableId: varId, |
| 381 | modeId: darkModeId, |
| 382 | value: normalizeFigmaColor(t.dark), |
| 383 | }); |
| 384 | } |
| 385 | |
| 386 | // Build base collection |
| 387 | const variableCollections: NonNullable<FigmaPayload["variableCollections"]> = |
| 388 | [ |
| 389 | { |
| 390 | action: "CREATE", |
| 391 | id: baseCollectionId, |
| 392 | name: collectionName, |
| 393 | initialModeId: lightModeId, |
| 394 | }, |
| 395 | ]; |
| 396 | |
| 397 | // Step 3: Build extension collections for each extended mode |
| 398 | // Extension collections inherit from base and override specific tokens |
| 399 | // They still have Light/Dark modes that map to the parent's Light/Dark |
| 400 | for (const extMode of extendedModes) { |
| 401 | const extCollectionId = `ext_${extMode.name.toLowerCase()}`; |
| 402 | const extLightModeId = `${extCollectionId}_light`; |
| 403 | const extDarkModeId = `${extCollectionId}_dark`; |
| 404 | |
| 405 | // Create extension collection with mode mapping to parent |
| 406 | variableCollections.push({ |
| 407 | action: "CREATE", |
| 408 | id: extCollectionId, |
| 409 | name: extMode.name, |
| 410 | parentVariableCollectionId: baseCollectionId, |
| 411 | initialModeIdToInitialParentModeIdMap: { |
| 412 | [extLightModeId]: lightModeId, |
| 413 | [extDarkModeId]: darkModeId, |
| 414 | }, |
| 415 | }); |
| 416 | |
| 417 | // Add variable mode values for overrides in the extension collection |
| 418 | // Only add values for tokens that have overrides |
| 419 | for (const t of tokens) { |
| 420 | const varId = generateVariableId(t.name); |
| 421 | const override = extMode.overrides[t.name]; |
| 422 | |
| 423 | if (override) { |
| 424 | // Light mode override |
| 425 | variableModeValues.push({ |
| 426 | variableId: varId, |
| 427 | modeId: extLightModeId, |
| 428 | value: normalizeFigmaColor(override), |
| 429 | }); |
| 430 | |
| 431 | // Dark mode override (use same override - fedramp overrides apply to both modes) |
| 432 | variableModeValues.push({ |
| 433 | variableId: varId, |
| 434 | modeId: extDarkModeId, |
| 435 | value: normalizeFigmaColor(override), |
| 436 | }); |
| 437 | } |
| 438 | } |
| 439 | } |
| 440 | |
| 441 | const createPayload: FigmaPayload = { |
| 442 | variableCollections, |
| 443 | variableModes, |
| 444 | variables, |
| 445 | variableModeValues, |
| 446 | }; |
| 447 | |
| 448 | return sendFigmaPayload(fileKey, token, createPayload); |
| 449 | } |
| 450 | |
| 451 | /** |
| 452 | * Configuration for syncing typography tokens |
| 453 | */ |
| 454 | export type TypographySyncConfig = { |
| 455 | fileKey: string; |
| 456 | token: string; |
| 457 | collectionName: string; |
| 458 | /** Typography tokens with numeric values */ |
| 459 | tokens: ResolvedTypographyToken[]; |
| 460 | /** Mode name (e.g., "Desktop") */ |
| 461 | modeName?: string; |
| 462 | }; |
| 463 | |
| 464 | /** |
| 465 | * Sync typography tokens to Figma as FLOAT variables |
| 466 | * |
| 467 | * Creates a separate collection for typography with a single mode. |
| 468 | * Unlike color tokens, typography tokens don't have light/dark variants. |
| 469 | */ |
| 470 | export async function syncTypographyToFigma( |
| 471 | config: TypographySyncConfig, |
| 472 | ): Promise<SyncResult> { |
| 473 | const { |
| 474 | fileKey, |
| 475 | token, |
| 476 | collectionName, |
| 477 | tokens, |
| 478 | modeName = "Desktop", |
| 479 | } = config; |
| 480 | |
| 481 | if (!tokens.length) { |
| 482 | return { success: false, error: "No typography tokens to sync" }; |
| 483 | } |
| 484 | |
| 485 | // Build the typography collection payload |
| 486 | const typographyCollectionId = "typography_collection"; |
| 487 | const desktopModeId = generateModeId(modeName); |
| 488 | |
| 489 | // Build collection |
| 490 | const variableCollections: NonNullable<FigmaPayload["variableCollections"]> = |
| 491 | [ |
| 492 | { |
| 493 | action: "CREATE", |
| 494 | id: typographyCollectionId, |
| 495 | name: collectionName, |
| 496 | initialModeId: desktopModeId, |
| 497 | }, |
| 498 | ]; |
| 499 | |
| 500 | // Build mode (just rename the initial mode) |
| 501 | const variableModes: FigmaPayload["variableModes"] = [ |
| 502 | { |
| 503 | action: "UPDATE", |
| 504 | id: desktopModeId, |
| 505 | name: modeName, |
| 506 | variableCollectionId: typographyCollectionId, |
| 507 | }, |
| 508 | ]; |
| 509 | |
| 510 | // Build variables |
| 511 | const variables: FigmaPayload["variables"] = tokens.map((t) => ({ |
| 512 | action: "CREATE", |
| 513 | id: generateVariableId(t.name), |
| 514 | name: t.name, |
| 515 | variableCollectionId: typographyCollectionId, |
| 516 | resolvedType: "FLOAT", |
| 517 | })); |
| 518 | |
| 519 | // Build mode values |
| 520 | const variableModeValues: FigmaPayload["variableModeValues"] = tokens.map( |
| 521 | (t) => ({ |
| 522 | variableId: generateVariableId(t.name), |
| 523 | modeId: desktopModeId, |
| 524 | value: t.value, |
| 525 | }), |
| 526 | ); |
| 527 | |
| 528 | const createPayload: FigmaPayload = { |
| 529 | variableCollections, |
| 530 | variableModes, |
| 531 | variables, |
| 532 | variableModeValues, |
| 533 | }; |
| 534 | |
| 535 | return sendFigmaPayload(fileKey, token, createPayload); |
| 536 | } |
| 537 | |
| 538 | /** |
| 539 | * Combined sync configuration for both colors and typography |
| 540 | */ |
| 541 | export type CombinedSyncConfig = { |
| 542 | fileKey: string; |
| 543 | token: string; |
| 544 | /** Color collection configuration */ |
| 545 | colors: { |
| 546 | collectionName: string; |
| 547 | tokens: ResolvedToken[]; |
| 548 | extendedModes?: ExtendedMode[]; |
| 549 | }; |
| 550 | /** Typography collection configuration */ |
| 551 | typography?: { |
| 552 | collectionName: string; |
| 553 | tokens: ResolvedTypographyToken[]; |
| 554 | modeName?: string; |
| 555 | }; |
| 556 | }; |
| 557 | |
| 558 | /** |
| 559 | * Sync both color and typography tokens to Figma in a single operation |
| 560 | * |
| 561 | * This purges all existing variables and recreates both collections. |
| 562 | */ |
| 563 | export async function syncAllToFigma( |
| 564 | config: CombinedSyncConfig, |
| 565 | ): Promise<SyncResult> { |
| 566 | const { fileKey, token, colors, typography } = config; |
| 567 | |
| 568 | // Step 1: Purge all existing variables |
| 569 | const purgeResult = await purgeAllVariables(fileKey, token); |
| 570 | if (!purgeResult.success) { |
| 571 | return { success: false, error: `Failed to purge: ${purgeResult.error}` }; |
| 572 | } |
| 573 | |
| 574 | // Step 2: Build combined payload for both collections |
| 575 | const colorCollectionId = "kumo_collection"; |
| 576 | const lightModeId = generateModeId("Light"); |
| 577 | const darkModeId = generateModeId("Dark"); |
| 578 | |
| 579 | // Build color collection |
| 580 | const variableCollections: NonNullable<FigmaPayload["variableCollections"]> = |
| 581 | [ |
| 582 | { |
| 583 | action: "CREATE", |
| 584 | id: colorCollectionId, |
| 585 | name: colors.collectionName, |
| 586 | initialModeId: lightModeId, |
| 587 | }, |
| 588 | ]; |
| 589 | |
| 590 | // Build color modes |
| 591 | const variableModes: FigmaPayload["variableModes"] = [ |
| 592 | { |
| 593 | action: "UPDATE", |
| 594 | id: lightModeId, |
| 595 | name: "Light", |
| 596 | variableCollectionId: colorCollectionId, |
| 597 | }, |
| 598 | { |
| 599 | action: "CREATE", |
| 600 | id: darkModeId, |
| 601 | name: "Dark", |
| 602 | variableCollectionId: colorCollectionId, |
| 603 | }, |
| 604 | ]; |
| 605 | |
| 606 | // Build color variables |
| 607 | const variables: FigmaPayload["variables"] = colors.tokens.map((t) => ({ |
| 608 | action: "CREATE", |
| 609 | id: generateVariableId(t.name), |
| 610 | name: t.name, |
| 611 | variableCollectionId: colorCollectionId, |
| 612 | resolvedType: "COLOR", |
| 613 | })); |
| 614 | |
| 615 | // Build color mode values |
| 616 | const variableModeValues: FigmaPayload["variableModeValues"] = []; |
| 617 | |
| 618 | for (const t of colors.tokens) { |
| 619 | const varId = generateVariableId(t.name); |
| 620 | |
| 621 | // Light mode |
| 622 | variableModeValues.push({ |
| 623 | variableId: varId, |
| 624 | modeId: lightModeId, |
| 625 | value: normalizeFigmaColor(t.light), |
| 626 | }); |
| 627 | |
| 628 | // Dark mode |
| 629 | variableModeValues.push({ |
| 630 | variableId: varId, |
| 631 | modeId: darkModeId, |
| 632 | value: normalizeFigmaColor(t.dark), |
| 633 | }); |
| 634 | } |
| 635 | |
| 636 | // Add extended modes for colors |
| 637 | const extendedModes = colors.extendedModes ?? []; |
| 638 | for (const extMode of extendedModes) { |
| 639 | const extCollectionId = `ext_${extMode.name.toLowerCase()}`; |
| 640 | const extLightModeId = `${extCollectionId}_light`; |
| 641 | const extDarkModeId = `${extCollectionId}_dark`; |
| 642 | |
| 643 | variableCollections.push({ |
| 644 | action: "CREATE", |
| 645 | id: extCollectionId, |
| 646 | name: extMode.name, |
| 647 | parentVariableCollectionId: colorCollectionId, |
| 648 | initialModeIdToInitialParentModeIdMap: { |
| 649 | [extLightModeId]: lightModeId, |
| 650 | [extDarkModeId]: darkModeId, |
| 651 | }, |
| 652 | }); |
| 653 | |
| 654 | for (const t of colors.tokens) { |
| 655 | const varId = generateVariableId(t.name); |
| 656 | const override = extMode.overrides[t.name]; |
| 657 | |
| 658 | if (override) { |
| 659 | variableModeValues.push({ |
| 660 | variableId: varId, |
| 661 | modeId: extLightModeId, |
| 662 | value: normalizeFigmaColor(override), |
| 663 | }); |
| 664 | |
| 665 | variableModeValues.push({ |
| 666 | variableId: varId, |
| 667 | modeId: extDarkModeId, |
| 668 | value: normalizeFigmaColor(override), |
| 669 | }); |
| 670 | } |
| 671 | } |
| 672 | } |
| 673 | |
| 674 | // Add typography collection if provided |
| 675 | if (typography && typography.tokens.length > 0) { |
| 676 | const typographyCollectionId = "typography_collection"; |
| 677 | const typographyModeId = generateModeId(typography.modeName ?? "Desktop"); |
| 678 | |
| 679 | variableCollections.push({ |
| 680 | action: "CREATE", |
| 681 | id: typographyCollectionId, |
| 682 | name: typography.collectionName, |
| 683 | initialModeId: typographyModeId, |
| 684 | }); |
| 685 | |
| 686 | variableModes.push({ |
| 687 | action: "UPDATE", |
| 688 | id: typographyModeId, |
| 689 | name: typography.modeName ?? "Desktop", |
| 690 | variableCollectionId: typographyCollectionId, |
| 691 | }); |
| 692 | |
| 693 | for (const t of typography.tokens) { |
| 694 | const varId = generateVariableId(`typography_${t.name}`); |
| 695 | |
| 696 | variables.push({ |
| 697 | action: "CREATE", |
| 698 | id: varId, |
| 699 | name: t.name, |
| 700 | variableCollectionId: typographyCollectionId, |
| 701 | resolvedType: "FLOAT", |
| 702 | }); |
| 703 | |
| 704 | variableModeValues.push({ |
| 705 | variableId: varId, |
| 706 | modeId: typographyModeId, |
| 707 | value: t.value, |
| 708 | }); |
| 709 | } |
| 710 | } |
| 711 | |
| 712 | const createPayload: FigmaPayload = { |
| 713 | variableCollections, |
| 714 | variableModes, |
| 715 | variables, |
| 716 | variableModeValues, |
| 717 | }; |
| 718 | |
| 719 | return sendFigmaPayload(fileKey, token, createPayload); |
| 720 | } |
| 721 | |
| 722 | // Legacy export for backwards compatibility |
| 723 | export { syncToFigma as syncToFigmaLegacy }; |
| 724 | |