@seed-design/figma 1.1.13 → 1.1.14

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.
Files changed (43) hide show
  1. package/lib/codegen/index.cjs +636 -114
  2. package/lib/codegen/index.d.ts +136 -96
  3. package/lib/codegen/index.d.ts.map +1 -1
  4. package/lib/codegen/index.js +636 -114
  5. package/lib/codegen/targets/react/index.cjs +682 -134
  6. package/lib/codegen/targets/react/index.d.ts +31 -11
  7. package/lib/codegen/targets/react/index.d.ts.map +1 -1
  8. package/lib/codegen/targets/react/index.js +682 -135
  9. package/lib/index.cjs +1254 -433
  10. package/lib/index.d.ts +46 -10
  11. package/lib/index.d.ts.map +1 -1
  12. package/lib/index.js +1254 -433
  13. package/package.json +1 -1
  14. package/src/codegen/component-properties.ts +5 -5
  15. package/src/codegen/core/value-resolver.ts +49 -12
  16. package/src/codegen/targets/figma/frame.ts +1 -0
  17. package/src/codegen/targets/figma/pipeline.ts +5 -0
  18. package/src/codegen/targets/figma/props.ts +30 -1
  19. package/src/codegen/targets/figma/shape.ts +1 -0
  20. package/src/codegen/targets/figma/value-resolver.ts +20 -0
  21. package/src/codegen/targets/react/component/handlers/menu-sheet.ts +1 -1
  22. package/src/codegen/targets/react/component/handlers/page-banner.ts +2 -2
  23. package/src/codegen/targets/react/component/handlers/{radio-mark.ts → radiomark.ts} +4 -4
  24. package/src/codegen/targets/react/component/handlers/result-section.ts +1 -1
  25. package/src/codegen/targets/react/component/handlers/{switch-mark.ts → switchmark.ts} +4 -4
  26. package/src/codegen/targets/react/component/index.ts +4 -4
  27. package/src/codegen/targets/react/frame.ts +16 -2
  28. package/src/codegen/targets/react/pipeline.ts +6 -1
  29. package/src/codegen/targets/react/props.ts +26 -0
  30. package/src/codegen/targets/react/shape.ts +5 -1
  31. package/src/codegen/targets/react/value-resolver.ts +26 -0
  32. package/src/entities/data/__generated__/component-sets/index.d.ts +84 -89
  33. package/src/entities/data/__generated__/component-sets/index.mjs +84 -89
  34. package/src/entities/data/__generated__/components/index.d.ts +2 -2
  35. package/src/entities/data/__generated__/components/index.mjs +2 -2
  36. package/src/entities/data/__generated__/icons/index.mjs +14 -0
  37. package/src/entities/data/__generated__/styles/index.mjs +190 -1
  38. package/src/entities/data/__generated__/variable-collections/index.mjs +11 -1
  39. package/src/entities/data/__generated__/variables/index.mjs +280 -0
  40. package/src/normalizer/from-plugin.ts +427 -258
  41. package/src/normalizer/from-rest.ts +428 -58
  42. package/src/normalizer/types.ts +63 -10
  43. package/src/utils/figma-node.ts +15 -10
@@ -1,4 +1,9 @@
1
- import type * as FigmaRestSpec from "@figma/rest-api-spec";
1
+ /**
2
+ * from-plugin is guaranteed to be run in the Figma Plugin environment
3
+ * so we can use the Plugin API types directly (figma.getNodeByIdAsync, node.getMainComponentAsync etc)
4
+ * however it could be better to make users can DI later
5
+ */
6
+
2
7
  import type {
3
8
  NormalizedSceneNode,
4
9
  NormalizedFrameNode,
@@ -8,129 +13,303 @@ import type {
8
13
  NormalizedInstanceNode,
9
14
  NormalizedVectorNode,
10
15
  NormalizedBooleanOperationNode,
16
+ NormalizedHasEffectsTrait,
17
+ NormalizedShadow,
18
+ NormalizedDefaultShapeTrait,
19
+ NormalizedHasFramePropertiesTrait,
20
+ NormalizedCornerTrait,
21
+ NormalizedIsLayerTrait,
22
+ NormalizedPaint,
23
+ NormalizedTextSegment,
11
24
  } from "./types";
12
25
  import { convertTransformToGradientHandles } from "@/utils/figma-gradient";
13
26
 
14
- export function createPluginNormalizer() {
27
+ export function createPluginNormalizer(): (node: SceneNode) => Promise<NormalizedSceneNode> {
15
28
  async function normalizeNodes(nodes: readonly SceneNode[]): Promise<NormalizedSceneNode[]> {
16
29
  return Promise.all(nodes.filter((node) => node.visible).map(normalizeNode));
17
30
  }
18
31
 
19
32
  async function normalizeNode(node: SceneNode): Promise<NormalizedSceneNode> {
20
- if (node.type === "FRAME") {
21
- return normalizeFrameNode(node);
22
- }
23
- if (node.type === "GROUP") {
24
- return normalizeGroupNode(node);
25
- }
26
- if (node.type === "RECTANGLE") {
27
- return normalizeRectangleNode(node);
28
- }
29
- if (node.type === "VECTOR") {
30
- return normalizeVectorNode(node);
31
- }
32
- if (node.type === "BOOLEAN_OPERATION") {
33
- return normalizeBooleanOperationNode(node);
34
- }
35
- if (node.type === "TEXT") {
36
- return normalizeTextNode(node);
33
+ switch (node.type) {
34
+ case "FRAME":
35
+ return normalizeFrameNode(node);
36
+ case "RECTANGLE":
37
+ return normalizeRectangleNode(node);
38
+ case "TEXT":
39
+ return normalizeTextNode(node);
40
+ case "COMPONENT":
41
+ return normalizeComponentNode(node);
42
+ case "INSTANCE":
43
+ return normalizeInstanceNode(node);
44
+ case "VECTOR":
45
+ return normalizeVectorNode(node);
46
+ case "BOOLEAN_OPERATION":
47
+ return normalizeBooleanOperationNode(node);
48
+ case "GROUP":
49
+ return normalizeGroupNodeAsFrameNode(node);
50
+ default:
51
+ return {
52
+ type: "UNHANDLED",
53
+ id: node.id,
54
+ original: node,
55
+ };
37
56
  }
38
- if (node.type === "COMPONENT") {
39
- return normalizeComponentNode(node);
57
+ }
58
+
59
+ /**
60
+ * Pick specific fields from boundVariables
61
+ */
62
+ function normalizeBoundVariables({
63
+ boundVariables,
64
+ }: Pick<FrameNode, "boundVariables">): NormalizedIsLayerTrait["boundVariables"] {
65
+ if (!boundVariables) return undefined;
66
+
67
+ return {
68
+ fills: boundVariables.fills,
69
+ strokes: boundVariables.strokes,
70
+ itemSpacing: boundVariables.itemSpacing,
71
+ counterAxisSpacing: boundVariables.counterAxisSpacing,
72
+ topLeftRadius: boundVariables.topLeftRadius,
73
+ topRightRadius: boundVariables.topRightRadius,
74
+ bottomLeftRadius: boundVariables.bottomLeftRadius,
75
+ bottomRightRadius: boundVariables.bottomRightRadius,
76
+ paddingTop: boundVariables.paddingTop,
77
+ paddingRight: boundVariables.paddingRight,
78
+ paddingBottom: boundVariables.paddingBottom,
79
+ paddingLeft: boundVariables.paddingLeft,
80
+ minWidth: boundVariables.minWidth,
81
+ maxWidth: boundVariables.maxWidth,
82
+ minHeight: boundVariables.minHeight,
83
+ maxHeight: boundVariables.maxHeight,
84
+ fontSize: boundVariables.fontSize,
85
+ fontWeight: boundVariables.fontWeight,
86
+ lineHeight: boundVariables.lineHeight,
87
+ size: {
88
+ x: boundVariables.width,
89
+ y: boundVariables.height,
90
+ },
91
+ };
92
+ }
93
+
94
+ function normalizeSolidPaint(paint: SolidPaint): NormalizedPaint {
95
+ return {
96
+ type: paint.type,
97
+ color: {
98
+ r: paint.color.r,
99
+ g: paint.color.g,
100
+ b: paint.color.b,
101
+ a: paint.opacity ?? 1,
102
+ },
103
+ visible: paint.visible,
104
+ blendMode: paint.blendMode ?? "NORMAL",
105
+ opacity: paint.opacity,
106
+ boundVariables: paint.boundVariables,
107
+ };
108
+ }
109
+
110
+ function normalizePaint(paint: Paint): NormalizedPaint {
111
+ switch (paint.type) {
112
+ case "SOLID":
113
+ return normalizeSolidPaint(paint);
114
+ case "IMAGE":
115
+ return {
116
+ type: "IMAGE",
117
+ scaleMode: paint.scaleMode === "CROP" ? "STRETCH" : paint.scaleMode,
118
+ imageTransform: paint.imageTransform,
119
+ scalingFactor: paint.scalingFactor,
120
+ filters: paint.filters,
121
+ rotation: paint.rotation,
122
+ imageRef: paint.imageHash ?? "",
123
+ blendMode: paint.blendMode ?? "NORMAL",
124
+ visible: paint.visible,
125
+ opacity: paint.opacity,
126
+ };
127
+ case "GRADIENT_LINEAR":
128
+ case "GRADIENT_RADIAL":
129
+ case "GRADIENT_ANGULAR":
130
+ case "GRADIENT_DIAMOND":
131
+ return {
132
+ type: paint.type,
133
+ gradientStops: [...paint.gradientStops],
134
+ visible: paint.visible,
135
+ opacity: paint.opacity,
136
+ blendMode: paint.blendMode ?? "NORMAL",
137
+ gradientHandlePositions: convertTransformToGradientHandles(paint.gradientTransform),
138
+ };
139
+ default:
140
+ throw new Error(`Unimplemented paint type: ${paint.type}`);
40
141
  }
41
- if (node.type === "INSTANCE") {
42
- return normalizeInstanceNode(node);
142
+ }
143
+
144
+ function normalizePaints(fills: readonly Paint[] | PluginAPI["mixed"]): NormalizedPaint[] {
145
+ if (fills === figma.mixed) {
146
+ console.warn("Mixed fills are not supported");
147
+
148
+ return [];
43
149
  }
44
150
 
151
+ return fills.map(normalizePaint);
152
+ }
153
+
154
+ function normalizeRadiusProps(
155
+ node: Pick<
156
+ RectangleNode,
157
+ "cornerRadius" | "topLeftRadius" | "topRightRadius" | "bottomRightRadius" | "bottomLeftRadius"
158
+ >,
159
+ ): NormalizedCornerTrait {
45
160
  return {
46
- type: "UNHANDLED",
47
- id: node.id,
48
- original: node,
161
+ cornerRadius: node.cornerRadius === figma.mixed ? undefined : node.cornerRadius,
162
+ rectangleCornerRadii: [
163
+ node.topLeftRadius,
164
+ node.topRightRadius,
165
+ node.bottomRightRadius,
166
+ node.bottomLeftRadius,
167
+ ],
49
168
  };
50
169
  }
51
170
 
52
- async function normalizeFrameNode(node: FrameNode): Promise<NormalizedFrameNode> {
171
+ async function normalizeEffectProps(
172
+ node: Pick<RectangleNode, "effects" | "effectStyleId">,
173
+ ): Promise<NormalizedHasEffectsTrait> {
174
+ const effectStyleKey =
175
+ typeof node.effectStyleId === "string"
176
+ ? (await figma.getStyleByIdAsync(node.effectStyleId))?.key
177
+ : undefined;
178
+
179
+ const effects = node.effects
180
+ .filter((effect): effect is DropShadowEffect | InnerShadowEffect => {
181
+ if (!effect.visible) return false;
182
+
183
+ return effect.type === "DROP_SHADOW" || effect.type === "INNER_SHADOW";
184
+ })
185
+ .map(({ blendMode, visible, ...rest }): NormalizedShadow => rest);
186
+
53
187
  return {
54
- type: node.type,
55
- id: node.id,
56
- name: node.name,
57
- boundVariables: await normalizeBoundVariables(node),
58
- ...normalizeRadiusProps(node),
59
- ...(await normalizeAutolayoutProps(node)),
60
- children: await normalizeNodes(node.children),
188
+ ...(effectStyleKey ? { effectStyleKey } : {}),
189
+ effects,
61
190
  };
62
191
  }
63
192
 
64
- async function normalizeGroupNode(
65
- node: GroupNode & { inferredAutoLayout?: FrameNode["inferredAutoLayout"] },
66
- ): Promise<NormalizedFrameNode> {
193
+ async function normalizeShapeProps(
194
+ node: Pick<
195
+ RectangleNode,
196
+ | "fills"
197
+ | "fillStyleId"
198
+ | "strokes"
199
+ | "strokeWeight"
200
+ | "layoutGrow"
201
+ | "layoutAlign"
202
+ | "layoutSizingHorizontal"
203
+ | "layoutSizingVertical"
204
+ | "absoluteBoundingBox"
205
+ | "relativeTransform"
206
+ | "layoutPositioning"
207
+ | "minHeight"
208
+ | "minWidth"
209
+ | "maxHeight"
210
+ | "maxWidth"
211
+ | "effects"
212
+ | "effectStyleId"
213
+ > &
214
+ Partial<Pick<FrameNode, "inferredAutoLayout">>,
215
+ ): Promise<Omit<NormalizedDefaultShapeTrait, keyof NormalizedIsLayerTrait>> {
216
+ const fillStyleKey =
217
+ typeof node.fillStyleId === "string"
218
+ ? (await figma.getStyleByIdAsync(node.fillStyleId))?.key
219
+ : undefined;
220
+
67
221
  return {
68
- type: "FRAME",
69
- id: node.id,
70
- name: node.name,
71
- boundVariables: await normalizeBoundVariables(node),
222
+ // NormalizedHasLayoutTrait
72
223
  layoutGrow: (node.inferredAutoLayout?.layoutGrow ?? node.layoutGrow) as 0 | 1 | undefined,
73
224
  layoutAlign: node.inferredAutoLayout?.layoutAlign ?? node.layoutAlign,
74
225
  layoutSizingHorizontal: node.layoutSizingHorizontal,
75
226
  layoutSizingVertical: node.layoutSizingVertical,
76
227
  absoluteBoundingBox: node.absoluteBoundingBox,
77
228
  relativeTransform: node.relativeTransform,
78
- layoutMode: node.inferredAutoLayout?.layoutMode,
79
- layoutWrap: node.inferredAutoLayout?.layoutWrap,
80
- paddingLeft: node.inferredAutoLayout?.paddingLeft,
81
- paddingRight: node.inferredAutoLayout?.paddingRight,
82
- paddingTop: node.inferredAutoLayout?.paddingTop,
83
- paddingBottom: node.inferredAutoLayout?.paddingBottom,
84
- primaryAxisAlignItems: node.inferredAutoLayout?.primaryAxisAlignItems,
85
- counterAxisAlignItems: node.inferredAutoLayout?.counterAxisAlignItems,
86
- primaryAxisSizingMode: node.inferredAutoLayout?.primaryAxisSizingMode,
87
- counterAxisSizingMode: node.inferredAutoLayout?.counterAxisSizingMode,
88
- itemSpacing: node.inferredAutoLayout?.itemSpacing,
89
- counterAxisSpacing: node.inferredAutoLayout?.counterAxisSpacing ?? undefined,
90
- fills: [],
91
- strokes: [],
92
- children: await normalizeNodes(node.children),
229
+ layoutPositioning: node.layoutPositioning,
230
+ minHeight: node.minHeight ?? undefined,
231
+ minWidth: node.minWidth ?? undefined,
232
+ maxHeight: node.maxHeight ?? undefined,
233
+ maxWidth: node.maxWidth ?? undefined,
234
+
235
+ // NormalizedHasGeometryTrait
236
+ fills: await normalizePaints(node.fills),
237
+ fillStyleKey,
238
+ strokes: await normalizePaints(node.strokes),
239
+ strokeWeight: node.strokeWeight === figma.mixed ? undefined : node.strokeWeight,
240
+
241
+ // NormalizedHasEffectsTrait
242
+ ...(await normalizeEffectProps(node)),
93
243
  };
94
244
  }
95
245
 
96
- async function normalizeRectangleNode(node: RectangleNode): Promise<NormalizedRectangleNode> {
246
+ async function normalizeAutolayoutProps(
247
+ node: Omit<FrameNode, "type" | "clone">,
248
+ ): Promise<NormalizedHasFramePropertiesTrait> {
97
249
  return {
98
- type: node.type,
99
- id: node.id,
100
- name: node.name,
101
- boundVariables: await normalizeBoundVariables(node),
102
- ...normalizeRadiusProps(node),
103
- ...(await normalizeShapeProps(node)),
250
+ layoutMode: node.inferredAutoLayout?.layoutMode ?? node.layoutMode,
251
+ layoutWrap: node.inferredAutoLayout?.layoutWrap ?? node.layoutWrap,
252
+ paddingLeft: node.inferredAutoLayout?.paddingLeft ?? node.paddingLeft,
253
+ paddingRight: node.inferredAutoLayout?.paddingRight ?? node.paddingRight,
254
+ paddingTop: node.inferredAutoLayout?.paddingTop ?? node.paddingTop,
255
+ paddingBottom: node.inferredAutoLayout?.paddingBottom ?? node.paddingBottom,
256
+ primaryAxisAlignItems:
257
+ node.inferredAutoLayout?.primaryAxisAlignItems ?? node.primaryAxisAlignItems,
258
+ counterAxisAlignItems:
259
+ node.inferredAutoLayout?.counterAxisAlignItems ?? node.counterAxisAlignItems,
260
+ primaryAxisSizingMode:
261
+ node.inferredAutoLayout?.primaryAxisSizingMode ?? node.primaryAxisSizingMode,
262
+ counterAxisSizingMode:
263
+ node.inferredAutoLayout?.counterAxisSizingMode ?? node.counterAxisSizingMode,
264
+ itemSpacing: node.inferredAutoLayout?.itemSpacing ?? node.itemSpacing,
265
+ counterAxisSpacing:
266
+ node.inferredAutoLayout?.counterAxisSpacing ?? node.counterAxisSpacing ?? undefined,
104
267
  };
105
268
  }
106
269
 
107
- async function normalizeVectorNode(node: VectorNode): Promise<NormalizedVectorNode> {
270
+ async function normalizeFrameNode(node: FrameNode): Promise<NormalizedFrameNode> {
108
271
  return {
272
+ // NormalizedIsLayerTrait
109
273
  type: node.type,
110
274
  id: node.id,
111
275
  name: node.name,
112
- boundVariables: await normalizeBoundVariables(node),
276
+ boundVariables: normalizeBoundVariables(node),
277
+
278
+ // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait
113
279
  ...(await normalizeShapeProps(node)),
280
+
281
+ // NormalizedCornerTrait
282
+ ...normalizeRadiusProps(node),
283
+
284
+ // NormalizedHasFramePropertiesTrait
285
+ ...(await normalizeAutolayoutProps(node)),
286
+
287
+ // NormalizedHasChildrenTrait
288
+ children: await normalizeNodes(node.children),
114
289
  };
115
290
  }
116
291
 
117
- async function normalizeBooleanOperationNode(
118
- node: BooleanOperationNode,
119
- ): Promise<NormalizedBooleanOperationNode> {
292
+ async function normalizeRectangleNode(node: RectangleNode): Promise<NormalizedRectangleNode> {
120
293
  return {
294
+ // NormalizedIsLayerTrait
121
295
  type: node.type,
122
296
  id: node.id,
123
297
  name: node.name,
124
- boundVariables: await normalizeBoundVariables(node),
125
- children: await normalizeNodes(node.children),
298
+ boundVariables: normalizeBoundVariables(node),
299
+
300
+ // NormalizedCornerTrait
301
+ ...normalizeRadiusProps(node),
302
+
303
+ // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait
126
304
  ...(await normalizeShapeProps(node)),
127
305
  };
128
306
  }
307
+
129
308
  async function normalizeTextNode(node: TextNode): Promise<NormalizedTextNode> {
130
309
  const segments = node.getStyledTextSegments([
131
- "fontSize",
132
- "fontWeight",
133
310
  "fontName",
311
+ "fontWeight",
312
+ "fontSize",
134
313
  "letterSpacing",
135
314
  "lineHeight",
136
315
  "paragraphSpacing",
@@ -139,31 +318,74 @@ export function createPluginNormalizer() {
139
318
  "boundVariables",
140
319
  "textDecoration",
141
320
  ]);
142
- const first = segments[0]!;
321
+ const first = segments[0];
143
322
 
144
323
  const textStyleKey =
145
324
  typeof node.textStyleId === "string"
146
325
  ? (await figma.getStyleByIdAsync(node.textStyleId))?.key
147
326
  : undefined;
148
327
 
328
+ const normalizeLetterSpacing = (
329
+ letterSpacing: LetterSpacing,
330
+ fontSize: number,
331
+ ): NormalizedTextSegment["style"]["letterSpacing"] => {
332
+ if (letterSpacing.unit === "PIXELS") return letterSpacing.value;
333
+ if (letterSpacing.unit === "PERCENT") return (fontSize * letterSpacing.value) / 100;
334
+
335
+ return undefined;
336
+ };
337
+
338
+ const normalizeLineHeight = (
339
+ lineHeight: LineHeight,
340
+ fontSize: number,
341
+ ): NormalizedTextSegment["style"]["lineHeight"] => {
342
+ if (lineHeight.unit === "PIXELS") return lineHeight.value;
343
+ if (lineHeight.unit === "PERCENT") return (fontSize * lineHeight.value) / 100;
344
+
345
+ return undefined;
346
+ };
347
+
348
+ const isItalic = (fontName: FontName): boolean => {
349
+ // {
350
+ // family: "SF Mono",
351
+ // style: "Bold Italic"
352
+ // }
353
+ return fontName.style.toLowerCase().includes("italic");
354
+ };
355
+
149
356
  return {
357
+ // NormalizedIsLayerTrait
150
358
  type: node.type,
151
359
  id: node.id,
152
360
  name: node.name,
153
- boundVariables: await normalizeBoundVariables(node),
361
+ boundVariables: normalizeBoundVariables(node),
362
+
363
+ // NormalizedTypePropertiesTrait
364
+ // NOTE: this normalization is incomplete compared to from-rest.ts normalizer
154
365
  style: {
155
- fontSize: first.fontSize,
156
- fontWeight: first.fontWeight,
157
366
  fontFamily: first.fontName.family,
158
- // TODO: handle other units
159
- letterSpacing:
160
- first.letterSpacing.unit === "PIXELS" ? first.letterSpacing.value : undefined,
161
- lineHeightPx: first.lineHeight.unit === "PIXELS" ? first.lineHeight.value : undefined,
162
- paragraphSpacing: first.paragraphSpacing,
367
+ fontPostScriptName: null,
368
+ fontStyle: first.fontName.style,
369
+ italic: isItalic(first.fontName),
370
+ fontWeight: first.fontWeight,
371
+ fontSize: first.fontSize,
163
372
  textAlignHorizontal: node.textAlignHorizontal,
373
+ textAlignVertical: node.textAlignVertical,
374
+ letterSpacing: normalizeLetterSpacing(first.letterSpacing, first.fontSize),
375
+ paragraphSpacing: first.paragraphSpacing,
376
+ textDecoration: first.textDecoration,
377
+ lineHeightPx: normalizeLineHeight(first.lineHeight, first.fontSize),
378
+ lineHeightUnit:
379
+ first.lineHeight.unit === "PIXELS"
380
+ ? "PIXELS"
381
+ : first.lineHeight.unit === "PERCENT"
382
+ ? "FONT_SIZE_%"
383
+ : undefined,
384
+ boundVariables: first.boundVariables,
385
+ maxLines: node.maxLines ?? undefined,
164
386
  },
165
- ...(textStyleKey ? { textStyleKey } : {}),
166
387
  characters: node.characters,
388
+ textStyleKey,
167
389
  segments: segments.map((segment) => ({
168
390
  characters: segment.characters,
169
391
  start: segment.start,
@@ -172,23 +394,36 @@ export function createPluginNormalizer() {
172
394
  fontSize: segment.fontSize,
173
395
  fontWeight: segment.fontWeight,
174
396
  fontFamily: segment.fontName.family,
175
- letterSpacing:
176
- segment.letterSpacing.unit === "PIXELS" ? segment.letterSpacing.value : undefined,
177
- lineHeightPx: segment.lineHeight.unit === "PIXELS" ? segment.lineHeight.value : undefined,
397
+ italic: isItalic(segment.fontName),
398
+ letterSpacing: normalizeLetterSpacing(segment.letterSpacing, segment.fontSize),
399
+ lineHeight: normalizeLineHeight(segment.lineHeight, segment.fontSize),
400
+ textDecoration: segment.textDecoration,
178
401
  },
179
402
  })),
403
+
404
+ // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait
180
405
  ...(await normalizeShapeProps(node)),
181
406
  };
182
407
  }
183
408
 
184
409
  async function normalizeComponentNode(node: ComponentNode): Promise<NormalizedComponentNode> {
185
410
  return {
411
+ // NormalizedIsLayerTrait
186
412
  type: node.type,
187
413
  id: node.id,
188
414
  name: node.name,
189
- boundVariables: await normalizeBoundVariables(node),
415
+ boundVariables: normalizeBoundVariables(node),
416
+
417
+ // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait
418
+ ...(await normalizeShapeProps(node)),
419
+
420
+ // NormalizedCornerTrait
190
421
  ...normalizeRadiusProps(node),
422
+
423
+ // NormalizedHasFramePropertiesTrait
191
424
  ...(await normalizeAutolayoutProps(node)),
425
+
426
+ // NormalizedHasChildrenTrait
192
427
  children: await normalizeNodes(node.children),
193
428
  };
194
429
  }
@@ -200,232 +435,166 @@ export function createPluginNormalizer() {
200
435
  }
201
436
 
202
437
  const componentProperties: NormalizedInstanceNode["componentProperties"] = {};
438
+
203
439
  for (const [key, value] of Object.entries(node.componentProperties)) {
204
440
  componentProperties[key] = value;
441
+
205
442
  if (value.type === "INSTANCE_SWAP") {
206
- const mainComponent = (await figma.getNodeByIdAsync(
443
+ // unless value.type === "BOOLEAN", value.value is string
444
+ const swappedComponent = (await figma.getNodeByIdAsync(
207
445
  value.value as string,
208
446
  )) as ComponentNode;
209
- if (mainComponent) {
210
- componentProperties[key].componentKey = mainComponent.key;
211
- if (mainComponent.parent?.type === "COMPONENT_SET") {
212
- componentProperties[key].componentSetKey = mainComponent.parent.key;
447
+
448
+ if (swappedComponent) {
449
+ componentProperties[key].componentKey = swappedComponent.key;
450
+
451
+ if (swappedComponent.parent?.type === "COMPONENT_SET") {
452
+ componentProperties[key].componentSetKey = swappedComponent.parent.key;
213
453
  }
214
454
  }
215
455
  }
216
456
  }
217
457
 
218
458
  return {
459
+ // NormalizedIsLayerTrait
219
460
  type: node.type,
220
461
  id: node.id,
221
462
  name: node.name,
222
- boundVariables: await normalizeBoundVariables(node),
463
+ boundVariables: normalizeBoundVariables(node),
464
+
465
+ // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait
466
+ ...(await normalizeShapeProps(node)),
467
+
468
+ // NormalizedCornerTrait
223
469
  ...normalizeRadiusProps(node),
470
+
471
+ // NormalizedHasFramePropertiesTrait
224
472
  ...(await normalizeAutolayoutProps(node)),
473
+
474
+ // NormalizedHasChildrenTrait
225
475
  children: await normalizeNodes(node.children),
476
+
477
+ // NormalizedInstanceNode specific
478
+ componentProperties,
226
479
  componentKey: mainComponent.key,
227
480
  componentSetKey:
228
481
  mainComponent.parent?.type === "COMPONENT_SET" ? mainComponent.parent.key : undefined,
229
- componentProperties,
230
482
  overrides: node.overrides,
231
483
  };
232
484
  }
233
485
 
234
- function normalizeSolidPaint(paint: SolidPaint): FigmaRestSpec.SolidPaint {
486
+ async function normalizeVectorNode(node: VectorNode): Promise<NormalizedVectorNode> {
235
487
  return {
236
- type: paint.type,
237
- color: {
238
- r: paint.color.r,
239
- g: paint.color.g,
240
- b: paint.color.b,
241
- a: paint.opacity ?? 1,
242
- },
243
- visible: paint.visible,
244
- blendMode: paint.blendMode ?? "NORMAL",
245
- boundVariables: paint.boundVariables,
246
- };
247
- }
248
-
249
- function normalizePaint(paint: Paint): FigmaRestSpec.Paint {
250
- switch (paint.type) {
251
- case "SOLID":
252
- return normalizeSolidPaint(paint);
253
- case "IMAGE":
254
- return {
255
- type: "IMAGE",
256
- scaleMode: paint.scaleMode === "CROP" ? "STRETCH" : paint.scaleMode,
257
- imageTransform: paint.imageTransform,
258
- scalingFactor: paint.scalingFactor,
259
- filters: paint.filters,
260
- rotation: paint.rotation,
261
- imageRef: paint.imageHash ?? "",
262
- blendMode: paint.blendMode ?? "NORMAL",
263
- visible: paint.visible,
264
- opacity: paint.opacity,
265
- };
266
- case "GRADIENT_LINEAR":
267
- case "GRADIENT_RADIAL":
268
- case "GRADIENT_ANGULAR":
269
- case "GRADIENT_DIAMOND":
270
- return {
271
- type: paint.type,
272
- gradientStops: [...paint.gradientStops],
273
- visible: paint.visible,
274
- opacity: paint.opacity,
275
- blendMode: paint.blendMode ?? "NORMAL",
276
- gradientHandlePositions: convertTransformToGradientHandles(paint.gradientTransform),
277
- };
278
- default:
279
- throw new Error(`Unimplemented paint type: ${paint.type}`);
280
- }
281
- }
282
-
283
- function normalizePaints(fills: readonly Paint[] | PluginAPI["mixed"]): FigmaRestSpec.Paint[] {
284
- if (fills === figma.mixed) {
285
- console.warn("Mixed fills are not supported");
286
-
287
- return [];
288
- }
289
-
290
- return fills.map(normalizePaint);
291
- }
488
+ // NormalizedIsLayerTrait
489
+ type: node.type,
490
+ id: node.id,
491
+ name: node.name,
492
+ boundVariables: normalizeBoundVariables(node),
292
493
 
293
- function normalizeRadiusProps(
294
- node: Pick<
295
- RectangleNode,
296
- "cornerRadius" | "topLeftRadius" | "topRightRadius" | "bottomRightRadius" | "bottomLeftRadius"
297
- >,
298
- ) {
299
- return {
494
+ // NormalizedCornerTrait
300
495
  cornerRadius: node.cornerRadius === figma.mixed ? undefined : node.cornerRadius,
301
- rectangleCornerRadii: [
302
- node.topLeftRadius,
303
- node.topRightRadius,
304
- node.bottomRightRadius,
305
- node.bottomLeftRadius,
306
- ],
496
+ rectangleCornerRadii: undefined, // VectorNode does not have individual corner radii
497
+
498
+ // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait
499
+ ...(await normalizeShapeProps(node)),
307
500
  };
308
501
  }
309
502
 
310
- async function normalizeShapeProps(
311
- node: Pick<
312
- RectangleNode,
313
- | "fills"
314
- | "fillStyleId"
315
- | "strokes"
316
- | "strokeWeight"
317
- | "layoutGrow"
318
- | "layoutAlign"
319
- | "layoutSizingHorizontal"
320
- | "layoutSizingVertical"
321
- | "absoluteBoundingBox"
322
- | "relativeTransform"
323
- | "minHeight"
324
- | "minWidth"
325
- | "maxHeight"
326
- | "maxWidth"
327
- > &
328
- Partial<Pick<FrameNode, "inferredAutoLayout">>,
329
- ) {
503
+ async function normalizeBooleanOperationNode(
504
+ node: BooleanOperationNode,
505
+ ): Promise<NormalizedBooleanOperationNode> {
330
506
  const fillStyleKey =
331
507
  typeof node.fillStyleId === "string"
332
508
  ? (await figma.getStyleByIdAsync(node.fillStyleId))?.key
333
509
  : undefined;
334
510
 
335
511
  return {
336
- layoutGrow: (node.inferredAutoLayout?.layoutGrow ?? node.layoutGrow) as 0 | 1 | undefined,
337
- layoutAlign: node.inferredAutoLayout?.layoutAlign ?? node.layoutAlign,
512
+ // NormalizedIsLayerTrait
513
+ type: node.type,
514
+ id: node.id,
515
+ name: node.name,
516
+ boundVariables: normalizeBoundVariables(node),
517
+
518
+ // NormalizedHasLayoutTrait
519
+ layoutGrow: node.layoutGrow as 0 | 1 | undefined,
520
+ layoutAlign: node.layoutAlign,
338
521
  layoutSizingHorizontal: node.layoutSizingHorizontal,
339
522
  layoutSizingVertical: node.layoutSizingVertical,
340
523
  absoluteBoundingBox: node.absoluteBoundingBox,
341
524
  relativeTransform: node.relativeTransform,
342
- fills: normalizePaints(node.fills),
343
- ...(fillStyleKey ? { fillStyleKey } : {}),
344
- strokes: normalizePaints(node.strokes),
345
- strokeWeight: node.strokeWeight === figma.mixed ? undefined : node.strokeWeight,
525
+ layoutPositioning: node.layoutPositioning,
346
526
  minHeight: node.minHeight ?? undefined,
347
527
  minWidth: node.minWidth ?? undefined,
348
528
  maxHeight: node.maxHeight ?? undefined,
349
529
  maxWidth: node.maxWidth ?? undefined,
530
+
531
+ // NormalizedHasGeometryTrait
532
+ fills: await normalizePaints(node.fills),
533
+ fillStyleKey,
534
+ strokes: await normalizePaints(node.strokes),
535
+ strokeWeight: node.strokeWeight === figma.mixed ? undefined : node.strokeWeight,
536
+
537
+ // NormalizedHasEffectsTrait
538
+ ...(await normalizeEffectProps(node)),
539
+
540
+ // NormalizedHasChildrenTrait
541
+ children: await normalizeNodes(node.children),
350
542
  };
351
543
  }
352
544
 
353
- async function normalizeAutolayoutProps(node: Omit<FrameNode, "type" | "clone">) {
545
+ async function normalizeGroupNodeAsFrameNode(
546
+ node: GroupNode & { inferredAutoLayout?: FrameNode["inferredAutoLayout"] },
547
+ ): Promise<NormalizedFrameNode> {
354
548
  return {
355
- ...(await normalizeShapeProps(node)),
356
- layoutMode: node.inferredAutoLayout?.layoutMode ?? node.layoutMode,
357
- layoutWrap: node.inferredAutoLayout?.layoutWrap ?? node.layoutWrap,
358
- paddingLeft: node.inferredAutoLayout?.paddingLeft ?? node.paddingLeft,
359
- paddingRight: node.inferredAutoLayout?.paddingRight ?? node.paddingRight,
360
- paddingTop: node.inferredAutoLayout?.paddingTop ?? node.paddingTop,
361
- paddingBottom: node.inferredAutoLayout?.paddingBottom ?? node.paddingBottom,
362
- primaryAxisAlignItems:
363
- node.inferredAutoLayout?.primaryAxisAlignItems ?? node.primaryAxisAlignItems,
364
- counterAxisAlignItems:
365
- node.inferredAutoLayout?.counterAxisAlignItems ?? node.counterAxisAlignItems,
366
- primaryAxisSizingMode:
367
- node.inferredAutoLayout?.primaryAxisSizingMode ?? node.primaryAxisSizingMode,
368
- counterAxisSizingMode:
369
- node.inferredAutoLayout?.counterAxisSizingMode ?? node.counterAxisSizingMode,
370
- itemSpacing: node.inferredAutoLayout?.itemSpacing ?? node.itemSpacing,
371
- counterAxisSpacing:
372
- node.inferredAutoLayout?.counterAxisSpacing ?? node.counterAxisSpacing ?? undefined,
373
- };
374
- }
549
+ // NormalizedIsLayerTrait
550
+ type: "FRAME",
551
+ id: node.id,
552
+ name: node.name,
553
+ boundVariables: normalizeBoundVariables(node),
375
554
 
376
- async function normalizeBoundVariables({
377
- boundVariables,
378
- }: Pick<FrameNode, "boundVariables">): Promise<FigmaRestSpec.IsLayerTrait["boundVariables"]> {
379
- if (!boundVariables) return undefined;
555
+ // NormalizedHasLayoutTrait
556
+ layoutGrow: (node.inferredAutoLayout?.layoutGrow ?? node.layoutGrow) as 0 | 1 | undefined,
557
+ layoutAlign: node.inferredAutoLayout?.layoutAlign ?? node.layoutAlign,
558
+ layoutSizingHorizontal: node.layoutSizingHorizontal,
559
+ layoutSizingVertical: node.layoutSizingVertical,
560
+ absoluteBoundingBox: node.absoluteBoundingBox,
561
+ relativeTransform: node.relativeTransform,
562
+ layoutPositioning: node.layoutPositioning,
563
+ minHeight: node.minHeight ?? undefined,
564
+ minWidth: node.minWidth ?? undefined,
565
+ maxHeight: node.maxHeight ?? undefined,
566
+ maxWidth: node.maxWidth ?? undefined,
380
567
 
381
- const { width, height, componentProperties: _componentProperties, ...rest } = boundVariables;
568
+ // NormalizedHasGeometryTrait
569
+ fills: [],
570
+ fillStyleKey: undefined,
571
+ strokes: [],
572
+ strokeWeight: undefined,
382
573
 
383
- // replace VariableAlias' id with the actual variable key
384
- const resolveVariableId = async (variable: VariableAlias): Promise<VariableAlias> => ({
385
- ...variable,
386
- id: (await figma.variables.getVariableByIdAsync(variable.id))?.key ?? variable.id,
387
- });
574
+ // NormalizedHasEffectsTrait
575
+ effects: [],
576
+ effectStyleKey: undefined,
388
577
 
389
- const needsResolution = [
390
- "fills",
391
- "itemSpacing",
392
- "counterAxisSpacing",
393
- "bottomLeftRadius",
394
- "bottomRightRadius",
395
- "topLeftRadius",
396
- "topRightRadius",
397
- "paddingBottom",
398
- "paddingLeft",
399
- "paddingRight",
400
- "paddingTop",
401
- "maxHeight",
402
- "minHeight",
403
- "maxWidth",
404
- "minWidth",
405
- ];
406
-
407
- // Process all properties in parallel
408
- const resolvedEntries = await Promise.all(
409
- Object.entries(rest).map(async ([key, value]) => {
410
- if (!value || !needsResolution.includes(key)) return [key, value];
411
-
412
- if (Array.isArray(value)) {
413
- return [key, await Promise.all(value.map(resolveVariableId))];
414
- }
578
+ // NormalizedCornerTrait
579
+ cornerRadius: undefined,
580
+ rectangleCornerRadii: undefined,
415
581
 
416
- return [key, await resolveVariableId(value)];
417
- }),
418
- );
582
+ // NormalizedHasFramePropertiesTrait
583
+ layoutMode: node.inferredAutoLayout?.layoutMode,
584
+ layoutWrap: node.inferredAutoLayout?.layoutWrap,
585
+ paddingLeft: node.inferredAutoLayout?.paddingLeft,
586
+ paddingRight: node.inferredAutoLayout?.paddingRight,
587
+ paddingTop: node.inferredAutoLayout?.paddingTop,
588
+ paddingBottom: node.inferredAutoLayout?.paddingBottom,
589
+ primaryAxisAlignItems: node.inferredAutoLayout?.primaryAxisAlignItems,
590
+ counterAxisAlignItems: node.inferredAutoLayout?.counterAxisAlignItems,
591
+ primaryAxisSizingMode: node.inferredAutoLayout?.primaryAxisSizingMode,
592
+ counterAxisSizingMode: node.inferredAutoLayout?.counterAxisSizingMode,
593
+ itemSpacing: node.inferredAutoLayout?.itemSpacing,
594
+ counterAxisSpacing: node.inferredAutoLayout?.counterAxisSpacing ?? undefined,
419
595
 
420
- return {
421
- ...Object.fromEntries(resolvedEntries),
422
- ...(width &&
423
- height && {
424
- size: {
425
- x: width,
426
- y: height,
427
- },
428
- }),
596
+ // NormalizedHasChildrenTrait
597
+ children: await normalizeNodes(node.children),
429
598
  };
430
599
  }
431
600