cloudflare/kumo
Publicmirrored fromhttps://github.com/cloudflare/kumoAvailable
packages/kumo-figma/src/code.ts
506lines ยท modecode
| 1 | /** |
| 2 | * Kumo UI Kit Generator - Main Plugin Entry Point |
| 3 | * |
| 4 | * Generates Figma components from Kumo component definitions. |
| 5 | * Runs destructive sync - purges and recreates all components on each run. |
| 6 | * |
| 7 | * Target file: sKKZc6pC6W1TtzWBLxDGSU (kumo-ai) |
| 8 | */ |
| 9 | |
| 10 | import { generateBadgeComponents } from "./generators/badge"; |
| 11 | import { generateBannerComponents } from "./generators/banner"; |
| 12 | import { generateBreadcrumbsComponents } from "./generators/breadcrumbs"; |
| 13 | import { generateButtonComponents } from "./generators/button"; |
| 14 | import { generateCheckboxComponents } from "./generators/checkbox"; |
| 15 | import { generateClipboardTextComponents } from "./generators/clipboard-text"; |
| 16 | import { generateCodeComponents } from "./generators/code"; |
| 17 | import { generateCodeBlockComponents } from "./generators/code-block"; |
| 18 | import { generateCollapsibleComponents } from "./generators/collapsible"; |
| 19 | import { generateCommandPaletteComponents } from "./generators/command-palette"; |
| 20 | import { generateComboboxComponents } from "./generators/combobox"; |
| 21 | import { generateDateRangePickerComponents } from "./generators/date-range-picker"; |
| 22 | import { generateDialogComponents } from "./generators/dialog"; |
| 23 | import { generateDropdownComponents } from "./generators/dropdown"; |
| 24 | import { generateEmptyComponents } from "./generators/empty"; |
| 25 | import { generateInputComponents } from "./generators/input"; |
| 26 | import { generateInputAreaComponents } from "./generators/input-area"; |
| 27 | import { generateLayerCardComponents } from "./generators/layer-card"; |
| 28 | import { generateLabelComponents } from "./generators/label"; |
| 29 | import { generateLinkComponents } from "./generators/link"; |
| 30 | import { generateLoaderComponents } from "./generators/loader"; |
| 31 | import { generateLinkButtonComponents } from "./generators/link-button"; |
| 32 | import { generateMenuBarComponents } from "./generators/menubar"; |
| 33 | import { generateMeterComponents } from "./generators/meter"; |
| 34 | |
| 35 | import { generatePaginationComponents } from "./generators/pagination"; |
| 36 | import { |
| 37 | generateRadioComponents, |
| 38 | generateRadioGroupComponents, |
| 39 | } from "./generators/radio"; |
| 40 | import { generateRefreshButtonComponents } from "./generators/refresh-button"; |
| 41 | import { generateSelectComponents } from "./generators/select"; |
| 42 | import { generateSensitiveInputComponents } from "./generators/sensitive-input"; |
| 43 | import { generateSurfaceComponents } from "./generators/surface"; |
| 44 | import { |
| 45 | generateSwitchComponents, |
| 46 | generateSwitchGroupComponents, |
| 47 | } from "./generators/switch"; |
| 48 | import { generateTableComponents } from "./generators/table"; |
| 49 | import { generateTabsComponents } from "./generators/tabs"; |
| 50 | import { generateTextComponents } from "./generators/text"; |
| 51 | import { generateToastComponents } from "./generators/toast"; |
| 52 | import { generateTooltipComponents } from "./generators/tooltip"; |
| 53 | import { |
| 54 | initializeColorVariables, |
| 55 | clearColorVariables, |
| 56 | } from "./generators/shared"; |
| 57 | import { logInfo, logError } from "./logger"; |
| 58 | |
| 59 | figma.showUI(__html__, { width: 320, height: 220 }); |
| 60 | |
| 61 | /** |
| 62 | * Page name for the UI kit (icons + components on same page) |
| 63 | */ |
| 64 | const UI_KIT_PAGE_NAME = "ui kit"; |
| 65 | |
| 66 | /** |
| 67 | * Destructive sync: ensures only the UI Kit page exists. |
| 68 | * - Creates UI Kit page if it doesn't exist |
| 69 | * - Removes ALL other pages |
| 70 | * - Purges all content from UI Kit page |
| 71 | * |
| 72 | * @returns The clean UI Kit page ready for generation |
| 73 | */ |
| 74 | function destructiveSyncPages(): PageNode { |
| 75 | logInfo("๐ Starting destructive sync..."); |
| 76 | |
| 77 | // Step 1: Find or create UI Kit page |
| 78 | let uiKitPage = figma.root.children.find( |
| 79 | (page) => |
| 80 | page.type === "PAGE" && |
| 81 | page.name.trim().toLowerCase() === UI_KIT_PAGE_NAME, |
| 82 | ) as PageNode | undefined; |
| 83 | |
| 84 | if (!uiKitPage) { |
| 85 | logInfo("๐ Creating UI Kit page"); |
| 86 | uiKitPage = figma.createPage(); |
| 87 | uiKitPage.name = UI_KIT_PAGE_NAME; |
| 88 | } else { |
| 89 | logInfo("โ
Found existing UI Kit page"); |
| 90 | } |
| 91 | |
| 92 | // IMPORTANT: Switch away from the current page before removing others. |
| 93 | // Figma can throw when attempting to remove the active page. |
| 94 | if (figma.currentPage.id !== uiKitPage.id) { |
| 95 | figma.currentPage = uiKitPage; |
| 96 | } |
| 97 | |
| 98 | // Step 2: Remove ALL other pages (Figma requires at least one page, so we keep uiKitPage) |
| 99 | const pagesToRemove = figma.root.children.filter( |
| 100 | (page) => page.type === "PAGE" && page.id !== uiKitPage!.id, |
| 101 | ); |
| 102 | |
| 103 | if (pagesToRemove.length > 0) { |
| 104 | logInfo(`๐๏ธ Removing ${pagesToRemove.length} other page(s)...`); |
| 105 | for (const page of pagesToRemove) { |
| 106 | logInfo(` - Removing page: "${page.name}"`); |
| 107 | page.remove(); |
| 108 | } |
| 109 | } |
| 110 | |
| 111 | // Step 3: Purge all content from UI Kit page |
| 112 | const children = [...uiKitPage.children]; |
| 113 | if (children.length > 0) { |
| 114 | logInfo(`๐๏ธ Purging ${children.length} items from UI Kit page`); |
| 115 | for (const node of children) { |
| 116 | node.remove(); |
| 117 | } |
| 118 | } |
| 119 | |
| 120 | logInfo("โ
Destructive sync complete - single clean page ready"); |
| 121 | return uiKitPage; |
| 122 | } |
| 123 | |
| 124 | /** |
| 125 | * Starting Y position for first section |
| 126 | */ |
| 127 | const START_Y = 100; |
| 128 | |
| 129 | /** |
| 130 | * Component generator configuration |
| 131 | * Each entry defines a component generator with its display name and execution function |
| 132 | */ |
| 133 | type GeneratorConfig = { |
| 134 | name: string; |
| 135 | execute: ( |
| 136 | page: PageNode, |
| 137 | currentY: number, |
| 138 | ) => Promise<{ nextY: number } | void>; |
| 139 | }; |
| 140 | |
| 141 | figma.ui.onmessage = async (msg: { type: string }) => { |
| 142 | if (msg.type === "generate") { |
| 143 | try { |
| 144 | figma.notify("Starting Kumo UI Kit generation..."); |
| 145 | |
| 146 | // Destructive sync: remove all other pages, purge UI Kit page content |
| 147 | const uiKitPage = destructiveSyncPages(); |
| 148 | figma.currentPage = uiKitPage; |
| 149 | |
| 150 | // Destructive sync: clear and recreate color variables |
| 151 | logInfo("๐จ Syncing color variables..."); |
| 152 | clearColorVariables(); |
| 153 | initializeColorVariables(); |
| 154 | |
| 155 | // Track Y position for sequential section placement |
| 156 | let nextY = START_Y; |
| 157 | |
| 158 | /** |
| 159 | * Generator registry - add new generators here in any order. |
| 160 | * They will be auto-sorted alphabetically at runtime. |
| 161 | * |
| 162 | * Component generators registry - add new generators here in any order. |
| 163 | * They will be auto-sorted alphabetically at runtime. |
| 164 | */ |
| 165 | const GENERATORS: (GeneratorConfig & { priority?: boolean })[] = [ |
| 166 | { |
| 167 | name: "Badge", |
| 168 | execute: async (_page, y) => { |
| 169 | const result = await generateBadgeComponents(y); |
| 170 | return { nextY: result }; |
| 171 | }, |
| 172 | }, |
| 173 | { |
| 174 | name: "Banner", |
| 175 | execute: async (_page, y) => { |
| 176 | const result = await generateBannerComponents(y); |
| 177 | return { nextY: result }; |
| 178 | }, |
| 179 | }, |
| 180 | { |
| 181 | name: "Breadcrumbs", |
| 182 | execute: async (_page, y) => { |
| 183 | const result = await generateBreadcrumbsComponents(y); |
| 184 | return { nextY: result }; |
| 185 | }, |
| 186 | }, |
| 187 | { |
| 188 | name: "Button", |
| 189 | execute: async (page, y) => { |
| 190 | const result = await generateButtonComponents(page, y); |
| 191 | return { nextY: result }; |
| 192 | }, |
| 193 | }, |
| 194 | { |
| 195 | name: "Checkbox", |
| 196 | execute: async (page, y) => { |
| 197 | const result = await generateCheckboxComponents(page, y); |
| 198 | return { nextY: result }; |
| 199 | }, |
| 200 | }, |
| 201 | { |
| 202 | name: "ClipboardText", |
| 203 | execute: async (_page, y) => { |
| 204 | const result = await generateClipboardTextComponents(y); |
| 205 | return { nextY: result }; |
| 206 | }, |
| 207 | }, |
| 208 | { |
| 209 | name: "Code", |
| 210 | execute: async (page, y) => { |
| 211 | const result = await generateCodeComponents(page, y); |
| 212 | return { nextY: result }; |
| 213 | }, |
| 214 | }, |
| 215 | { |
| 216 | name: "CodeBlock", |
| 217 | execute: async (page, y) => { |
| 218 | const result = await generateCodeBlockComponents(page, y); |
| 219 | return { nextY: result }; |
| 220 | }, |
| 221 | }, |
| 222 | { |
| 223 | name: "Collapsible", |
| 224 | execute: async (_page, y) => { |
| 225 | const result = await generateCollapsibleComponents(y); |
| 226 | return { nextY: result }; |
| 227 | }, |
| 228 | }, |
| 229 | { |
| 230 | name: "Combobox", |
| 231 | execute: async (_page, y) => { |
| 232 | const result = await generateComboboxComponents(y); |
| 233 | return { nextY: result }; |
| 234 | }, |
| 235 | }, |
| 236 | { |
| 237 | name: "CommandPalette", |
| 238 | execute: async (page, y) => { |
| 239 | const result = await generateCommandPaletteComponents(page, y); |
| 240 | return { nextY: result }; |
| 241 | }, |
| 242 | }, |
| 243 | { |
| 244 | name: "DateRangePicker", |
| 245 | execute: async (page, y) => { |
| 246 | const result = await generateDateRangePickerComponents(page, y); |
| 247 | return { nextY: result }; |
| 248 | }, |
| 249 | }, |
| 250 | { |
| 251 | name: "Dialog", |
| 252 | execute: async (page, y) => { |
| 253 | const result = await generateDialogComponents(page, y); |
| 254 | return { nextY: result }; |
| 255 | }, |
| 256 | }, |
| 257 | { |
| 258 | name: "Dropdown", |
| 259 | execute: async (page, y) => { |
| 260 | const result = await generateDropdownComponents(page, y); |
| 261 | return { nextY: result }; |
| 262 | }, |
| 263 | }, |
| 264 | { |
| 265 | name: "Empty", |
| 266 | execute: async (_page, y) => { |
| 267 | const result = await generateEmptyComponents(y); |
| 268 | return { nextY: result }; |
| 269 | }, |
| 270 | }, |
| 271 | { |
| 272 | name: "Input", |
| 273 | execute: async (page, y) => { |
| 274 | const result = await generateInputComponents(page, y); |
| 275 | return { nextY: result }; |
| 276 | }, |
| 277 | }, |
| 278 | { |
| 279 | name: "InputArea", |
| 280 | execute: async (page, y) => { |
| 281 | const result = await generateInputAreaComponents(page, y); |
| 282 | return { nextY: result }; |
| 283 | }, |
| 284 | }, |
| 285 | { |
| 286 | name: "Label", |
| 287 | execute: async (page, y) => { |
| 288 | const result = await generateLabelComponents(page, y); |
| 289 | return { nextY: result }; |
| 290 | }, |
| 291 | }, |
| 292 | { |
| 293 | name: "LayerCard", |
| 294 | execute: async (page, y) => { |
| 295 | const result = await generateLayerCardComponents(page, y); |
| 296 | return { nextY: result }; |
| 297 | }, |
| 298 | }, |
| 299 | { |
| 300 | name: "Link", |
| 301 | execute: async (page, y) => { |
| 302 | const result = await generateLinkComponents(page, y); |
| 303 | return { nextY: result }; |
| 304 | }, |
| 305 | }, |
| 306 | { |
| 307 | name: "LinkButton", |
| 308 | execute: async (page, y) => { |
| 309 | const result = await generateLinkButtonComponents(page, y); |
| 310 | return { nextY: result }; |
| 311 | }, |
| 312 | }, |
| 313 | { |
| 314 | name: "Loader", |
| 315 | execute: async (page, y) => { |
| 316 | const result = await generateLoaderComponents(page, y); |
| 317 | return { nextY: result }; |
| 318 | }, |
| 319 | }, |
| 320 | { |
| 321 | name: "MenuBar", |
| 322 | execute: async (page, y) => { |
| 323 | const result = await generateMenuBarComponents(page, y); |
| 324 | return { nextY: result }; |
| 325 | }, |
| 326 | }, |
| 327 | { |
| 328 | name: "Meter", |
| 329 | execute: async (_page, y) => { |
| 330 | const result = await generateMeterComponents(y); |
| 331 | return { nextY: result }; |
| 332 | }, |
| 333 | }, |
| 334 | |
| 335 | { |
| 336 | name: "Pagination", |
| 337 | execute: async (_page, y) => { |
| 338 | const result = await generatePaginationComponents(y); |
| 339 | return { nextY: result }; |
| 340 | }, |
| 341 | }, |
| 342 | { |
| 343 | name: "Radio", |
| 344 | execute: async (page, y) => { |
| 345 | const result = await generateRadioComponents(page, y); |
| 346 | return { nextY: result }; |
| 347 | }, |
| 348 | }, |
| 349 | { |
| 350 | name: "Radio.Group", |
| 351 | execute: async (page, y) => { |
| 352 | const result = await generateRadioGroupComponents(page, y); |
| 353 | return { nextY: result }; |
| 354 | }, |
| 355 | }, |
| 356 | { |
| 357 | name: "RefreshButton", |
| 358 | execute: async (page, y) => { |
| 359 | const result = await generateRefreshButtonComponents(page, y); |
| 360 | return { nextY: result }; |
| 361 | }, |
| 362 | }, |
| 363 | { |
| 364 | name: "Select", |
| 365 | execute: async (page, y) => { |
| 366 | const result = await generateSelectComponents(page, y); |
| 367 | return { nextY: result }; |
| 368 | }, |
| 369 | }, |
| 370 | { |
| 371 | name: "SensitiveInput", |
| 372 | execute: async (page, y) => { |
| 373 | const result = await generateSensitiveInputComponents(page, y); |
| 374 | return { nextY: result }; |
| 375 | }, |
| 376 | }, |
| 377 | { |
| 378 | name: "Surface", |
| 379 | execute: async (page, y) => { |
| 380 | const result = await generateSurfaceComponents(page, y); |
| 381 | return { nextY: result }; |
| 382 | }, |
| 383 | }, |
| 384 | { |
| 385 | name: "Switch", |
| 386 | execute: async (page, y) => { |
| 387 | const result = await generateSwitchComponents(page, y); |
| 388 | return { nextY: result }; |
| 389 | }, |
| 390 | }, |
| 391 | { |
| 392 | name: "Switch.Group", |
| 393 | execute: async (page, y) => { |
| 394 | const result = await generateSwitchGroupComponents(page, y); |
| 395 | return { nextY: result }; |
| 396 | }, |
| 397 | }, |
| 398 | { |
| 399 | name: "Table", |
| 400 | execute: async (page, y) => { |
| 401 | const result = await generateTableComponents(page, y); |
| 402 | return { nextY: result }; |
| 403 | }, |
| 404 | }, |
| 405 | { |
| 406 | name: "Tabs", |
| 407 | execute: async (page, y) => { |
| 408 | const result = await generateTabsComponents(page, y); |
| 409 | return { nextY: result }; |
| 410 | }, |
| 411 | }, |
| 412 | { |
| 413 | name: "Text", |
| 414 | execute: async (page, y) => { |
| 415 | const result = await generateTextComponents(page, y); |
| 416 | return { nextY: result }; |
| 417 | }, |
| 418 | }, |
| 419 | { |
| 420 | name: "Toast", |
| 421 | execute: async (page, y) => { |
| 422 | const result = await generateToastComponents(page, y); |
| 423 | return { nextY: result }; |
| 424 | }, |
| 425 | }, |
| 426 | { |
| 427 | name: "Tooltip", |
| 428 | execute: async (page, y) => { |
| 429 | const result = await generateTooltipComponents(page, y); |
| 430 | return { nextY: result }; |
| 431 | }, |
| 432 | }, |
| 433 | ]; |
| 434 | |
| 435 | // Sort generators: priority items first, then alphabetically by name |
| 436 | const sortedGenerators = [...GENERATORS].sort((a, b) => { |
| 437 | // Priority items come first |
| 438 | if (a.priority && !b.priority) return -1; |
| 439 | if (!a.priority && b.priority) return 1; |
| 440 | // Then sort alphabetically |
| 441 | return a.name.localeCompare(b.name); |
| 442 | }); |
| 443 | |
| 444 | // Dynamically calculated total from generator array |
| 445 | const TOTAL_COMPONENTS = sortedGenerators.length; |
| 446 | |
| 447 | // Step 3: Execute all generators sequentially (sorted alphabetically, priority first) |
| 448 | for (let i = 0; i < sortedGenerators.length; i++) { |
| 449 | const generator = sortedGenerators[i]; |
| 450 | const componentIndex = i + 1; |
| 451 | |
| 452 | figma.notify( |
| 453 | `Generating ${generator.name} (${componentIndex}/${TOTAL_COMPONENTS})...`, |
| 454 | ); |
| 455 | |
| 456 | const result = await generator.execute(uiKitPage, nextY); |
| 457 | |
| 458 | // Update nextY if generator returns a new position |
| 459 | if (result && result.nextY !== undefined) { |
| 460 | nextY = result.nextY; |
| 461 | } |
| 462 | } |
| 463 | |
| 464 | figma.notify("โ
Generation complete!", { timeout: 3000 }); |
| 465 | figma.closePlugin( |
| 466 | `Generation complete - created ${TOTAL_COMPONENTS} component types with light + dark modes`, |
| 467 | ); |
| 468 | } catch (error) { |
| 469 | const message = error instanceof Error ? error.message : String(error); |
| 470 | logError("Generation error:", error); |
| 471 | |
| 472 | // Provide specific error guidance based on error type |
| 473 | let errorNotification = `Error: ${message}`; |
| 474 | |
| 475 | // Check for missing kumo-colors collection |
| 476 | if (message.includes("kumo-colors") || message.includes("collection")) { |
| 477 | errorNotification = |
| 478 | "Missing kumo-colors collection. Run token sync first: npx tsx sync-tokens-to-figma.ts"; |
| 479 | logError( |
| 480 | "Kumo semantic color tokens not found. Please sync tokens from CSS to Figma variables.", |
| 481 | ); |
| 482 | } |
| 483 | // Check for missing font |
| 484 | else if (message.includes("font") || message.includes("Inter")) { |
| 485 | errorNotification = |
| 486 | "Missing Inter font. Please install the Inter font family and restart Figma."; |
| 487 | logError( |
| 488 | "Inter font family not available. Install from https://rsms.me/inter/", |
| 489 | ); |
| 490 | } |
| 491 | // Generic mid-generation failure |
| 492 | else { |
| 493 | logError( |
| 494 | `Generation failed during component creation. Partial state may exist on Components page.`, |
| 495 | ); |
| 496 | } |
| 497 | |
| 498 | figma.notify(errorNotification, { error: true, timeout: 5000 }); |
| 499 | figma.closePlugin(); |
| 500 | } |
| 501 | } |
| 502 | |
| 503 | if (msg.type === "cancel") { |
| 504 | figma.closePlugin(); |
| 505 | } |
| 506 | }; |
| 507 | |