@seed-design/figma 0.0.2 → 0.0.4
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 +556 -471
- package/lib/index.d.ts +29 -4
- package/lib/index.js +556 -471
- package/package.json +2 -2
- package/src/component/handlers/action-button.ts +66 -0
- package/src/component/handlers/action-chip.ts +71 -0
- package/src/component/handlers/action-sheet.ts +74 -0
- package/src/component/handlers/app-bar.ts +183 -0
- package/src/component/handlers/avatar-stack.ts +35 -0
- package/src/component/handlers/avatar.ts +37 -0
- package/src/component/handlers/badge.ts +20 -0
- package/src/component/handlers/callout.ts +87 -0
- package/src/component/handlers/checkbox.ts +32 -0
- package/src/component/handlers/chip-tabs.ts +51 -0
- package/src/component/handlers/control-chip.ts +75 -0
- package/src/component/handlers/error-state.ts +37 -0
- package/src/component/handlers/extended-action-sheet.ts +86 -0
- package/src/component/handlers/extended-fab.ts +24 -0
- package/src/component/handlers/fab.ts +17 -0
- package/src/component/handlers/help-bubble.ts +66 -0
- package/src/component/handlers/identity-placeholder.ts +16 -0
- package/src/component/handlers/inline-banner.ts +83 -0
- package/src/component/handlers/manner-temp-badge.ts +17 -0
- package/src/component/handlers/multiline-text-field.ts +80 -0
- package/src/component/handlers/progress-circle.ts +49 -0
- package/src/component/handlers/reaction-button.ts +36 -0
- package/src/component/handlers/segmented-control.ts +51 -0
- package/src/component/handlers/select-box.ts +76 -0
- package/src/component/handlers/skeleton.ts +51 -0
- package/src/component/handlers/snackbar.ts +21 -0
- package/src/component/handlers/switch.ts +29 -0
- package/src/component/handlers/tabs.ts +107 -0
- package/src/component/handlers/text-button.ts +57 -0
- package/src/component/handlers/text-field.ts +108 -0
- package/src/component/handlers/toggle-button.ts +44 -0
- package/src/component/index.ts +32 -1644
- package/src/component/type-helper.ts +11 -0
- package/src/generate-code.ts +183 -145
- package/src/icon.ts +2 -2
- package/src/index.ts +1 -2
- package/src/jsx.ts +1 -1
- package/src/layout.ts +23 -281
- package/src/normalizer/from-plugin.ts +24 -4
- package/src/normalizer/from-rest.ts +22 -4
- package/src/normalizer/index.ts +3 -0
- package/src/normalizer/types.ts +3 -1
- package/src/{color.ts → props/color.ts} +1 -1
- package/src/props/layout.ts +292 -0
- package/src/{sizing.ts → props/sizing.ts} +17 -17
- package/src/{text.ts → props/text.ts} +2 -1
- package/src/{variable.ts → props/variable.ts} +1 -1
- package/src/{util.ts → utils/common.ts} +0 -2
- package/src/{node-util.ts → utils/figma-node.ts} +1 -1
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import type { ElementNode } from "../jsx";
|
|
2
|
+
import type { NormalizedInstanceNode } from "../normalizer";
|
|
3
|
+
|
|
1
4
|
interface ComponentPropertyDefinition {
|
|
2
5
|
type: ComponentPropertyType;
|
|
3
6
|
preferredValues?: InstanceSwapPreferredValue[];
|
|
@@ -27,3 +30,11 @@ export type InferFromDefinition<T extends Record<string, ComponentPropertyDefini
|
|
|
27
30
|
};
|
|
28
31
|
};
|
|
29
32
|
};
|
|
33
|
+
|
|
34
|
+
export interface ComponentHandler<
|
|
35
|
+
T extends
|
|
36
|
+
NormalizedInstanceNode["componentProperties"] = NormalizedInstanceNode["componentProperties"],
|
|
37
|
+
> {
|
|
38
|
+
key: string;
|
|
39
|
+
codegen: (node: NormalizedInstanceNode & { componentProperties: T }) => Promise<ElementNode>;
|
|
40
|
+
}
|
package/src/generate-code.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { camelCase } from "change-case";
|
|
2
|
-
import { createBackgroundProps, createBorderProps } from "./color";
|
|
3
2
|
import { componentHandlerMap, ignoredComponentKeys } from "./component";
|
|
4
3
|
import { iconRecord } from "./data/icons";
|
|
4
|
+
import { FIGMA_TEXT_STYLES } from "./data/styles";
|
|
5
5
|
import { createIconTagNameFromKey, createMonochromeIconColorProps, isIconComponent } from "./icon";
|
|
6
6
|
import type { ElementNode } from "./jsx";
|
|
7
7
|
import { createElement, stringifyElement } from "./jsx";
|
|
8
|
-
import {
|
|
8
|
+
import { inferLayoutComponent } from "./layout";
|
|
9
9
|
import type {
|
|
10
10
|
NormalizedComponentNode,
|
|
11
11
|
NormalizedFrameNode,
|
|
@@ -13,179 +13,217 @@ import type {
|
|
|
13
13
|
NormalizedRectangleNode,
|
|
14
14
|
NormalizedSceneNode,
|
|
15
15
|
NormalizedTextNode,
|
|
16
|
-
} from "./normalizer
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
export async function generateCode(selection: NormalizedSceneNode) {
|
|
24
|
-
async function handleFrameNode(
|
|
25
|
-
node: NormalizedFrameNode | NormalizedComponentNode | NormalizedInstanceNode,
|
|
26
|
-
) {
|
|
27
|
-
const children = node.children;
|
|
28
|
-
|
|
29
|
-
const props = {
|
|
30
|
-
...createLayoutProps(node),
|
|
31
|
-
...createSizingProps(node),
|
|
32
|
-
...createBackgroundProps(node),
|
|
33
|
-
...createBorderProps(node),
|
|
34
|
-
};
|
|
16
|
+
} from "./normalizer";
|
|
17
|
+
import { createBackgroundProps, createBorderProps } from "./props/color";
|
|
18
|
+
import { createLayoutProps } from "./props/layout";
|
|
19
|
+
import { createSizingProps } from "./props/sizing";
|
|
20
|
+
import { createTextProps } from "./props/text";
|
|
21
|
+
import { getColorVariableName, getLayoutVariableName, inferDimension } from "./props/variable";
|
|
22
|
+
import { compactObject } from "./utils/common";
|
|
35
23
|
|
|
36
|
-
|
|
37
|
-
props.flexDirection === "row" &&
|
|
38
|
-
props.alignItems === "flexStart" &&
|
|
39
|
-
props.justifyContent === "flexStart" &&
|
|
40
|
-
props.flexWrap === "wrap"
|
|
41
|
-
) {
|
|
42
|
-
const { flexDirection, flexWrap, alignItems, justifyContent, ...rest } = props;
|
|
24
|
+
type PromiseLikeMaybe<T> = Promise<T | undefined> | T | undefined;
|
|
43
25
|
|
|
44
|
-
|
|
45
|
-
}
|
|
26
|
+
export type FigmaNodeHandler = (node: NormalizedSceneNode) => PromiseLikeMaybe<ElementNode>;
|
|
46
27
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
props.flexWrap === "nowrap"
|
|
51
|
-
) {
|
|
52
|
-
const { flexDirection, flexWrap, justifyContent, ...rest } = props;
|
|
28
|
+
type FigmaNodeHandlerFactory<T extends NormalizedSceneNode> = (
|
|
29
|
+
traverse: FigmaNodeHandler,
|
|
30
|
+
) => (node: T) => PromiseLikeMaybe<ElementNode>;
|
|
53
31
|
|
|
54
|
-
|
|
32
|
+
export type FrameNodeHandlerFactory = FigmaNodeHandlerFactory<
|
|
33
|
+
NormalizedFrameNode | NormalizedComponentNode | NormalizedInstanceNode
|
|
34
|
+
>;
|
|
55
35
|
|
|
56
|
-
|
|
57
|
-
"Columns",
|
|
58
|
-
rest,
|
|
59
|
-
childrenResult.map((child) => createElement("Column", {}, child)),
|
|
60
|
-
);
|
|
61
|
-
}
|
|
36
|
+
export type TextNodeHandlerFactory = FigmaNodeHandlerFactory<NormalizedTextNode>;
|
|
62
37
|
|
|
63
|
-
|
|
64
|
-
const { flexDirection, ...rest } = props;
|
|
38
|
+
export type RectangleNodeHandlerFactory = FigmaNodeHandlerFactory<NormalizedRectangleNode>;
|
|
65
39
|
|
|
66
|
-
|
|
67
|
-
|
|
40
|
+
export type ComponentNodeHandlerFactory = FigmaNodeHandlerFactory<NormalizedComponentNode>;
|
|
41
|
+
|
|
42
|
+
export type InstanceNodeHandlerFactory = FigmaNodeHandlerFactory<NormalizedInstanceNode>;
|
|
43
|
+
|
|
44
|
+
const defaultFrameHandler: FrameNodeHandlerFactory = (traverse) => async (node) => {
|
|
45
|
+
const children = node.children;
|
|
46
|
+
|
|
47
|
+
const props = {
|
|
48
|
+
...createLayoutProps(node),
|
|
49
|
+
...createSizingProps(node),
|
|
50
|
+
...createBackgroundProps(node),
|
|
51
|
+
...createBorderProps(node),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const layoutComponent = inferLayoutComponent(props);
|
|
68
55
|
|
|
69
|
-
|
|
56
|
+
if (layoutComponent === "Stack") {
|
|
57
|
+
const { flexDirection, ...rest } = props;
|
|
58
|
+
|
|
59
|
+
return createElement("Stack", rest, await Promise.all(children.map(traverse)));
|
|
70
60
|
}
|
|
71
61
|
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
node.style.textTruncation === "ENDING" ? (node.style.maxLines ?? undefined) : undefined;
|
|
62
|
+
if (layoutComponent === "Inline") {
|
|
63
|
+
const { flexDirection, flexWrap, alignItems, justifyContent, ...rest } = props;
|
|
75
64
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
65
|
+
return createElement("Inline", rest, await Promise.all(children.map(traverse)));
|
|
66
|
+
}
|
|
79
67
|
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
onlyFill && onlyFill.type === "SOLID" ? (onlyFill.boundVariables?.color?.id ?? null) : null;
|
|
83
|
-
const color = fillBoundVariableId ? getColorVariableName(fillBoundVariableId) : undefined;
|
|
84
|
-
|
|
85
|
-
const style = FIGMA_TEXT_STYLES.find((s) => s.key === node.textStyleKey);
|
|
86
|
-
|
|
87
|
-
if (style) {
|
|
88
|
-
const styleNameSlugs = style.name.split("/");
|
|
89
|
-
const styleName = styleNameSlugs[styleNameSlugs.length - 1]!;
|
|
90
|
-
return createElement(
|
|
91
|
-
"Text",
|
|
92
|
-
compactObject({
|
|
93
|
-
textStyle: camelCase(styleName, { mergeAmbiguousCharacters: true }),
|
|
94
|
-
maxLines,
|
|
95
|
-
color,
|
|
96
|
-
}),
|
|
97
|
-
node.characters.replace(/\n/g, "<br />"),
|
|
98
|
-
color ? "" : "color 프로퍼티는 반영되지 않았습니다.",
|
|
99
|
-
);
|
|
100
|
-
}
|
|
68
|
+
if (layoutComponent === "Columns") {
|
|
69
|
+
const { flexDirection, flexWrap, justifyContent, ...rest } = props;
|
|
101
70
|
|
|
102
|
-
const
|
|
71
|
+
const childrenResult = await Promise.all(children.map(traverse));
|
|
103
72
|
|
|
73
|
+
return createElement(
|
|
74
|
+
"Columns",
|
|
75
|
+
rest,
|
|
76
|
+
childrenResult.map((child) => createElement("Column", {}, child)),
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return createElement("Flex", props, await Promise.all(children.map(traverse)));
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const defaultTextNodeHandler: TextNodeHandlerFactory = () => (node) => {
|
|
84
|
+
const maxLines =
|
|
85
|
+
node.style.textTruncation === "ENDING" ? (node.style.maxLines ?? undefined) : undefined;
|
|
86
|
+
|
|
87
|
+
if (node.fills.length > 1) {
|
|
88
|
+
throw new Error("Expected a single fill");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const onlyFill = node.fills.length === 1 ? node.fills[0] : null;
|
|
92
|
+
const fillBoundVariableId =
|
|
93
|
+
onlyFill && onlyFill.type === "SOLID" ? (onlyFill.boundVariables?.color?.id ?? null) : null;
|
|
94
|
+
const color = fillBoundVariableId ? getColorVariableName(fillBoundVariableId) : undefined;
|
|
95
|
+
|
|
96
|
+
const style = FIGMA_TEXT_STYLES.find((s) => s.key === node.textStyleKey);
|
|
97
|
+
|
|
98
|
+
if (style) {
|
|
99
|
+
const styleNameSlugs = style.name.split("/");
|
|
100
|
+
const styleName = styleNameSlugs[styleNameSlugs.length - 1]!;
|
|
104
101
|
return createElement(
|
|
105
102
|
"Text",
|
|
106
103
|
compactObject({
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
lineHeight,
|
|
104
|
+
textStyle: camelCase(styleName, { mergeAmbiguousCharacters: true }),
|
|
105
|
+
maxLines,
|
|
110
106
|
color,
|
|
111
107
|
}),
|
|
112
108
|
node.characters.replace(/\n/g, "<br />"),
|
|
109
|
+
color ? "" : "color 프로퍼티는 반영되지 않았습니다.",
|
|
113
110
|
);
|
|
114
111
|
}
|
|
115
112
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
113
|
+
const { fontSize, fontWeight, lineHeight } = createTextProps(node.boundVariables);
|
|
114
|
+
|
|
115
|
+
return createElement(
|
|
116
|
+
"Text",
|
|
117
|
+
compactObject({
|
|
118
|
+
fontSize,
|
|
119
|
+
fontWeight,
|
|
120
|
+
lineHeight,
|
|
121
|
+
color,
|
|
122
|
+
}),
|
|
123
|
+
node.characters.replace(/\n/g, "<br />"),
|
|
124
|
+
);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const defaultRectangleNodeHandler: RectangleNodeHandlerFactory = () => (node) => {
|
|
128
|
+
return createElement(
|
|
129
|
+
"Box",
|
|
130
|
+
{ ...createSizingProps(node), background: "palette.gray200" },
|
|
131
|
+
undefined,
|
|
132
|
+
"Rectangle Node Placeholder",
|
|
133
|
+
);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const defaultComponentNodeHandler: ComponentNodeHandlerFactory = (traverse) => async (node) => {
|
|
137
|
+
return defaultFrameHandler(traverse)(node);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const defaultInstanceNodeHandler: InstanceNodeHandlerFactory = (traverse) => async (node) => {
|
|
141
|
+
const { componentKey, componentSetKey } = node;
|
|
142
|
+
|
|
143
|
+
if (isIconComponent(componentKey)) {
|
|
144
|
+
const iconElement = createElement(createIconTagNameFromKey(componentKey));
|
|
145
|
+
|
|
146
|
+
switch (iconRecord[componentKey]?.type) {
|
|
147
|
+
case "monochrome":
|
|
148
|
+
return createElement("Icon", {
|
|
149
|
+
size:
|
|
150
|
+
getLayoutVariableName(node.boundVariables?.size?.x?.id) ??
|
|
151
|
+
inferDimension(node.absoluteBoundingBox?.width ?? 0),
|
|
152
|
+
...createMonochromeIconColorProps(node),
|
|
153
|
+
svg: iconElement,
|
|
154
|
+
});
|
|
155
|
+
case "multicolor":
|
|
156
|
+
return iconElement;
|
|
157
|
+
default:
|
|
158
|
+
return createElement("Icon", {
|
|
159
|
+
size:
|
|
160
|
+
getLayoutVariableName(node.boundVariables?.size?.x?.id) ??
|
|
161
|
+
inferDimension(node.absoluteBoundingBox?.width ?? 0),
|
|
162
|
+
svg: iconElement,
|
|
163
|
+
...createMonochromeIconColorProps(node),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
123
166
|
}
|
|
124
167
|
|
|
125
|
-
|
|
126
|
-
return
|
|
168
|
+
if (ignoredComponentKeys.has(componentSetKey ?? componentKey)) {
|
|
169
|
+
return;
|
|
127
170
|
}
|
|
128
171
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (isIconComponent(componentKey)) {
|
|
133
|
-
const iconElement = createElement(createIconTagNameFromKey(componentKey));
|
|
134
|
-
|
|
135
|
-
switch (iconRecord[componentKey]?.type) {
|
|
136
|
-
case "monochrome":
|
|
137
|
-
return createElement("Icon", {
|
|
138
|
-
size:
|
|
139
|
-
getLayoutVariableName(node.boundVariables?.size?.x?.id) ??
|
|
140
|
-
inferDimension(node.absoluteBoundingBox?.width ?? 0),
|
|
141
|
-
...createMonochromeIconColorProps(node),
|
|
142
|
-
svg: iconElement,
|
|
143
|
-
});
|
|
144
|
-
case "multicolor":
|
|
145
|
-
return iconElement;
|
|
146
|
-
default:
|
|
147
|
-
return createElement("Icon", {
|
|
148
|
-
size:
|
|
149
|
-
getLayoutVariableName(node.boundVariables?.size?.x?.id) ??
|
|
150
|
-
inferDimension(node.absoluteBoundingBox?.width ?? 0),
|
|
151
|
-
svg: iconElement,
|
|
152
|
-
...createMonochromeIconColorProps(node),
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (ignoredComponentKeys.has(componentSetKey ?? componentKey)) {
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const componentData = componentSetKey
|
|
162
|
-
? componentHandlerMap.get(componentSetKey)
|
|
163
|
-
: componentHandlerMap.get(componentKey);
|
|
172
|
+
const componentData = componentSetKey
|
|
173
|
+
? componentHandlerMap.get(componentSetKey)
|
|
174
|
+
: componentHandlerMap.get(componentKey);
|
|
164
175
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// if (node.id === selection.id) {
|
|
170
|
-
return await handleFrameNode(node);
|
|
171
|
-
// }
|
|
172
|
-
|
|
173
|
-
// const mainComponent = node.mainComponent;
|
|
174
|
-
|
|
175
|
-
// return createElement(
|
|
176
|
-
// mainComponent.parent?.type === "COMPONENT_SET"
|
|
177
|
-
// ? mainComponent.parent.name
|
|
178
|
-
// : mainComponent.name,
|
|
179
|
-
// Object.fromEntries(
|
|
180
|
-
// Object.entries(node.componentProperties)
|
|
181
|
-
// .filter(([_, props]) => props.type === "VARIANT" || props.type === "TEXT")
|
|
182
|
-
// .map(([key, props]) => [camelCase(key), camelCase(props.value as string)]),
|
|
183
|
-
// ),
|
|
184
|
-
// undefined,
|
|
185
|
-
// "Custom Component",
|
|
186
|
-
// );
|
|
176
|
+
if (componentData) {
|
|
177
|
+
return componentData.codegen(node);
|
|
187
178
|
}
|
|
188
179
|
|
|
180
|
+
// if (node.id === selection.id) {
|
|
181
|
+
return await defaultFrameHandler(traverse)(node);
|
|
182
|
+
// }
|
|
183
|
+
|
|
184
|
+
// const mainComponent = node.mainComponent;
|
|
185
|
+
|
|
186
|
+
// return createElement(
|
|
187
|
+
// mainComponent.parent?.type === "COMPONENT_SET"
|
|
188
|
+
// ? mainComponent.parent.name
|
|
189
|
+
// : mainComponent.name,
|
|
190
|
+
// Object.fromEntries(
|
|
191
|
+
// Object.entries(node.componentProperties)
|
|
192
|
+
// .filter(([_, props]) => props.type === "VARIANT" || props.type === "TEXT")
|
|
193
|
+
// .map(([key, props]) => [camelCase(key), camelCase(props.value as string)]),
|
|
194
|
+
// ),
|
|
195
|
+
// undefined,
|
|
196
|
+
// "Custom Component",
|
|
197
|
+
// );
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export async function generateCode(
|
|
201
|
+
selection: NormalizedSceneNode,
|
|
202
|
+
options?: {
|
|
203
|
+
handlers?: {
|
|
204
|
+
frame?: FrameNodeHandlerFactory;
|
|
205
|
+
text?: TextNodeHandlerFactory;
|
|
206
|
+
rectangle?: RectangleNodeHandlerFactory;
|
|
207
|
+
component?: ComponentNodeHandlerFactory;
|
|
208
|
+
instance?: InstanceNodeHandlerFactory;
|
|
209
|
+
};
|
|
210
|
+
},
|
|
211
|
+
) {
|
|
212
|
+
const handlers = {
|
|
213
|
+
frame: defaultFrameHandler,
|
|
214
|
+
text: defaultTextNodeHandler,
|
|
215
|
+
rectangle: defaultRectangleNodeHandler,
|
|
216
|
+
component: defaultComponentNodeHandler,
|
|
217
|
+
instance: defaultInstanceNodeHandler,
|
|
218
|
+
...options?.handlers,
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const handleFrameNode = handlers.frame(traverse);
|
|
222
|
+
const handleTextNode = handlers.text(traverse);
|
|
223
|
+
const handleRectangleNode = handlers.rectangle(traverse);
|
|
224
|
+
const handleComponentNode = handlers.component(traverse);
|
|
225
|
+
const handleInstanceNode = handlers.instance(traverse);
|
|
226
|
+
|
|
189
227
|
async function traverse(node: NormalizedSceneNode): Promise<ElementNode | undefined> {
|
|
190
228
|
if ("visible" in node && !node.visible) {
|
|
191
229
|
return;
|
package/src/icon.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { pascalCase } from "change-case";
|
|
2
2
|
|
|
3
|
-
import { createColorProps } from "./color";
|
|
3
|
+
import { createColorProps } from "./props/color";
|
|
4
4
|
import { iconRecord } from "./data/icons";
|
|
5
|
-
import type { NormalizedInstanceNode } from "./normalizer
|
|
5
|
+
import type { NormalizedInstanceNode } from "./normalizer";
|
|
6
6
|
|
|
7
7
|
export function isIconComponent(componentKey: string) {
|
|
8
8
|
return !!iconRecord[componentKey];
|
package/src/index.ts
CHANGED
package/src/jsx.ts
CHANGED