|
1 | 1 | import { getComponentName } from '../utils' |
| 2 | +import { toCamel } from '../utils/to-camel' |
2 | 3 | import { getProps } from './props' |
3 | 4 | import { getPositionProps } from './props/position' |
4 | 5 | import { getSelectorProps, sanitizePropertyName } from './props/selector' |
@@ -354,12 +355,13 @@ export class Codegen { |
354 | 355 | ) |
355 | 356 | } |
356 | 357 |
|
357 | | - // Handle native Figma SLOT nodes — render as {children} in the component. |
| 358 | + // Handle native Figma SLOT nodes — render as {slotName} in the component. |
358 | 359 | // SLOT is a newer Figma node type not yet in @figma/plugin-typings. |
| 360 | + // The slot name is sanitized here; addComponentTree renames single slots to 'children'. |
359 | 361 | if ((node.type as string) === 'SLOT') { |
360 | 362 | perfEnd('buildTree()', tBuild) |
361 | 363 | return { |
362 | | - component: 'children', |
| 364 | + component: toCamel(sanitizePropertyName(node.name)), |
363 | 365 | props: {}, |
364 | 366 | children: [], |
365 | 367 | nodeType: 'SLOT', |
@@ -409,15 +411,42 @@ export class Codegen { |
409 | 411 |
|
410 | 412 | // Check for native SLOT children and build their overridden content. |
411 | 413 | // SLOT children contain the content placed into the component's slot. |
412 | | - const slotChildren: NodeTree[] = [] |
| 414 | + // Group by slot name to distinguish single-slot (children) vs multi-slot (named props). |
| 415 | + const slotsByName = new Map<string, NodeTree[]>() |
413 | 416 | if ('children' in node) { |
414 | 417 | for (const child of node.children) { |
415 | 418 | if ((child.type as string) === 'SLOT' && 'children' in child) { |
| 419 | + const slotName = toCamel(sanitizePropertyName(child.name)) |
| 420 | + const content: NodeTree[] = [] |
416 | 421 | for (const slotContent of (child as SceneNode & ChildrenMixin) |
417 | 422 | .children) { |
418 | | - slotChildren.push(await this.buildTree(slotContent)) |
| 423 | + content.push(await this.buildTree(slotContent)) |
419 | 424 | } |
| 425 | + if (content.length > 0) { |
| 426 | + slotsByName.set(slotName, content) |
| 427 | + } |
| 428 | + } |
| 429 | + } |
| 430 | + } |
| 431 | + |
| 432 | + // Single SLOT → pass content as children (renders as <Comp>content</Comp>) |
| 433 | + // Multiple SLOTs → render each as a named JSX prop (renders as <Comp header={<X/>} content={<Y/>} />) |
| 434 | + let slotChildren: NodeTree[] = [] |
| 435 | + if (slotsByName.size === 1) { |
| 436 | + slotChildren = [...slotsByName.values()][0] |
| 437 | + } else if (slotsByName.size > 1) { |
| 438 | + for (const [slotName, content] of slotsByName) { |
| 439 | + let jsx: string |
| 440 | + if (content.length === 1) { |
| 441 | + jsx = Codegen.renderTree(content[0], 0) |
| 442 | + } else { |
| 443 | + const children = content.map((c) => Codegen.renderTree(c, 0)) |
| 444 | + const childrenStr = children |
| 445 | + .map((c) => paddingLeftMultiline(c, 1)) |
| 446 | + .join('\n') |
| 447 | + jsx = `<>\n${childrenStr}\n</>` |
420 | 448 | } |
| 449 | + variantProps[slotName] = { __jsxSlot: true, jsx } |
421 | 450 | } |
422 | 451 | } |
423 | 452 |
|
@@ -653,14 +682,28 @@ export class Codegen { |
653 | 682 | } |
654 | 683 | const variantComments = selectorProps?.variantComments || {} |
655 | 684 |
|
656 | | - // Detect native SLOT children — add children: React.ReactNode to variants |
657 | | - if ( |
658 | | - !variants.children && |
659 | | - childrenTrees.some( |
660 | | - (child) => child.nodeType === 'SLOT' && child.component === 'children', |
661 | | - ) |
662 | | - ) { |
663 | | - variants.children = 'React.ReactNode' |
| 685 | + // Detect native SLOT children — single slot becomes 'children', multiple keep names. |
| 686 | + // Exclude INSTANCE_SWAP-created slots (they're already handled by selectorProps.variants). |
| 687 | + const instanceSwapNames = new Set(instanceSwapSlots.values()) |
| 688 | + const nativeSlots = childrenTrees.filter( |
| 689 | + (child) => |
| 690 | + child.nodeType === 'SLOT' && |
| 691 | + child.isSlot && |
| 692 | + !instanceSwapNames.has(child.component), |
| 693 | + ) |
| 694 | + if (nativeSlots.length === 1) { |
| 695 | + // Single SLOT → rename to 'children' for idiomatic React |
| 696 | + nativeSlots[0].component = 'children' |
| 697 | + if (!variants.children) { |
| 698 | + variants.children = 'React.ReactNode' |
| 699 | + } |
| 700 | + } else if (nativeSlots.length > 1) { |
| 701 | + // Multiple SLOTs → keep sanitized names as individual React.ReactNode props |
| 702 | + for (const slot of nativeSlots) { |
| 703 | + if (!variants[slot.component]) { |
| 704 | + variants[slot.component] = 'React.ReactNode' |
| 705 | + } |
| 706 | + } |
664 | 707 | } |
665 | 708 |
|
666 | 709 | this.componentTrees.set(nodeId, { |
|
0 commit comments