cloudflare/kumo

Public

mirrored fromhttps://github.com/cloudflare/kumoAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
5260f1a5703bb69e6c7f7cf0ce8033a561cac8b5

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

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
10import { generateBadgeComponents } from "./generators/badge";
11import { generateBannerComponents } from "./generators/banner";
12import { generateBreadcrumbsComponents } from "./generators/breadcrumbs";
13import { generateButtonComponents } from "./generators/button";
14import { generateCheckboxComponents } from "./generators/checkbox";
15import { generateClipboardTextComponents } from "./generators/clipboard-text";
16import { generateCodeComponents } from "./generators/code";
17import { generateCodeBlockComponents } from "./generators/code-block";
18import { generateCollapsibleComponents } from "./generators/collapsible";
19import { generateCommandPaletteComponents } from "./generators/command-palette";
20import { generateComboboxComponents } from "./generators/combobox";
21import { generateDateRangePickerComponents } from "./generators/date-range-picker";
22import { generateDialogComponents } from "./generators/dialog";
23import { generateDropdownComponents } from "./generators/dropdown";
24import { generateEmptyComponents } from "./generators/empty";
25import { generateInputComponents } from "./generators/input";
26import { generateInputAreaComponents } from "./generators/input-area";
27import { generateLayerCardComponents } from "./generators/layer-card";
28import { generateLabelComponents } from "./generators/label";
29import { generateLinkComponents } from "./generators/link";
30import { generateLoaderComponents } from "./generators/loader";
31import { generateLinkButtonComponents } from "./generators/link-button";
32import { generateMenuBarComponents } from "./generators/menubar";
33import { generateMeterComponents } from "./generators/meter";
34
35import { generatePaginationComponents } from "./generators/pagination";
36import {
37 generateRadioComponents,
38 generateRadioGroupComponents,
39} from "./generators/radio";
40import { generateRefreshButtonComponents } from "./generators/refresh-button";
41import { generateSelectComponents } from "./generators/select";
42import { generateSensitiveInputComponents } from "./generators/sensitive-input";
43import { generateSurfaceComponents } from "./generators/surface";
44import {
45 generateSwitchComponents,
46 generateSwitchGroupComponents,
47} from "./generators/switch";
48import { generateTableComponents } from "./generators/table";
49import { generateTabsComponents } from "./generators/tabs";
50import { generateTextComponents } from "./generators/text";
51import { generateToastComponents } from "./generators/toast";
52import { generateTooltipComponents } from "./generators/tooltip";
53import {
54 initializeColorVariables,
55 clearColorVariables,
56} from "./generators/shared";
57import { logInfo, logError } from "./logger";
58
59figma.showUI(__html__, { width: 320, height: 220 });
60
61/**
62 * Page name for the UI kit (icons + components on same page)
63 */
64const 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 */
74function 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 */
127const START_Y = 100;
128
129/**
130 * Component generator configuration
131 * Each entry defines a component generator with its display name and execution function
132 */
133type GeneratorConfig = {
134 name: string;
135 execute: (
136 page: PageNode,
137 currentY: number,
138 ) => Promise<{ nextY: number } | void>;
139};
140
141figma.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