@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.
- package/lib/index.cjs +13918 -0
- package/lib/index.d.ts +63 -0
- package/lib/index.js +13916 -0
- package/package.json +44 -0
- package/src/color.ts +78 -0
- package/src/component/index.ts +1688 -0
- package/src/component/properties.ts +20 -0
- package/src/component/type-helper.ts +29 -0
- package/src/component/type.ts +318 -0
- package/src/data/__generated__/component-sets/action-button.d.ts +59 -0
- package/src/data/__generated__/component-sets/action-button.mjs +59 -0
- package/src/data/__generated__/component-sets/action-chip.d.ts +57 -0
- package/src/data/__generated__/component-sets/action-chip.mjs +57 -0
- package/src/data/__generated__/component-sets/action-sheet.d.ts +40 -0
- package/src/data/__generated__/component-sets/action-sheet.mjs +40 -0
- package/src/data/__generated__/component-sets/avatar-stack.d.ts +32 -0
- package/src/data/__generated__/component-sets/avatar-stack.mjs +32 -0
- package/src/data/__generated__/component-sets/avatar.d.ts +25 -0
- package/src/data/__generated__/component-sets/avatar.mjs +25 -0
- package/src/data/__generated__/component-sets/badge.d.ts +41 -0
- package/src/data/__generated__/component-sets/badge.mjs +41 -0
- package/src/data/__generated__/component-sets/bottom-navigation-global.d.ts +20 -0
- package/src/data/__generated__/component-sets/bottom-navigation-global.mjs +20 -0
- package/src/data/__generated__/component-sets/bottom-navigation-kr.d.ts +13 -0
- package/src/data/__generated__/component-sets/bottom-navigation-kr.mjs +13 -0
- package/src/data/__generated__/component-sets/bottom-sheet.d.ts +14 -0
- package/src/data/__generated__/component-sets/bottom-sheet.mjs +14 -0
- package/src/data/__generated__/component-sets/callout.d.ts +57 -0
- package/src/data/__generated__/component-sets/callout.mjs +57 -0
- package/src/data/__generated__/component-sets/checkbox.d.ts +44 -0
- package/src/data/__generated__/component-sets/checkbox.mjs +44 -0
- package/src/data/__generated__/component-sets/chip-tablist.d.ts +24 -0
- package/src/data/__generated__/component-sets/chip-tablist.mjs +24 -0
- package/src/data/__generated__/component-sets/control-chip.d.ts +60 -0
- package/src/data/__generated__/component-sets/control-chip.mjs +60 -0
- package/src/data/__generated__/component-sets/divider.d.ts +13 -0
- package/src/data/__generated__/component-sets/divider.mjs +13 -0
- package/src/data/__generated__/component-sets/error-state.d.ts +32 -0
- package/src/data/__generated__/component-sets/error-state.mjs +32 -0
- package/src/data/__generated__/component-sets/extended-action-sheet.d.ts +32 -0
- package/src/data/__generated__/component-sets/extended-action-sheet.mjs +32 -0
- package/src/data/__generated__/component-sets/extended-floating-action-button.d.ts +34 -0
- package/src/data/__generated__/component-sets/extended-floating-action-button.mjs +34 -0
- package/src/data/__generated__/component-sets/floating-action-button.d.ts +17 -0
- package/src/data/__generated__/component-sets/floating-action-button.mjs +17 -0
- package/src/data/__generated__/component-sets/help-bubble.d.ts +39 -0
- package/src/data/__generated__/component-sets/help-bubble.mjs +39 -0
- package/src/data/__generated__/component-sets/identity-placeholder.d.ts +13 -0
- package/src/data/__generated__/component-sets/identity-placeholder.mjs +13 -0
- package/src/data/__generated__/component-sets/index.d.ts +47 -0
- package/src/data/__generated__/component-sets/index.mjs +47 -0
- package/src/data/__generated__/component-sets/inline-banner.d.ts +40 -0
- package/src/data/__generated__/component-sets/inline-banner.mjs +40 -0
- package/src/data/__generated__/component-sets/main-tab-navigation-global.d.ts +26 -0
- package/src/data/__generated__/component-sets/main-tab-navigation-global.mjs +26 -0
- package/src/data/__generated__/component-sets/main-tab-navigation-kr.d.ts +23 -0
- package/src/data/__generated__/component-sets/main-tab-navigation-kr.mjs +23 -0
- package/src/data/__generated__/component-sets/manner-temp-badge.d.ts +17 -0
- package/src/data/__generated__/component-sets/manner-temp-badge.mjs +17 -0
- package/src/data/__generated__/component-sets/manner-temp-bar.d.ts +23 -0
- package/src/data/__generated__/component-sets/manner-temp-bar.mjs +23 -0
- package/src/data/__generated__/component-sets/manner-temp.d.ts +17 -0
- package/src/data/__generated__/component-sets/manner-temp.mjs +17 -0
- package/src/data/__generated__/component-sets/multiline-text-field.d.ts +68 -0
- package/src/data/__generated__/component-sets/multiline-text-field.mjs +68 -0
- package/src/data/__generated__/component-sets/progress-circle.d.ts +31 -0
- package/src/data/__generated__/component-sets/progress-circle.mjs +31 -0
- package/src/data/__generated__/component-sets/radio.d.ts +27 -0
- package/src/data/__generated__/component-sets/radio.mjs +27 -0
- package/src/data/__generated__/component-sets/range-slider.d.ts +31 -0
- package/src/data/__generated__/component-sets/range-slider.mjs +31 -0
- package/src/data/__generated__/component-sets/reaction-button.d.ts +39 -0
- package/src/data/__generated__/component-sets/reaction-button.mjs +39 -0
- package/src/data/__generated__/component-sets/segmented-control.d.ts +23 -0
- package/src/data/__generated__/component-sets/segmented-control.mjs +23 -0
- package/src/data/__generated__/component-sets/select-box.d.ts +31 -0
- package/src/data/__generated__/component-sets/select-box.mjs +31 -0
- package/src/data/__generated__/component-sets/skeleton.d.ts +15 -0
- package/src/data/__generated__/component-sets/skeleton.mjs +15 -0
- package/src/data/__generated__/component-sets/slider.d.ts +31 -0
- package/src/data/__generated__/component-sets/slider.mjs +31 -0
- package/src/data/__generated__/component-sets/snackbar.d.ts +23 -0
- package/src/data/__generated__/component-sets/snackbar.mjs +23 -0
- package/src/data/__generated__/component-sets/standard-navigation.d.ts +23 -0
- package/src/data/__generated__/component-sets/standard-navigation.mjs +23 -0
- package/src/data/__generated__/component-sets/switch.d.ts +25 -0
- package/src/data/__generated__/component-sets/switch.mjs +25 -0
- package/src/data/__generated__/component-sets/tablist.d.ts +29 -0
- package/src/data/__generated__/component-sets/tablist.mjs +29 -0
- package/src/data/__generated__/component-sets/template-bottom-fixed-bar.d.ts +42 -0
- package/src/data/__generated__/component-sets/template-bottom-fixed-bar.mjs +42 -0
- package/src/data/__generated__/component-sets/template-button-group.d.ts +51 -0
- package/src/data/__generated__/component-sets/template-button-group.mjs +51 -0
- package/src/data/__generated__/component-sets/template-chip-group.d.ts +26 -0
- package/src/data/__generated__/component-sets/template-chip-group.mjs +26 -0
- package/src/data/__generated__/component-sets/template-select-box-group.d.ts +24 -0
- package/src/data/__generated__/component-sets/template-select-box-group.mjs +24 -0
- package/src/data/__generated__/component-sets/template-top-navigation.d.ts +25 -0
- package/src/data/__generated__/component-sets/template-top-navigation.mjs +25 -0
- package/src/data/__generated__/component-sets/text-button.d.ts +45 -0
- package/src/data/__generated__/component-sets/text-button.mjs +45 -0
- package/src/data/__generated__/component-sets/text-field.d.ts +100 -0
- package/src/data/__generated__/component-sets/text-field.mjs +100 -0
- package/src/data/__generated__/component-sets/toggle-button.d.ts +50 -0
- package/src/data/__generated__/component-sets/toggle-button.mjs +50 -0
- package/src/data/icons.ts +2774 -0
- package/src/data/styles.ts +142 -0
- package/src/data/variables.ts +7772 -0
- package/src/generate-code.ts +213 -0
- package/src/icon.ts +46 -0
- package/src/index.ts +3 -0
- package/src/jsx.ts +104 -0
- package/src/layout.ts +289 -0
- package/src/node-util.ts +49 -0
- package/src/normalizer/from-plugin.ts +345 -0
- package/src/normalizer/from-rest.ts +178 -0
- package/src/normalizer/types.ts +94 -0
- package/src/sizing.ts +58 -0
- package/src/text.ts +20 -0
- package/src/util.ts +17 -0
- 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
|
+
}
|