@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.
Files changed (53) hide show
  1. package/lib/index.cjs +556 -471
  2. package/lib/index.d.ts +29 -4
  3. package/lib/index.js +556 -471
  4. package/package.json +2 -2
  5. package/src/component/handlers/action-button.ts +66 -0
  6. package/src/component/handlers/action-chip.ts +71 -0
  7. package/src/component/handlers/action-sheet.ts +74 -0
  8. package/src/component/handlers/app-bar.ts +183 -0
  9. package/src/component/handlers/avatar-stack.ts +35 -0
  10. package/src/component/handlers/avatar.ts +37 -0
  11. package/src/component/handlers/badge.ts +20 -0
  12. package/src/component/handlers/callout.ts +87 -0
  13. package/src/component/handlers/checkbox.ts +32 -0
  14. package/src/component/handlers/chip-tabs.ts +51 -0
  15. package/src/component/handlers/control-chip.ts +75 -0
  16. package/src/component/handlers/error-state.ts +37 -0
  17. package/src/component/handlers/extended-action-sheet.ts +86 -0
  18. package/src/component/handlers/extended-fab.ts +24 -0
  19. package/src/component/handlers/fab.ts +17 -0
  20. package/src/component/handlers/help-bubble.ts +66 -0
  21. package/src/component/handlers/identity-placeholder.ts +16 -0
  22. package/src/component/handlers/inline-banner.ts +83 -0
  23. package/src/component/handlers/manner-temp-badge.ts +17 -0
  24. package/src/component/handlers/multiline-text-field.ts +80 -0
  25. package/src/component/handlers/progress-circle.ts +49 -0
  26. package/src/component/handlers/reaction-button.ts +36 -0
  27. package/src/component/handlers/segmented-control.ts +51 -0
  28. package/src/component/handlers/select-box.ts +76 -0
  29. package/src/component/handlers/skeleton.ts +51 -0
  30. package/src/component/handlers/snackbar.ts +21 -0
  31. package/src/component/handlers/switch.ts +29 -0
  32. package/src/component/handlers/tabs.ts +107 -0
  33. package/src/component/handlers/text-button.ts +57 -0
  34. package/src/component/handlers/text-field.ts +108 -0
  35. package/src/component/handlers/toggle-button.ts +44 -0
  36. package/src/component/index.ts +32 -1644
  37. package/src/component/type-helper.ts +11 -0
  38. package/src/generate-code.ts +183 -145
  39. package/src/icon.ts +2 -2
  40. package/src/index.ts +1 -2
  41. package/src/jsx.ts +1 -1
  42. package/src/layout.ts +23 -281
  43. package/src/normalizer/from-plugin.ts +24 -4
  44. package/src/normalizer/from-rest.ts +22 -4
  45. package/src/normalizer/index.ts +3 -0
  46. package/src/normalizer/types.ts +3 -1
  47. package/src/{color.ts → props/color.ts} +1 -1
  48. package/src/props/layout.ts +292 -0
  49. package/src/{sizing.ts → props/sizing.ts} +17 -17
  50. package/src/{text.ts → props/text.ts} +2 -1
  51. package/src/{variable.ts → props/variable.ts} +1 -1
  52. package/src/{util.ts → utils/common.ts} +0 -2
  53. 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
+ }
@@ -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 { createLayoutProps } from "./layout";
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/types";
17
- import { createSizingProps } from "./sizing";
18
- import { createTextProps } from "./text";
19
- import { getColorVariableName, getLayoutVariableName, inferDimension } from "./variable";
20
- import { FIGMA_TEXT_STYLES } from "./data/styles";
21
- import { compactObject } from "./util";
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
- if (
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
- return createElement("Inline", rest, await Promise.all(children.map(traverse)));
45
- }
26
+ export type FigmaNodeHandler = (node: NormalizedSceneNode) => PromiseLikeMaybe<ElementNode>;
46
27
 
47
- if (
48
- props.flexDirection === "row" &&
49
- props.justifyContent === "flexStart" &&
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
- const childrenResult = await Promise.all(children.map(traverse));
32
+ export type FrameNodeHandlerFactory = FigmaNodeHandlerFactory<
33
+ NormalizedFrameNode | NormalizedComponentNode | NormalizedInstanceNode
34
+ >;
55
35
 
56
- return createElement(
57
- "Columns",
58
- rest,
59
- childrenResult.map((child) => createElement("Column", {}, child)),
60
- );
61
- }
36
+ export type TextNodeHandlerFactory = FigmaNodeHandlerFactory<NormalizedTextNode>;
62
37
 
63
- if (props.flexDirection === "column") {
64
- const { flexDirection, ...rest } = props;
38
+ export type RectangleNodeHandlerFactory = FigmaNodeHandlerFactory<NormalizedRectangleNode>;
65
39
 
66
- return createElement("Stack", rest, await Promise.all(children.map(traverse)));
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
- return createElement("Flex", props, await Promise.all(children.map(traverse)));
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
- function handleTextNode(node: NormalizedTextNode): ElementNode {
73
- const maxLines =
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
- if (node.fills.length > 1) {
77
- throw new Error("Expected a single fill");
78
- }
65
+ return createElement("Inline", rest, await Promise.all(children.map(traverse)));
66
+ }
79
67
 
80
- const onlyFill = node.fills.length === 1 ? node.fills[0] : null;
81
- const fillBoundVariableId =
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 { fontSize, fontWeight, lineHeight } = createTextProps(node.boundVariables);
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
- fontSize,
108
- fontWeight,
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
- async function handleRectangleNode(node: NormalizedRectangleNode) {
117
- return createElement(
118
- "Box",
119
- { ...createSizingProps(node), background: "palette.gray200" },
120
- undefined,
121
- "Rectangle Node Placeholder",
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
- async function handleComponentNode(node: NormalizedComponentNode) {
126
- return await handleFrameNode(node);
168
+ if (ignoredComponentKeys.has(componentSetKey ?? componentKey)) {
169
+ return;
127
170
  }
128
171
 
129
- async function handleInstanceNode(node: NormalizedInstanceNode) {
130
- const { componentKey, componentSetKey } = node;
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
- if (componentData) {
166
- return componentData.codegen(node);
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/types";
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
@@ -1,3 +1,2 @@
1
1
  export { generateCode } from "./generate-code";
2
- export { createPluginNormalizer } from "./normalizer/from-plugin";
3
- export { createRestNormalizer } from "./normalizer/from-rest";
2
+ export { createPluginNormalizer, createRestNormalizer } from "./normalizer";
package/src/jsx.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ensureArray, exists } from "./util";
1
+ import { ensureArray, exists } from "./utils/common";
2
2
 
3
3
  export interface ElementNode {
4
4
  __IS_JSX_ELEMENT_NODE: true;