@seed-design/figma 0.0.2 → 0.0.5

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 (185) hide show
  1. package/lib/index.cjs +10622 -12209
  2. package/lib/index.d.ts +1734 -18
  3. package/lib/index.js +10598 -12210
  4. package/package.json +2 -2
  5. package/src/codegen/context.ts +148 -0
  6. package/src/{component/type-helper.ts → codegen/core/component.ts} +1 -1
  7. package/src/codegen/core/index.ts +14 -0
  8. package/src/{jsx.ts → codegen/core/jsx.ts} +13 -3
  9. package/src/codegen/core/transformer.ts +40 -0
  10. package/src/{data → codegen/data}/icons.ts +2 -6
  11. package/src/{data → codegen/data}/styles.ts +87 -29
  12. package/src/codegen/data/variable-collections.ts +310 -0
  13. package/src/{data → codegen/data}/variables.ts +2821 -5887
  14. package/src/codegen/domain/codegen.service.ts +69 -0
  15. package/src/codegen/domain/figma-component.service.ts +21 -0
  16. package/src/codegen/domain/frame.service.ts +108 -0
  17. package/src/codegen/domain/icon.interface.ts +5 -0
  18. package/src/codegen/domain/icon.repository.ts +11 -0
  19. package/src/codegen/domain/icon.service.ts +35 -0
  20. package/src/codegen/domain/index.ts +22 -0
  21. package/src/codegen/domain/instance.service.ts +91 -0
  22. package/src/codegen/domain/props/container-layout-props.service.ts +248 -0
  23. package/src/codegen/domain/props/fill-props.service.ts +75 -0
  24. package/src/codegen/domain/props/radius-props.service.ts +105 -0
  25. package/src/codegen/domain/props/self-layout-props.service.ts +127 -0
  26. package/src/codegen/domain/props/stroke-props.service.ts +45 -0
  27. package/src/codegen/domain/props/type-style-props.service.ts +31 -0
  28. package/src/codegen/domain/rectangle.service.ts +31 -0
  29. package/src/codegen/domain/seed-component/deps.interface.ts +6 -0
  30. package/src/codegen/domain/seed-component/index.ts +75 -0
  31. package/src/{component/type.ts → codegen/domain/seed-component/properties.type.ts} +2 -2
  32. package/src/{component/properties.ts → codegen/domain/seed-component/size.ts} +4 -2
  33. package/src/codegen/domain/seed-component/transformers/action-button.ts +69 -0
  34. package/src/codegen/domain/seed-component/transformers/action-chip.ts +82 -0
  35. package/src/codegen/domain/seed-component/transformers/action-sheet.ts +78 -0
  36. package/src/codegen/domain/seed-component/transformers/app-bar.ts +186 -0
  37. package/src/codegen/domain/seed-component/transformers/avatar-stack.ts +30 -0
  38. package/src/codegen/domain/seed-component/transformers/avatar.ts +39 -0
  39. package/src/codegen/domain/seed-component/transformers/badge.ts +22 -0
  40. package/src/codegen/domain/seed-component/transformers/callout.ts +90 -0
  41. package/src/codegen/domain/seed-component/transformers/checkbox.ts +34 -0
  42. package/src/codegen/domain/seed-component/transformers/chip-tabs.ts +55 -0
  43. package/src/codegen/domain/seed-component/transformers/control-chip.ts +86 -0
  44. package/src/codegen/domain/seed-component/transformers/error-state.ts +39 -0
  45. package/src/codegen/domain/seed-component/transformers/extended-action-sheet.ts +99 -0
  46. package/src/codegen/domain/seed-component/transformers/extended-fab.ts +25 -0
  47. package/src/codegen/domain/seed-component/transformers/fab.ts +18 -0
  48. package/src/codegen/domain/seed-component/transformers/help-bubble.ts +68 -0
  49. package/src/codegen/domain/seed-component/transformers/identity-placeholder.ts +18 -0
  50. package/src/codegen/domain/seed-component/transformers/inline-banner.ts +81 -0
  51. package/src/codegen/domain/seed-component/transformers/manner-temp-badge.ts +19 -0
  52. package/src/codegen/domain/seed-component/transformers/multiline-text-field.ts +82 -0
  53. package/src/codegen/domain/seed-component/transformers/progress-circle.ts +51 -0
  54. package/src/codegen/domain/seed-component/transformers/reaction-button.ts +37 -0
  55. package/src/codegen/domain/seed-component/transformers/segmented-control.ts +59 -0
  56. package/src/codegen/domain/seed-component/transformers/select-box.ts +82 -0
  57. package/src/codegen/domain/seed-component/transformers/skeleton.ts +52 -0
  58. package/src/codegen/domain/seed-component/transformers/snackbar.ts +23 -0
  59. package/src/codegen/domain/seed-component/transformers/switch.ts +31 -0
  60. package/src/codegen/domain/seed-component/transformers/tabs.ts +129 -0
  61. package/src/codegen/domain/seed-component/transformers/text-button.ts +55 -0
  62. package/src/codegen/domain/seed-component/transformers/text-field.ts +109 -0
  63. package/src/codegen/domain/seed-component/transformers/toggle-button.ts +47 -0
  64. package/src/codegen/domain/style.interface.ts +5 -0
  65. package/src/codegen/domain/style.repository.ts +23 -0
  66. package/src/codegen/domain/style.service.ts +38 -0
  67. package/src/codegen/domain/text.service.ts +62 -0
  68. package/src/codegen/domain/variable.interface.ts +18 -0
  69. package/src/codegen/domain/variable.repository.ts +44 -0
  70. package/src/codegen/domain/variable.service.ts +95 -0
  71. package/src/codegen/index.ts +13 -0
  72. package/src/index.ts +2 -3
  73. package/src/normalizer/from-plugin.ts +52 -31
  74. package/src/normalizer/from-rest.ts +27 -5
  75. package/src/normalizer/index.ts +3 -0
  76. package/src/normalizer/types.ts +81 -31
  77. package/src/utils/common.ts +34 -0
  78. package/src/utils/css.ts +13 -0
  79. package/src/{node-util.ts → utils/figma-node.ts} +1 -1
  80. package/src/utils/figma-variable.ts +13 -0
  81. package/src/color.ts +0 -78
  82. package/src/component/index.ts +0 -1688
  83. package/src/generate-code.ts +0 -213
  84. package/src/icon.ts +0 -46
  85. package/src/layout.ts +0 -289
  86. package/src/sizing.ts +0 -58
  87. package/src/text.ts +0 -20
  88. package/src/util.ts +0 -17
  89. package/src/variable.ts +0 -66
  90. /package/src/{data → codegen/data}/__generated__/component-sets/action-button.d.ts +0 -0
  91. /package/src/{data → codegen/data}/__generated__/component-sets/action-button.mjs +0 -0
  92. /package/src/{data → codegen/data}/__generated__/component-sets/action-chip.d.ts +0 -0
  93. /package/src/{data → codegen/data}/__generated__/component-sets/action-chip.mjs +0 -0
  94. /package/src/{data → codegen/data}/__generated__/component-sets/action-sheet.d.ts +0 -0
  95. /package/src/{data → codegen/data}/__generated__/component-sets/action-sheet.mjs +0 -0
  96. /package/src/{data → codegen/data}/__generated__/component-sets/avatar-stack.d.ts +0 -0
  97. /package/src/{data → codegen/data}/__generated__/component-sets/avatar-stack.mjs +0 -0
  98. /package/src/{data → codegen/data}/__generated__/component-sets/avatar.d.ts +0 -0
  99. /package/src/{data → codegen/data}/__generated__/component-sets/avatar.mjs +0 -0
  100. /package/src/{data → codegen/data}/__generated__/component-sets/badge.d.ts +0 -0
  101. /package/src/{data → codegen/data}/__generated__/component-sets/badge.mjs +0 -0
  102. /package/src/{data → codegen/data}/__generated__/component-sets/bottom-navigation-global.d.ts +0 -0
  103. /package/src/{data → codegen/data}/__generated__/component-sets/bottom-navigation-global.mjs +0 -0
  104. /package/src/{data → codegen/data}/__generated__/component-sets/bottom-navigation-kr.d.ts +0 -0
  105. /package/src/{data → codegen/data}/__generated__/component-sets/bottom-navigation-kr.mjs +0 -0
  106. /package/src/{data → codegen/data}/__generated__/component-sets/bottom-sheet.d.ts +0 -0
  107. /package/src/{data → codegen/data}/__generated__/component-sets/bottom-sheet.mjs +0 -0
  108. /package/src/{data → codegen/data}/__generated__/component-sets/callout.d.ts +0 -0
  109. /package/src/{data → codegen/data}/__generated__/component-sets/callout.mjs +0 -0
  110. /package/src/{data → codegen/data}/__generated__/component-sets/checkbox.d.ts +0 -0
  111. /package/src/{data → codegen/data}/__generated__/component-sets/checkbox.mjs +0 -0
  112. /package/src/{data → codegen/data}/__generated__/component-sets/chip-tablist.d.ts +0 -0
  113. /package/src/{data → codegen/data}/__generated__/component-sets/chip-tablist.mjs +0 -0
  114. /package/src/{data → codegen/data}/__generated__/component-sets/control-chip.d.ts +0 -0
  115. /package/src/{data → codegen/data}/__generated__/component-sets/control-chip.mjs +0 -0
  116. /package/src/{data → codegen/data}/__generated__/component-sets/divider.d.ts +0 -0
  117. /package/src/{data → codegen/data}/__generated__/component-sets/divider.mjs +0 -0
  118. /package/src/{data → codegen/data}/__generated__/component-sets/error-state.d.ts +0 -0
  119. /package/src/{data → codegen/data}/__generated__/component-sets/error-state.mjs +0 -0
  120. /package/src/{data → codegen/data}/__generated__/component-sets/extended-action-sheet.d.ts +0 -0
  121. /package/src/{data → codegen/data}/__generated__/component-sets/extended-action-sheet.mjs +0 -0
  122. /package/src/{data → codegen/data}/__generated__/component-sets/extended-floating-action-button.d.ts +0 -0
  123. /package/src/{data → codegen/data}/__generated__/component-sets/extended-floating-action-button.mjs +0 -0
  124. /package/src/{data → codegen/data}/__generated__/component-sets/floating-action-button.d.ts +0 -0
  125. /package/src/{data → codegen/data}/__generated__/component-sets/floating-action-button.mjs +0 -0
  126. /package/src/{data → codegen/data}/__generated__/component-sets/help-bubble.d.ts +0 -0
  127. /package/src/{data → codegen/data}/__generated__/component-sets/help-bubble.mjs +0 -0
  128. /package/src/{data → codegen/data}/__generated__/component-sets/identity-placeholder.d.ts +0 -0
  129. /package/src/{data → codegen/data}/__generated__/component-sets/identity-placeholder.mjs +0 -0
  130. /package/src/{data → codegen/data}/__generated__/component-sets/index.d.ts +0 -0
  131. /package/src/{data → codegen/data}/__generated__/component-sets/index.mjs +0 -0
  132. /package/src/{data → codegen/data}/__generated__/component-sets/inline-banner.d.ts +0 -0
  133. /package/src/{data → codegen/data}/__generated__/component-sets/inline-banner.mjs +0 -0
  134. /package/src/{data → codegen/data}/__generated__/component-sets/main-tab-navigation-global.d.ts +0 -0
  135. /package/src/{data → codegen/data}/__generated__/component-sets/main-tab-navigation-global.mjs +0 -0
  136. /package/src/{data → codegen/data}/__generated__/component-sets/main-tab-navigation-kr.d.ts +0 -0
  137. /package/src/{data → codegen/data}/__generated__/component-sets/main-tab-navigation-kr.mjs +0 -0
  138. /package/src/{data → codegen/data}/__generated__/component-sets/manner-temp-badge.d.ts +0 -0
  139. /package/src/{data → codegen/data}/__generated__/component-sets/manner-temp-badge.mjs +0 -0
  140. /package/src/{data → codegen/data}/__generated__/component-sets/manner-temp-bar.d.ts +0 -0
  141. /package/src/{data → codegen/data}/__generated__/component-sets/manner-temp-bar.mjs +0 -0
  142. /package/src/{data → codegen/data}/__generated__/component-sets/manner-temp.d.ts +0 -0
  143. /package/src/{data → codegen/data}/__generated__/component-sets/manner-temp.mjs +0 -0
  144. /package/src/{data → codegen/data}/__generated__/component-sets/multiline-text-field.d.ts +0 -0
  145. /package/src/{data → codegen/data}/__generated__/component-sets/multiline-text-field.mjs +0 -0
  146. /package/src/{data → codegen/data}/__generated__/component-sets/progress-circle.d.ts +0 -0
  147. /package/src/{data → codegen/data}/__generated__/component-sets/progress-circle.mjs +0 -0
  148. /package/src/{data → codegen/data}/__generated__/component-sets/radio.d.ts +0 -0
  149. /package/src/{data → codegen/data}/__generated__/component-sets/radio.mjs +0 -0
  150. /package/src/{data → codegen/data}/__generated__/component-sets/range-slider.d.ts +0 -0
  151. /package/src/{data → codegen/data}/__generated__/component-sets/range-slider.mjs +0 -0
  152. /package/src/{data → codegen/data}/__generated__/component-sets/reaction-button.d.ts +0 -0
  153. /package/src/{data → codegen/data}/__generated__/component-sets/reaction-button.mjs +0 -0
  154. /package/src/{data → codegen/data}/__generated__/component-sets/segmented-control.d.ts +0 -0
  155. /package/src/{data → codegen/data}/__generated__/component-sets/segmented-control.mjs +0 -0
  156. /package/src/{data → codegen/data}/__generated__/component-sets/select-box.d.ts +0 -0
  157. /package/src/{data → codegen/data}/__generated__/component-sets/select-box.mjs +0 -0
  158. /package/src/{data → codegen/data}/__generated__/component-sets/skeleton.d.ts +0 -0
  159. /package/src/{data → codegen/data}/__generated__/component-sets/skeleton.mjs +0 -0
  160. /package/src/{data → codegen/data}/__generated__/component-sets/slider.d.ts +0 -0
  161. /package/src/{data → codegen/data}/__generated__/component-sets/slider.mjs +0 -0
  162. /package/src/{data → codegen/data}/__generated__/component-sets/snackbar.d.ts +0 -0
  163. /package/src/{data → codegen/data}/__generated__/component-sets/snackbar.mjs +0 -0
  164. /package/src/{data → codegen/data}/__generated__/component-sets/standard-navigation.d.ts +0 -0
  165. /package/src/{data → codegen/data}/__generated__/component-sets/standard-navigation.mjs +0 -0
  166. /package/src/{data → codegen/data}/__generated__/component-sets/switch.d.ts +0 -0
  167. /package/src/{data → codegen/data}/__generated__/component-sets/switch.mjs +0 -0
  168. /package/src/{data → codegen/data}/__generated__/component-sets/tablist.d.ts +0 -0
  169. /package/src/{data → codegen/data}/__generated__/component-sets/tablist.mjs +0 -0
  170. /package/src/{data → codegen/data}/__generated__/component-sets/template-bottom-fixed-bar.d.ts +0 -0
  171. /package/src/{data → codegen/data}/__generated__/component-sets/template-bottom-fixed-bar.mjs +0 -0
  172. /package/src/{data → codegen/data}/__generated__/component-sets/template-button-group.d.ts +0 -0
  173. /package/src/{data → codegen/data}/__generated__/component-sets/template-button-group.mjs +0 -0
  174. /package/src/{data → codegen/data}/__generated__/component-sets/template-chip-group.d.ts +0 -0
  175. /package/src/{data → codegen/data}/__generated__/component-sets/template-chip-group.mjs +0 -0
  176. /package/src/{data → codegen/data}/__generated__/component-sets/template-select-box-group.d.ts +0 -0
  177. /package/src/{data → codegen/data}/__generated__/component-sets/template-select-box-group.mjs +0 -0
  178. /package/src/{data → codegen/data}/__generated__/component-sets/template-top-navigation.d.ts +0 -0
  179. /package/src/{data → codegen/data}/__generated__/component-sets/template-top-navigation.mjs +0 -0
  180. /package/src/{data → codegen/data}/__generated__/component-sets/text-button.d.ts +0 -0
  181. /package/src/{data → codegen/data}/__generated__/component-sets/text-button.mjs +0 -0
  182. /package/src/{data → codegen/data}/__generated__/component-sets/text-field.d.ts +0 -0
  183. /package/src/{data → codegen/data}/__generated__/component-sets/text-field.mjs +0 -0
  184. /package/src/{data → codegen/data}/__generated__/component-sets/toggle-button.d.ts +0 -0
  185. /package/src/{data → codegen/data}/__generated__/component-sets/toggle-button.mjs +0 -0
@@ -7,9 +7,14 @@ import type {
7
7
  NormalizedComponentNode,
8
8
  NormalizedInstanceNode,
9
9
  NormalizedVectorNode,
10
+ NormalizedBooleanOperationNode,
10
11
  } from "./types";
11
12
 
12
13
  export function createPluginNormalizer() {
14
+ async function normalizeNodes(nodes: readonly SceneNode[]): Promise<NormalizedSceneNode[]> {
15
+ return Promise.all(nodes.filter((node) => node.visible).map(normalizeNode));
16
+ }
17
+
13
18
  async function normalizeNode(node: SceneNode): Promise<NormalizedSceneNode> {
14
19
  if (node.type === "FRAME") {
15
20
  return normalizeFrameNode(node);
@@ -23,6 +28,9 @@ export function createPluginNormalizer() {
23
28
  if (node.type === "VECTOR") {
24
29
  return normalizeVectorNode(node);
25
30
  }
31
+ if (node.type === "BOOLEAN_OPERATION") {
32
+ return normalizeBooleanOperationNode(node);
33
+ }
26
34
  if (node.type === "TEXT") {
27
35
  return normalizeTextNode(node);
28
36
  }
@@ -33,7 +41,11 @@ export function createPluginNormalizer() {
33
41
  return normalizeInstanceNode(node);
34
42
  }
35
43
 
36
- throw new Error(`Unimplemented node type: ${node.type}`);
44
+ return {
45
+ type: "UNHANDLED",
46
+ id: node.id,
47
+ original: node,
48
+ };
37
49
  }
38
50
 
39
51
  async function normalizeFrameNode(node: FrameNode): Promise<NormalizedFrameNode> {
@@ -44,7 +56,7 @@ export function createPluginNormalizer() {
44
56
  boundVariables: await normalizeBoundVariables(node),
45
57
  ...normalizeRadiusProps(node),
46
58
  ...normalizeAutolayoutProps(node),
47
- children: await Promise.all(node.children.map(normalizeNode)),
59
+ children: await normalizeNodes(node.children),
48
60
  };
49
61
  }
50
62
 
@@ -75,7 +87,7 @@ export function createPluginNormalizer() {
75
87
  counterAxisSpacing: node.inferredAutoLayout?.counterAxisSpacing ?? undefined,
76
88
  fills: [],
77
89
  strokes: [],
78
- children: await Promise.all(node.children.map(normalizeNode)),
90
+ children: await normalizeNodes(node.children),
79
91
  };
80
92
  }
81
93
 
@@ -100,6 +112,18 @@ export function createPluginNormalizer() {
100
112
  };
101
113
  }
102
114
 
115
+ async function normalizeBooleanOperationNode(
116
+ node: BooleanOperationNode,
117
+ ): Promise<NormalizedBooleanOperationNode> {
118
+ return {
119
+ type: node.type,
120
+ id: node.id,
121
+ name: node.name,
122
+ boundVariables: await normalizeBoundVariables(node),
123
+ children: await normalizeNodes(node.children),
124
+ ...normalizeShapeProps(node),
125
+ };
126
+ }
103
127
  async function normalizeTextNode(node: TextNode): Promise<NormalizedTextNode> {
104
128
  const segments = node.getStyledTextSegments([
105
129
  "fontSize",
@@ -112,7 +136,7 @@ export function createPluginNormalizer() {
112
136
  "fills",
113
137
  "boundVariables",
114
138
  ]);
115
- const first = segments[0]!; // TODO: handle multiple segments
139
+ const first = segments[0]!;
116
140
 
117
141
  const textStyleKey =
118
142
  typeof node.textStyleId === "string"
@@ -124,8 +148,6 @@ export function createPluginNormalizer() {
124
148
  id: node.id,
125
149
  name: node.name,
126
150
  boundVariables: await normalizeBoundVariables(node),
127
- layoutGrow: node.layoutGrow as 0 | 1 | undefined,
128
- layoutAlign: node.layoutAlign,
129
151
  style: {
130
152
  fontSize: first.fontSize,
131
153
  fontWeight: first.fontWeight,
@@ -139,29 +161,20 @@ export function createPluginNormalizer() {
139
161
  },
140
162
  ...(textStyleKey ? { textStyleKey } : {}),
141
163
  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),
164
+ segments: segments.map((segment) => ({
165
+ characters: segment.characters,
166
+ start: segment.start,
167
+ end: segment.end,
168
+ style: {
169
+ fontSize: segment.fontSize,
170
+ fontWeight: segment.fontWeight,
171
+ fontFamily: segment.fontName.family,
172
+ letterSpacing:
173
+ segment.letterSpacing.unit === "PIXELS" ? segment.letterSpacing.value : undefined,
174
+ lineHeightPx: segment.lineHeight.unit === "PIXELS" ? segment.lineHeight.value : undefined,
175
+ },
176
+ })),
177
+ ...normalizeShapeProps(node),
165
178
  };
166
179
  }
167
180
 
@@ -173,7 +186,7 @@ export function createPluginNormalizer() {
173
186
  boundVariables: await normalizeBoundVariables(node),
174
187
  ...normalizeRadiusProps(node),
175
188
  ...normalizeAutolayoutProps(node),
176
- children: await Promise.all(node.children.map(normalizeNode)),
189
+ children: await normalizeNodes(node.children),
177
190
  };
178
191
  }
179
192
 
@@ -203,7 +216,7 @@ export function createPluginNormalizer() {
203
216
  boundVariables: await normalizeBoundVariables(node),
204
217
  ...normalizeRadiusProps(node),
205
218
  ...normalizeAutolayoutProps(node),
206
- children: await Promise.all(node.children.map(normalizeNode)),
219
+ children: await normalizeNodes(node.children),
207
220
  componentKey: mainComponent.key,
208
221
  componentSetKey:
209
222
  mainComponent.parent?.type === "COMPONENT_SET" ? mainComponent.parent.key : undefined,
@@ -283,6 +296,10 @@ export function createPluginNormalizer() {
283
296
  | "layoutSizingHorizontal"
284
297
  | "layoutSizingVertical"
285
298
  | "absoluteBoundingBox"
299
+ | "minHeight"
300
+ | "minWidth"
301
+ | "maxHeight"
302
+ | "maxWidth"
286
303
  > &
287
304
  Partial<Pick<FrameNode, "inferredAutoLayout">>,
288
305
  ) {
@@ -295,6 +312,10 @@ export function createPluginNormalizer() {
295
312
  fills: normalizePaints(node.fills),
296
313
  strokes: normalizePaints(node.strokes),
297
314
  strokeWeight: node.strokeWeight === figma.mixed ? undefined : node.strokeWeight,
315
+ minHeight: node.minHeight ?? undefined,
316
+ minWidth: node.minWidth ?? undefined,
317
+ maxHeight: node.maxHeight ?? undefined,
318
+ maxWidth: node.maxWidth ?? undefined,
298
319
  };
299
320
  }
300
321
 
@@ -8,6 +8,7 @@ import type {
8
8
  NormalizedInstanceNode,
9
9
  NormalizedTextSegment,
10
10
  NormalizedVectorNode,
11
+ NormalizedBooleanOperationNode,
11
12
  } from "./types";
12
13
 
13
14
  export interface RestNormalizerContext {
@@ -17,6 +18,11 @@ export interface RestNormalizerContext {
17
18
  }
18
19
 
19
20
  export function createRestNormalizer(ctx: RestNormalizerContext) {
21
+ function normalizeNodes(nodes: readonly FigmaRestSpec.Node[]): NormalizedSceneNode[] {
22
+ // Figma REST API omits default values for some fields, "visible" is one of them
23
+ return nodes.filter((node) => !("visible" in node) || node.visible).map(normalizeNode);
24
+ }
25
+
20
26
  function normalizeNode(node: FigmaRestSpec.Node): NormalizedSceneNode {
21
27
  if (node.type === "FRAME") {
22
28
  return normalizeFrameNode(node);
@@ -30,6 +36,9 @@ export function createRestNormalizer(ctx: RestNormalizerContext) {
30
36
  if (node.type === "VECTOR") {
31
37
  return normalizeVectorNode(node);
32
38
  }
39
+ if (node.type === "BOOLEAN_OPERATION") {
40
+ return normalizeBooleanOperationNode(node);
41
+ }
33
42
  if (node.type === "TEXT") {
34
43
  return normalizeTextNode(node);
35
44
  }
@@ -40,13 +49,17 @@ export function createRestNormalizer(ctx: RestNormalizerContext) {
40
49
  return normalizeInstanceNode(node);
41
50
  }
42
51
 
43
- throw new Error(`Unimplemented node type: ${node.type}, ${node.name}`);
52
+ return {
53
+ type: "UNHANDLED",
54
+ id: node.id,
55
+ original: node,
56
+ };
44
57
  }
45
58
 
46
59
  function normalizeFrameNode(node: FigmaRestSpec.FrameNode): NormalizedFrameNode {
47
60
  return {
48
61
  ...node,
49
- children: node.children.map(normalizeNode),
62
+ children: normalizeNodes(node.children),
50
63
  };
51
64
  }
52
65
 
@@ -54,7 +67,7 @@ export function createRestNormalizer(ctx: RestNormalizerContext) {
54
67
  return {
55
68
  ...node,
56
69
  type: "FRAME",
57
- children: node.children.map(normalizeNode),
70
+ children: normalizeNodes(node.children),
58
71
  };
59
72
  }
60
73
 
@@ -66,6 +79,15 @@ export function createRestNormalizer(ctx: RestNormalizerContext) {
66
79
  return node;
67
80
  }
68
81
 
82
+ function normalizeBooleanOperationNode(
83
+ node: FigmaRestSpec.BooleanOperationNode,
84
+ ): NormalizedBooleanOperationNode {
85
+ return {
86
+ ...node,
87
+ children: normalizeNodes(node.children),
88
+ };
89
+ }
90
+
69
91
  function normalizeTextNode(node: FigmaRestSpec.TextNode): NormalizedTextNode {
70
92
  // Function to segment a text node based on style overrides
71
93
  function segmentTextNode(textNode: FigmaRestSpec.TextNode): NormalizedTextSegment[] {
@@ -141,7 +163,7 @@ export function createRestNormalizer(ctx: RestNormalizerContext) {
141
163
  function normalizeComponentNode(node: FigmaRestSpec.ComponentNode): NormalizedComponentNode {
142
164
  return {
143
165
  ...node,
144
- children: node.children.map(normalizeNode),
166
+ children: normalizeNodes(node.children),
145
167
  };
146
168
  }
147
169
 
@@ -167,7 +189,7 @@ export function createRestNormalizer(ctx: RestNormalizerContext) {
167
189
 
168
190
  return {
169
191
  ...node,
170
- children: node.children.map(normalizeNode),
192
+ children: normalizeNodes(node.children),
171
193
  componentKey: mainComponent.key,
172
194
  componentSetKey: componentSet?.key,
173
195
  componentProperties,
@@ -0,0 +1,3 @@
1
+ export * from "./types";
2
+ export { createRestNormalizer } from "./from-rest";
3
+ export { createPluginNormalizer } from "./from-plugin";
@@ -1,20 +1,40 @@
1
1
  import type * as FigmaRestSpec from "@figma/rest-api-spec";
2
2
 
3
- export type CommonProps = "type" | "id" | "name" | "boundVariables";
3
+ export type NormalizedIsLayerTrait = Pick<
4
+ FigmaRestSpec.IsLayerTrait,
5
+ "type" | "id" | "name" | "boundVariables"
6
+ >;
4
7
 
5
- export type RadiusProps = "cornerRadius" | "rectangleCornerRadii";
8
+ export type NormalizedCornerTrait = Pick<
9
+ FigmaRestSpec.CornerTrait,
10
+ "cornerRadius" | "rectangleCornerRadii"
11
+ >;
6
12
 
7
- export type ShapeProps =
8
- | "layoutGrow"
13
+ export type NormalizedHasChildrenTrait = {
14
+ children: NormalizedSceneNode[];
15
+ };
16
+
17
+ export type NormalizedHasLayoutTrait = Pick<
18
+ FigmaRestSpec.HasLayoutTrait,
9
19
  | "layoutAlign"
20
+ | "layoutGrow"
21
+ | "absoluteBoundingBox"
22
+ | "layoutPositioning"
10
23
  | "layoutSizingHorizontal"
11
24
  | "layoutSizingVertical"
12
- | "absoluteBoundingBox"
13
- | "fills"
14
- | "strokes"
15
- | "strokeWeight";
16
-
17
- export type LayoutProps =
25
+ | "minHeight"
26
+ | "minWidth"
27
+ | "maxHeight"
28
+ | "maxWidth"
29
+ >;
30
+
31
+ export type NormalizedHasGeometryTrait = Pick<
32
+ FigmaRestSpec.HasGeometryTrait,
33
+ "fills" | "strokes" | "strokeWeight" | "styles"
34
+ >;
35
+
36
+ export type NormalizedHasFramePropertiesTrait = Pick<
37
+ FigmaRestSpec.HasFramePropertiesTrait,
18
38
  | "layoutMode"
19
39
  | "layoutWrap"
20
40
  | "paddingLeft"
@@ -22,26 +42,43 @@ export type LayoutProps =
22
42
  | "paddingTop"
23
43
  | "paddingBottom"
24
44
  | "primaryAxisAlignItems"
25
- | "counterAxisAlignItems"
26
45
  | "primaryAxisSizingMode"
46
+ | "counterAxisAlignItems"
27
47
  | "counterAxisSizingMode"
28
48
  | "itemSpacing"
29
- | "counterAxisSpacing";
30
-
31
- export interface NormalizedFrameNode
32
- extends Pick<FigmaRestSpec.FrameNode, CommonProps | ShapeProps | RadiusProps | LayoutProps> {
33
- children: NormalizedSceneNode[];
49
+ | "counterAxisSpacing"
50
+ >;
51
+
52
+ export type NormalizedDefaultShapeTrait = NormalizedIsLayerTrait &
53
+ NormalizedHasLayoutTrait &
54
+ NormalizedHasGeometryTrait;
55
+
56
+ export type NormalizedFrameTrait = NormalizedIsLayerTrait &
57
+ NormalizedHasLayoutTrait &
58
+ NormalizedHasGeometryTrait &
59
+ NormalizedHasChildrenTrait &
60
+ NormalizedCornerTrait &
61
+ NormalizedHasFramePropertiesTrait;
62
+
63
+ export interface NormalizedFrameNode extends NormalizedFrameTrait {
64
+ type: FigmaRestSpec.FrameNode["type"];
34
65
  }
35
66
 
36
67
  export interface NormalizedRectangleNode
37
- extends Pick<FigmaRestSpec.RectangleNode, CommonProps | ShapeProps | RadiusProps> {}
68
+ extends NormalizedDefaultShapeTrait,
69
+ NormalizedCornerTrait {
70
+ type: FigmaRestSpec.RectangleNode["type"];
71
+ }
72
+
73
+ export interface NormalizedTextNode extends NormalizedDefaultShapeTrait {
74
+ type: FigmaRestSpec.TextNode["type"];
75
+
76
+ style: FigmaRestSpec.TextNode["style"];
77
+
78
+ characters: FigmaRestSpec.TextNode["characters"];
38
79
 
39
- export interface NormalizedTextNode
40
- extends Pick<
41
- FigmaRestSpec.TextNode,
42
- CommonProps | "layoutGrow" | "layoutAlign" | "style" | "characters" | "fills"
43
- > {
44
80
  segments: NormalizedTextSegment[];
81
+
45
82
  textStyleKey?: string;
46
83
  }
47
84
 
@@ -60,13 +97,13 @@ export interface NormalizedTextSegment {
60
97
  };
61
98
  }
62
99
 
63
- export interface NormalizedComponentNode
64
- extends Pick<FigmaRestSpec.ComponentNode, CommonProps | ShapeProps | RadiusProps | LayoutProps> {
65
- children: NormalizedSceneNode[];
100
+ export interface NormalizedComponentNode extends NormalizedFrameTrait {
101
+ type: FigmaRestSpec.ComponentNode["type"];
66
102
  }
67
103
 
68
- export interface NormalizedInstanceNode
69
- extends Pick<FigmaRestSpec.InstanceNode, CommonProps | ShapeProps | RadiusProps | LayoutProps> {
104
+ export interface NormalizedInstanceNode extends NormalizedFrameTrait {
105
+ type: FigmaRestSpec.InstanceNode["type"];
106
+
70
107
  componentProperties: {
71
108
  [key: string]: FigmaRestSpec.ComponentProperty & { componentKey?: string };
72
109
  };
@@ -78,11 +115,23 @@ export interface NormalizedInstanceNode
78
115
  children: NormalizedSceneNode[];
79
116
  }
80
117
 
81
- export interface NormalizedVectorNode
82
- extends Pick<FigmaRestSpec.VectorNode, CommonProps | ShapeProps> {}
118
+ export interface NormalizedVectorNode extends NormalizedDefaultShapeTrait, NormalizedCornerTrait {
119
+ type: FigmaRestSpec.VectorNode["type"];
120
+ }
83
121
 
84
122
  export interface NormalizedBooleanOperationNode
85
- extends Pick<FigmaRestSpec.BooleanOperationNode, CommonProps | "fills"> {}
123
+ extends NormalizedIsLayerTrait,
124
+ NormalizedHasChildrenTrait,
125
+ NormalizedHasLayoutTrait,
126
+ NormalizedHasGeometryTrait {
127
+ type: FigmaRestSpec.BooleanOperationNode["type"];
128
+ }
129
+
130
+ export interface NormalizedUnhandledNode {
131
+ type: "UNHANDLED";
132
+ id: string;
133
+ original: FigmaRestSpec.Node | SceneNode;
134
+ }
86
135
 
87
136
  export type NormalizedSceneNode =
88
137
  | NormalizedFrameNode
@@ -91,4 +140,5 @@ export type NormalizedSceneNode =
91
140
  | NormalizedComponentNode
92
141
  | NormalizedInstanceNode
93
142
  | NormalizedVectorNode
94
- | NormalizedBooleanOperationNode;
143
+ | NormalizedBooleanOperationNode
144
+ | NormalizedUnhandledNode;
@@ -0,0 +1,34 @@
1
+ import { camelCase } from "change-case";
2
+
3
+ export function ensureArray<T>(maybeArray: T | T[]): T[] {
4
+ if (Array.isArray(maybeArray)) {
5
+ return maybeArray;
6
+ }
7
+
8
+ return [maybeArray];
9
+ }
10
+
11
+ export function exists<T>(value: T | null | undefined): value is T {
12
+ return value != null;
13
+ }
14
+
15
+ export function compactObject<T extends Record<string, unknown>>(obj: T): T {
16
+ return Object.fromEntries(Object.entries(obj).filter(([, value]) => value != null)) as T;
17
+ }
18
+
19
+ export function objectEntries<T extends Record<string, unknown>>(obj: T) {
20
+ return Object.entries(obj) as [keyof T, T[keyof T]][];
21
+ }
22
+
23
+ /**
24
+ * camelCase but preserve underscore between numbers.
25
+ * temporary workaround to avoid x1_5 -> x15
26
+ * @example "color-1_5" -> "color1_5"
27
+ */
28
+ export function camelCasePreserveUnderscoreBetweenNumbers(input: string) {
29
+ return camelCase(input, {
30
+ mergeAmbiguousCharacters: false,
31
+ })
32
+ .replaceAll(/(\D)_(\d)/g, "$1$2")
33
+ .replaceAll(/(\d)_(\D)/g, "$1$2");
34
+ }
@@ -0,0 +1,13 @@
1
+ import type { RGBA } from "@figma/rest-api-spec";
2
+
3
+ export function toCssPixel(value: number) {
4
+ return `${value}px`;
5
+ }
6
+
7
+ export function toCssRgba(color: RGBA) {
8
+ if (color.a === 1) {
9
+ return `rgb(${color.r * 255}, ${color.g * 255}, ${color.b * 255})`;
10
+ }
11
+
12
+ return `rgba(${color.r * 255}, ${color.g * 255}, ${color.b * 255}, ${color.a})`;
13
+ }
@@ -1,4 +1,4 @@
1
- import type { NormalizedInstanceNode, NormalizedSceneNode } from "./normalizer/types";
1
+ import type { NormalizedInstanceNode, NormalizedSceneNode } from "../normalizer";
2
2
 
3
3
  export function traverseNode(
4
4
  node: NormalizedSceneNode,
@@ -0,0 +1,13 @@
1
+ export function isVariableAlias(value: unknown): value is VariableAlias {
2
+ return (
3
+ typeof value === "object" &&
4
+ value !== null &&
5
+ "type" in value &&
6
+ value.type === "VARIABLE_ALIAS"
7
+ );
8
+ }
9
+
10
+ // boundVariable.id is formatted as "VariableID:{key}/{localId}", we have to extract the key
11
+ export function sanitizeVariableId(id: string) {
12
+ return id.replace("VariableID:", "").split("/")[0]!;
13
+ }
package/src/color.ts DELETED
@@ -1,78 +0,0 @@
1
- import type { NormalizedFrameNode, NormalizedTextNode } from "./normalizer/types";
2
- import { getColorVariableName } from "./variable";
3
-
4
- export function createBackgroundProps(
5
- node: Pick<NormalizedFrameNode, "fills" | "boundVariables">,
6
- ): Partial<Record<"background", string | undefined>> {
7
- const fills = node.fills;
8
- if (fills.length === 0) {
9
- return {};
10
- }
11
-
12
- const fill = fills[0];
13
- if (!fill || ("visible" in fill && !fill.visible) || fill.type !== "SOLID") {
14
- return {};
15
- }
16
-
17
- if (node.boundVariables?.fills?.length === 1) {
18
- return {
19
- background: getColorVariableName(node.boundVariables.fills[0]!.id),
20
- };
21
- }
22
-
23
- const color = fill.color;
24
- return {
25
- background: `rgba(${color.r * 255}, ${color.g * 255}, ${color.b * 255}, ${fill.opacity})`,
26
- };
27
- }
28
-
29
- export function createColorProps(
30
- node: Pick<NormalizedTextNode, "fills" | "boundVariables">,
31
- ): Partial<Record<"color", string | undefined>> {
32
- const fills = node.fills;
33
- if (fills.length === 0) {
34
- return {};
35
- }
36
-
37
- const fill = fills[0];
38
- if (!fill || ("visible" in fill && !fill.visible) || fill.type !== "SOLID") {
39
- return {};
40
- }
41
- if (node.boundVariables?.fills?.length === 1) {
42
- return {
43
- color: getColorVariableName(node.boundVariables.fills[0]!.id),
44
- };
45
- }
46
-
47
- const color = fill.color;
48
- return {
49
- color: `rgba(${color.r * 255}, ${color.g * 255}, ${color.b * 255}, ${fill.opacity})`,
50
- };
51
- }
52
-
53
- export function createBorderProps(
54
- node: Pick<NormalizedFrameNode, "strokeWeight" | "strokes" | "boundVariables">,
55
- ): Partial<Record<"borderWidth" | "borderColor", string | number | undefined>> {
56
- const strokes = node.strokes;
57
- if (strokes === undefined || strokes.length === 0) {
58
- return {};
59
- }
60
-
61
- const stroke = strokes[0];
62
- if (!stroke || ("visible" in stroke && !stroke.visible) || stroke.type !== "SOLID") {
63
- return {};
64
- }
65
-
66
- if (node.boundVariables?.strokes?.length === 1) {
67
- return {
68
- borderWidth: node.strokeWeight as number,
69
- borderColor: getColorVariableName(node.boundVariables.strokes[0]!.id),
70
- };
71
- }
72
-
73
- const color = stroke.color;
74
- return {
75
- borderWidth: node.strokeWeight as number,
76
- borderColor: `rgba(${color.r * 255}, ${color.g * 255}, ${color.b * 255}, ${stroke.opacity})`,
77
- };
78
- }