@dannote/figma-use 0.6.2 → 0.7.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.
@@ -180461,11 +180461,13 @@ function encodeNodeChangeWithVariables(nodeChange) {
180461
180461
  }
180462
180462
  const baseBytes = compiledSchema.encodeNodeChange(cleanNodeChange);
180463
180463
  let hex4 = Buffer.from(baseBytes).toString("hex");
180464
- if (hasFillBinding && nodeChange.fillPaints?.[0]?.colorVariableBinding) {
180465
- hex4 = injectVariableBinding(hex4, "2601", nodeChange.fillPaints[0].colorVariableBinding);
180464
+ const fillBinding = nodeChange.fillPaints?.[0]?.colorVariableBinding;
180465
+ if (hasFillBinding && fillBinding) {
180466
+ hex4 = injectVariableBinding(hex4, "2601", fillBinding);
180466
180467
  }
180467
- if (hasStrokeBinding && nodeChange.strokePaints?.[0]?.colorVariableBinding) {
180468
- hex4 = injectVariableBinding(hex4, "2701", nodeChange.strokePaints[0].colorVariableBinding);
180468
+ const strokeBinding = nodeChange.strokePaints?.[0]?.colorVariableBinding;
180469
+ if (hasStrokeBinding && strokeBinding) {
180470
+ hex4 = injectVariableBinding(hex4, "2701", strokeBinding);
180469
180471
  }
180470
180472
  return new Uint8Array(hex4.match(/.{2}/g).map((b2) => parseInt(b2, 16)));
180471
180473
  }
@@ -209254,7 +209256,7 @@ async function executeCommand(command, args, timeoutMs) {
209254
209256
  return result;
209255
209257
  }
209256
209258
  var mcpSessions = new Map;
209257
- async function handleMcpRequest(req, sessionId) {
209259
+ async function handleMcpRequest(req, _sessionId) {
209258
209260
  const { id, method, params } = req;
209259
209261
  try {
209260
209262
  switch (method) {
@@ -209414,7 +209416,7 @@ new Elysia().ws("/plugin", {
209414
209416
  pluginConnected: sendToPlugin !== null,
209415
209417
  multiplayer: getConnectionStatus()
209416
209418
  })).post("/render", async ({ body }) => {
209417
- const { fileKey, nodeChanges, parentGUID } = body;
209419
+ const { fileKey, nodeChanges } = body;
209418
209420
  if (!fileKey) {
209419
209421
  return { error: "fileKey is required" };
209420
209422
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dannote/figma-use",
3
- "version": "0.6.2",
3
+ "version": "0.7.0",
4
4
  "description": "Control Figma from the command line. Full read/write access for AI agents.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -56,11 +56,18 @@
56
56
  "elysia": "^1.2.25"
57
57
  },
58
58
  "devDependencies": {
59
+ "@iconify/core": "^4.1.0",
60
+ "@iconify/types": "^2.0.0",
61
+ "@iconify/utils": "^3.1.0",
59
62
  "@types/bun": "^1.3.6",
63
+ "@types/pngjs": "^6.0.5",
64
+ "@types/ws": "^8.18.1",
60
65
  "esbuild": "^0.25.4",
61
66
  "kiwi-schema": "^0.5.0",
62
67
  "oxfmt": "^0.24.0",
63
68
  "oxlint": "^1.39.0",
69
+ "pixelmatch": "^7.1.0",
70
+ "pngjs": "^7.0.0",
64
71
  "react": "19",
65
72
  "typescript": "^5.8.3"
66
73
  }
@@ -44,7 +44,11 @@ interface ComponentSetDef<V extends VariantDef> {
44
44
  symbol: symbol
45
45
  }
46
46
 
47
- const componentSetRegistry = new Map<symbol, ComponentSetDef<VariantDef>>()
47
+ // Use global registry to avoid module duplication issues between bundled CLI and source imports
48
+ const REGISTRY_KEY = '__figma_use_component_set_registry__'
49
+ const componentSetRegistry: Map<symbol, ComponentSetDef<VariantDef>> =
50
+ (globalThis as Record<string, unknown>)[REGISTRY_KEY] as Map<symbol, ComponentSetDef<VariantDef>> ||
51
+ ((globalThis as Record<string, unknown>)[REGISTRY_KEY] = new Map<symbol, ComponentSetDef<VariantDef>>())
48
52
 
49
53
  export function resetComponentSetRegistry() {
50
54
  componentSetRegistry.clear()
@@ -115,8 +119,8 @@ export function generateVariantCombinations<V extends VariantDef>(
115
119
  return
116
120
  }
117
121
 
118
- const key = keys[index]
119
- for (const value of variants[key]) {
122
+ const key = keys[index]!
123
+ for (const value of variants[key]!) {
120
124
  combine(index + 1, { ...current, [key]: value })
121
125
  }
122
126
  }
@@ -70,53 +70,11 @@ export function defineComponent<P extends BaseProps = BaseProps>(
70
70
  return ComponentInstance
71
71
  }
72
72
 
73
- // Style types
74
- interface Style {
75
- width?: number | string
76
- height?: number | string
77
- x?: number
78
- y?: number
79
- flexDirection?: 'row' | 'column'
80
- justifyContent?: 'flex-start' | 'center' | 'flex-end' | 'space-between'
81
- alignItems?: 'flex-start' | 'center' | 'flex-end' | 'stretch'
82
- gap?: number
83
- padding?: number
84
- paddingTop?: number
85
- paddingRight?: number
86
- paddingBottom?: number
87
- paddingLeft?: number
88
- backgroundColor?: string
89
- opacity?: number
90
- borderRadius?: number
91
- borderTopLeftRadius?: number
92
- borderTopRightRadius?: number
93
- borderBottomLeftRadius?: number
94
- borderBottomRightRadius?: number
95
- borderWidth?: number
96
- borderColor?: string
97
- }
73
+ // Style types - use StyleProps from shorthands which includes both full and shorthand names
74
+ import type { StyleProps } from './shorthands.ts'
98
75
 
99
- interface TextStyle extends Style {
100
- fontSize?: number
101
- fontFamily?: string
102
- fontWeight?:
103
- | 'normal'
104
- | 'bold'
105
- | '100'
106
- | '200'
107
- | '300'
108
- | '400'
109
- | '500'
110
- | '600'
111
- | '700'
112
- | '800'
113
- | '900'
114
- fontStyle?: 'normal' | 'italic'
115
- color?: string
116
- textAlign?: 'left' | 'center' | 'right'
117
- lineHeight?: number
118
- letterSpacing?: number
119
- }
76
+ type Style = StyleProps
77
+ type TextStyle = StyleProps
120
78
 
121
79
  interface BaseProps {
122
80
  name?: string
@@ -140,6 +98,15 @@ interface InstanceProps extends BaseProps {
140
98
  componentId?: string
141
99
  }
142
100
 
101
+ interface IconProps extends BaseProps {
102
+ /** Icon name in format "prefix:name" (e.g., "mdi:home", "lucide:star") */
103
+ icon: string
104
+ /** Icon size in pixels (default: 24) */
105
+ size?: number
106
+ /** Icon color (hex) */
107
+ color?: string
108
+ }
109
+
143
110
  // Component factory - creates intrinsic element wrapper
144
111
  const c =
145
112
  <T extends BaseProps>(type: string): React.FC<T> =>
@@ -160,6 +127,7 @@ export const Instance = c<InstanceProps>('instance')
160
127
  export const Group = c<BaseProps>('group')
161
128
  export const Page = c<BaseProps>('page')
162
129
  export const View = Frame
130
+ export const Icon = c<IconProps>('icon')
163
131
 
164
132
  // All component names for JSX transform
165
133
  export const INTRINSIC_ELEMENTS = [
@@ -175,5 +143,6 @@ export const INTRINSIC_ELEMENTS = [
175
143
  'Instance',
176
144
  'Group',
177
145
  'Page',
178
- 'View'
146
+ 'View',
147
+ 'Icon'
179
148
  ] as const
@@ -0,0 +1,163 @@
1
+ import { loadIcon } from '@iconify/core/lib/api/icons'
2
+ import { setAPIModule } from '@iconify/core/lib/api/modules'
3
+ import { fetchAPIModule } from '@iconify/core/lib/api/modules/fetch'
4
+ import { iconToSVG } from '@iconify/utils'
5
+ import type { IconifyIcon } from '@iconify/types'
6
+ import type { ReactElement, ReactNode } from 'react'
7
+
8
+ // Initialize API module
9
+ setAPIModule('', fetchAPIModule)
10
+
11
+ export interface IconData {
12
+ svg: string
13
+ width: number
14
+ height: number
15
+ body: string
16
+ viewBox: { left: number; top: number; width: number; height: number }
17
+ }
18
+
19
+ const iconCache = new Map<string, IconData>()
20
+
21
+ // Raw icon data cache (before size transformation)
22
+ const rawIconCache = new Map<string, IconifyIcon>()
23
+
24
+ /**
25
+ * Load raw icon data (without size transformation)
26
+ */
27
+ async function loadRawIcon(name: string): Promise<IconifyIcon | null> {
28
+ if (rawIconCache.has(name)) {
29
+ return rawIconCache.get(name)!
30
+ }
31
+
32
+ const icon = await loadIcon(name)
33
+ if (!icon) {
34
+ return null
35
+ }
36
+
37
+ rawIconCache.set(name, icon)
38
+ return icon
39
+ }
40
+
41
+ /**
42
+ * Load icon from Iconify and return SVG string
43
+ * @param name Icon name in format "prefix:name" (e.g., "mdi:home", "lucide:star")
44
+ * @param size Optional size (default: 24)
45
+ */
46
+ export async function loadIconSvg(name: string, size: number = 24): Promise<IconData | null> {
47
+ const cacheKey = `${name}@${size}`
48
+
49
+ if (iconCache.has(cacheKey)) {
50
+ return iconCache.get(cacheKey)!
51
+ }
52
+
53
+ const icon = await loadRawIcon(name)
54
+ if (!icon) {
55
+ return null
56
+ }
57
+
58
+ const result = iconToSVG(icon, { height: size, width: size })
59
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" ${Object.entries(result.attributes).map(([k, v]) => `${k}="${v}"`).join(' ')}>${result.body}</svg>`
60
+
61
+ const data: IconData = {
62
+ svg,
63
+ width: size,
64
+ height: size,
65
+ body: result.body,
66
+ viewBox: {
67
+ left: result.viewBox[0],
68
+ top: result.viewBox[1],
69
+ width: result.viewBox[2],
70
+ height: result.viewBox[3]
71
+ }
72
+ }
73
+
74
+ iconCache.set(cacheKey, data)
75
+ return data
76
+ }
77
+
78
+ /**
79
+ * Get cached icon data (synchronous, for use in React components)
80
+ * Returns null if icon not preloaded
81
+ */
82
+ export function getIconData(name: string, size: number = 24): IconData | null {
83
+ return iconCache.get(`${name}@${size}`) || null
84
+ }
85
+
86
+ /**
87
+ * Preload icons for use in JSX render
88
+ * Call before rendering to ensure icons are available synchronously
89
+ */
90
+ export async function preloadIcons(icons: Array<{ name: string; size?: number }>): Promise<void> {
91
+ await Promise.all(icons.map(({ name, size }) => loadIconSvg(name, size || 24)))
92
+ }
93
+
94
+ /**
95
+ * Get list of popular icon sets
96
+ */
97
+ export const iconSets = {
98
+ mdi: 'Material Design Icons',
99
+ lucide: 'Lucide',
100
+ heroicons: 'Heroicons',
101
+ 'heroicons-outline': 'Heroicons Outline',
102
+ 'heroicons-solid': 'Heroicons Solid',
103
+ tabler: 'Tabler Icons',
104
+ 'fa-solid': 'Font Awesome Solid',
105
+ 'fa-regular': 'Font Awesome Regular',
106
+ 'fa-brands': 'Font Awesome Brands',
107
+ ri: 'Remix Icon',
108
+ ph: 'Phosphor',
109
+ 'ph-bold': 'Phosphor Bold',
110
+ 'ph-fill': 'Phosphor Fill',
111
+ carbon: 'Carbon',
112
+ fluent: 'Fluent UI',
113
+ ion: 'Ionicons',
114
+ bi: 'Bootstrap Icons'
115
+ }
116
+
117
+ /**
118
+ * Recursively collect Icon primitives from React element tree
119
+ */
120
+ export function collectIcons(element: ReactElement): Array<{ name: string; size?: number }> {
121
+ const icons: Array<{ name: string; size?: number }> = []
122
+
123
+ function traverse(node: ReactNode): void {
124
+ if (!node || typeof node !== 'object') return
125
+
126
+ if (Array.isArray(node)) {
127
+ node.forEach(traverse)
128
+ return
129
+ }
130
+
131
+ const el = node as ReactElement
132
+ if (!el.type) return
133
+
134
+ if (el.type === 'icon') {
135
+ const props = el.props as { icon?: string; size?: number }
136
+ if (props.icon) {
137
+ icons.push({ name: props.icon, size: props.size })
138
+ }
139
+ }
140
+
141
+ if (typeof el.type === 'function') {
142
+ try {
143
+ const Component = el.type as (props: Record<string, unknown>) => ReactNode
144
+ const rendered = Component(el.props as Record<string, unknown>)
145
+ if (rendered) traverse(rendered)
146
+ } catch {
147
+ // Ignore render errors during collection
148
+ }
149
+ }
150
+
151
+ const props = el.props as { children?: ReactNode }
152
+ if (props.children) {
153
+ if (Array.isArray(props.children)) {
154
+ props.children.forEach(traverse)
155
+ } else {
156
+ traverse(props.children)
157
+ }
158
+ }
159
+ }
160
+
161
+ traverse(element)
162
+ return icons
163
+ }
@@ -7,9 +7,12 @@ export {
7
7
  resetRenderedComponents,
8
8
  getPendingComponentSetInstances,
9
9
  clearPendingComponentSetInstances,
10
+ getPendingIcons,
11
+ clearPendingIcons,
10
12
  type RenderOptions,
11
13
  type RenderResult,
12
- type PendingComponentSetInstance
14
+ type PendingComponentSetInstance,
15
+ type PendingIcon
13
16
  } from './reconciler.ts'
14
17
  export {
15
18
  Frame,
@@ -25,6 +28,7 @@ export {
25
28
  Group,
26
29
  Page,
27
30
  View,
31
+ Icon,
28
32
  INTRINSIC_ELEMENTS,
29
33
  // Variable bindings (StyleX-inspired)
30
34
  defineVars,
@@ -39,6 +43,8 @@ export {
39
43
  getComponentRegistry
40
44
  } from './components.tsx'
41
45
 
46
+ export { preloadIcons, loadIconSvg, getIconData, collectIcons } from './icon.ts'
47
+
42
48
  export {
43
49
  // ComponentSet (variants)
44
50
  defineComponentSet,
@@ -34,14 +34,16 @@ import Reconciler from 'react-reconciler'
34
34
  import { consola } from 'consola'
35
35
  import type { NodeChange, Paint } from '../multiplayer/codec.ts'
36
36
  import { parseColor } from '../color.ts'
37
- import { isVariable, resolveVariable, type FigmaVariable } from './vars.ts'
37
+ import { isVariable, resolveVariable } from './vars.ts'
38
38
  import { getComponentRegistry } from './components.tsx'
39
+ import { normalizeStyle, type StyleProps } from './shorthands.ts'
39
40
  import {
40
41
  getComponentSetRegistry,
41
42
  generateVariantCombinations,
42
43
  buildVariantName,
43
44
  buildStateGroupPropertyValueOrders
44
45
  } from './component-set.tsx'
46
+ import { getIconData } from './icon.ts'
45
47
 
46
48
  // Track rendered components: symbol -> GUID
47
49
  const renderedComponents = new Map<symbol, { sessionID: number; localID: number }>()
@@ -73,6 +75,25 @@ export function clearPendingComponentSetInstances() {
73
75
  pendingComponentSetInstances.length = 0
74
76
  }
75
77
 
78
+ // Pending Icon imports to create via Plugin API
79
+ export interface PendingIcon {
80
+ svg: string
81
+ parentGUID: { sessionID: number; localID: number }
82
+ childIndex: number
83
+ x: number
84
+ y: number
85
+ name: string
86
+ }
87
+ const pendingIcons: PendingIcon[] = []
88
+
89
+ export function getPendingIcons(): PendingIcon[] {
90
+ return [...pendingIcons]
91
+ }
92
+
93
+ export function clearPendingIcons() {
94
+ pendingIcons.length = 0
95
+ }
96
+
76
97
  export interface RenderOptions {
77
98
  sessionID: number
78
99
  parentGUID: { sessionID: number; localID: number }
@@ -107,7 +128,7 @@ function styleToNodeChange(
107
128
  position: string,
108
129
  textContent?: string
109
130
  ): NodeChange {
110
- const style = (props.style || {}) as Record<string, unknown>
131
+ const style = normalizeStyle((props.style || {}) as StyleProps)
111
132
  const name = (props.name as string) || type
112
133
 
113
134
  const nodeChange: NodeChange = {
@@ -119,6 +140,11 @@ function styleToNodeChange(
119
140
  visible: true,
120
141
  opacity: typeof style.opacity === 'number' ? style.opacity : 1
121
142
  }
143
+
144
+ // Disable clipsContent for FRAME (Figma default is true which hides overflowing content)
145
+ if (mapType(type) === 'FRAME') {
146
+ nodeChange.clipsContent = false
147
+ }
122
148
 
123
149
  // Size
124
150
  const width = style.width ?? props.width
@@ -254,10 +280,8 @@ function styleToNodeChange(
254
280
  const pb = style.paddingBottom ?? style.padding
255
281
  const pl = style.paddingLeft ?? style.padding
256
282
 
257
- if (pt !== undefined)
258
- (nodeChange as unknown as Record<string, unknown>).stackVerticalPadding = Number(pt)
259
- if (pl !== undefined)
260
- (nodeChange as unknown as Record<string, unknown>).stackHorizontalPadding = Number(pl)
283
+ if (pt !== undefined) nodeChange.stackVerticalPadding = Number(pt)
284
+ if (pl !== undefined) nodeChange.stackHorizontalPadding = Number(pl)
261
285
  if (pr !== undefined) nodeChange.stackPaddingRight = Number(pr)
262
286
  if (pb !== undefined) nodeChange.stackPaddingBottom = Number(pb)
263
287
 
@@ -298,27 +322,26 @@ function styleToNodeChange(
298
322
  // Text-specific
299
323
  if (type.toLowerCase() === 'text' && textContent) {
300
324
  // Text content via textData.characters
301
- const nc = nodeChange as unknown as Record<string, unknown>
302
- nc.textData = { characters: textContent }
303
- nc.textAutoResize = 'WIDTH_AND_HEIGHT'
304
- nc.textAlignVertical = 'TOP' // Required for text height calculation
325
+ nodeChange.textData = { characters: textContent }
326
+ nodeChange.textAutoResize = 'WIDTH_AND_HEIGHT'
327
+ nodeChange.textAlignVertical = 'TOP' // Required for text height calculation
305
328
 
306
- if (style.fontSize) nc.fontSize = Number(style.fontSize)
329
+ if (style.fontSize) nodeChange.fontSize = Number(style.fontSize)
307
330
  // CRITICAL: lineHeight MUST be { value: 100, units: 'PERCENT' } for text to have height
308
331
  // Without this, TEXT nodes render with height=0 and are invisible
309
332
  // Discovered via sniffing Figma's own text creation - see scripts/sniff-text.ts
310
- nc.lineHeight = { value: 100, units: 'PERCENT' }
333
+ nodeChange.lineHeight = { value: 100, units: 'PERCENT' }
311
334
  // fontName is ALWAYS required for TEXT nodes, even without explicit fontFamily
312
335
  const family = (style.fontFamily as string) || 'Inter'
313
336
  const fontStyle = mapFontWeight(style.fontWeight as string)
314
- nc.fontName = {
337
+ nodeChange.fontName = {
315
338
  family,
316
339
  style: fontStyle,
317
340
  postscript: `${family}-${fontStyle}`.replace(/\s+/g, '')
318
341
  }
319
342
  if (style.textAlign) {
320
343
  const map: Record<string, string> = { left: 'LEFT', center: 'CENTER', right: 'RIGHT' }
321
- nc.textAlignHorizontal = map[style.textAlign as string] || 'LEFT'
344
+ nodeChange.textAlignHorizontal = map[style.textAlign as string] || 'LEFT'
322
345
  }
323
346
  if (style.color) {
324
347
  const textColor = style.color
@@ -354,11 +377,10 @@ function styleToNodeChange(
354
377
  if (type.toLowerCase() === 'instance' && props.componentId) {
355
378
  const match = String(props.componentId).match(/(\d+):(\d+)/)
356
379
  if (match) {
357
- const nc = nodeChange as unknown as Record<string, unknown>
358
- nc.symbolData = {
380
+ nodeChange.symbolData = {
359
381
  symbolID: {
360
- sessionID: parseInt(match[1], 10),
361
- localID: parseInt(match[2], 10)
382
+ sessionID: parseInt(match[1]!, 10),
383
+ localID: parseInt(match[2]!, 10)
362
384
  }
363
385
  }
364
386
  }
@@ -408,9 +430,47 @@ function collectNodeChanges(
408
430
  sessionID: number,
409
431
  parentGUID: { sessionID: number; localID: number },
410
432
  position: string,
433
+ childIndex: number,
411
434
  result: NodeChange[],
412
435
  container: Container
413
436
  ): void {
437
+ // Handle Icon primitive
438
+ if (instance.type === 'icon') {
439
+ const props = instance.props as {
440
+ icon: string
441
+ size?: number
442
+ color?: string
443
+ name?: string
444
+ style?: Record<string, unknown>
445
+ }
446
+ const { icon: iconName, size = 24, color, name: nodeName, style = {} } = props
447
+
448
+ const iconData = getIconData(iconName, size)
449
+ if (!iconData) {
450
+ consola.error(`Icon "${iconName}" not found. Did you call preloadIcons()?`)
451
+ return
452
+ }
453
+
454
+ // Replace currentColor with specified color
455
+ let svg = iconData.svg
456
+ if (color) {
457
+ svg = svg.replace(/currentColor/g, color)
458
+ } else {
459
+ svg = svg.replace(/currentColor/g, '#000000')
460
+ }
461
+
462
+ // Add to pending icons for Plugin API import
463
+ pendingIcons.push({
464
+ svg,
465
+ parentGUID,
466
+ childIndex,
467
+ x: (style.x as number) || 0,
468
+ y: (style.y as number) || 0,
469
+ name: nodeName || iconName.replace(':', '/')
470
+ })
471
+ return
472
+ }
473
+
414
474
  // Handle defineComponent instances
415
475
  if (instance.type === '__component_instance__') {
416
476
  const sym = instance.props.__componentSymbol as symbol
@@ -443,8 +503,8 @@ function collectNodeChanges(
443
503
  container.localIDCounter = componentResult.nextLocalID
444
504
 
445
505
  // Change first node to be SYMBOL type and add to results
446
- if (componentResult.nodeChanges.length > 0) {
447
- const rootChange = componentResult.nodeChanges[0]
506
+ const rootChange = componentResult.nodeChanges[0]
507
+ if (rootChange) {
448
508
  const originalRootGUID = { ...rootChange.guid }
449
509
 
450
510
  // Replace root node's guid with componentGUID
@@ -457,6 +517,7 @@ function collectNodeChanges(
457
517
  for (let i = 1; i < componentResult.nodeChanges.length; i++) {
458
518
  const child = componentResult.nodeChanges[i]
459
519
  if (
520
+ child &&
460
521
  child.parentIndex?.guid.localID === originalRootGUID.localID &&
461
522
  child.parentIndex?.guid.sessionID === originalRootGUID.sessionID
462
523
  ) {
@@ -493,8 +554,7 @@ function collectNodeChanges(
493
554
  }
494
555
 
495
556
  // Link to component
496
- const nc = instanceChange as unknown as Record<string, unknown>
497
- nc.symbolData = { symbolID: componentGUID }
557
+ instanceChange.symbolData = { symbolID: componentGUID }
498
558
 
499
559
  result.push(instanceChange)
500
560
  }
@@ -538,13 +598,12 @@ function collectNodeChanges(
538
598
  opacity: 1,
539
599
  size: { x: 1, y: 1 } // Will be auto-sized
540
600
  }
541
- const setNc = setChange as unknown as Record<string, unknown>
542
- setNc.isStateGroup = true
543
- setNc.stateGroupPropertyValueOrders = buildStateGroupPropertyValueOrders(variants)
544
- setNc.stackMode = 'HORIZONTAL'
545
- setNc.stackSpacing = 20
546
- setNc.stackPrimarySizing = 'RESIZE_TO_FIT'
547
- setNc.stackCounterSizing = 'RESIZE_TO_FIT'
601
+ setChange.isStateGroup = true
602
+ setChange.stateGroupPropertyValueOrders = buildStateGroupPropertyValueOrders(variants)
603
+ setChange.stackMode = 'HORIZONTAL'
604
+ setChange.stackSpacing = 20
605
+ setChange.stackPrimarySizing = 'RESIZE_TO_FIT'
606
+ setChange.stackCounterSizing = 'RESIZE_TO_FIT'
548
607
 
549
608
  result.push(setChange)
550
609
 
@@ -564,8 +623,8 @@ function collectNodeChanges(
564
623
  })
565
624
  container.localIDCounter = variantResult.nextLocalID
566
625
 
567
- if (variantResult.nodeChanges.length > 0) {
568
- const rootChange = variantResult.nodeChanges[0]
626
+ const rootChange = variantResult.nodeChanges[0]
627
+ if (rootChange) {
569
628
  const originalRootGUID = { ...rootChange.guid }
570
629
 
571
630
  rootChange.guid = variantGUID
@@ -580,6 +639,7 @@ function collectNodeChanges(
580
639
  for (let j = 1; j < variantResult.nodeChanges.length; j++) {
581
640
  const child = variantResult.nodeChanges[j]
582
641
  if (
642
+ child &&
583
643
  child.parentIndex?.guid.localID === originalRootGUID.localID &&
584
644
  child.parentIndex?.guid.sessionID === originalRootGUID.sessionID
585
645
  ) {
@@ -641,7 +701,7 @@ function collectNodeChanges(
641
701
  const thisGUID = { sessionID, localID: instance.localID }
642
702
  instance.children.forEach((child, i) => {
643
703
  const childPosition = String.fromCharCode(33 + (i % 90))
644
- collectNodeChanges(child, sessionID, thisGUID, childPosition, result, container)
704
+ collectNodeChanges(child, sessionID, thisGUID, childPosition, i, result, container)
645
705
  })
646
706
  }
647
707
 
@@ -845,6 +905,7 @@ export function renderToNodeChanges(
845
905
  options.sessionID,
846
906
  options.parentGUID,
847
907
  position,
908
+ i,
848
909
  nodeChanges,
849
910
  container
850
911
  )
@@ -860,8 +921,9 @@ export function renderToNodeChanges(
860
921
  function getDefaultVariants(variants: Record<string, readonly string[]>): Record<string, string> {
861
922
  const defaults: Record<string, string> = {}
862
923
  for (const [key, values] of Object.entries(variants)) {
863
- if (values.length > 0) {
864
- defaults[key] = values[0]
924
+ const firstValue = values[0]
925
+ if (firstValue !== undefined) {
926
+ defaults[key] = firstValue
865
927
  }
866
928
  }
867
929
  return defaults