@seed-design/figma 0.0.0-alpha-20260324091316

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 (181) hide show
  1. package/lib/codegen/index.cjs +23543 -0
  2. package/lib/codegen/index.d.ts +2957 -0
  3. package/lib/codegen/index.d.ts.map +1 -0
  4. package/lib/codegen/index.js +23514 -0
  5. package/lib/codegen/targets/react/index.cjs +31980 -0
  6. package/lib/codegen/targets/react/index.d.ts +308 -0
  7. package/lib/codegen/targets/react/index.d.ts.map +1 -0
  8. package/lib/codegen/targets/react/index.js +31961 -0
  9. package/lib/index.cjs +26905 -0
  10. package/lib/index.d.ts +221 -0
  11. package/lib/index.d.ts.map +1 -0
  12. package/lib/index.js +26884 -0
  13. package/package.json +56 -0
  14. package/src/codegen/component-properties.archive.ts +1019 -0
  15. package/src/codegen/component-properties.ts +369 -0
  16. package/src/codegen/core/codegen.ts +112 -0
  17. package/src/codegen/core/component-handler.ts +23 -0
  18. package/src/codegen/core/component-type-helper.ts +35 -0
  19. package/src/codegen/core/element-transformer.ts +13 -0
  20. package/src/codegen/core/index.ts +19 -0
  21. package/src/codegen/core/infer-layout.test.ts +286 -0
  22. package/src/codegen/core/infer-layout.ts +416 -0
  23. package/src/codegen/core/jsx.ts +174 -0
  24. package/src/codegen/core/props-converter.ts +78 -0
  25. package/src/codegen/core/value-resolver.ts +381 -0
  26. package/src/codegen/default-services.ts +44 -0
  27. package/src/codegen/index.ts +3 -0
  28. package/src/codegen/skip-components.ts +7 -0
  29. package/src/codegen/targets/figma/frame.ts +38 -0
  30. package/src/codegen/targets/figma/index.ts +6 -0
  31. package/src/codegen/targets/figma/instance.ts +36 -0
  32. package/src/codegen/targets/figma/pipeline.ts +106 -0
  33. package/src/codegen/targets/figma/props.ts +262 -0
  34. package/src/codegen/targets/figma/shape.ts +65 -0
  35. package/src/codegen/targets/figma/text.ts +30 -0
  36. package/src/codegen/targets/figma/value-resolver.ts +75 -0
  37. package/src/codegen/targets/index.ts +2 -0
  38. package/src/codegen/targets/react/component/deps.interface.ts +7 -0
  39. package/src/codegen/targets/react/component/handlers/action-button.ts +149 -0
  40. package/src/codegen/targets/react/component/handlers/alert-dialog.ts +120 -0
  41. package/src/codegen/targets/react/component/handlers/app-bar.ts +169 -0
  42. package/src/codegen/targets/react/component/handlers/archive/action-button.ts +144 -0
  43. package/src/codegen/targets/react/component/handlers/archive/alert-dialog.ts +122 -0
  44. package/src/codegen/targets/react/component/handlers/archive/app-bar.ts +149 -0
  45. package/src/codegen/targets/react/component/handlers/archive/avatar-stack.ts +35 -0
  46. package/src/codegen/targets/react/component/handlers/archive/avatar.ts +55 -0
  47. package/src/codegen/targets/react/component/handlers/archive/badge.ts +18 -0
  48. package/src/codegen/targets/react/component/handlers/archive/bottom-sheet.ts +70 -0
  49. package/src/codegen/targets/react/component/handlers/archive/callout.ts +88 -0
  50. package/src/codegen/targets/react/component/handlers/archive/checkbox.ts +43 -0
  51. package/src/codegen/targets/react/component/handlers/archive/checkmark.ts +29 -0
  52. package/src/codegen/targets/react/component/handlers/archive/chip.ts +90 -0
  53. package/src/codegen/targets/react/component/handlers/archive/contextual-floating-button.ts +52 -0
  54. package/src/codegen/targets/react/component/handlers/archive/divider.ts +25 -0
  55. package/src/codegen/targets/react/component/handlers/archive/field-button.ts +197 -0
  56. package/src/codegen/targets/react/component/handlers/archive/field.ts +167 -0
  57. package/src/codegen/targets/react/component/handlers/archive/floating-action-button.ts +48 -0
  58. package/src/codegen/targets/react/component/handlers/archive/help-bubble.ts +73 -0
  59. package/src/codegen/targets/react/component/handlers/archive/identity-placeholder.ts +21 -0
  60. package/src/codegen/targets/react/component/handlers/archive/index.ts +40 -0
  61. package/src/codegen/targets/react/component/handlers/archive/legacy-select-box.ts +89 -0
  62. package/src/codegen/targets/react/component/handlers/archive/legacy-text-field.ts +198 -0
  63. package/src/codegen/targets/react/component/handlers/archive/list-header.ts +20 -0
  64. package/src/codegen/targets/react/component/handlers/archive/list-item.ts +162 -0
  65. package/src/codegen/targets/react/component/handlers/archive/manner-temp-badge.ts +21 -0
  66. package/src/codegen/targets/react/component/handlers/archive/manner-temp.ts +18 -0
  67. package/src/codegen/targets/react/component/handlers/archive/menu-sheet.ts +108 -0
  68. package/src/codegen/targets/react/component/handlers/archive/page-banner.ts +101 -0
  69. package/src/codegen/targets/react/component/handlers/archive/progress-circle.ts +55 -0
  70. package/src/codegen/targets/react/component/handlers/archive/radio-group.ts +31 -0
  71. package/src/codegen/targets/react/component/handlers/archive/radiomark.ts +27 -0
  72. package/src/codegen/targets/react/component/handlers/archive/reaction-button.ts +37 -0
  73. package/src/codegen/targets/react/component/handlers/archive/result-section.ts +67 -0
  74. package/src/codegen/targets/react/component/handlers/archive/segmented-control.ts +64 -0
  75. package/src/codegen/targets/react/component/handlers/archive/skeleton.ts +26 -0
  76. package/src/codegen/targets/react/component/handlers/archive/slider.ts +114 -0
  77. package/src/codegen/targets/react/component/handlers/archive/snackbar.ts +25 -0
  78. package/src/codegen/targets/react/component/handlers/archive/switch.ts +39 -0
  79. package/src/codegen/targets/react/component/handlers/archive/switchmark.ts +26 -0
  80. package/src/codegen/targets/react/component/handlers/archive/tabs.ts +297 -0
  81. package/src/codegen/targets/react/component/handlers/archive/tag-group.ts +86 -0
  82. package/src/codegen/targets/react/component/handlers/archive/text-field.ts +264 -0
  83. package/src/codegen/targets/react/component/handlers/archive/toggle-button.ts +43 -0
  84. package/src/codegen/targets/react/component/handlers/avatar-stack.ts +38 -0
  85. package/src/codegen/targets/react/component/handlers/avatar.ts +58 -0
  86. package/src/codegen/targets/react/component/handlers/badge.ts +18 -0
  87. package/src/codegen/targets/react/component/handlers/bottom-sheet.ts +74 -0
  88. package/src/codegen/targets/react/component/handlers/callout.ts +88 -0
  89. package/src/codegen/targets/react/component/handlers/checkbox.ts +129 -0
  90. package/src/codegen/targets/react/component/handlers/checkmark.ts +29 -0
  91. package/src/codegen/targets/react/component/handlers/chip.ts +93 -0
  92. package/src/codegen/targets/react/component/handlers/content-placeholder.ts +20 -0
  93. package/src/codegen/targets/react/component/handlers/contextual-floating-button.ts +52 -0
  94. package/src/codegen/targets/react/component/handlers/divider.ts +25 -0
  95. package/src/codegen/targets/react/component/handlers/field-button.ts +192 -0
  96. package/src/codegen/targets/react/component/handlers/field.ts +164 -0
  97. package/src/codegen/targets/react/component/handlers/floating-action-button.ts +45 -0
  98. package/src/codegen/targets/react/component/handlers/help-bubble.ts +73 -0
  99. package/src/codegen/targets/react/component/handlers/identity-placeholder.ts +20 -0
  100. package/src/codegen/targets/react/component/handlers/image-frame.ts +147 -0
  101. package/src/codegen/targets/react/component/handlers/index.ts +43 -0
  102. package/src/codegen/targets/react/component/handlers/legacy-select-box.ts +87 -0
  103. package/src/codegen/targets/react/component/handlers/legacy-text-field.ts +196 -0
  104. package/src/codegen/targets/react/component/handlers/list-header.ts +20 -0
  105. package/src/codegen/targets/react/component/handlers/list-item.ts +163 -0
  106. package/src/codegen/targets/react/component/handlers/manner-temp-badge.ts +21 -0
  107. package/src/codegen/targets/react/component/handlers/manner-temp.ts +18 -0
  108. package/src/codegen/targets/react/component/handlers/menu-sheet.ts +111 -0
  109. package/src/codegen/targets/react/component/handlers/page-banner.ts +106 -0
  110. package/src/codegen/targets/react/component/handlers/progress-circle.ts +55 -0
  111. package/src/codegen/targets/react/component/handlers/radio-group.ts +109 -0
  112. package/src/codegen/targets/react/component/handlers/radiomark.ts +27 -0
  113. package/src/codegen/targets/react/component/handlers/reaction-button.ts +37 -0
  114. package/src/codegen/targets/react/component/handlers/result-section.ts +67 -0
  115. package/src/codegen/targets/react/component/handlers/segmented-control.ts +63 -0
  116. package/src/codegen/targets/react/component/handlers/select-box.ts +333 -0
  117. package/src/codegen/targets/react/component/handlers/skeleton.ts +26 -0
  118. package/src/codegen/targets/react/component/handlers/slider.ts +117 -0
  119. package/src/codegen/targets/react/component/handlers/snackbar.ts +25 -0
  120. package/src/codegen/targets/react/component/handlers/switch.ts +35 -0
  121. package/src/codegen/targets/react/component/handlers/switchmark.ts +26 -0
  122. package/src/codegen/targets/react/component/handlers/tabs.ts +298 -0
  123. package/src/codegen/targets/react/component/handlers/tag-group.ts +90 -0
  124. package/src/codegen/targets/react/component/handlers/text-field.ts +253 -0
  125. package/src/codegen/targets/react/component/handlers/toggle-button.ts +43 -0
  126. package/src/codegen/targets/react/component/index.ts +24 -0
  127. package/src/codegen/targets/react/component/size.ts +22 -0
  128. package/src/codegen/targets/react/element-factories.ts +59 -0
  129. package/src/codegen/targets/react/frame.ts +96 -0
  130. package/src/codegen/targets/react/icon.ts +55 -0
  131. package/src/codegen/targets/react/index.ts +7 -0
  132. package/src/codegen/targets/react/instance.ts +82 -0
  133. package/src/codegen/targets/react/pipeline.ts +133 -0
  134. package/src/codegen/targets/react/props.ts +417 -0
  135. package/src/codegen/targets/react/shape.ts +47 -0
  136. package/src/codegen/targets/react/text.ts +31 -0
  137. package/src/codegen/targets/react/value-resolver.ts +93 -0
  138. package/src/entities/component.interface.ts +7 -0
  139. package/src/entities/component.repository.ts +16 -0
  140. package/src/entities/data/__generated__/archive/component-sets/index.d.ts +2074 -0
  141. package/src/entities/data/__generated__/archive/component-sets/index.mjs +2074 -0
  142. package/src/entities/data/__generated__/archive/components/index.d.ts +116 -0
  143. package/src/entities/data/__generated__/archive/components/index.mjs +116 -0
  144. package/src/entities/data/__generated__/archive/styles/index.d.ts +3 -0
  145. package/src/entities/data/__generated__/archive/styles/index.mjs +429 -0
  146. package/src/entities/data/__generated__/archive/variable-collections/index.d.ts +3 -0
  147. package/src/entities/data/__generated__/archive/variable-collections/index.mjs +501 -0
  148. package/src/entities/data/__generated__/archive/variables/index.d.ts +3 -0
  149. package/src/entities/data/__generated__/archive/variables/index.mjs +7019 -0
  150. package/src/entities/data/__generated__/component-sets/index.d.ts +4325 -0
  151. package/src/entities/data/__generated__/component-sets/index.mjs +4325 -0
  152. package/src/entities/data/__generated__/components/index.d.ts +378 -0
  153. package/src/entities/data/__generated__/components/index.mjs +378 -0
  154. package/src/entities/data/__generated__/icons/index.d.ts +3 -0
  155. package/src/entities/data/__generated__/icons/index.mjs +3476 -0
  156. package/src/entities/data/__generated__/styles/index.d.ts +3 -0
  157. package/src/entities/data/__generated__/styles/index.mjs +436 -0
  158. package/src/entities/data/__generated__/variable-collections/index.d.ts +3 -0
  159. package/src/entities/data/__generated__/variable-collections/index.mjs +479 -0
  160. package/src/entities/data/__generated__/variables/index.d.ts +3 -0
  161. package/src/entities/data/__generated__/variables/index.mjs +6969 -0
  162. package/src/entities/icon.interface.ts +5 -0
  163. package/src/entities/icon.repository.ts +11 -0
  164. package/src/entities/icon.service.ts +26 -0
  165. package/src/entities/index.ts +60 -0
  166. package/src/entities/style.interface.ts +5 -0
  167. package/src/entities/style.repository.ts +27 -0
  168. package/src/entities/style.service.ts +36 -0
  169. package/src/entities/variable.interface.ts +18 -0
  170. package/src/entities/variable.repository.ts +57 -0
  171. package/src/entities/variable.service.ts +101 -0
  172. package/src/index.ts +3 -0
  173. package/src/normalizer/from-plugin.ts +602 -0
  174. package/src/normalizer/from-rest.ts +577 -0
  175. package/src/normalizer/index.ts +3 -0
  176. package/src/normalizer/types.ts +208 -0
  177. package/src/utils/common.ts +38 -0
  178. package/src/utils/css.ts +19 -0
  179. package/src/utils/figma-gradient.ts +72 -0
  180. package/src/utils/figma-node.ts +95 -0
  181. package/src/utils/figma-variable.ts +49 -0
@@ -0,0 +1,602 @@
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
+
7
+ import type {
8
+ NormalizedSceneNode,
9
+ NormalizedFrameNode,
10
+ NormalizedRectangleNode,
11
+ NormalizedTextNode,
12
+ NormalizedComponentNode,
13
+ NormalizedInstanceNode,
14
+ NormalizedVectorNode,
15
+ NormalizedBooleanOperationNode,
16
+ NormalizedHasEffectsTrait,
17
+ NormalizedShadow,
18
+ NormalizedDefaultShapeTrait,
19
+ NormalizedHasFramePropertiesTrait,
20
+ NormalizedCornerTrait,
21
+ NormalizedIsLayerTrait,
22
+ NormalizedPaint,
23
+ NormalizedTextSegment,
24
+ } from "./types";
25
+ import { convertTransformToGradientHandles } from "@/utils/figma-gradient";
26
+
27
+ export function createPluginNormalizer(): (node: SceneNode) => Promise<NormalizedSceneNode> {
28
+ async function normalizeNodes(nodes: readonly SceneNode[]): Promise<NormalizedSceneNode[]> {
29
+ return Promise.all(nodes.filter((node) => node.visible).map(normalizeNode));
30
+ }
31
+
32
+ async function normalizeNode(node: SceneNode): Promise<NormalizedSceneNode> {
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
+ };
56
+ }
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}`);
141
+ }
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 [];
149
+ }
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 {
160
+ return {
161
+ cornerRadius: node.cornerRadius === figma.mixed ? undefined : node.cornerRadius,
162
+ rectangleCornerRadii: [
163
+ node.topLeftRadius,
164
+ node.topRightRadius,
165
+ node.bottomRightRadius,
166
+ node.bottomLeftRadius,
167
+ ],
168
+ };
169
+ }
170
+
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
+
187
+ return {
188
+ ...(effectStyleKey ? { effectStyleKey } : {}),
189
+ effects,
190
+ };
191
+ }
192
+
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
+
221
+ return {
222
+ // NormalizedHasLayoutTrait
223
+ layoutGrow: (node.inferredAutoLayout?.layoutGrow ?? node.layoutGrow) as 0 | 1 | undefined,
224
+ layoutAlign: node.inferredAutoLayout?.layoutAlign ?? node.layoutAlign,
225
+ layoutSizingHorizontal: node.layoutSizingHorizontal,
226
+ layoutSizingVertical: node.layoutSizingVertical,
227
+ absoluteBoundingBox: node.absoluteBoundingBox,
228
+ relativeTransform: node.relativeTransform,
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)),
243
+ };
244
+ }
245
+
246
+ async function normalizeAutolayoutProps(
247
+ node: Omit<FrameNode, "type" | "clone">,
248
+ ): Promise<NormalizedHasFramePropertiesTrait> {
249
+ return {
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,
267
+ };
268
+ }
269
+
270
+ async function normalizeFrameNode(node: FrameNode): Promise<NormalizedFrameNode> {
271
+ return {
272
+ // NormalizedIsLayerTrait
273
+ type: node.type,
274
+ id: node.id,
275
+ name: node.name,
276
+ boundVariables: normalizeBoundVariables(node),
277
+
278
+ // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait
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),
289
+ };
290
+ }
291
+
292
+ async function normalizeRectangleNode(node: RectangleNode): Promise<NormalizedRectangleNode> {
293
+ return {
294
+ // NormalizedIsLayerTrait
295
+ type: node.type,
296
+ id: node.id,
297
+ name: node.name,
298
+ boundVariables: normalizeBoundVariables(node),
299
+
300
+ // NormalizedCornerTrait
301
+ ...normalizeRadiusProps(node),
302
+
303
+ // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait
304
+ ...(await normalizeShapeProps(node)),
305
+ };
306
+ }
307
+
308
+ async function normalizeTextNode(node: TextNode): Promise<NormalizedTextNode> {
309
+ const segments = node.getStyledTextSegments([
310
+ "fontName",
311
+ "fontWeight",
312
+ "fontSize",
313
+ "letterSpacing",
314
+ "lineHeight",
315
+ "paragraphSpacing",
316
+ "textStyleId",
317
+ "fills",
318
+ "boundVariables",
319
+ "textDecoration",
320
+ ]);
321
+ const first = segments[0];
322
+
323
+ const textStyleKey =
324
+ typeof node.textStyleId === "string"
325
+ ? (await figma.getStyleByIdAsync(node.textStyleId))?.key
326
+ : undefined;
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
+
356
+ return {
357
+ // NormalizedIsLayerTrait
358
+ type: node.type,
359
+ id: node.id,
360
+ name: node.name,
361
+ boundVariables: normalizeBoundVariables(node),
362
+
363
+ // NormalizedTypePropertiesTrait
364
+ // NOTE: this normalization is incomplete compared to from-rest.ts normalizer
365
+ style: {
366
+ fontFamily: first.fontName.family,
367
+ fontPostScriptName: null,
368
+ fontStyle: first.fontName.style,
369
+ italic: isItalic(first.fontName),
370
+ fontWeight: first.fontWeight,
371
+ fontSize: first.fontSize,
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,
386
+ },
387
+ characters: node.characters,
388
+ textStyleKey,
389
+ segments: segments.map((segment) => ({
390
+ characters: segment.characters,
391
+ start: segment.start,
392
+ end: segment.end,
393
+ style: {
394
+ fontSize: segment.fontSize,
395
+ fontWeight: segment.fontWeight,
396
+ fontFamily: segment.fontName.family,
397
+ italic: isItalic(segment.fontName),
398
+ letterSpacing: normalizeLetterSpacing(segment.letterSpacing, segment.fontSize),
399
+ lineHeight: normalizeLineHeight(segment.lineHeight, segment.fontSize),
400
+ textDecoration: segment.textDecoration,
401
+ },
402
+ })),
403
+
404
+ // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait
405
+ ...(await normalizeShapeProps(node)),
406
+ };
407
+ }
408
+
409
+ async function normalizeComponentNode(node: ComponentNode): Promise<NormalizedComponentNode> {
410
+ return {
411
+ // NormalizedIsLayerTrait
412
+ type: node.type,
413
+ id: node.id,
414
+ name: node.name,
415
+ boundVariables: normalizeBoundVariables(node),
416
+
417
+ // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait
418
+ ...(await normalizeShapeProps(node)),
419
+
420
+ // NormalizedCornerTrait
421
+ ...normalizeRadiusProps(node),
422
+
423
+ // NormalizedHasFramePropertiesTrait
424
+ ...(await normalizeAutolayoutProps(node)),
425
+
426
+ // NormalizedHasChildrenTrait
427
+ children: await normalizeNodes(node.children),
428
+ };
429
+ }
430
+
431
+ async function normalizeInstanceNode(node: InstanceNode): Promise<NormalizedInstanceNode> {
432
+ const mainComponent = await node.getMainComponentAsync();
433
+ if (!mainComponent) {
434
+ throw new Error("Instance node has no main component");
435
+ }
436
+
437
+ const componentProperties: NormalizedInstanceNode["componentProperties"] = {};
438
+
439
+ for (const [key, value] of Object.entries(node.componentProperties)) {
440
+ componentProperties[key] = value;
441
+
442
+ if (value.type === "INSTANCE_SWAP") {
443
+ // unless value.type === "BOOLEAN", value.value is string
444
+ const swappedComponent = (await figma.getNodeByIdAsync(
445
+ value.value as string,
446
+ )) as ComponentNode;
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;
453
+ }
454
+ }
455
+ }
456
+ }
457
+
458
+ return {
459
+ // NormalizedIsLayerTrait
460
+ type: node.type,
461
+ id: node.id,
462
+ name: node.name,
463
+ boundVariables: normalizeBoundVariables(node),
464
+
465
+ // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait
466
+ ...(await normalizeShapeProps(node)),
467
+
468
+ // NormalizedCornerTrait
469
+ ...normalizeRadiusProps(node),
470
+
471
+ // NormalizedHasFramePropertiesTrait
472
+ ...(await normalizeAutolayoutProps(node)),
473
+
474
+ // NormalizedHasChildrenTrait
475
+ children: await normalizeNodes(node.children),
476
+
477
+ // NormalizedInstanceNode specific
478
+ componentProperties,
479
+ componentKey: mainComponent.key,
480
+ componentSetKey:
481
+ mainComponent.parent?.type === "COMPONENT_SET" ? mainComponent.parent.key : undefined,
482
+ overrides: node.overrides,
483
+ };
484
+ }
485
+
486
+ async function normalizeVectorNode(node: VectorNode): Promise<NormalizedVectorNode> {
487
+ return {
488
+ // NormalizedIsLayerTrait
489
+ type: node.type,
490
+ id: node.id,
491
+ name: node.name,
492
+ boundVariables: normalizeBoundVariables(node),
493
+
494
+ // NormalizedCornerTrait
495
+ cornerRadius: node.cornerRadius === figma.mixed ? undefined : node.cornerRadius,
496
+ rectangleCornerRadii: undefined, // VectorNode does not have individual corner radii
497
+
498
+ // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait
499
+ ...(await normalizeShapeProps(node)),
500
+ };
501
+ }
502
+
503
+ async function normalizeBooleanOperationNode(
504
+ node: BooleanOperationNode,
505
+ ): Promise<NormalizedBooleanOperationNode> {
506
+ const fillStyleKey =
507
+ typeof node.fillStyleId === "string"
508
+ ? (await figma.getStyleByIdAsync(node.fillStyleId))?.key
509
+ : undefined;
510
+
511
+ return {
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,
521
+ layoutSizingHorizontal: node.layoutSizingHorizontal,
522
+ layoutSizingVertical: node.layoutSizingVertical,
523
+ absoluteBoundingBox: node.absoluteBoundingBox,
524
+ relativeTransform: node.relativeTransform,
525
+ layoutPositioning: node.layoutPositioning,
526
+ minHeight: node.minHeight ?? undefined,
527
+ minWidth: node.minWidth ?? undefined,
528
+ maxHeight: node.maxHeight ?? undefined,
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),
542
+ };
543
+ }
544
+
545
+ async function normalizeGroupNodeAsFrameNode(
546
+ node: GroupNode & { inferredAutoLayout?: FrameNode["inferredAutoLayout"] },
547
+ ): Promise<NormalizedFrameNode> {
548
+ return {
549
+ // NormalizedIsLayerTrait
550
+ type: "FRAME",
551
+ id: node.id,
552
+ name: node.name,
553
+ boundVariables: normalizeBoundVariables(node),
554
+
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,
567
+
568
+ // NormalizedHasGeometryTrait
569
+ fills: [],
570
+ fillStyleKey: undefined,
571
+ strokes: [],
572
+ strokeWeight: undefined,
573
+
574
+ // NormalizedHasEffectsTrait
575
+ effects: [],
576
+ effectStyleKey: undefined,
577
+
578
+ // NormalizedCornerTrait
579
+ cornerRadius: undefined,
580
+ rectangleCornerRadii: undefined,
581
+
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,
595
+
596
+ // NormalizedHasChildrenTrait
597
+ children: await normalizeNodes(node.children),
598
+ };
599
+ }
600
+
601
+ return normalizeNode;
602
+ }