@seed-design/figma 0.0.2

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 (121) hide show
  1. package/lib/index.cjs +13918 -0
  2. package/lib/index.d.ts +63 -0
  3. package/lib/index.js +13916 -0
  4. package/package.json +44 -0
  5. package/src/color.ts +78 -0
  6. package/src/component/index.ts +1688 -0
  7. package/src/component/properties.ts +20 -0
  8. package/src/component/type-helper.ts +29 -0
  9. package/src/component/type.ts +318 -0
  10. package/src/data/__generated__/component-sets/action-button.d.ts +59 -0
  11. package/src/data/__generated__/component-sets/action-button.mjs +59 -0
  12. package/src/data/__generated__/component-sets/action-chip.d.ts +57 -0
  13. package/src/data/__generated__/component-sets/action-chip.mjs +57 -0
  14. package/src/data/__generated__/component-sets/action-sheet.d.ts +40 -0
  15. package/src/data/__generated__/component-sets/action-sheet.mjs +40 -0
  16. package/src/data/__generated__/component-sets/avatar-stack.d.ts +32 -0
  17. package/src/data/__generated__/component-sets/avatar-stack.mjs +32 -0
  18. package/src/data/__generated__/component-sets/avatar.d.ts +25 -0
  19. package/src/data/__generated__/component-sets/avatar.mjs +25 -0
  20. package/src/data/__generated__/component-sets/badge.d.ts +41 -0
  21. package/src/data/__generated__/component-sets/badge.mjs +41 -0
  22. package/src/data/__generated__/component-sets/bottom-navigation-global.d.ts +20 -0
  23. package/src/data/__generated__/component-sets/bottom-navigation-global.mjs +20 -0
  24. package/src/data/__generated__/component-sets/bottom-navigation-kr.d.ts +13 -0
  25. package/src/data/__generated__/component-sets/bottom-navigation-kr.mjs +13 -0
  26. package/src/data/__generated__/component-sets/bottom-sheet.d.ts +14 -0
  27. package/src/data/__generated__/component-sets/bottom-sheet.mjs +14 -0
  28. package/src/data/__generated__/component-sets/callout.d.ts +57 -0
  29. package/src/data/__generated__/component-sets/callout.mjs +57 -0
  30. package/src/data/__generated__/component-sets/checkbox.d.ts +44 -0
  31. package/src/data/__generated__/component-sets/checkbox.mjs +44 -0
  32. package/src/data/__generated__/component-sets/chip-tablist.d.ts +24 -0
  33. package/src/data/__generated__/component-sets/chip-tablist.mjs +24 -0
  34. package/src/data/__generated__/component-sets/control-chip.d.ts +60 -0
  35. package/src/data/__generated__/component-sets/control-chip.mjs +60 -0
  36. package/src/data/__generated__/component-sets/divider.d.ts +13 -0
  37. package/src/data/__generated__/component-sets/divider.mjs +13 -0
  38. package/src/data/__generated__/component-sets/error-state.d.ts +32 -0
  39. package/src/data/__generated__/component-sets/error-state.mjs +32 -0
  40. package/src/data/__generated__/component-sets/extended-action-sheet.d.ts +32 -0
  41. package/src/data/__generated__/component-sets/extended-action-sheet.mjs +32 -0
  42. package/src/data/__generated__/component-sets/extended-floating-action-button.d.ts +34 -0
  43. package/src/data/__generated__/component-sets/extended-floating-action-button.mjs +34 -0
  44. package/src/data/__generated__/component-sets/floating-action-button.d.ts +17 -0
  45. package/src/data/__generated__/component-sets/floating-action-button.mjs +17 -0
  46. package/src/data/__generated__/component-sets/help-bubble.d.ts +39 -0
  47. package/src/data/__generated__/component-sets/help-bubble.mjs +39 -0
  48. package/src/data/__generated__/component-sets/identity-placeholder.d.ts +13 -0
  49. package/src/data/__generated__/component-sets/identity-placeholder.mjs +13 -0
  50. package/src/data/__generated__/component-sets/index.d.ts +47 -0
  51. package/src/data/__generated__/component-sets/index.mjs +47 -0
  52. package/src/data/__generated__/component-sets/inline-banner.d.ts +40 -0
  53. package/src/data/__generated__/component-sets/inline-banner.mjs +40 -0
  54. package/src/data/__generated__/component-sets/main-tab-navigation-global.d.ts +26 -0
  55. package/src/data/__generated__/component-sets/main-tab-navigation-global.mjs +26 -0
  56. package/src/data/__generated__/component-sets/main-tab-navigation-kr.d.ts +23 -0
  57. package/src/data/__generated__/component-sets/main-tab-navigation-kr.mjs +23 -0
  58. package/src/data/__generated__/component-sets/manner-temp-badge.d.ts +17 -0
  59. package/src/data/__generated__/component-sets/manner-temp-badge.mjs +17 -0
  60. package/src/data/__generated__/component-sets/manner-temp-bar.d.ts +23 -0
  61. package/src/data/__generated__/component-sets/manner-temp-bar.mjs +23 -0
  62. package/src/data/__generated__/component-sets/manner-temp.d.ts +17 -0
  63. package/src/data/__generated__/component-sets/manner-temp.mjs +17 -0
  64. package/src/data/__generated__/component-sets/multiline-text-field.d.ts +68 -0
  65. package/src/data/__generated__/component-sets/multiline-text-field.mjs +68 -0
  66. package/src/data/__generated__/component-sets/progress-circle.d.ts +31 -0
  67. package/src/data/__generated__/component-sets/progress-circle.mjs +31 -0
  68. package/src/data/__generated__/component-sets/radio.d.ts +27 -0
  69. package/src/data/__generated__/component-sets/radio.mjs +27 -0
  70. package/src/data/__generated__/component-sets/range-slider.d.ts +31 -0
  71. package/src/data/__generated__/component-sets/range-slider.mjs +31 -0
  72. package/src/data/__generated__/component-sets/reaction-button.d.ts +39 -0
  73. package/src/data/__generated__/component-sets/reaction-button.mjs +39 -0
  74. package/src/data/__generated__/component-sets/segmented-control.d.ts +23 -0
  75. package/src/data/__generated__/component-sets/segmented-control.mjs +23 -0
  76. package/src/data/__generated__/component-sets/select-box.d.ts +31 -0
  77. package/src/data/__generated__/component-sets/select-box.mjs +31 -0
  78. package/src/data/__generated__/component-sets/skeleton.d.ts +15 -0
  79. package/src/data/__generated__/component-sets/skeleton.mjs +15 -0
  80. package/src/data/__generated__/component-sets/slider.d.ts +31 -0
  81. package/src/data/__generated__/component-sets/slider.mjs +31 -0
  82. package/src/data/__generated__/component-sets/snackbar.d.ts +23 -0
  83. package/src/data/__generated__/component-sets/snackbar.mjs +23 -0
  84. package/src/data/__generated__/component-sets/standard-navigation.d.ts +23 -0
  85. package/src/data/__generated__/component-sets/standard-navigation.mjs +23 -0
  86. package/src/data/__generated__/component-sets/switch.d.ts +25 -0
  87. package/src/data/__generated__/component-sets/switch.mjs +25 -0
  88. package/src/data/__generated__/component-sets/tablist.d.ts +29 -0
  89. package/src/data/__generated__/component-sets/tablist.mjs +29 -0
  90. package/src/data/__generated__/component-sets/template-bottom-fixed-bar.d.ts +42 -0
  91. package/src/data/__generated__/component-sets/template-bottom-fixed-bar.mjs +42 -0
  92. package/src/data/__generated__/component-sets/template-button-group.d.ts +51 -0
  93. package/src/data/__generated__/component-sets/template-button-group.mjs +51 -0
  94. package/src/data/__generated__/component-sets/template-chip-group.d.ts +26 -0
  95. package/src/data/__generated__/component-sets/template-chip-group.mjs +26 -0
  96. package/src/data/__generated__/component-sets/template-select-box-group.d.ts +24 -0
  97. package/src/data/__generated__/component-sets/template-select-box-group.mjs +24 -0
  98. package/src/data/__generated__/component-sets/template-top-navigation.d.ts +25 -0
  99. package/src/data/__generated__/component-sets/template-top-navigation.mjs +25 -0
  100. package/src/data/__generated__/component-sets/text-button.d.ts +45 -0
  101. package/src/data/__generated__/component-sets/text-button.mjs +45 -0
  102. package/src/data/__generated__/component-sets/text-field.d.ts +100 -0
  103. package/src/data/__generated__/component-sets/text-field.mjs +100 -0
  104. package/src/data/__generated__/component-sets/toggle-button.d.ts +50 -0
  105. package/src/data/__generated__/component-sets/toggle-button.mjs +50 -0
  106. package/src/data/icons.ts +2774 -0
  107. package/src/data/styles.ts +142 -0
  108. package/src/data/variables.ts +7772 -0
  109. package/src/generate-code.ts +213 -0
  110. package/src/icon.ts +46 -0
  111. package/src/index.ts +3 -0
  112. package/src/jsx.ts +104 -0
  113. package/src/layout.ts +289 -0
  114. package/src/node-util.ts +49 -0
  115. package/src/normalizer/from-plugin.ts +345 -0
  116. package/src/normalizer/from-rest.ts +178 -0
  117. package/src/normalizer/types.ts +94 -0
  118. package/src/sizing.ts +58 -0
  119. package/src/text.ts +20 -0
  120. package/src/util.ts +17 -0
  121. package/src/variable.ts +66 -0
@@ -0,0 +1,345 @@
1
+ import type * as FigmaRestSpec from "@figma/rest-api-spec";
2
+ import type {
3
+ NormalizedSceneNode,
4
+ NormalizedFrameNode,
5
+ NormalizedRectangleNode,
6
+ NormalizedTextNode,
7
+ NormalizedComponentNode,
8
+ NormalizedInstanceNode,
9
+ NormalizedVectorNode,
10
+ } from "./types";
11
+
12
+ export function createPluginNormalizer() {
13
+ async function normalizeNode(node: SceneNode): Promise<NormalizedSceneNode> {
14
+ if (node.type === "FRAME") {
15
+ return normalizeFrameNode(node);
16
+ }
17
+ if (node.type === "GROUP") {
18
+ return normalizeGroupNode(node);
19
+ }
20
+ if (node.type === "RECTANGLE") {
21
+ return normalizeRectangleNode(node);
22
+ }
23
+ if (node.type === "VECTOR") {
24
+ return normalizeVectorNode(node);
25
+ }
26
+ if (node.type === "TEXT") {
27
+ return normalizeTextNode(node);
28
+ }
29
+ if (node.type === "COMPONENT") {
30
+ return normalizeComponentNode(node);
31
+ }
32
+ if (node.type === "INSTANCE") {
33
+ return normalizeInstanceNode(node);
34
+ }
35
+
36
+ throw new Error(`Unimplemented node type: ${node.type}`);
37
+ }
38
+
39
+ async function normalizeFrameNode(node: FrameNode): Promise<NormalizedFrameNode> {
40
+ return {
41
+ type: node.type,
42
+ id: node.id,
43
+ name: node.name,
44
+ boundVariables: await normalizeBoundVariables(node),
45
+ ...normalizeRadiusProps(node),
46
+ ...normalizeAutolayoutProps(node),
47
+ children: await Promise.all(node.children.map(normalizeNode)),
48
+ };
49
+ }
50
+
51
+ async function normalizeGroupNode(
52
+ node: GroupNode & { inferredAutoLayout?: FrameNode["inferredAutoLayout"] },
53
+ ): Promise<NormalizedFrameNode> {
54
+ return {
55
+ type: "FRAME",
56
+ id: node.id,
57
+ name: node.name,
58
+ boundVariables: await normalizeBoundVariables(node),
59
+ layoutGrow: (node.inferredAutoLayout?.layoutGrow ?? node.layoutGrow) as 0 | 1 | undefined,
60
+ layoutAlign: node.inferredAutoLayout?.layoutAlign ?? node.layoutAlign,
61
+ layoutSizingHorizontal: node.layoutSizingHorizontal,
62
+ layoutSizingVertical: node.layoutSizingVertical,
63
+ absoluteBoundingBox: node.absoluteBoundingBox,
64
+ layoutMode: node.inferredAutoLayout?.layoutMode,
65
+ layoutWrap: node.inferredAutoLayout?.layoutWrap,
66
+ paddingLeft: node.inferredAutoLayout?.paddingLeft,
67
+ paddingRight: node.inferredAutoLayout?.paddingRight,
68
+ paddingTop: node.inferredAutoLayout?.paddingTop,
69
+ paddingBottom: node.inferredAutoLayout?.paddingBottom,
70
+ primaryAxisAlignItems: node.inferredAutoLayout?.primaryAxisAlignItems,
71
+ counterAxisAlignItems: node.inferredAutoLayout?.counterAxisAlignItems,
72
+ primaryAxisSizingMode: node.inferredAutoLayout?.primaryAxisSizingMode,
73
+ counterAxisSizingMode: node.inferredAutoLayout?.counterAxisSizingMode,
74
+ itemSpacing: node.inferredAutoLayout?.itemSpacing,
75
+ counterAxisSpacing: node.inferredAutoLayout?.counterAxisSpacing ?? undefined,
76
+ fills: [],
77
+ strokes: [],
78
+ children: await Promise.all(node.children.map(normalizeNode)),
79
+ };
80
+ }
81
+
82
+ async function normalizeRectangleNode(node: RectangleNode): Promise<NormalizedRectangleNode> {
83
+ return {
84
+ type: node.type,
85
+ id: node.id,
86
+ name: node.name,
87
+ boundVariables: await normalizeBoundVariables(node),
88
+ ...normalizeRadiusProps(node),
89
+ ...normalizeShapeProps(node),
90
+ };
91
+ }
92
+
93
+ async function normalizeVectorNode(node: VectorNode): Promise<NormalizedVectorNode> {
94
+ return {
95
+ type: node.type,
96
+ id: node.id,
97
+ name: node.name,
98
+ boundVariables: await normalizeBoundVariables(node),
99
+ ...normalizeShapeProps(node),
100
+ };
101
+ }
102
+
103
+ async function normalizeTextNode(node: TextNode): Promise<NormalizedTextNode> {
104
+ const segments = node.getStyledTextSegments([
105
+ "fontSize",
106
+ "fontWeight",
107
+ "fontName",
108
+ "letterSpacing",
109
+ "lineHeight",
110
+ "paragraphSpacing",
111
+ "textStyleId",
112
+ "fills",
113
+ "boundVariables",
114
+ ]);
115
+ const first = segments[0]!; // TODO: handle multiple segments
116
+
117
+ const textStyleKey =
118
+ typeof node.textStyleId === "string"
119
+ ? (await figma.getStyleByIdAsync(node.textStyleId))?.key
120
+ : undefined;
121
+
122
+ return {
123
+ type: node.type,
124
+ id: node.id,
125
+ name: node.name,
126
+ boundVariables: await normalizeBoundVariables(node),
127
+ layoutGrow: node.layoutGrow as 0 | 1 | undefined,
128
+ layoutAlign: node.layoutAlign,
129
+ style: {
130
+ fontSize: first.fontSize,
131
+ fontWeight: first.fontWeight,
132
+ fontFamily: first.fontName.family,
133
+ // TODO: handle other units
134
+ letterSpacing:
135
+ first.letterSpacing.unit === "PIXELS" ? first.letterSpacing.value : undefined,
136
+ lineHeightPx: first.lineHeight.unit === "PIXELS" ? first.lineHeight.value : undefined,
137
+ paragraphSpacing: first.paragraphSpacing,
138
+ textAlignHorizontal: node.textAlignHorizontal,
139
+ },
140
+ ...(textStyleKey ? { textStyleKey } : {}),
141
+ characters: node.characters,
142
+ segments: node
143
+ .getStyledTextSegments([
144
+ "fontSize",
145
+ "fontWeight",
146
+ "fontName",
147
+ "letterSpacing",
148
+ "lineHeight",
149
+ ])
150
+ .map((segment) => ({
151
+ characters: segment.characters,
152
+ start: segment.start,
153
+ end: segment.end,
154
+ style: {
155
+ fontSize: segment.fontSize,
156
+ fontWeight: segment.fontWeight,
157
+ fontFamily: segment.fontName.family,
158
+ letterSpacing:
159
+ segment.letterSpacing.unit === "PIXELS" ? segment.letterSpacing.value : undefined,
160
+ lineHeightPx:
161
+ segment.lineHeight.unit === "PIXELS" ? segment.lineHeight.value : undefined,
162
+ },
163
+ })),
164
+ fills: normalizePaints(node.fills),
165
+ };
166
+ }
167
+
168
+ async function normalizeComponentNode(node: ComponentNode): Promise<NormalizedComponentNode> {
169
+ return {
170
+ type: node.type,
171
+ id: node.id,
172
+ name: node.name,
173
+ boundVariables: await normalizeBoundVariables(node),
174
+ ...normalizeRadiusProps(node),
175
+ ...normalizeAutolayoutProps(node),
176
+ children: await Promise.all(node.children.map(normalizeNode)),
177
+ };
178
+ }
179
+
180
+ async function normalizeInstanceNode(node: InstanceNode): Promise<NormalizedInstanceNode> {
181
+ const mainComponent = await node.getMainComponentAsync();
182
+ if (!mainComponent) {
183
+ throw new Error("Instance node has no main component");
184
+ }
185
+
186
+ const componentProperties: NormalizedInstanceNode["componentProperties"] = {};
187
+ for (const [key, value] of Object.entries(node.componentProperties)) {
188
+ componentProperties[key] = value;
189
+ if (value.type === "INSTANCE_SWAP") {
190
+ const mainComponent = (await figma.getNodeByIdAsync(
191
+ value.value as string,
192
+ )) as ComponentNode;
193
+ if (mainComponent) {
194
+ componentProperties[key].componentKey = mainComponent.key;
195
+ }
196
+ }
197
+ }
198
+
199
+ return {
200
+ type: node.type,
201
+ id: node.id,
202
+ name: node.name,
203
+ boundVariables: await normalizeBoundVariables(node),
204
+ ...normalizeRadiusProps(node),
205
+ ...normalizeAutolayoutProps(node),
206
+ children: await Promise.all(node.children.map(normalizeNode)),
207
+ componentKey: mainComponent.key,
208
+ componentSetKey:
209
+ mainComponent.parent?.type === "COMPONENT_SET" ? mainComponent.parent.key : undefined,
210
+ componentProperties,
211
+ };
212
+ }
213
+
214
+ function normalizeSolidPaint(paint: SolidPaint): FigmaRestSpec.SolidPaint {
215
+ return {
216
+ type: paint.type,
217
+ color: {
218
+ r: paint.color.r,
219
+ g: paint.color.g,
220
+ b: paint.color.b,
221
+ a: paint.opacity ?? 1,
222
+ },
223
+ visible: paint.visible,
224
+ blendMode: paint.blendMode ?? "NORMAL",
225
+ boundVariables: paint.boundVariables,
226
+ };
227
+ }
228
+
229
+ function normalizePaint(paint: Paint): FigmaRestSpec.Paint {
230
+ if (paint.type === "SOLID") {
231
+ return normalizeSolidPaint(paint);
232
+ }
233
+ if (paint.type === "IMAGE") {
234
+ return {
235
+ type: "IMAGE",
236
+ scaleMode: paint.scaleMode === "CROP" ? "STRETCH" : paint.scaleMode,
237
+ imageTransform: paint.imageTransform,
238
+ scalingFactor: paint.scalingFactor,
239
+ filters: paint.filters,
240
+ rotation: paint.rotation,
241
+ imageRef: paint.imageHash ?? "",
242
+ blendMode: paint.blendMode ?? "NORMAL",
243
+ visible: paint.visible,
244
+ opacity: paint.opacity,
245
+ };
246
+ }
247
+ throw new Error(`Unimplemented paint type: ${paint.type}`);
248
+ }
249
+
250
+ function normalizePaints(fills: readonly Paint[] | PluginAPI["mixed"]): FigmaRestSpec.Paint[] {
251
+ if (fills === figma.mixed) {
252
+ throw new Error("Mixed fills are not supported");
253
+ }
254
+
255
+ return fills.map(normalizePaint);
256
+ }
257
+
258
+ function normalizeRadiusProps(
259
+ node: Pick<
260
+ RectangleNode,
261
+ "cornerRadius" | "topLeftRadius" | "topRightRadius" | "bottomRightRadius" | "bottomLeftRadius"
262
+ >,
263
+ ) {
264
+ return {
265
+ cornerRadius: node.cornerRadius === figma.mixed ? undefined : node.cornerRadius,
266
+ rectangleCornerRadii: [
267
+ node.topLeftRadius,
268
+ node.topRightRadius,
269
+ node.bottomRightRadius,
270
+ node.bottomLeftRadius,
271
+ ],
272
+ };
273
+ }
274
+
275
+ function normalizeShapeProps(
276
+ node: Pick<
277
+ RectangleNode,
278
+ | "fills"
279
+ | "strokes"
280
+ | "strokeWeight"
281
+ | "layoutGrow"
282
+ | "layoutAlign"
283
+ | "layoutSizingHorizontal"
284
+ | "layoutSizingVertical"
285
+ | "absoluteBoundingBox"
286
+ > &
287
+ Partial<Pick<FrameNode, "inferredAutoLayout">>,
288
+ ) {
289
+ return {
290
+ layoutGrow: (node.inferredAutoLayout?.layoutGrow ?? node.layoutGrow) as 0 | 1 | undefined,
291
+ layoutAlign: node.inferredAutoLayout?.layoutAlign ?? node.layoutAlign,
292
+ layoutSizingHorizontal: node.layoutSizingHorizontal,
293
+ layoutSizingVertical: node.layoutSizingVertical,
294
+ absoluteBoundingBox: node.absoluteBoundingBox,
295
+ fills: normalizePaints(node.fills),
296
+ strokes: normalizePaints(node.strokes),
297
+ strokeWeight: node.strokeWeight === figma.mixed ? undefined : node.strokeWeight,
298
+ };
299
+ }
300
+
301
+ function normalizeAutolayoutProps(node: Omit<FrameNode, "type" | "clone">) {
302
+ return {
303
+ ...normalizeShapeProps(node),
304
+ layoutMode: node.inferredAutoLayout?.layoutMode ?? node.layoutMode,
305
+ layoutWrap: node.inferredAutoLayout?.layoutWrap ?? node.layoutWrap,
306
+ paddingLeft: node.inferredAutoLayout?.paddingLeft ?? node.paddingLeft,
307
+ paddingRight: node.inferredAutoLayout?.paddingRight ?? node.paddingRight,
308
+ paddingTop: node.inferredAutoLayout?.paddingTop ?? node.paddingTop,
309
+ paddingBottom: node.inferredAutoLayout?.paddingBottom ?? node.paddingBottom,
310
+ primaryAxisAlignItems:
311
+ node.inferredAutoLayout?.primaryAxisAlignItems ?? node.primaryAxisAlignItems,
312
+ counterAxisAlignItems:
313
+ node.inferredAutoLayout?.counterAxisAlignItems ?? node.counterAxisAlignItems,
314
+ primaryAxisSizingMode:
315
+ node.inferredAutoLayout?.primaryAxisSizingMode ?? node.primaryAxisSizingMode,
316
+ counterAxisSizingMode:
317
+ node.inferredAutoLayout?.counterAxisSizingMode ?? node.counterAxisSizingMode,
318
+ itemSpacing: node.inferredAutoLayout?.itemSpacing ?? node.itemSpacing,
319
+ counterAxisSpacing:
320
+ node.inferredAutoLayout?.counterAxisSpacing ?? node.counterAxisSpacing ?? undefined,
321
+ };
322
+ }
323
+
324
+ async function normalizeBoundVariables(node: Pick<FrameNode, "boundVariables">) {
325
+ return {
326
+ ...node.boundVariables,
327
+ fills: await Promise.all(
328
+ node.boundVariables?.fills?.map((fill) =>
329
+ figma.variables.getVariableByIdAsync(fill.id).then((res) => {
330
+ return {
331
+ ...fill,
332
+ id: res?.key ?? fill.id,
333
+ };
334
+ }),
335
+ ) ?? [],
336
+ ),
337
+ size: {
338
+ x: node.boundVariables?.width,
339
+ y: node.boundVariables?.height,
340
+ },
341
+ };
342
+ }
343
+
344
+ return normalizeNode;
345
+ }
@@ -0,0 +1,178 @@
1
+ import type * as FigmaRestSpec from "@figma/rest-api-spec";
2
+ import type {
3
+ NormalizedSceneNode,
4
+ NormalizedFrameNode,
5
+ NormalizedRectangleNode,
6
+ NormalizedTextNode,
7
+ NormalizedComponentNode,
8
+ NormalizedInstanceNode,
9
+ NormalizedTextSegment,
10
+ NormalizedVectorNode,
11
+ } from "./types";
12
+
13
+ export interface RestNormalizerContext {
14
+ styles: Record<string, FigmaRestSpec.Style>;
15
+ components: Record<string, FigmaRestSpec.Component>;
16
+ componentSets: Record<string, FigmaRestSpec.ComponentSet>;
17
+ }
18
+
19
+ export function createRestNormalizer(ctx: RestNormalizerContext) {
20
+ function normalizeNode(node: FigmaRestSpec.Node): NormalizedSceneNode {
21
+ if (node.type === "FRAME") {
22
+ return normalizeFrameNode(node);
23
+ }
24
+ if (node.type === "GROUP") {
25
+ return normalizeGroupNode(node);
26
+ }
27
+ if (node.type === "RECTANGLE") {
28
+ return normalizeRectangleNode(node);
29
+ }
30
+ if (node.type === "VECTOR") {
31
+ return normalizeVectorNode(node);
32
+ }
33
+ if (node.type === "TEXT") {
34
+ return normalizeTextNode(node);
35
+ }
36
+ if (node.type === "COMPONENT") {
37
+ return normalizeComponentNode(node);
38
+ }
39
+ if (node.type === "INSTANCE") {
40
+ return normalizeInstanceNode(node);
41
+ }
42
+
43
+ throw new Error(`Unimplemented node type: ${node.type}, ${node.name}`);
44
+ }
45
+
46
+ function normalizeFrameNode(node: FigmaRestSpec.FrameNode): NormalizedFrameNode {
47
+ return {
48
+ ...node,
49
+ children: node.children.map(normalizeNode),
50
+ };
51
+ }
52
+
53
+ function normalizeGroupNode(node: FigmaRestSpec.GroupNode): NormalizedFrameNode {
54
+ return {
55
+ ...node,
56
+ type: "FRAME",
57
+ children: node.children.map(normalizeNode),
58
+ };
59
+ }
60
+
61
+ function normalizeRectangleNode(node: FigmaRestSpec.RectangleNode): NormalizedRectangleNode {
62
+ return node;
63
+ }
64
+
65
+ function normalizeVectorNode(node: FigmaRestSpec.VectorNode): NormalizedVectorNode {
66
+ return node;
67
+ }
68
+
69
+ function normalizeTextNode(node: FigmaRestSpec.TextNode): NormalizedTextNode {
70
+ // Function to segment a text node based on style overrides
71
+ function segmentTextNode(textNode: FigmaRestSpec.TextNode): NormalizedTextSegment[] {
72
+ const segments: NormalizedTextSegment[] = [];
73
+ const characters = textNode.characters;
74
+ const styleOverrides = textNode.characterStyleOverrides || [];
75
+ const styleTable = textNode.styleOverrideTable || {};
76
+
77
+ // If no style overrides, return the entire text as one segment
78
+ if (!styleOverrides.length) {
79
+ return [
80
+ {
81
+ characters: characters,
82
+ start: 0,
83
+ end: characters.length,
84
+ style: textNode.style || {},
85
+ },
86
+ ];
87
+ }
88
+
89
+ let currentSegment: NormalizedTextSegment = {
90
+ characters: "",
91
+ start: 0,
92
+ end: 0,
93
+ style: {},
94
+ };
95
+
96
+ let currentStyleId: string | null = null;
97
+
98
+ for (let i = 0; i < characters.length; i++) {
99
+ const styleId = styleOverrides[i]?.toString() || null;
100
+
101
+ // If style changes or it's the first character
102
+ if (styleId !== currentStyleId || i === 0) {
103
+ // Add the previous segment if it exists
104
+ if (i > 0) {
105
+ currentSegment.end = i;
106
+ currentSegment.characters = characters.substring(
107
+ currentSegment.start,
108
+ currentSegment.end,
109
+ );
110
+ segments.push({ ...currentSegment });
111
+ }
112
+
113
+ // Start a new segment
114
+ currentStyleId = styleId;
115
+ currentSegment = {
116
+ characters: "",
117
+ start: i,
118
+ end: 0,
119
+ style: styleId ? styleTable[styleId] || {} : {},
120
+ };
121
+ }
122
+ }
123
+
124
+ // Add the last segment
125
+ if (currentSegment.start < characters.length) {
126
+ currentSegment.end = characters.length;
127
+ currentSegment.characters = characters.substring(currentSegment.start, currentSegment.end);
128
+ segments.push(currentSegment);
129
+ }
130
+
131
+ return segments;
132
+ }
133
+
134
+ return {
135
+ ...node,
136
+ textStyleKey: node.styles?.["text"] ? ctx.styles[node.styles["text"]]?.key : undefined,
137
+ segments: segmentTextNode(node),
138
+ };
139
+ }
140
+
141
+ function normalizeComponentNode(node: FigmaRestSpec.ComponentNode): NormalizedComponentNode {
142
+ return {
143
+ ...node,
144
+ children: node.children.map(normalizeNode),
145
+ };
146
+ }
147
+
148
+ function normalizeInstanceNode(node: FigmaRestSpec.InstanceNode): NormalizedInstanceNode {
149
+ const mainComponent = ctx.components[node.componentId];
150
+ if (!mainComponent) {
151
+ throw new Error(`Component ${node.componentId} not found`);
152
+ }
153
+ const componentSet = mainComponent.componentSetId
154
+ ? ctx.componentSets[mainComponent.componentSetId]
155
+ : undefined;
156
+ const componentProperties: NormalizedInstanceNode["componentProperties"] = {};
157
+
158
+ for (const [key, value] of Object.entries(node.componentProperties ?? {})) {
159
+ componentProperties[key] = value;
160
+ if (value.type === "INSTANCE_SWAP") {
161
+ const mainComponent = ctx.components[value.value as string];
162
+ if (mainComponent) {
163
+ componentProperties[key].componentKey = mainComponent.key;
164
+ }
165
+ }
166
+ }
167
+
168
+ return {
169
+ ...node,
170
+ children: node.children.map(normalizeNode),
171
+ componentKey: mainComponent.key,
172
+ componentSetKey: componentSet?.key,
173
+ componentProperties,
174
+ };
175
+ }
176
+
177
+ return normalizeNode;
178
+ }
@@ -0,0 +1,94 @@
1
+ import type * as FigmaRestSpec from "@figma/rest-api-spec";
2
+
3
+ export type CommonProps = "type" | "id" | "name" | "boundVariables";
4
+
5
+ export type RadiusProps = "cornerRadius" | "rectangleCornerRadii";
6
+
7
+ export type ShapeProps =
8
+ | "layoutGrow"
9
+ | "layoutAlign"
10
+ | "layoutSizingHorizontal"
11
+ | "layoutSizingVertical"
12
+ | "absoluteBoundingBox"
13
+ | "fills"
14
+ | "strokes"
15
+ | "strokeWeight";
16
+
17
+ export type LayoutProps =
18
+ | "layoutMode"
19
+ | "layoutWrap"
20
+ | "paddingLeft"
21
+ | "paddingRight"
22
+ | "paddingTop"
23
+ | "paddingBottom"
24
+ | "primaryAxisAlignItems"
25
+ | "counterAxisAlignItems"
26
+ | "primaryAxisSizingMode"
27
+ | "counterAxisSizingMode"
28
+ | "itemSpacing"
29
+ | "counterAxisSpacing";
30
+
31
+ export interface NormalizedFrameNode
32
+ extends Pick<FigmaRestSpec.FrameNode, CommonProps | ShapeProps | RadiusProps | LayoutProps> {
33
+ children: NormalizedSceneNode[];
34
+ }
35
+
36
+ export interface NormalizedRectangleNode
37
+ extends Pick<FigmaRestSpec.RectangleNode, CommonProps | ShapeProps | RadiusProps> {}
38
+
39
+ export interface NormalizedTextNode
40
+ extends Pick<
41
+ FigmaRestSpec.TextNode,
42
+ CommonProps | "layoutGrow" | "layoutAlign" | "style" | "characters" | "fills"
43
+ > {
44
+ segments: NormalizedTextSegment[];
45
+ textStyleKey?: string;
46
+ }
47
+
48
+ export interface NormalizedTextSegment {
49
+ characters: string;
50
+ start: number;
51
+ end: number;
52
+ style: {
53
+ fontFamily?: string;
54
+ fontWeight?: number;
55
+ fontSize?: number;
56
+ italic?: boolean;
57
+ textDecoration?: string;
58
+ letterSpacing?: number;
59
+ lineHeight?: number | { unit: string; value: number };
60
+ };
61
+ }
62
+
63
+ export interface NormalizedComponentNode
64
+ extends Pick<FigmaRestSpec.ComponentNode, CommonProps | ShapeProps | RadiusProps | LayoutProps> {
65
+ children: NormalizedSceneNode[];
66
+ }
67
+
68
+ export interface NormalizedInstanceNode
69
+ extends Pick<FigmaRestSpec.InstanceNode, CommonProps | ShapeProps | RadiusProps | LayoutProps> {
70
+ componentProperties: {
71
+ [key: string]: FigmaRestSpec.ComponentProperty & { componentKey?: string };
72
+ };
73
+
74
+ componentKey: string;
75
+
76
+ componentSetKey?: string;
77
+
78
+ children: NormalizedSceneNode[];
79
+ }
80
+
81
+ export interface NormalizedVectorNode
82
+ extends Pick<FigmaRestSpec.VectorNode, CommonProps | ShapeProps> {}
83
+
84
+ export interface NormalizedBooleanOperationNode
85
+ extends Pick<FigmaRestSpec.BooleanOperationNode, CommonProps | "fills"> {}
86
+
87
+ export type NormalizedSceneNode =
88
+ | NormalizedFrameNode
89
+ | NormalizedRectangleNode
90
+ | NormalizedTextNode
91
+ | NormalizedComponentNode
92
+ | NormalizedInstanceNode
93
+ | NormalizedVectorNode
94
+ | NormalizedBooleanOperationNode;
package/src/sizing.ts ADDED
@@ -0,0 +1,58 @@
1
+ import type { NormalizedFrameNode } from "./normalizer/types";
2
+ import { getLayoutVariableName, inferDimension } from "./variable";
3
+
4
+ type SizingPropHandler = (props: {
5
+ boundVariables: NonNullable<NormalizedFrameNode["boundVariables"]>;
6
+ layoutSizingHorizontal: FrameNode["layoutSizingHorizontal"];
7
+ layoutSizingVertical: FrameNode["layoutSizingVertical"];
8
+ width: FrameNode["width"];
9
+ height: FrameNode["height"];
10
+ }) => string | number | boolean | undefined;
11
+
12
+ const sizingPropHandlers = {
13
+ height: ({ boundVariables, layoutSizingVertical, height }) =>
14
+ layoutSizingVertical === "FIXED"
15
+ ? boundVariables.size?.y
16
+ ? getLayoutVariableName(boundVariables.size.y.id)
17
+ : inferDimension(height)
18
+ : undefined,
19
+ width: ({ boundVariables, layoutSizingHorizontal, width }) =>
20
+ layoutSizingHorizontal === "FIXED"
21
+ ? boundVariables.size?.x
22
+ ? getLayoutVariableName(boundVariables.size.x.id)
23
+ : inferDimension(width)
24
+ : undefined,
25
+ } satisfies Record<string, SizingPropHandler>;
26
+
27
+ export function createSizingProps(
28
+ node: Pick<
29
+ NormalizedFrameNode,
30
+ "boundVariables" | "layoutSizingHorizontal" | "layoutSizingVertical" | "absoluteBoundingBox"
31
+ >,
32
+ ): Record<string, string | number | boolean> {
33
+ const boundVariables = node.boundVariables;
34
+ const layoutSizingHorizontal = node.layoutSizingHorizontal ?? "FIXED";
35
+ const layoutSizingVertical = node.layoutSizingVertical ?? "FIXED";
36
+ const { width, height } = node.absoluteBoundingBox ?? { width: 0, height: 0 };
37
+
38
+ if (!boundVariables) {
39
+ return {};
40
+ }
41
+
42
+ const result: Record<string, string | number | boolean> = {};
43
+
44
+ for (const [prop, handler] of Object.entries(sizingPropHandlers)) {
45
+ const value = handler({
46
+ boundVariables,
47
+ layoutSizingHorizontal,
48
+ layoutSizingVertical,
49
+ width,
50
+ height,
51
+ });
52
+ if (value !== undefined) {
53
+ result[prop] = value;
54
+ }
55
+ }
56
+
57
+ return result;
58
+ }
package/src/text.ts ADDED
@@ -0,0 +1,20 @@
1
+ import type { NormalizedTextNode } from "./normalizer/types";
2
+ import { getTypographyVariableName } from "./variable";
3
+
4
+ export function createTextProps(boundVariables: NormalizedTextNode["boundVariables"]) {
5
+ const fontSizeBoundVariables = boundVariables?.fontSize?.[0];
6
+ const fontStyleBoundVariables = boundVariables?.fontStyle?.[0];
7
+ const lineHeightBoundVariables = boundVariables?.lineHeight?.[0];
8
+
9
+ return {
10
+ fontSize: fontSizeBoundVariables
11
+ ? getTypographyVariableName(fontSizeBoundVariables.id)
12
+ : undefined,
13
+ fontWeight: fontStyleBoundVariables
14
+ ? getTypographyVariableName(fontStyleBoundVariables.id)
15
+ : undefined,
16
+ lineHeight: lineHeightBoundVariables
17
+ ? getTypographyVariableName(lineHeightBoundVariables.id)
18
+ : undefined,
19
+ };
20
+ }