@seed-design/figma 1.1.13 → 1.1.14
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/codegen/index.cjs +636 -114
- package/lib/codegen/index.d.ts +136 -96
- package/lib/codegen/index.d.ts.map +1 -1
- package/lib/codegen/index.js +636 -114
- package/lib/codegen/targets/react/index.cjs +682 -134
- package/lib/codegen/targets/react/index.d.ts +31 -11
- package/lib/codegen/targets/react/index.d.ts.map +1 -1
- package/lib/codegen/targets/react/index.js +682 -135
- package/lib/index.cjs +1254 -433
- package/lib/index.d.ts +46 -10
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1254 -433
- package/package.json +1 -1
- package/src/codegen/component-properties.ts +5 -5
- package/src/codegen/core/value-resolver.ts +49 -12
- package/src/codegen/targets/figma/frame.ts +1 -0
- package/src/codegen/targets/figma/pipeline.ts +5 -0
- package/src/codegen/targets/figma/props.ts +30 -1
- package/src/codegen/targets/figma/shape.ts +1 -0
- package/src/codegen/targets/figma/value-resolver.ts +20 -0
- package/src/codegen/targets/react/component/handlers/menu-sheet.ts +1 -1
- package/src/codegen/targets/react/component/handlers/page-banner.ts +2 -2
- package/src/codegen/targets/react/component/handlers/{radio-mark.ts → radiomark.ts} +4 -4
- package/src/codegen/targets/react/component/handlers/result-section.ts +1 -1
- package/src/codegen/targets/react/component/handlers/{switch-mark.ts → switchmark.ts} +4 -4
- package/src/codegen/targets/react/component/index.ts +4 -4
- package/src/codegen/targets/react/frame.ts +16 -2
- package/src/codegen/targets/react/pipeline.ts +6 -1
- package/src/codegen/targets/react/props.ts +26 -0
- package/src/codegen/targets/react/shape.ts +5 -1
- package/src/codegen/targets/react/value-resolver.ts +26 -0
- package/src/entities/data/__generated__/component-sets/index.d.ts +84 -89
- package/src/entities/data/__generated__/component-sets/index.mjs +84 -89
- package/src/entities/data/__generated__/components/index.d.ts +2 -2
- package/src/entities/data/__generated__/components/index.mjs +2 -2
- package/src/entities/data/__generated__/icons/index.mjs +14 -0
- package/src/entities/data/__generated__/styles/index.mjs +190 -1
- package/src/entities/data/__generated__/variable-collections/index.mjs +11 -1
- package/src/entities/data/__generated__/variables/index.mjs +280 -0
- package/src/normalizer/from-plugin.ts +427 -258
- package/src/normalizer/from-rest.ts +428 -58
- package/src/normalizer/types.ts +63 -10
- package/src/utils/figma-node.ts +15 -10
package/lib/index.d.ts
CHANGED
|
@@ -4,15 +4,26 @@ export { VariableScope } from '@figma/rest-api-spec';
|
|
|
4
4
|
import { ComponentPropertyDefinition } from './codegen/index.js';
|
|
5
5
|
export * from './codegen/index.js';
|
|
6
6
|
|
|
7
|
-
type NormalizedIsLayerTrait = Pick<FigmaRestSpec.IsLayerTrait, "type" | "id" | "name"
|
|
7
|
+
type NormalizedIsLayerTrait = Pick<FigmaRestSpec.IsLayerTrait, "type" | "id" | "name"> & {
|
|
8
|
+
boundVariables?: Pick<NonNullable<FigmaRestSpec.IsLayerTrait["boundVariables"]>, "fills" | "strokes" | "itemSpacing" | "counterAxisSpacing" | "bottomLeftRadius" | "bottomRightRadius" | "topLeftRadius" | "topRightRadius" | "paddingBottom" | "paddingLeft" | "paddingRight" | "paddingTop" | "maxHeight" | "minHeight" | "maxWidth" | "minWidth" | "fontSize" | "fontWeight" | "lineHeight" | "size">;
|
|
9
|
+
};
|
|
8
10
|
type NormalizedCornerTrait = Pick<FigmaRestSpec.CornerTrait, "cornerRadius" | "rectangleCornerRadii">;
|
|
9
11
|
type NormalizedHasChildrenTrait = {
|
|
10
12
|
children: NormalizedSceneNode[];
|
|
11
13
|
};
|
|
12
14
|
type NormalizedHasLayoutTrait = Pick<FigmaRestSpec.HasLayoutTrait, "layoutAlign" | "layoutGrow" | "absoluteBoundingBox" | "relativeTransform" | "layoutPositioning" | "layoutSizingHorizontal" | "layoutSizingVertical" | "minHeight" | "minWidth" | "maxHeight" | "maxWidth">;
|
|
13
|
-
type
|
|
15
|
+
type NormalizedSolidPaint = FigmaRestSpec.SolidPaint;
|
|
16
|
+
type NormalizedPaint = NormalizedSolidPaint | FigmaRestSpec.GradientPaint | FigmaRestSpec.ImagePaint;
|
|
17
|
+
type NormalizedHasGeometryTrait = Omit<Pick<FigmaRestSpec.HasGeometryTrait, "fills" | "strokes" | "strokeWeight">, "fills" | "strokes"> & {
|
|
18
|
+
fills: NormalizedPaint[];
|
|
19
|
+
strokes: NormalizedPaint[];
|
|
14
20
|
fillStyleKey?: string;
|
|
15
21
|
};
|
|
22
|
+
type NormalizedShadow = (Pick<FigmaRestSpec.DropShadowEffect, "color" | "offset" | "radius" | "spread" | "boundVariables"> & Required<Pick<FigmaRestSpec.DropShadowEffect, "type">>) | (Pick<FigmaRestSpec.InnerShadowEffect, "color" | "offset" | "radius" | "spread" | "boundVariables"> & Required<Pick<FigmaRestSpec.InnerShadowEffect, "type">>);
|
|
23
|
+
type NormalizedHasEffectsTrait = Omit<FigmaRestSpec.HasEffectsTrait, "effects"> & {
|
|
24
|
+
effects: NormalizedShadow[];
|
|
25
|
+
effectStyleKey?: string;
|
|
26
|
+
};
|
|
16
27
|
type NormalizedHasFramePropertiesTrait = Pick<FigmaRestSpec.HasFramePropertiesTrait, "layoutMode" | "layoutWrap" | "paddingLeft" | "paddingRight" | "paddingTop" | "paddingBottom" | "primaryAxisAlignItems" | "primaryAxisSizingMode" | "counterAxisAlignItems" | "counterAxisSizingMode" | "itemSpacing" | "counterAxisSpacing">;
|
|
17
28
|
interface NormalizedTextSegment {
|
|
18
29
|
characters: string;
|
|
@@ -25,18 +36,18 @@ interface NormalizedTextSegment {
|
|
|
25
36
|
italic?: boolean;
|
|
26
37
|
textDecoration?: string;
|
|
27
38
|
letterSpacing?: number;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
39
|
+
/**
|
|
40
|
+
* in pixels
|
|
41
|
+
*/
|
|
42
|
+
lineHeight?: number;
|
|
32
43
|
};
|
|
33
44
|
}
|
|
34
45
|
type NormalizedTypePropertiesTrait = Pick<FigmaRestSpec.TypePropertiesTrait, "style" | "characters"> & {
|
|
35
46
|
segments: NormalizedTextSegment[];
|
|
36
47
|
textStyleKey?: string;
|
|
37
48
|
};
|
|
38
|
-
type NormalizedDefaultShapeTrait = NormalizedIsLayerTrait & NormalizedHasLayoutTrait & NormalizedHasGeometryTrait;
|
|
39
|
-
type NormalizedFrameTrait = NormalizedIsLayerTrait & NormalizedHasLayoutTrait & NormalizedHasGeometryTrait & NormalizedHasChildrenTrait & NormalizedCornerTrait & NormalizedHasFramePropertiesTrait;
|
|
49
|
+
type NormalizedDefaultShapeTrait = NormalizedIsLayerTrait & NormalizedHasLayoutTrait & NormalizedHasGeometryTrait & NormalizedHasEffectsTrait;
|
|
50
|
+
type NormalizedFrameTrait = NormalizedIsLayerTrait & NormalizedHasLayoutTrait & NormalizedHasGeometryTrait & NormalizedHasEffectsTrait & NormalizedHasChildrenTrait & NormalizedCornerTrait & NormalizedHasFramePropertiesTrait;
|
|
40
51
|
interface NormalizedFrameNode extends NormalizedFrameTrait {
|
|
41
52
|
type: FigmaRestSpec.FrameNode["type"];
|
|
42
53
|
}
|
|
@@ -65,7 +76,7 @@ interface NormalizedInstanceNode extends NormalizedFrameTrait {
|
|
|
65
76
|
interface NormalizedVectorNode extends NormalizedDefaultShapeTrait, NormalizedCornerTrait {
|
|
66
77
|
type: FigmaRestSpec.VectorNode["type"];
|
|
67
78
|
}
|
|
68
|
-
interface NormalizedBooleanOperationNode extends NormalizedIsLayerTrait, NormalizedHasChildrenTrait, NormalizedHasLayoutTrait, NormalizedHasGeometryTrait {
|
|
79
|
+
interface NormalizedBooleanOperationNode extends NormalizedIsLayerTrait, NormalizedHasChildrenTrait, NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait {
|
|
69
80
|
type: FigmaRestSpec.BooleanOperationNode["type"];
|
|
70
81
|
}
|
|
71
82
|
interface NormalizedUnhandledNode {
|
|
@@ -75,13 +86,38 @@ interface NormalizedUnhandledNode {
|
|
|
75
86
|
}
|
|
76
87
|
type NormalizedSceneNode = NormalizedFrameNode | NormalizedRectangleNode | NormalizedTextNode | NormalizedComponentNode | NormalizedInstanceNode | NormalizedVectorNode | NormalizedBooleanOperationNode | NormalizedUnhandledNode;
|
|
77
88
|
|
|
89
|
+
/**
|
|
90
|
+
* from-rest could be run outside of the Figma Plugin environment
|
|
91
|
+
* so we cannot use the Plugin API types directly e.g. getNodeByIdAsync
|
|
92
|
+
*/
|
|
93
|
+
/**
|
|
94
|
+
* NOTE: types of MinimalFillsTrait["styles"] can be found here:
|
|
95
|
+
* https://developers.figma.com/docs/rest-api/component-types/#style-type
|
|
96
|
+
* Record<"text" | "fill" | "stroke" | "effect" | "grid", string>
|
|
97
|
+
*/
|
|
98
|
+
|
|
78
99
|
interface RestNormalizerContext {
|
|
100
|
+
/**
|
|
101
|
+
* A map of style **ID** to style data
|
|
102
|
+
*/
|
|
79
103
|
styles: Record<string, FigmaRestSpec.Style>;
|
|
104
|
+
/**
|
|
105
|
+
* A map of component **ID** to component data
|
|
106
|
+
*/
|
|
80
107
|
components: Record<string, FigmaRestSpec.Component>;
|
|
108
|
+
/**
|
|
109
|
+
* A map of component set **ID** to component set data
|
|
110
|
+
*/
|
|
81
111
|
componentSets: Record<string, FigmaRestSpec.ComponentSet>;
|
|
82
112
|
}
|
|
83
113
|
declare function createRestNormalizer(ctx: RestNormalizerContext): (node: FigmaRestSpec.Node) => NormalizedSceneNode;
|
|
84
114
|
|
|
115
|
+
/**
|
|
116
|
+
* from-plugin is guaranteed to be run in the Figma Plugin environment
|
|
117
|
+
* so we can use the Plugin API types directly (figma.getNodeByIdAsync, node.getMainComponentAsync etc)
|
|
118
|
+
* however it could be better to make users can DI later
|
|
119
|
+
*/
|
|
120
|
+
|
|
85
121
|
declare function createPluginNormalizer(): (node: SceneNode) => Promise<NormalizedSceneNode>;
|
|
86
122
|
|
|
87
123
|
interface ComponentMetadata {
|
|
@@ -181,5 +217,5 @@ declare function getFigmaStyleKey(name: string): string | undefined;
|
|
|
181
217
|
declare function getFigmaColorVariableNames(scopes: Array<"fg" | "bg" | "stroke" | "palette">): string[];
|
|
182
218
|
|
|
183
219
|
export { componentRepository, createIconService, createPluginNormalizer, createRestNormalizer, createStaticComponentRepository, createStaticIconRepository, createStaticStyleRepository, createStaticVariableRepository, createStyleService, createVariableService, getFigmaColorVariableNames, getFigmaStyleKey, getFigmaVariableKey, iconRepository, styleRepository, variableRepository };
|
|
184
|
-
export type { ComponentMetadata, ComponentRepository, IconData, IconRepository, IconService, NormalizedBooleanOperationNode, NormalizedComponentNode, NormalizedCornerTrait, NormalizedDefaultShapeTrait, NormalizedFrameNode, NormalizedFrameTrait, NormalizedHasChildrenTrait, NormalizedHasFramePropertiesTrait, NormalizedHasGeometryTrait, NormalizedHasLayoutTrait, NormalizedInstanceNode, NormalizedIsLayerTrait, NormalizedRectangleNode, NormalizedSceneNode, NormalizedTextNode, NormalizedTextSegment, NormalizedTypePropertiesTrait, NormalizedUnhandledNode, NormalizedVectorNode, Style, StyleRepository, StyleService, StyleType, Variable, VariableCollection, VariableRepository, VariableService, VariableServiceDeps, VariableType, VariableValue, VariableValueResolved };
|
|
220
|
+
export type { ComponentMetadata, ComponentRepository, IconData, IconRepository, IconService, NormalizedBooleanOperationNode, NormalizedComponentNode, NormalizedCornerTrait, NormalizedDefaultShapeTrait, NormalizedFrameNode, NormalizedFrameTrait, NormalizedHasChildrenTrait, NormalizedHasEffectsTrait, NormalizedHasFramePropertiesTrait, NormalizedHasGeometryTrait, NormalizedHasLayoutTrait, NormalizedInstanceNode, NormalizedIsLayerTrait, NormalizedPaint, NormalizedRectangleNode, NormalizedSceneNode, NormalizedShadow, NormalizedSolidPaint, NormalizedTextNode, NormalizedTextSegment, NormalizedTypePropertiesTrait, NormalizedUnhandledNode, NormalizedVectorNode, Style, StyleRepository, StyleService, StyleType, Variable, VariableCollection, VariableRepository, VariableService, VariableServiceDeps, VariableType, VariableValue, VariableValueResolved };
|
|
185
221
|
//# sourceMappingURL=index.d.ts.map
|
package/lib/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sources":["../src/normalizer/types.ts","../src/normalizer/from-rest.ts","../src/normalizer/from-plugin.ts","../src/entities/component.interface.ts","../src/entities/icon.interface.ts","../src/entities/variable.interface.ts","../src/entities/variable.repository.ts","../src/entities/style.interface.ts","../src/entities/style.repository.ts","../src/entities/icon.repository.ts","../src/entities/icon.service.ts","../src/entities/style.service.ts","../src/entities/variable.service.ts","../src/entities/component.repository.ts","../src/entities/index.ts"],"sourcesContent":["import type * as FigmaRestSpec from \"@figma/rest-api-spec\";\n\nexport type NormalizedIsLayerTrait = Pick<\n FigmaRestSpec.IsLayerTrait,\n \"type\" | \"id\" | \"name\" | \"boundVariables\"\n>;\n\nexport type NormalizedCornerTrait = Pick<\n FigmaRestSpec.CornerTrait,\n \"cornerRadius\" | \"rectangleCornerRadii\"\n>;\n\nexport type NormalizedHasChildrenTrait = {\n children: NormalizedSceneNode[];\n};\n\nexport type NormalizedHasLayoutTrait = Pick<\n FigmaRestSpec.HasLayoutTrait,\n | \"layoutAlign\"\n | \"layoutGrow\"\n | \"absoluteBoundingBox\"\n | \"relativeTransform\"\n | \"layoutPositioning\"\n | \"layoutSizingHorizontal\"\n | \"layoutSizingVertical\"\n | \"minHeight\"\n | \"minWidth\"\n | \"maxHeight\"\n | \"maxWidth\"\n>;\n\nexport type NormalizedHasGeometryTrait = Pick<\n FigmaRestSpec.HasGeometryTrait,\n \"fills\" | \"strokes\" | \"strokeWeight\" | \"styles\"\n> & {\n fillStyleKey?: string;\n};\n\nexport type NormalizedHasFramePropertiesTrait = Pick<\n FigmaRestSpec.HasFramePropertiesTrait,\n | \"layoutMode\"\n | \"layoutWrap\"\n | \"paddingLeft\"\n | \"paddingRight\"\n | \"paddingTop\"\n | \"paddingBottom\"\n | \"primaryAxisAlignItems\"\n | \"primaryAxisSizingMode\"\n | \"counterAxisAlignItems\"\n | \"counterAxisSizingMode\"\n | \"itemSpacing\"\n | \"counterAxisSpacing\"\n>;\n\nexport interface NormalizedTextSegment {\n characters: string;\n start: number;\n end: number;\n style: {\n fontFamily?: string;\n fontWeight?: number;\n fontSize?: number;\n italic?: boolean;\n textDecoration?: string;\n letterSpacing?: number;\n lineHeight?: number | { unit: string; value: number };\n };\n}\n\nexport type NormalizedTypePropertiesTrait = Pick<\n FigmaRestSpec.TypePropertiesTrait,\n \"style\" | \"characters\"\n> & {\n segments: NormalizedTextSegment[];\n\n textStyleKey?: string;\n};\n\nexport type NormalizedDefaultShapeTrait = NormalizedIsLayerTrait &\n NormalizedHasLayoutTrait &\n NormalizedHasGeometryTrait;\n\nexport type NormalizedFrameTrait = NormalizedIsLayerTrait &\n NormalizedHasLayoutTrait &\n NormalizedHasGeometryTrait &\n NormalizedHasChildrenTrait &\n NormalizedCornerTrait &\n NormalizedHasFramePropertiesTrait;\n\nexport interface NormalizedFrameNode extends NormalizedFrameTrait {\n type: FigmaRestSpec.FrameNode[\"type\"];\n}\n\nexport interface NormalizedRectangleNode\n extends NormalizedDefaultShapeTrait,\n NormalizedCornerTrait {\n type: FigmaRestSpec.RectangleNode[\"type\"];\n}\n\nexport interface NormalizedTextNode\n extends NormalizedDefaultShapeTrait,\n NormalizedTypePropertiesTrait {\n type: FigmaRestSpec.TextNode[\"type\"];\n}\n\nexport interface NormalizedComponentNode extends NormalizedFrameTrait {\n type: FigmaRestSpec.ComponentNode[\"type\"];\n}\n\nexport interface NormalizedInstanceNode extends NormalizedFrameTrait {\n type: FigmaRestSpec.InstanceNode[\"type\"];\n\n componentProperties: {\n [key: string]: FigmaRestSpec.ComponentProperty & {\n componentKey?: string;\n componentSetKey?: string;\n };\n };\n\n componentKey: string;\n\n componentSetKey?: string;\n\n overrides?: FigmaRestSpec.InstanceNode[\"overrides\"];\n\n children: NormalizedSceneNode[];\n}\n\nexport interface NormalizedVectorNode extends NormalizedDefaultShapeTrait, NormalizedCornerTrait {\n type: FigmaRestSpec.VectorNode[\"type\"];\n}\n\nexport interface NormalizedBooleanOperationNode\n extends NormalizedIsLayerTrait,\n NormalizedHasChildrenTrait,\n NormalizedHasLayoutTrait,\n NormalizedHasGeometryTrait {\n type: FigmaRestSpec.BooleanOperationNode[\"type\"];\n}\n\nexport interface NormalizedUnhandledNode {\n type: \"UNHANDLED\";\n id: string;\n original: FigmaRestSpec.Node | SceneNode;\n}\n\nexport type NormalizedSceneNode =\n | NormalizedFrameNode\n | NormalizedRectangleNode\n | NormalizedTextNode\n | NormalizedComponentNode\n | NormalizedInstanceNode\n | NormalizedVectorNode\n | NormalizedBooleanOperationNode\n | NormalizedUnhandledNode;\n","import type * as FigmaRestSpec from \"@figma/rest-api-spec\";\nimport type {\n NormalizedSceneNode,\n NormalizedFrameNode,\n NormalizedRectangleNode,\n NormalizedTextNode,\n NormalizedComponentNode,\n NormalizedInstanceNode,\n NormalizedTextSegment,\n NormalizedVectorNode,\n NormalizedBooleanOperationNode,\n} from \"./types\";\n\nexport interface RestNormalizerContext {\n styles: Record<string, FigmaRestSpec.Style>;\n components: Record<string, FigmaRestSpec.Component>;\n componentSets: Record<string, FigmaRestSpec.ComponentSet>;\n}\n\nexport function createRestNormalizer(ctx: RestNormalizerContext) {\n function normalizeNodes(nodes: readonly FigmaRestSpec.Node[]): NormalizedSceneNode[] {\n // Figma REST API omits default values for some fields, \"visible\" is one of them\n return nodes.filter((node) => !(\"visible\" in node) || node.visible).map(normalizeNode);\n }\n\n function normalizeNode(node: FigmaRestSpec.Node): NormalizedSceneNode {\n if (node.type === \"FRAME\") {\n return normalizeFrameNode(node);\n }\n if (node.type === \"GROUP\") {\n return normalizeGroupNode(node);\n }\n if (node.type === \"RECTANGLE\") {\n return normalizeRectangleNode(node);\n }\n if (node.type === \"VECTOR\") {\n return normalizeVectorNode(node);\n }\n if (node.type === \"BOOLEAN_OPERATION\") {\n return normalizeBooleanOperationNode(node);\n }\n if (node.type === \"TEXT\") {\n return normalizeTextNode(node);\n }\n if (node.type === \"COMPONENT\") {\n return normalizeComponentNode(node);\n }\n if (node.type === \"INSTANCE\") {\n return normalizeInstanceNode(node);\n }\n\n return {\n type: \"UNHANDLED\",\n id: node.id,\n original: node,\n };\n }\n\n function normalizeFrameNode(node: FigmaRestSpec.FrameNode): NormalizedFrameNode {\n return {\n ...node,\n children: normalizeNodes(node.children),\n };\n }\n\n function normalizeGroupNode(node: FigmaRestSpec.GroupNode): NormalizedFrameNode {\n return {\n ...node,\n type: \"FRAME\",\n children: normalizeNodes(node.children),\n };\n }\n\n function normalizeRectangleNode(node: FigmaRestSpec.RectangleNode): NormalizedRectangleNode {\n return node;\n }\n\n function normalizeVectorNode(node: FigmaRestSpec.VectorNode): NormalizedVectorNode {\n return node;\n }\n\n function normalizeBooleanOperationNode(\n node: FigmaRestSpec.BooleanOperationNode,\n ): NormalizedBooleanOperationNode {\n return {\n ...node,\n children: normalizeNodes(node.children),\n };\n }\n\n function normalizeTextNode(node: FigmaRestSpec.TextNode): NormalizedTextNode {\n // Function to segment a text node based on style overrides\n function segmentTextNode(textNode: FigmaRestSpec.TextNode): NormalizedTextSegment[] {\n const segments: NormalizedTextSegment[] = [];\n const characters = textNode.characters;\n const styleOverrides = textNode.characterStyleOverrides || [];\n const styleTable = textNode.styleOverrideTable || {};\n\n // If no style overrides, return the entire text as one segment\n if (!styleOverrides.length) {\n return [\n {\n characters: characters,\n start: 0,\n end: characters.length,\n style: textNode.style || {},\n },\n ];\n }\n\n let currentSegment: NormalizedTextSegment = {\n characters: \"\",\n start: 0,\n end: 0,\n style: {},\n };\n\n let currentStyleId: string | null = null;\n\n for (let i = 0; i < characters.length; i++) {\n const styleId = styleOverrides[i]?.toString() || null;\n\n // If style changes or it's the first character\n if (styleId !== currentStyleId || i === 0) {\n // Add the previous segment if it exists\n if (i > 0) {\n currentSegment.end = i;\n currentSegment.characters = characters.substring(\n currentSegment.start,\n currentSegment.end,\n );\n segments.push({ ...currentSegment });\n }\n\n // Start a new segment\n currentStyleId = styleId;\n currentSegment = {\n characters: \"\",\n start: i,\n end: 0,\n style: styleId ? styleTable[styleId] || {} : {},\n };\n }\n }\n\n // Add the last segment\n if (currentSegment.start < characters.length) {\n currentSegment.end = characters.length;\n currentSegment.characters = characters.substring(currentSegment.start, currentSegment.end);\n segments.push(currentSegment);\n }\n\n return segments;\n }\n\n return {\n ...node,\n textStyleKey: node.styles?.[\"text\"] ? ctx.styles[node.styles[\"text\"]]?.key : undefined,\n segments: segmentTextNode(node),\n };\n }\n\n function normalizeComponentNode(node: FigmaRestSpec.ComponentNode): NormalizedComponentNode {\n return {\n ...node,\n children: normalizeNodes(node.children),\n };\n }\n\n function normalizeInstanceNode(node: FigmaRestSpec.InstanceNode): NormalizedInstanceNode {\n const mainComponent = ctx.components[node.componentId];\n if (!mainComponent) {\n throw new Error(`Component ${node.componentId} not found`);\n }\n const componentSet = mainComponent.componentSetId\n ? ctx.componentSets[mainComponent.componentSetId]\n : undefined;\n const componentProperties: NormalizedInstanceNode[\"componentProperties\"] = {};\n\n for (const [key, value] of Object.entries(node.componentProperties ?? {})) {\n componentProperties[key] = value;\n if (value.type === \"INSTANCE_SWAP\") {\n const mainComponent = ctx.components[value.value as string];\n if (mainComponent) {\n componentProperties[key].componentKey = mainComponent.key;\n }\n const mainComponentSet = mainComponent?.componentSetId\n ? ctx.componentSets[mainComponent.componentSetId]\n : undefined;\n if (mainComponentSet) {\n componentProperties[key].componentSetKey = mainComponentSet.key;\n }\n }\n }\n\n return {\n ...node,\n children: normalizeNodes(node.children),\n componentKey: mainComponent.key,\n componentSetKey: componentSet?.key,\n componentProperties,\n };\n }\n\n return normalizeNode;\n}\n","import type * as FigmaRestSpec from \"@figma/rest-api-spec\";\nimport type {\n NormalizedSceneNode,\n NormalizedFrameNode,\n NormalizedRectangleNode,\n NormalizedTextNode,\n NormalizedComponentNode,\n NormalizedInstanceNode,\n NormalizedVectorNode,\n NormalizedBooleanOperationNode,\n} from \"./types\";\nimport { convertTransformToGradientHandles } from \"@/utils/figma-gradient\";\n\nexport function createPluginNormalizer() {\n async function normalizeNodes(nodes: readonly SceneNode[]): Promise<NormalizedSceneNode[]> {\n return Promise.all(nodes.filter((node) => node.visible).map(normalizeNode));\n }\n\n async function normalizeNode(node: SceneNode): Promise<NormalizedSceneNode> {\n if (node.type === \"FRAME\") {\n return normalizeFrameNode(node);\n }\n if (node.type === \"GROUP\") {\n return normalizeGroupNode(node);\n }\n if (node.type === \"RECTANGLE\") {\n return normalizeRectangleNode(node);\n }\n if (node.type === \"VECTOR\") {\n return normalizeVectorNode(node);\n }\n if (node.type === \"BOOLEAN_OPERATION\") {\n return normalizeBooleanOperationNode(node);\n }\n if (node.type === \"TEXT\") {\n return normalizeTextNode(node);\n }\n if (node.type === \"COMPONENT\") {\n return normalizeComponentNode(node);\n }\n if (node.type === \"INSTANCE\") {\n return normalizeInstanceNode(node);\n }\n\n return {\n type: \"UNHANDLED\",\n id: node.id,\n original: node,\n };\n }\n\n async function normalizeFrameNode(node: FrameNode): Promise<NormalizedFrameNode> {\n return {\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: await normalizeBoundVariables(node),\n ...normalizeRadiusProps(node),\n ...(await normalizeAutolayoutProps(node)),\n children: await normalizeNodes(node.children),\n };\n }\n\n async function normalizeGroupNode(\n node: GroupNode & { inferredAutoLayout?: FrameNode[\"inferredAutoLayout\"] },\n ): Promise<NormalizedFrameNode> {\n return {\n type: \"FRAME\",\n id: node.id,\n name: node.name,\n boundVariables: await normalizeBoundVariables(node),\n layoutGrow: (node.inferredAutoLayout?.layoutGrow ?? node.layoutGrow) as 0 | 1 | undefined,\n layoutAlign: node.inferredAutoLayout?.layoutAlign ?? node.layoutAlign,\n layoutSizingHorizontal: node.layoutSizingHorizontal,\n layoutSizingVertical: node.layoutSizingVertical,\n absoluteBoundingBox: node.absoluteBoundingBox,\n relativeTransform: node.relativeTransform,\n layoutMode: node.inferredAutoLayout?.layoutMode,\n layoutWrap: node.inferredAutoLayout?.layoutWrap,\n paddingLeft: node.inferredAutoLayout?.paddingLeft,\n paddingRight: node.inferredAutoLayout?.paddingRight,\n paddingTop: node.inferredAutoLayout?.paddingTop,\n paddingBottom: node.inferredAutoLayout?.paddingBottom,\n primaryAxisAlignItems: node.inferredAutoLayout?.primaryAxisAlignItems,\n counterAxisAlignItems: node.inferredAutoLayout?.counterAxisAlignItems,\n primaryAxisSizingMode: node.inferredAutoLayout?.primaryAxisSizingMode,\n counterAxisSizingMode: node.inferredAutoLayout?.counterAxisSizingMode,\n itemSpacing: node.inferredAutoLayout?.itemSpacing,\n counterAxisSpacing: node.inferredAutoLayout?.counterAxisSpacing ?? undefined,\n fills: [],\n strokes: [],\n children: await normalizeNodes(node.children),\n };\n }\n\n async function normalizeRectangleNode(node: RectangleNode): Promise<NormalizedRectangleNode> {\n return {\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: await normalizeBoundVariables(node),\n ...normalizeRadiusProps(node),\n ...(await normalizeShapeProps(node)),\n };\n }\n\n async function normalizeVectorNode(node: VectorNode): Promise<NormalizedVectorNode> {\n return {\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: await normalizeBoundVariables(node),\n ...(await normalizeShapeProps(node)),\n };\n }\n\n async function normalizeBooleanOperationNode(\n node: BooleanOperationNode,\n ): Promise<NormalizedBooleanOperationNode> {\n return {\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: await normalizeBoundVariables(node),\n children: await normalizeNodes(node.children),\n ...(await normalizeShapeProps(node)),\n };\n }\n async function normalizeTextNode(node: TextNode): Promise<NormalizedTextNode> {\n const segments = node.getStyledTextSegments([\n \"fontSize\",\n \"fontWeight\",\n \"fontName\",\n \"letterSpacing\",\n \"lineHeight\",\n \"paragraphSpacing\",\n \"textStyleId\",\n \"fills\",\n \"boundVariables\",\n \"textDecoration\",\n ]);\n const first = segments[0]!;\n\n const textStyleKey =\n typeof node.textStyleId === \"string\"\n ? (await figma.getStyleByIdAsync(node.textStyleId))?.key\n : undefined;\n\n return {\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: await normalizeBoundVariables(node),\n style: {\n fontSize: first.fontSize,\n fontWeight: first.fontWeight,\n fontFamily: first.fontName.family,\n // TODO: handle other units\n letterSpacing:\n first.letterSpacing.unit === \"PIXELS\" ? first.letterSpacing.value : undefined,\n lineHeightPx: first.lineHeight.unit === \"PIXELS\" ? first.lineHeight.value : undefined,\n paragraphSpacing: first.paragraphSpacing,\n textAlignHorizontal: node.textAlignHorizontal,\n },\n ...(textStyleKey ? { textStyleKey } : {}),\n characters: node.characters,\n segments: segments.map((segment) => ({\n characters: segment.characters,\n start: segment.start,\n end: segment.end,\n style: {\n fontSize: segment.fontSize,\n fontWeight: segment.fontWeight,\n fontFamily: segment.fontName.family,\n letterSpacing:\n segment.letterSpacing.unit === \"PIXELS\" ? segment.letterSpacing.value : undefined,\n lineHeightPx: segment.lineHeight.unit === \"PIXELS\" ? segment.lineHeight.value : undefined,\n },\n })),\n ...(await normalizeShapeProps(node)),\n };\n }\n\n async function normalizeComponentNode(node: ComponentNode): Promise<NormalizedComponentNode> {\n return {\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: await normalizeBoundVariables(node),\n ...normalizeRadiusProps(node),\n ...(await normalizeAutolayoutProps(node)),\n children: await normalizeNodes(node.children),\n };\n }\n\n async function normalizeInstanceNode(node: InstanceNode): Promise<NormalizedInstanceNode> {\n const mainComponent = await node.getMainComponentAsync();\n if (!mainComponent) {\n throw new Error(\"Instance node has no main component\");\n }\n\n const componentProperties: NormalizedInstanceNode[\"componentProperties\"] = {};\n for (const [key, value] of Object.entries(node.componentProperties)) {\n componentProperties[key] = value;\n if (value.type === \"INSTANCE_SWAP\") {\n const mainComponent = (await figma.getNodeByIdAsync(\n value.value as string,\n )) as ComponentNode;\n if (mainComponent) {\n componentProperties[key].componentKey = mainComponent.key;\n if (mainComponent.parent?.type === \"COMPONENT_SET\") {\n componentProperties[key].componentSetKey = mainComponent.parent.key;\n }\n }\n }\n }\n\n return {\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: await normalizeBoundVariables(node),\n ...normalizeRadiusProps(node),\n ...(await normalizeAutolayoutProps(node)),\n children: await normalizeNodes(node.children),\n componentKey: mainComponent.key,\n componentSetKey:\n mainComponent.parent?.type === \"COMPONENT_SET\" ? mainComponent.parent.key : undefined,\n componentProperties,\n overrides: node.overrides,\n };\n }\n\n function normalizeSolidPaint(paint: SolidPaint): FigmaRestSpec.SolidPaint {\n return {\n type: paint.type,\n color: {\n r: paint.color.r,\n g: paint.color.g,\n b: paint.color.b,\n a: paint.opacity ?? 1,\n },\n visible: paint.visible,\n blendMode: paint.blendMode ?? \"NORMAL\",\n boundVariables: paint.boundVariables,\n };\n }\n\n function normalizePaint(paint: Paint): FigmaRestSpec.Paint {\n switch (paint.type) {\n case \"SOLID\":\n return normalizeSolidPaint(paint);\n case \"IMAGE\":\n return {\n type: \"IMAGE\",\n scaleMode: paint.scaleMode === \"CROP\" ? \"STRETCH\" : paint.scaleMode,\n imageTransform: paint.imageTransform,\n scalingFactor: paint.scalingFactor,\n filters: paint.filters,\n rotation: paint.rotation,\n imageRef: paint.imageHash ?? \"\",\n blendMode: paint.blendMode ?? \"NORMAL\",\n visible: paint.visible,\n opacity: paint.opacity,\n };\n case \"GRADIENT_LINEAR\":\n case \"GRADIENT_RADIAL\":\n case \"GRADIENT_ANGULAR\":\n case \"GRADIENT_DIAMOND\":\n return {\n type: paint.type,\n gradientStops: [...paint.gradientStops],\n visible: paint.visible,\n opacity: paint.opacity,\n blendMode: paint.blendMode ?? \"NORMAL\",\n gradientHandlePositions: convertTransformToGradientHandles(paint.gradientTransform),\n };\n default:\n throw new Error(`Unimplemented paint type: ${paint.type}`);\n }\n }\n\n function normalizePaints(fills: readonly Paint[] | PluginAPI[\"mixed\"]): FigmaRestSpec.Paint[] {\n if (fills === figma.mixed) {\n console.warn(\"Mixed fills are not supported\");\n\n return [];\n }\n\n return fills.map(normalizePaint);\n }\n\n function normalizeRadiusProps(\n node: Pick<\n RectangleNode,\n \"cornerRadius\" | \"topLeftRadius\" | \"topRightRadius\" | \"bottomRightRadius\" | \"bottomLeftRadius\"\n >,\n ) {\n return {\n cornerRadius: node.cornerRadius === figma.mixed ? undefined : node.cornerRadius,\n rectangleCornerRadii: [\n node.topLeftRadius,\n node.topRightRadius,\n node.bottomRightRadius,\n node.bottomLeftRadius,\n ],\n };\n }\n\n async function normalizeShapeProps(\n node: Pick<\n RectangleNode,\n | \"fills\"\n | \"fillStyleId\"\n | \"strokes\"\n | \"strokeWeight\"\n | \"layoutGrow\"\n | \"layoutAlign\"\n | \"layoutSizingHorizontal\"\n | \"layoutSizingVertical\"\n | \"absoluteBoundingBox\"\n | \"relativeTransform\"\n | \"minHeight\"\n | \"minWidth\"\n | \"maxHeight\"\n | \"maxWidth\"\n > &\n Partial<Pick<FrameNode, \"inferredAutoLayout\">>,\n ) {\n const fillStyleKey =\n typeof node.fillStyleId === \"string\"\n ? (await figma.getStyleByIdAsync(node.fillStyleId))?.key\n : undefined;\n\n return {\n layoutGrow: (node.inferredAutoLayout?.layoutGrow ?? node.layoutGrow) as 0 | 1 | undefined,\n layoutAlign: node.inferredAutoLayout?.layoutAlign ?? node.layoutAlign,\n layoutSizingHorizontal: node.layoutSizingHorizontal,\n layoutSizingVertical: node.layoutSizingVertical,\n absoluteBoundingBox: node.absoluteBoundingBox,\n relativeTransform: node.relativeTransform,\n fills: normalizePaints(node.fills),\n ...(fillStyleKey ? { fillStyleKey } : {}),\n strokes: normalizePaints(node.strokes),\n strokeWeight: node.strokeWeight === figma.mixed ? undefined : node.strokeWeight,\n minHeight: node.minHeight ?? undefined,\n minWidth: node.minWidth ?? undefined,\n maxHeight: node.maxHeight ?? undefined,\n maxWidth: node.maxWidth ?? undefined,\n };\n }\n\n async function normalizeAutolayoutProps(node: Omit<FrameNode, \"type\" | \"clone\">) {\n return {\n ...(await normalizeShapeProps(node)),\n layoutMode: node.inferredAutoLayout?.layoutMode ?? node.layoutMode,\n layoutWrap: node.inferredAutoLayout?.layoutWrap ?? node.layoutWrap,\n paddingLeft: node.inferredAutoLayout?.paddingLeft ?? node.paddingLeft,\n paddingRight: node.inferredAutoLayout?.paddingRight ?? node.paddingRight,\n paddingTop: node.inferredAutoLayout?.paddingTop ?? node.paddingTop,\n paddingBottom: node.inferredAutoLayout?.paddingBottom ?? node.paddingBottom,\n primaryAxisAlignItems:\n node.inferredAutoLayout?.primaryAxisAlignItems ?? node.primaryAxisAlignItems,\n counterAxisAlignItems:\n node.inferredAutoLayout?.counterAxisAlignItems ?? node.counterAxisAlignItems,\n primaryAxisSizingMode:\n node.inferredAutoLayout?.primaryAxisSizingMode ?? node.primaryAxisSizingMode,\n counterAxisSizingMode:\n node.inferredAutoLayout?.counterAxisSizingMode ?? node.counterAxisSizingMode,\n itemSpacing: node.inferredAutoLayout?.itemSpacing ?? node.itemSpacing,\n counterAxisSpacing:\n node.inferredAutoLayout?.counterAxisSpacing ?? node.counterAxisSpacing ?? undefined,\n };\n }\n\n async function normalizeBoundVariables({\n boundVariables,\n }: Pick<FrameNode, \"boundVariables\">): Promise<FigmaRestSpec.IsLayerTrait[\"boundVariables\"]> {\n if (!boundVariables) return undefined;\n\n const { width, height, componentProperties: _componentProperties, ...rest } = boundVariables;\n\n // replace VariableAlias' id with the actual variable key\n const resolveVariableId = async (variable: VariableAlias): Promise<VariableAlias> => ({\n ...variable,\n id: (await figma.variables.getVariableByIdAsync(variable.id))?.key ?? variable.id,\n });\n\n const needsResolution = [\n \"fills\",\n \"itemSpacing\",\n \"counterAxisSpacing\",\n \"bottomLeftRadius\",\n \"bottomRightRadius\",\n \"topLeftRadius\",\n \"topRightRadius\",\n \"paddingBottom\",\n \"paddingLeft\",\n \"paddingRight\",\n \"paddingTop\",\n \"maxHeight\",\n \"minHeight\",\n \"maxWidth\",\n \"minWidth\",\n ];\n\n // Process all properties in parallel\n const resolvedEntries = await Promise.all(\n Object.entries(rest).map(async ([key, value]) => {\n if (!value || !needsResolution.includes(key)) return [key, value];\n\n if (Array.isArray(value)) {\n return [key, await Promise.all(value.map(resolveVariableId))];\n }\n\n return [key, await resolveVariableId(value)];\n }),\n );\n\n return {\n ...Object.fromEntries(resolvedEntries),\n ...(width &&\n height && {\n size: {\n x: width,\n y: height,\n },\n }),\n };\n }\n\n return normalizeNode;\n}\n","import type { ComponentPropertyDefinition } from \"@/codegen\";\n\nexport interface ComponentMetadata {\n name: string;\n key: string;\n componentPropertyDefinitions: Record<string, ComponentPropertyDefinition>;\n}\n","export interface IconData {\n name: string;\n type: \"monochrome\" | \"multicolor\";\n weight?: string;\n}\n","import type {\n LocalVariable,\n LocalVariableCollection,\n VariableAlias,\n VariableResolvedDataType,\n} from \"@figma/rest-api-spec\";\n\nexport type Variable = LocalVariable;\n\nexport type VariableCollection = LocalVariableCollection;\n\nexport type VariableType = VariableResolvedDataType;\n\nexport type VariableValue = Variable[\"valuesByMode\"][string];\n\nexport type VariableValueResolved = Exclude<VariableValue, VariableAlias>;\n\nexport type { VariableScope } from \"@figma/rest-api-spec\";\n","import type { Variable, VariableCollection } from \"./variable.interface\";\n\nexport interface VariableRepository {\n getVariableList(): Variable[];\n getVariableCollectionList(): VariableCollection[];\n findVariableByKey(key: string): Variable | undefined;\n findVariableById(id: string): Variable | undefined;\n findVariableByName(name: string): Variable | undefined;\n findVariableCollectionByKey(key: string): VariableCollection | undefined;\n findVariableCollectionById(id: string): VariableCollection | undefined;\n}\n\nexport function createStaticVariableRepository({\n variables,\n variableCollections,\n}: {\n variables: Record<string, Variable>;\n variableCollections: Record<string, VariableCollection>;\n}): VariableRepository {\n const variablesKeyMap = new Map<string, Variable>();\n const variablesIdMap = new Map<string, Variable>();\n const variablesNameMap = new Map<string, Variable>();\n const variableCollectionsKeyMap = new Map<string, VariableCollection>();\n const variableCollectionsIdMap = new Map<string, VariableCollection>();\n\n for (const variable of Object.values(variables)) {\n if (variable.remote) {\n continue;\n }\n\n variablesKeyMap.set(variable.key, variable);\n variablesIdMap.set(variable.id, variable);\n variablesNameMap.set(variable.name, variable);\n }\n\n for (const variableCollection of Object.values(variableCollections)) {\n if (variableCollection.remote) {\n continue;\n }\n\n variableCollectionsKeyMap.set(variableCollection.key, variableCollection);\n variableCollectionsIdMap.set(variableCollection.id, variableCollection);\n }\n\n const variablesList = [...variablesKeyMap.values()];\n const variableCollectionsList = [...variableCollectionsKeyMap.values()];\n\n return {\n getVariableList: () => variablesList,\n getVariableCollectionList: () => variableCollectionsList,\n findVariableByName: (name: string) => variablesNameMap.get(name),\n findVariableByKey: (key: string) => variablesKeyMap.get(key),\n findVariableById: (id: string) => variablesIdMap.get(id),\n findVariableCollectionByKey: (key: string) => variableCollectionsKeyMap.get(key),\n findVariableCollectionById: (id: string) => variableCollectionsIdMap.get(id),\n };\n}\n","import type { Style as FigmaStyle, StyleType as FigmaStyleType } from \"@figma/rest-api-spec\";\n\nexport type Style = FigmaStyle;\n\nexport type StyleType = FigmaStyleType;\n","import type { Style } from \"./style.interface\";\n\nexport interface StyleRepository {\n getAll(): Style[];\n getTextStyles(): Style[];\n getColorStyles(): Style[];\n findOneByKey(key: string): Style | undefined;\n findOneByName(name: string): Style | undefined;\n}\n\nexport function createStaticStyleRepository(styles: Style[]): StyleRepository {\n const stylesMap = new Map<string, Style>();\n const stylesNameMap = new Map<string, Style>();\n\n for (const style of styles) {\n stylesMap.set(style.key, style);\n stylesNameMap.set(style.name, style);\n }\n\n return {\n getAll: () => styles,\n getTextStyles: () => styles.filter((style) => style.styleType === \"TEXT\"),\n getColorStyles: () => styles.filter((style) => style.styleType === \"FILL\"),\n findOneByKey: (key) => stylesMap.get(key),\n findOneByName: (name) => stylesNameMap.get(name),\n };\n}\n","import type { IconData } from \"./icon.interface\";\n\nexport interface IconRepository {\n getOne(key: string): IconData;\n}\n\nexport function createStaticIconRepository(iconRecord: Record<string, IconData>) {\n return {\n getOne: (key: string) => iconRecord[key],\n };\n}\n","import type { IconData } from \"./icon.interface\";\nimport type { IconRepository } from \"./icon.repository\";\n\nexport interface IconService {\n isAvailable: (componentKey: string) => boolean;\n getOne: (componentKey: string) => IconData;\n}\n\nexport function createIconService({\n iconRepository,\n}: {\n iconRepository: IconRepository;\n}): IconService {\n function isAvailable(componentKey: string) {\n return iconRepository.getOne(componentKey) !== undefined;\n }\n\n function getOne(componentKey: string) {\n return iconRepository.getOne(componentKey);\n }\n\n return {\n isAvailable,\n getOne,\n };\n}\n","import type { StyleRepository } from \"./style.repository\";\n\nexport interface StyleService {\n getSlug: (id: string) => string[] | undefined;\n}\n\n// TODO: inferStyleName 추가해야 함, rest api에서 style value가 제공되지 않고 있어 보류\nexport function createStyleService({\n styleRepository,\n}: {\n styleRepository: StyleRepository;\n}): StyleService {\n function getName(id: string) {\n const style = styleRepository.findOneByKey(id);\n\n if (!style) {\n return undefined;\n }\n\n return style.name;\n }\n\n function getSlug(id: string): string[] | undefined {\n const name = getName(id);\n\n if (!name) {\n return undefined;\n }\n\n return name.split(\"/\");\n }\n\n return {\n getSlug,\n };\n}\n","import {\n isIdenticalVariableValue,\n isInsideScope,\n isVariableAlias,\n sanitizeVariableId,\n} from \"@/utils/figma-variable\";\nimport type { Variable, VariableScope, VariableValueResolved } from \"./variable.interface\";\nimport type { VariableRepository } from \"./variable.repository\";\n\nexport interface VariableService {\n getSlug: (id: string) => string[] | undefined;\n resolveValue: (variable: Variable, mode: string) => VariableValueResolved;\n infer: (value: VariableValueResolved, scope: VariableScope) => Variable | undefined;\n}\n\nexport interface VariableServiceDeps {\n variableRepository: VariableRepository;\n inferCompareFunction: (a: Variable, b: Variable) => number;\n}\n\nexport function createVariableService({\n variableRepository,\n inferCompareFunction,\n}: VariableServiceDeps): VariableService {\n const variables = variableRepository.getVariableList();\n\n // private\n function getName(key: string) {\n const sanitizedId = sanitizeVariableId(key);\n const variable = variableRepository.findVariableByKey(sanitizedId);\n\n if (!variable) {\n return undefined;\n }\n\n return variable.name;\n }\n\n function getDefaultModeId(variable: Variable) {\n const variableCollection = variableRepository.findVariableCollectionById(\n variable.variableCollectionId,\n );\n\n if (!variableCollection) {\n // Variable collection not found: ${variable.variableCollectionId}, falling back to variable.valuesByMode key\n return Object.keys(variable.valuesByMode)[0]!;\n }\n\n return variableCollection.defaultModeId;\n }\n\n // public\n function getSlug(key: string): string[] | undefined {\n const name = getName(key);\n\n if (!name) {\n return undefined;\n }\n\n return name.split(\"/\");\n }\n\n function resolveValue(variable: Variable, mode: string): VariableValueResolved {\n const value = variable.valuesByMode[mode];\n\n if (value === undefined) {\n throw new Error(`Variable value not found: ${variable.id} ${mode}`);\n }\n\n if (isVariableAlias(value)) {\n const resolvedVariable = variableRepository.findVariableById(value.id);\n\n if (!resolvedVariable) {\n throw new Error(`Variable not found: ${value.id}`);\n }\n\n return resolveValue(resolvedVariable, mode);\n }\n\n return value;\n }\n\n function infer(value: VariableValueResolved, scope: VariableScope) {\n // NOTE: We assume that the variable is in the default mode or value is equal between all modes for simplicity.\n const inferredVariables = variables.filter(\n (variable) =>\n isInsideScope(variable, scope) &&\n isIdenticalVariableValue(resolveValue(variable, getDefaultModeId(variable)), value),\n );\n\n const sortedVariables = inferredVariables.sort(inferCompareFunction);\n\n return sortedVariables[0];\n }\n\n return {\n getSlug,\n resolveValue,\n infer,\n };\n}\n","import type { ComponentMetadata } from \"./component.interface\";\n\nexport interface ComponentRepository {\n getOne(key: string): ComponentMetadata | undefined;\n}\n\nexport function createStaticComponentRepository(data: Record<string, ComponentMetadata>) {\n const componentRecord: Record<string, ComponentMetadata> = {};\n Object.values(data).forEach((component) => {\n componentRecord[component.key] = component;\n });\n\n return {\n getOne: (key: string) => componentRecord[key],\n };\n}\n","import { createStaticIconRepository } from \"./icon.repository\";\nimport { FIGMA_ICONS } from \"./data/__generated__/icons\";\nimport { FIGMA_STYLES } from \"./data/__generated__/styles\";\nimport { FIGMA_VARIABLE_COLLECTIONS } from \"./data/__generated__/variable-collections\";\nimport { FIGMA_VARIABLES } from \"./data/__generated__/variables\";\nimport * as FIGMA_COMPONENTS from \"./data/__generated__/component-sets\";\nimport { createStaticStyleRepository } from \"./style.repository\";\nimport { createStaticVariableRepository } from \"./variable.repository\";\nimport { createStaticComponentRepository } from \"./component.repository\";\n\nexport * from \"./icon.interface\";\nexport * from \"./icon.repository\";\nexport * from \"./icon.service\";\nexport * from \"./style.interface\";\nexport * from \"./style.repository\";\nexport * from \"./style.service\";\nexport * from \"./variable.interface\";\nexport * from \"./variable.repository\";\nexport * from \"./variable.service\";\nexport * from \"./component.interface\";\nexport * from \"./component.repository\";\n\nexport const styleRepository = createStaticStyleRepository(FIGMA_STYLES);\nexport const variableRepository = createStaticVariableRepository({\n variables: FIGMA_VARIABLES,\n variableCollections: FIGMA_VARIABLE_COLLECTIONS,\n});\nexport const iconRepository = createStaticIconRepository(FIGMA_ICONS);\nexport const componentRepository = createStaticComponentRepository(FIGMA_COMPONENTS);\n\nexport function getFigmaVariableKey(name: string) {\n return variableRepository.findVariableByName(name)?.key;\n}\n\nexport function getFigmaStyleKey(name: string) {\n return styleRepository.findOneByName(name)?.key;\n}\n\nexport function getFigmaColorVariableNames(scopes: Array<\"fg\" | \"bg\" | \"stroke\" | \"palette\">) {\n const variables = variableRepository.getVariableList();\n return variables\n .filter((variable) =>\n scopes.includes(variable.name.split(\"/\")[0] as \"fg\" | \"bg\" | \"stroke\" | \"palette\"),\n )\n .map((variable) => variable.name);\n}\n"],"names":[],"mappings":";;;;;;AACO;AACA;AACA;AACP;AACA;AACO;AACA;AACP;AACA;AACO;AACA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACO;AACA;AACA;AACP;AACA;AACO;AACP;AACA;AACO;AACP;AACA;AACO;AACP;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACO;AACP;AACA;AACO;AACP;AACA;AACA;AACA;AACO;;ACpEA;AACP;AACA;AACA;AACA;AACO;;ACNA;;ACAA;AACP;AACA;AACA;AACA;;ACLO;AACP;AACA;AACA;AACA;;ACHO;AACA;AACA;AACA;AACA;;ACJA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;;ACZO;AACA;;ACDA;AACP;AACA;AACA;AACA;AACA;AACA;AACO;;ACPA;AACP;AACA;AACO;AACP;AACA;;ACJO;AACP;AACA;AACA;AACO;AACP;AACA;;ACPO;AACP;AACA;AACO;AACP;AACA;;ACJO;AACP;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACO;;ACVA;AACP;AACA;AACO;AACP;AACA;;ACKO;AACA;AACA;AACP;AACA;AACO;AACP;AACA;AACO;AACA;AACA;;;"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sources":["../src/normalizer/types.ts","../src/normalizer/from-rest.ts","../src/normalizer/from-plugin.ts","../src/entities/component.interface.ts","../src/entities/icon.interface.ts","../src/entities/variable.interface.ts","../src/entities/variable.repository.ts","../src/entities/style.interface.ts","../src/entities/style.repository.ts","../src/entities/icon.repository.ts","../src/entities/icon.service.ts","../src/entities/style.service.ts","../src/entities/variable.service.ts","../src/entities/component.repository.ts","../src/entities/index.ts"],"sourcesContent":["import type * as FigmaRestSpec from \"@figma/rest-api-spec\";\n\nexport type NormalizedIsLayerTrait = Pick<FigmaRestSpec.IsLayerTrait, \"type\" | \"id\" | \"name\"> & {\n boundVariables?: Pick<\n NonNullable<FigmaRestSpec.IsLayerTrait[\"boundVariables\"]>,\n | \"fills\"\n | \"strokes\"\n | \"itemSpacing\"\n | \"counterAxisSpacing\"\n | \"bottomLeftRadius\"\n | \"bottomRightRadius\"\n | \"topLeftRadius\"\n | \"topRightRadius\"\n | \"paddingBottom\"\n | \"paddingLeft\"\n | \"paddingRight\"\n | \"paddingTop\"\n | \"maxHeight\"\n | \"minHeight\"\n | \"maxWidth\"\n | \"minWidth\"\n | \"fontSize\"\n | \"fontWeight\"\n | \"lineHeight\"\n | \"size\"\n >;\n};\n\nexport type NormalizedCornerTrait = Pick<\n FigmaRestSpec.CornerTrait,\n \"cornerRadius\" | \"rectangleCornerRadii\"\n>;\n\nexport type NormalizedHasChildrenTrait = {\n children: NormalizedSceneNode[];\n};\n\nexport type NormalizedHasLayoutTrait = Pick<\n FigmaRestSpec.HasLayoutTrait,\n | \"layoutAlign\"\n | \"layoutGrow\"\n | \"absoluteBoundingBox\"\n | \"relativeTransform\"\n | \"layoutPositioning\"\n | \"layoutSizingHorizontal\"\n | \"layoutSizingVertical\"\n | \"minHeight\"\n | \"minWidth\"\n | \"maxHeight\"\n | \"maxWidth\"\n>;\n\nexport type NormalizedSolidPaint = FigmaRestSpec.SolidPaint;\n\nexport type NormalizedPaint =\n | NormalizedSolidPaint\n | FigmaRestSpec.GradientPaint\n | FigmaRestSpec.ImagePaint;\n\nexport type NormalizedHasGeometryTrait = Omit<\n Pick<FigmaRestSpec.HasGeometryTrait, \"fills\" | \"strokes\" | \"strokeWeight\">,\n \"fills\" | \"strokes\"\n> & {\n fills: NormalizedPaint[];\n strokes: NormalizedPaint[];\n fillStyleKey?: string;\n};\n\nexport type NormalizedShadow =\n | (Pick<\n FigmaRestSpec.DropShadowEffect,\n \"color\" | \"offset\" | \"radius\" | \"spread\" | \"boundVariables\"\n > &\n Required<Pick<FigmaRestSpec.DropShadowEffect, \"type\">>)\n | (Pick<\n FigmaRestSpec.InnerShadowEffect,\n \"color\" | \"offset\" | \"radius\" | \"spread\" | \"boundVariables\"\n > &\n Required<Pick<FigmaRestSpec.InnerShadowEffect, \"type\">>);\n\nexport type NormalizedHasEffectsTrait = Omit<FigmaRestSpec.HasEffectsTrait, \"effects\"> & {\n effects: NormalizedShadow[];\n effectStyleKey?: string;\n};\n\nexport type NormalizedHasFramePropertiesTrait = Pick<\n FigmaRestSpec.HasFramePropertiesTrait,\n | \"layoutMode\"\n | \"layoutWrap\"\n | \"paddingLeft\"\n | \"paddingRight\"\n | \"paddingTop\"\n | \"paddingBottom\"\n | \"primaryAxisAlignItems\"\n | \"primaryAxisSizingMode\"\n | \"counterAxisAlignItems\"\n | \"counterAxisSizingMode\"\n | \"itemSpacing\"\n | \"counterAxisSpacing\"\n>;\n\nexport interface NormalizedTextSegment {\n characters: string;\n start: number;\n end: number;\n style: {\n fontFamily?: string;\n fontWeight?: number;\n fontSize?: number;\n italic?: boolean;\n textDecoration?: string;\n letterSpacing?: number;\n /**\n * in pixels\n */\n lineHeight?: number;\n };\n}\n\nexport type NormalizedTypePropertiesTrait = Pick<\n FigmaRestSpec.TypePropertiesTrait,\n \"style\" | \"characters\"\n> & {\n segments: NormalizedTextSegment[];\n\n textStyleKey?: string;\n};\n\nexport type NormalizedDefaultShapeTrait = NormalizedIsLayerTrait &\n NormalizedHasLayoutTrait &\n NormalizedHasGeometryTrait &\n NormalizedHasEffectsTrait;\n\nexport type NormalizedFrameTrait = NormalizedIsLayerTrait &\n NormalizedHasLayoutTrait &\n NormalizedHasGeometryTrait &\n NormalizedHasEffectsTrait &\n NormalizedHasChildrenTrait &\n NormalizedCornerTrait &\n NormalizedHasFramePropertiesTrait;\n\nexport interface NormalizedFrameNode extends NormalizedFrameTrait {\n type: FigmaRestSpec.FrameNode[\"type\"];\n}\n\nexport interface NormalizedRectangleNode\n extends NormalizedDefaultShapeTrait,\n NormalizedCornerTrait {\n type: FigmaRestSpec.RectangleNode[\"type\"];\n}\n\nexport interface NormalizedTextNode\n extends NormalizedDefaultShapeTrait,\n NormalizedTypePropertiesTrait {\n type: FigmaRestSpec.TextNode[\"type\"];\n}\n\nexport interface NormalizedComponentNode extends NormalizedFrameTrait {\n type: FigmaRestSpec.ComponentNode[\"type\"];\n}\n\nexport interface NormalizedInstanceNode extends NormalizedFrameTrait {\n type: FigmaRestSpec.InstanceNode[\"type\"];\n\n componentProperties: {\n [key: string]: FigmaRestSpec.ComponentProperty & {\n componentKey?: string;\n componentSetKey?: string;\n };\n };\n\n componentKey: string;\n\n componentSetKey?: string;\n\n overrides?: FigmaRestSpec.InstanceNode[\"overrides\"];\n\n children: NormalizedSceneNode[];\n}\n\nexport interface NormalizedVectorNode extends NormalizedDefaultShapeTrait, NormalizedCornerTrait {\n type: FigmaRestSpec.VectorNode[\"type\"];\n}\n\nexport interface NormalizedBooleanOperationNode\n extends NormalizedIsLayerTrait,\n NormalizedHasChildrenTrait,\n NormalizedHasLayoutTrait,\n NormalizedHasGeometryTrait,\n NormalizedHasEffectsTrait {\n type: FigmaRestSpec.BooleanOperationNode[\"type\"];\n}\n\nexport interface NormalizedUnhandledNode {\n type: \"UNHANDLED\";\n id: string;\n original: FigmaRestSpec.Node | SceneNode;\n}\n\nexport type NormalizedSceneNode =\n | NormalizedFrameNode\n | NormalizedRectangleNode\n | NormalizedTextNode\n | NormalizedComponentNode\n | NormalizedInstanceNode\n | NormalizedVectorNode\n | NormalizedBooleanOperationNode\n | NormalizedUnhandledNode;\n","/**\n * from-rest could be run outside of the Figma Plugin environment\n * so we cannot use the Plugin API types directly e.g. getNodeByIdAsync\n */\n\n/**\n * NOTE: types of MinimalFillsTrait[\"styles\"] can be found here:\n * https://developers.figma.com/docs/rest-api/component-types/#style-type\n * Record<\"text\" | \"fill\" | \"stroke\" | \"effect\" | \"grid\", string>\n */\n\nimport type * as FigmaRestSpec from \"@figma/rest-api-spec\";\nimport type {\n NormalizedSceneNode,\n NormalizedFrameNode,\n NormalizedRectangleNode,\n NormalizedTextNode,\n NormalizedComponentNode,\n NormalizedInstanceNode,\n NormalizedTextSegment,\n NormalizedVectorNode,\n NormalizedBooleanOperationNode,\n NormalizedShadow,\n NormalizedCornerTrait,\n NormalizedHasFramePropertiesTrait,\n NormalizedPaint,\n NormalizedDefaultShapeTrait,\n NormalizedHasEffectsTrait,\n NormalizedIsLayerTrait,\n} from \"./types\";\n\nexport interface RestNormalizerContext {\n /**\n * A map of style **ID** to style data\n */\n styles: Record<string, FigmaRestSpec.Style>;\n /**\n * A map of component **ID** to component data\n */\n components: Record<string, FigmaRestSpec.Component>;\n /**\n * A map of component set **ID** to component set data\n */\n componentSets: Record<string, FigmaRestSpec.ComponentSet>;\n}\n\nexport function createRestNormalizer(\n ctx: RestNormalizerContext,\n): (node: FigmaRestSpec.Node) => NormalizedSceneNode {\n function normalizeNodes(nodes: readonly FigmaRestSpec.Node[]): NormalizedSceneNode[] {\n // Figma REST API omits default values for some fields, \"visible\" is one of them\n return nodes.filter((node) => !(\"visible\" in node) || node.visible).map(normalizeNode);\n }\n\n function normalizeNode(node: FigmaRestSpec.Node): NormalizedSceneNode {\n switch (node.type) {\n case \"FRAME\":\n return normalizeFrameNode(node);\n case \"RECTANGLE\":\n return normalizeRectangleNode(node);\n case \"TEXT\":\n return normalizeTextNode(node);\n case \"COMPONENT\":\n return normalizeComponentNode(node);\n case \"INSTANCE\":\n return normalizeInstanceNode(node);\n case \"VECTOR\":\n return normalizeVectorNode(node);\n case \"BOOLEAN_OPERATION\":\n return normalizeBooleanOperationNode(node);\n case \"GROUP\":\n return normalizeGroupNodeAsFrameNode(node);\n default:\n return {\n type: \"UNHANDLED\",\n id: node.id,\n original: node,\n };\n }\n }\n\n function normalizeBoundVariables(\n boundVariables: FigmaRestSpec.IsLayerTrait[\"boundVariables\"] | undefined,\n ) {\n if (!boundVariables) return undefined;\n\n return {\n fills: boundVariables.fills,\n strokes: boundVariables.strokes,\n itemSpacing: boundVariables.itemSpacing,\n counterAxisSpacing: boundVariables.counterAxisSpacing,\n topLeftRadius: boundVariables.topLeftRadius,\n topRightRadius: boundVariables.topRightRadius,\n bottomLeftRadius: boundVariables.bottomLeftRadius,\n bottomRightRadius: boundVariables.bottomRightRadius,\n paddingTop: boundVariables.paddingTop,\n paddingRight: boundVariables.paddingRight,\n paddingBottom: boundVariables.paddingBottom,\n paddingLeft: boundVariables.paddingLeft,\n minWidth: boundVariables.minWidth,\n maxWidth: boundVariables.maxWidth,\n minHeight: boundVariables.minHeight,\n maxHeight: boundVariables.maxHeight,\n fontSize: boundVariables.fontSize,\n fontWeight: boundVariables.fontWeight,\n lineHeight: boundVariables.lineHeight,\n size: boundVariables.size,\n };\n }\n\n function normalizePaint(paint: FigmaRestSpec.Paint): NormalizedPaint {\n switch (paint.type) {\n case \"SOLID\":\n case \"IMAGE\":\n case \"GRADIENT_LINEAR\":\n case \"GRADIENT_RADIAL\":\n case \"GRADIENT_ANGULAR\":\n case \"GRADIENT_DIAMOND\":\n return paint;\n default:\n throw new Error(`Unimplemented paint type: ${paint.type}`);\n }\n }\n\n function normalizePaints(paints: FigmaRestSpec.Paint[] | undefined): NormalizedPaint[] {\n if (!paints) return [];\n\n return paints.map(normalizePaint);\n }\n\n function normalizeRadiusProps({\n cornerRadius,\n rectangleCornerRadii,\n }: Pick<\n FigmaRestSpec.RectangleNode,\n \"cornerRadius\" | \"rectangleCornerRadii\"\n >): NormalizedCornerTrait {\n return { cornerRadius, rectangleCornerRadii };\n }\n\n function normalizeEffectProps(\n node: Pick<FigmaRestSpec.FrameNode, \"effects\" | \"styles\">,\n ): NormalizedHasEffectsTrait {\n const effects = (node.effects ?? [])\n .filter(\n (effect): effect is FigmaRestSpec.DropShadowEffect | FigmaRestSpec.InnerShadowEffect =>\n effect.visible !== false &&\n (effect.type === \"DROP_SHADOW\" || effect.type === \"INNER_SHADOW\"),\n )\n .map((effect): NormalizedShadow => {\n const { type, color, offset, radius, spread, boundVariables } = effect;\n\n return {\n type,\n color,\n offset,\n radius,\n spread,\n boundVariables,\n };\n });\n\n return {\n effects,\n effectStyleKey: node.styles?.[\"effect\"] ? ctx.styles[node.styles[\"effect\"]]?.key : undefined,\n };\n }\n\n function normalizeShapeProps(\n node: Pick<\n FigmaRestSpec.FrameNode,\n | \"fills\"\n | \"strokes\"\n | \"strokeWeight\"\n | \"styles\"\n | \"layoutGrow\"\n | \"layoutAlign\"\n | \"layoutSizingHorizontal\"\n | \"layoutSizingVertical\"\n | \"absoluteBoundingBox\"\n | \"relativeTransform\"\n | \"layoutPositioning\"\n | \"minHeight\"\n | \"minWidth\"\n | \"maxHeight\"\n | \"maxWidth\"\n | \"effects\"\n >,\n ): Omit<NormalizedDefaultShapeTrait, keyof NormalizedIsLayerTrait> {\n return {\n // NormalizedHasLayoutTrait\n layoutGrow: node.layoutGrow,\n layoutAlign: node.layoutAlign,\n layoutSizingHorizontal: node.layoutSizingHorizontal,\n layoutSizingVertical: node.layoutSizingVertical,\n absoluteBoundingBox: node.absoluteBoundingBox,\n relativeTransform: node.relativeTransform,\n layoutPositioning: node.layoutPositioning,\n minHeight: node.minHeight,\n minWidth: node.minWidth,\n maxHeight: node.maxHeight,\n maxWidth: node.maxWidth,\n\n // NormalizedHasGeometryTrait\n fills: normalizePaints(node.fills),\n fillStyleKey: node.styles?.[\"fill\"] ? ctx.styles[node.styles[\"fill\"]]?.key : undefined,\n strokes: normalizePaints(node.strokes),\n strokeWeight: node.strokeWeight,\n\n // NormalizedHasEffectsTrait\n ...normalizeEffectProps(node),\n };\n }\n\n function normalizeAutolayoutProps(\n node: Pick<\n FigmaRestSpec.FrameNode,\n | \"layoutMode\"\n | \"layoutWrap\"\n | \"paddingLeft\"\n | \"paddingRight\"\n | \"paddingTop\"\n | \"paddingBottom\"\n | \"primaryAxisAlignItems\"\n | \"primaryAxisSizingMode\"\n | \"counterAxisAlignItems\"\n | \"counterAxisSizingMode\"\n | \"itemSpacing\"\n | \"counterAxisSpacing\"\n >,\n ): NormalizedHasFramePropertiesTrait {\n return {\n layoutMode: node.layoutMode,\n layoutWrap: node.layoutWrap,\n paddingLeft: node.paddingLeft,\n paddingRight: node.paddingRight,\n paddingTop: node.paddingTop,\n paddingBottom: node.paddingBottom,\n primaryAxisAlignItems: node.primaryAxisAlignItems,\n primaryAxisSizingMode: node.primaryAxisSizingMode,\n counterAxisAlignItems: node.counterAxisAlignItems,\n counterAxisSizingMode: node.counterAxisSizingMode,\n itemSpacing: node.itemSpacing,\n counterAxisSpacing: node.counterAxisSpacing,\n };\n }\n\n function normalizeFrameNode(node: FigmaRestSpec.FrameNode): NormalizedFrameNode {\n return {\n // NormalizedIsLayerTrait\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node.boundVariables),\n\n // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait, NormalizedHasFramePropertiesTrait\n ...normalizeShapeProps(node),\n\n // NormalizedCornerTrait\n ...normalizeRadiusProps(node),\n\n // NormalizedHasFramePropertiesTrait\n ...normalizeAutolayoutProps(node),\n\n // NormalizedHasChildrenTrait\n children: normalizeNodes(node.children),\n };\n }\n\n function normalizeRectangleNode(node: FigmaRestSpec.RectangleNode): NormalizedRectangleNode {\n return {\n // NormalizedIsLayerTrait\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node.boundVariables),\n\n // NormalizedCornerTrait\n ...normalizeRadiusProps(node),\n\n // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait\n ...normalizeShapeProps(node),\n };\n }\n\n function normalizeTextNode(node: FigmaRestSpec.TextNode): NormalizedTextNode {\n // Convert TypeStyle to NormalizedTextSegment.style format\n function normalizeSegmentStyle(\n typeStyle: FigmaRestSpec.TypeStyle,\n ): NormalizedTextSegment[\"style\"] {\n return {\n fontFamily: typeStyle.fontFamily,\n fontWeight: typeStyle.fontWeight,\n fontSize: typeStyle.fontSize,\n italic: typeStyle.italic,\n textDecoration: typeStyle.textDecoration,\n letterSpacing: typeStyle.letterSpacing,\n lineHeight: typeStyle.lineHeightPx,\n };\n }\n\n // Function to segment a text node based on style overrides\n function segmentTextNode(textNode: FigmaRestSpec.TextNode): NormalizedTextSegment[] {\n const segments: NormalizedTextSegment[] = [];\n const characters = textNode.characters;\n const styleOverrides = textNode.characterStyleOverrides || [];\n const styleTable = textNode.styleOverrideTable || {};\n\n // If no style overrides, return the entire text as one segment\n if (!styleOverrides.length) {\n return [\n {\n characters: characters,\n start: 0,\n end: characters.length,\n style: normalizeSegmentStyle(textNode.style),\n },\n ];\n }\n\n let currentSegment: NormalizedTextSegment = {\n characters: \"\",\n start: 0,\n end: 0,\n style: {},\n };\n\n let currentStyleId: string | null = null;\n\n for (let i = 0; i < characters.length; i++) {\n const styleId = styleOverrides[i]?.toString() || null;\n\n // If style changes or it's the first character\n if (styleId !== currentStyleId || i === 0) {\n // Add the previous segment if it exists\n if (i > 0) {\n currentSegment.end = i;\n currentSegment.characters = characters.substring(\n currentSegment.start,\n currentSegment.end,\n );\n segments.push({ ...currentSegment });\n }\n\n // Start a new segment\n currentStyleId = styleId;\n currentSegment = {\n characters: \"\",\n start: i,\n end: 0,\n style: styleId ? normalizeSegmentStyle(styleTable[styleId]) : {},\n };\n }\n }\n\n // Add the last segment\n if (currentSegment.start < characters.length) {\n currentSegment.end = characters.length;\n currentSegment.characters = characters.substring(currentSegment.start, currentSegment.end);\n segments.push(currentSegment);\n }\n\n return segments;\n }\n\n return {\n // NormalizedIsLayerTrait\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node.boundVariables),\n\n // NormalizedTypePropertiesTrait\n style: node.style, // this style is the style of the first segment\n characters: node.characters,\n textStyleKey: node.styles?.[\"text\"] ? ctx.styles[node.styles[\"text\"]]?.key : undefined,\n segments: segmentTextNode(node),\n\n // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait\n ...normalizeShapeProps(node),\n };\n }\n\n function normalizeComponentNode(node: FigmaRestSpec.ComponentNode): NormalizedComponentNode {\n return {\n // NormalizedIsLayerTrait\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node.boundVariables),\n\n // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait\n ...normalizeShapeProps(node),\n\n // NormalizedHasCornerTrait\n ...normalizeRadiusProps(node),\n\n // NormalizedHasFramePropertiesTrait\n ...normalizeAutolayoutProps(node),\n\n // NormalizedHasChildrenTrait\n children: normalizeNodes(node.children),\n };\n }\n\n function normalizeInstanceNode(node: FigmaRestSpec.InstanceNode): NormalizedInstanceNode {\n const mainComponent = ctx.components[node.componentId];\n if (!mainComponent) {\n throw new Error(`Component ${node.componentId} not found`);\n }\n\n const componentSet = mainComponent.componentSetId\n ? ctx.componentSets[mainComponent.componentSetId]\n : undefined;\n\n const componentProperties: NormalizedInstanceNode[\"componentProperties\"] = {};\n\n for (const [key, value] of Object.entries(node.componentProperties ?? {})) {\n componentProperties[key] = value;\n\n if (value.type === \"INSTANCE_SWAP\") {\n // unless value.type === \"BOOLEAN\", value.value is string\n const swappedComponent = ctx.components[value.value as string];\n\n if (swappedComponent) {\n componentProperties[key].componentKey = swappedComponent.key;\n\n const swappedComponentSet = swappedComponent?.componentSetId\n ? ctx.componentSets[swappedComponent.componentSetId]\n : undefined;\n\n if (swappedComponentSet) {\n componentProperties[key].componentSetKey = swappedComponentSet.key;\n }\n }\n }\n }\n\n return {\n // NormalizedIsLayerTrait\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node.boundVariables),\n\n // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait\n ...normalizeShapeProps(node),\n\n // NormalizedCornerTrait\n ...normalizeRadiusProps(node),\n\n // NormalizedHasFramePropertiesTrait\n ...normalizeAutolayoutProps(node),\n\n // NormalizedHasChildrenTrait\n children: normalizeNodes(node.children),\n\n // NormalizedInstanceNode specific\n componentProperties,\n componentKey: mainComponent.key,\n componentSetKey: componentSet?.key,\n overrides: node.overrides,\n };\n }\n\n function normalizeVectorNode(node: FigmaRestSpec.VectorNode): NormalizedVectorNode {\n return {\n // NormalizedIsLayerTrait\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node.boundVariables),\n\n // NormalizedCornerTrait\n ...normalizeRadiusProps(node),\n\n // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait\n ...normalizeShapeProps(node),\n };\n }\n\n function normalizeBooleanOperationNode(\n node: FigmaRestSpec.BooleanOperationNode,\n ): NormalizedBooleanOperationNode {\n return {\n // NormalizedIsLayerTrait\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node.boundVariables),\n\n // NormalizedHasLayoutTrait\n layoutGrow: node.layoutGrow,\n layoutAlign: node.layoutAlign,\n layoutSizingHorizontal: node.layoutSizingHorizontal,\n layoutSizingVertical: node.layoutSizingVertical,\n absoluteBoundingBox: node.absoluteBoundingBox,\n relativeTransform: node.relativeTransform,\n layoutPositioning: node.layoutPositioning,\n minHeight: node.minHeight,\n minWidth: node.minWidth,\n maxHeight: node.maxHeight,\n maxWidth: node.maxWidth,\n\n // NormalizedHasGeometryTrait\n fills: normalizePaints(node.fills),\n fillStyleKey: node.styles?.[\"fill\"] ? ctx.styles[node.styles[\"fill\"]]?.key : undefined,\n strokes: normalizePaints(node.strokes),\n strokeWeight: node.strokeWeight,\n\n // NormalizedHasEffectsTrait\n ...normalizeEffectProps(node),\n\n // NormalizedHasChildrenTrait\n children: normalizeNodes(node.children),\n };\n }\n\n function normalizeGroupNodeAsFrameNode(node: FigmaRestSpec.GroupNode): NormalizedFrameNode {\n return {\n // NormalizedIsLayerTrait\n type: \"FRAME\",\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node.boundVariables),\n\n // NormalizedHasLayoutTrait\n layoutGrow: node.layoutGrow,\n layoutAlign: node.layoutAlign,\n layoutSizingHorizontal: node.layoutSizingHorizontal,\n layoutSizingVertical: node.layoutSizingVertical,\n absoluteBoundingBox: node.absoluteBoundingBox,\n relativeTransform: node.relativeTransform,\n layoutPositioning: node.layoutPositioning,\n minHeight: node.minHeight,\n minWidth: node.minWidth,\n maxHeight: node.maxHeight,\n maxWidth: node.maxWidth,\n\n // NormalizedHasGeometryTrait\n fills: [],\n fillStyleKey: undefined,\n strokes: [],\n strokeWeight: undefined,\n\n // NormalizedHasEffectsTrait\n effects: [],\n effectStyleKey: undefined,\n\n // NormalizedCornerTrait\n cornerRadius: undefined,\n rectangleCornerRadii: undefined,\n\n // NormalizedHasFramePropertiesTrait\n // these are undefined compared to from-plugin normalizer\n // since inferredAutoLayout isn't available in REST API\n layoutMode: undefined,\n layoutWrap: undefined,\n paddingLeft: undefined,\n paddingRight: undefined,\n paddingTop: undefined,\n paddingBottom: undefined,\n primaryAxisAlignItems: undefined,\n primaryAxisSizingMode: undefined,\n counterAxisAlignItems: undefined,\n counterAxisSizingMode: undefined,\n itemSpacing: undefined,\n counterAxisSpacing: undefined,\n\n // NormalizedHasChildrenTrait\n children: normalizeNodes(node.children),\n };\n }\n\n return normalizeNode;\n}\n","/**\n * from-plugin is guaranteed to be run in the Figma Plugin environment\n * so we can use the Plugin API types directly (figma.getNodeByIdAsync, node.getMainComponentAsync etc)\n * however it could be better to make users can DI later\n */\n\nimport type {\n NormalizedSceneNode,\n NormalizedFrameNode,\n NormalizedRectangleNode,\n NormalizedTextNode,\n NormalizedComponentNode,\n NormalizedInstanceNode,\n NormalizedVectorNode,\n NormalizedBooleanOperationNode,\n NormalizedHasEffectsTrait,\n NormalizedShadow,\n NormalizedDefaultShapeTrait,\n NormalizedHasFramePropertiesTrait,\n NormalizedCornerTrait,\n NormalizedIsLayerTrait,\n NormalizedPaint,\n NormalizedTextSegment,\n} from \"./types\";\nimport { convertTransformToGradientHandles } from \"@/utils/figma-gradient\";\n\nexport function createPluginNormalizer(): (node: SceneNode) => Promise<NormalizedSceneNode> {\n async function normalizeNodes(nodes: readonly SceneNode[]): Promise<NormalizedSceneNode[]> {\n return Promise.all(nodes.filter((node) => node.visible).map(normalizeNode));\n }\n\n async function normalizeNode(node: SceneNode): Promise<NormalizedSceneNode> {\n switch (node.type) {\n case \"FRAME\":\n return normalizeFrameNode(node);\n case \"RECTANGLE\":\n return normalizeRectangleNode(node);\n case \"TEXT\":\n return normalizeTextNode(node);\n case \"COMPONENT\":\n return normalizeComponentNode(node);\n case \"INSTANCE\":\n return normalizeInstanceNode(node);\n case \"VECTOR\":\n return normalizeVectorNode(node);\n case \"BOOLEAN_OPERATION\":\n return normalizeBooleanOperationNode(node);\n case \"GROUP\":\n return normalizeGroupNodeAsFrameNode(node);\n default:\n return {\n type: \"UNHANDLED\",\n id: node.id,\n original: node,\n };\n }\n }\n\n /**\n * Pick specific fields from boundVariables\n */\n function normalizeBoundVariables({\n boundVariables,\n }: Pick<FrameNode, \"boundVariables\">): NormalizedIsLayerTrait[\"boundVariables\"] {\n if (!boundVariables) return undefined;\n\n return {\n fills: boundVariables.fills,\n strokes: boundVariables.strokes,\n itemSpacing: boundVariables.itemSpacing,\n counterAxisSpacing: boundVariables.counterAxisSpacing,\n topLeftRadius: boundVariables.topLeftRadius,\n topRightRadius: boundVariables.topRightRadius,\n bottomLeftRadius: boundVariables.bottomLeftRadius,\n bottomRightRadius: boundVariables.bottomRightRadius,\n paddingTop: boundVariables.paddingTop,\n paddingRight: boundVariables.paddingRight,\n paddingBottom: boundVariables.paddingBottom,\n paddingLeft: boundVariables.paddingLeft,\n minWidth: boundVariables.minWidth,\n maxWidth: boundVariables.maxWidth,\n minHeight: boundVariables.minHeight,\n maxHeight: boundVariables.maxHeight,\n fontSize: boundVariables.fontSize,\n fontWeight: boundVariables.fontWeight,\n lineHeight: boundVariables.lineHeight,\n size: {\n x: boundVariables.width,\n y: boundVariables.height,\n },\n };\n }\n\n function normalizeSolidPaint(paint: SolidPaint): NormalizedPaint {\n return {\n type: paint.type,\n color: {\n r: paint.color.r,\n g: paint.color.g,\n b: paint.color.b,\n a: paint.opacity ?? 1,\n },\n visible: paint.visible,\n blendMode: paint.blendMode ?? \"NORMAL\",\n opacity: paint.opacity,\n boundVariables: paint.boundVariables,\n };\n }\n\n function normalizePaint(paint: Paint): NormalizedPaint {\n switch (paint.type) {\n case \"SOLID\":\n return normalizeSolidPaint(paint);\n case \"IMAGE\":\n return {\n type: \"IMAGE\",\n scaleMode: paint.scaleMode === \"CROP\" ? \"STRETCH\" : paint.scaleMode,\n imageTransform: paint.imageTransform,\n scalingFactor: paint.scalingFactor,\n filters: paint.filters,\n rotation: paint.rotation,\n imageRef: paint.imageHash ?? \"\",\n blendMode: paint.blendMode ?? \"NORMAL\",\n visible: paint.visible,\n opacity: paint.opacity,\n };\n case \"GRADIENT_LINEAR\":\n case \"GRADIENT_RADIAL\":\n case \"GRADIENT_ANGULAR\":\n case \"GRADIENT_DIAMOND\":\n return {\n type: paint.type,\n gradientStops: [...paint.gradientStops],\n visible: paint.visible,\n opacity: paint.opacity,\n blendMode: paint.blendMode ?? \"NORMAL\",\n gradientHandlePositions: convertTransformToGradientHandles(paint.gradientTransform),\n };\n default:\n throw new Error(`Unimplemented paint type: ${paint.type}`);\n }\n }\n\n function normalizePaints(fills: readonly Paint[] | PluginAPI[\"mixed\"]): NormalizedPaint[] {\n if (fills === figma.mixed) {\n console.warn(\"Mixed fills are not supported\");\n\n return [];\n }\n\n return fills.map(normalizePaint);\n }\n\n function normalizeRadiusProps(\n node: Pick<\n RectangleNode,\n \"cornerRadius\" | \"topLeftRadius\" | \"topRightRadius\" | \"bottomRightRadius\" | \"bottomLeftRadius\"\n >,\n ): NormalizedCornerTrait {\n return {\n cornerRadius: node.cornerRadius === figma.mixed ? undefined : node.cornerRadius,\n rectangleCornerRadii: [\n node.topLeftRadius,\n node.topRightRadius,\n node.bottomRightRadius,\n node.bottomLeftRadius,\n ],\n };\n }\n\n async function normalizeEffectProps(\n node: Pick<RectangleNode, \"effects\" | \"effectStyleId\">,\n ): Promise<NormalizedHasEffectsTrait> {\n const effectStyleKey =\n typeof node.effectStyleId === \"string\"\n ? (await figma.getStyleByIdAsync(node.effectStyleId))?.key\n : undefined;\n\n const effects = node.effects\n .filter((effect): effect is DropShadowEffect | InnerShadowEffect => {\n if (!effect.visible) return false;\n\n return effect.type === \"DROP_SHADOW\" || effect.type === \"INNER_SHADOW\";\n })\n .map(({ blendMode, visible, ...rest }): NormalizedShadow => rest);\n\n return {\n ...(effectStyleKey ? { effectStyleKey } : {}),\n effects,\n };\n }\n\n async function normalizeShapeProps(\n node: Pick<\n RectangleNode,\n | \"fills\"\n | \"fillStyleId\"\n | \"strokes\"\n | \"strokeWeight\"\n | \"layoutGrow\"\n | \"layoutAlign\"\n | \"layoutSizingHorizontal\"\n | \"layoutSizingVertical\"\n | \"absoluteBoundingBox\"\n | \"relativeTransform\"\n | \"layoutPositioning\"\n | \"minHeight\"\n | \"minWidth\"\n | \"maxHeight\"\n | \"maxWidth\"\n | \"effects\"\n | \"effectStyleId\"\n > &\n Partial<Pick<FrameNode, \"inferredAutoLayout\">>,\n ): Promise<Omit<NormalizedDefaultShapeTrait, keyof NormalizedIsLayerTrait>> {\n const fillStyleKey =\n typeof node.fillStyleId === \"string\"\n ? (await figma.getStyleByIdAsync(node.fillStyleId))?.key\n : undefined;\n\n return {\n // NormalizedHasLayoutTrait\n layoutGrow: (node.inferredAutoLayout?.layoutGrow ?? node.layoutGrow) as 0 | 1 | undefined,\n layoutAlign: node.inferredAutoLayout?.layoutAlign ?? node.layoutAlign,\n layoutSizingHorizontal: node.layoutSizingHorizontal,\n layoutSizingVertical: node.layoutSizingVertical,\n absoluteBoundingBox: node.absoluteBoundingBox,\n relativeTransform: node.relativeTransform,\n layoutPositioning: node.layoutPositioning,\n minHeight: node.minHeight ?? undefined,\n minWidth: node.minWidth ?? undefined,\n maxHeight: node.maxHeight ?? undefined,\n maxWidth: node.maxWidth ?? undefined,\n\n // NormalizedHasGeometryTrait\n fills: await normalizePaints(node.fills),\n fillStyleKey,\n strokes: await normalizePaints(node.strokes),\n strokeWeight: node.strokeWeight === figma.mixed ? undefined : node.strokeWeight,\n\n // NormalizedHasEffectsTrait\n ...(await normalizeEffectProps(node)),\n };\n }\n\n async function normalizeAutolayoutProps(\n node: Omit<FrameNode, \"type\" | \"clone\">,\n ): Promise<NormalizedHasFramePropertiesTrait> {\n return {\n layoutMode: node.inferredAutoLayout?.layoutMode ?? node.layoutMode,\n layoutWrap: node.inferredAutoLayout?.layoutWrap ?? node.layoutWrap,\n paddingLeft: node.inferredAutoLayout?.paddingLeft ?? node.paddingLeft,\n paddingRight: node.inferredAutoLayout?.paddingRight ?? node.paddingRight,\n paddingTop: node.inferredAutoLayout?.paddingTop ?? node.paddingTop,\n paddingBottom: node.inferredAutoLayout?.paddingBottom ?? node.paddingBottom,\n primaryAxisAlignItems:\n node.inferredAutoLayout?.primaryAxisAlignItems ?? node.primaryAxisAlignItems,\n counterAxisAlignItems:\n node.inferredAutoLayout?.counterAxisAlignItems ?? node.counterAxisAlignItems,\n primaryAxisSizingMode:\n node.inferredAutoLayout?.primaryAxisSizingMode ?? node.primaryAxisSizingMode,\n counterAxisSizingMode:\n node.inferredAutoLayout?.counterAxisSizingMode ?? node.counterAxisSizingMode,\n itemSpacing: node.inferredAutoLayout?.itemSpacing ?? node.itemSpacing,\n counterAxisSpacing:\n node.inferredAutoLayout?.counterAxisSpacing ?? node.counterAxisSpacing ?? undefined,\n };\n }\n\n async function normalizeFrameNode(node: FrameNode): Promise<NormalizedFrameNode> {\n return {\n // NormalizedIsLayerTrait\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node),\n\n // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait\n ...(await normalizeShapeProps(node)),\n\n // NormalizedCornerTrait\n ...normalizeRadiusProps(node),\n\n // NormalizedHasFramePropertiesTrait\n ...(await normalizeAutolayoutProps(node)),\n\n // NormalizedHasChildrenTrait\n children: await normalizeNodes(node.children),\n };\n }\n\n async function normalizeRectangleNode(node: RectangleNode): Promise<NormalizedRectangleNode> {\n return {\n // NormalizedIsLayerTrait\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node),\n\n // NormalizedCornerTrait\n ...normalizeRadiusProps(node),\n\n // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait\n ...(await normalizeShapeProps(node)),\n };\n }\n\n async function normalizeTextNode(node: TextNode): Promise<NormalizedTextNode> {\n const segments = node.getStyledTextSegments([\n \"fontName\",\n \"fontWeight\",\n \"fontSize\",\n \"letterSpacing\",\n \"lineHeight\",\n \"paragraphSpacing\",\n \"textStyleId\",\n \"fills\",\n \"boundVariables\",\n \"textDecoration\",\n ]);\n const first = segments[0];\n\n const textStyleKey =\n typeof node.textStyleId === \"string\"\n ? (await figma.getStyleByIdAsync(node.textStyleId))?.key\n : undefined;\n\n const normalizeLetterSpacing = (\n letterSpacing: LetterSpacing,\n fontSize: number,\n ): NormalizedTextSegment[\"style\"][\"letterSpacing\"] => {\n if (letterSpacing.unit === \"PIXELS\") return letterSpacing.value;\n if (letterSpacing.unit === \"PERCENT\") return (fontSize * letterSpacing.value) / 100;\n\n return undefined;\n };\n\n const normalizeLineHeight = (\n lineHeight: LineHeight,\n fontSize: number,\n ): NormalizedTextSegment[\"style\"][\"lineHeight\"] => {\n if (lineHeight.unit === \"PIXELS\") return lineHeight.value;\n if (lineHeight.unit === \"PERCENT\") return (fontSize * lineHeight.value) / 100;\n\n return undefined;\n };\n\n const isItalic = (fontName: FontName): boolean => {\n // {\n // family: \"SF Mono\",\n // style: \"Bold Italic\"\n // }\n return fontName.style.toLowerCase().includes(\"italic\");\n };\n\n return {\n // NormalizedIsLayerTrait\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node),\n\n // NormalizedTypePropertiesTrait\n // NOTE: this normalization is incomplete compared to from-rest.ts normalizer\n style: {\n fontFamily: first.fontName.family,\n fontPostScriptName: null,\n fontStyle: first.fontName.style,\n italic: isItalic(first.fontName),\n fontWeight: first.fontWeight,\n fontSize: first.fontSize,\n textAlignHorizontal: node.textAlignHorizontal,\n textAlignVertical: node.textAlignVertical,\n letterSpacing: normalizeLetterSpacing(first.letterSpacing, first.fontSize),\n paragraphSpacing: first.paragraphSpacing,\n textDecoration: first.textDecoration,\n lineHeightPx: normalizeLineHeight(first.lineHeight, first.fontSize),\n lineHeightUnit:\n first.lineHeight.unit === \"PIXELS\"\n ? \"PIXELS\"\n : first.lineHeight.unit === \"PERCENT\"\n ? \"FONT_SIZE_%\"\n : undefined,\n boundVariables: first.boundVariables,\n maxLines: node.maxLines ?? undefined,\n },\n characters: node.characters,\n textStyleKey,\n segments: segments.map((segment) => ({\n characters: segment.characters,\n start: segment.start,\n end: segment.end,\n style: {\n fontSize: segment.fontSize,\n fontWeight: segment.fontWeight,\n fontFamily: segment.fontName.family,\n italic: isItalic(segment.fontName),\n letterSpacing: normalizeLetterSpacing(segment.letterSpacing, segment.fontSize),\n lineHeight: normalizeLineHeight(segment.lineHeight, segment.fontSize),\n textDecoration: segment.textDecoration,\n },\n })),\n\n // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait\n ...(await normalizeShapeProps(node)),\n };\n }\n\n async function normalizeComponentNode(node: ComponentNode): Promise<NormalizedComponentNode> {\n return {\n // NormalizedIsLayerTrait\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node),\n\n // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait\n ...(await normalizeShapeProps(node)),\n\n // NormalizedCornerTrait\n ...normalizeRadiusProps(node),\n\n // NormalizedHasFramePropertiesTrait\n ...(await normalizeAutolayoutProps(node)),\n\n // NormalizedHasChildrenTrait\n children: await normalizeNodes(node.children),\n };\n }\n\n async function normalizeInstanceNode(node: InstanceNode): Promise<NormalizedInstanceNode> {\n const mainComponent = await node.getMainComponentAsync();\n if (!mainComponent) {\n throw new Error(\"Instance node has no main component\");\n }\n\n const componentProperties: NormalizedInstanceNode[\"componentProperties\"] = {};\n\n for (const [key, value] of Object.entries(node.componentProperties)) {\n componentProperties[key] = value;\n\n if (value.type === \"INSTANCE_SWAP\") {\n // unless value.type === \"BOOLEAN\", value.value is string\n const swappedComponent = (await figma.getNodeByIdAsync(\n value.value as string,\n )) as ComponentNode;\n\n if (swappedComponent) {\n componentProperties[key].componentKey = swappedComponent.key;\n\n if (swappedComponent.parent?.type === \"COMPONENT_SET\") {\n componentProperties[key].componentSetKey = swappedComponent.parent.key;\n }\n }\n }\n }\n\n return {\n // NormalizedIsLayerTrait\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node),\n\n // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait\n ...(await normalizeShapeProps(node)),\n\n // NormalizedCornerTrait\n ...normalizeRadiusProps(node),\n\n // NormalizedHasFramePropertiesTrait\n ...(await normalizeAutolayoutProps(node)),\n\n // NormalizedHasChildrenTrait\n children: await normalizeNodes(node.children),\n\n // NormalizedInstanceNode specific\n componentProperties,\n componentKey: mainComponent.key,\n componentSetKey:\n mainComponent.parent?.type === \"COMPONENT_SET\" ? mainComponent.parent.key : undefined,\n overrides: node.overrides,\n };\n }\n\n async function normalizeVectorNode(node: VectorNode): Promise<NormalizedVectorNode> {\n return {\n // NormalizedIsLayerTrait\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node),\n\n // NormalizedCornerTrait\n cornerRadius: node.cornerRadius === figma.mixed ? undefined : node.cornerRadius,\n rectangleCornerRadii: undefined, // VectorNode does not have individual corner radii\n\n // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait\n ...(await normalizeShapeProps(node)),\n };\n }\n\n async function normalizeBooleanOperationNode(\n node: BooleanOperationNode,\n ): Promise<NormalizedBooleanOperationNode> {\n const fillStyleKey =\n typeof node.fillStyleId === \"string\"\n ? (await figma.getStyleByIdAsync(node.fillStyleId))?.key\n : undefined;\n\n return {\n // NormalizedIsLayerTrait\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node),\n\n // NormalizedHasLayoutTrait\n layoutGrow: node.layoutGrow as 0 | 1 | undefined,\n layoutAlign: node.layoutAlign,\n layoutSizingHorizontal: node.layoutSizingHorizontal,\n layoutSizingVertical: node.layoutSizingVertical,\n absoluteBoundingBox: node.absoluteBoundingBox,\n relativeTransform: node.relativeTransform,\n layoutPositioning: node.layoutPositioning,\n minHeight: node.minHeight ?? undefined,\n minWidth: node.minWidth ?? undefined,\n maxHeight: node.maxHeight ?? undefined,\n maxWidth: node.maxWidth ?? undefined,\n\n // NormalizedHasGeometryTrait\n fills: await normalizePaints(node.fills),\n fillStyleKey,\n strokes: await normalizePaints(node.strokes),\n strokeWeight: node.strokeWeight === figma.mixed ? undefined : node.strokeWeight,\n\n // NormalizedHasEffectsTrait\n ...(await normalizeEffectProps(node)),\n\n // NormalizedHasChildrenTrait\n children: await normalizeNodes(node.children),\n };\n }\n\n async function normalizeGroupNodeAsFrameNode(\n node: GroupNode & { inferredAutoLayout?: FrameNode[\"inferredAutoLayout\"] },\n ): Promise<NormalizedFrameNode> {\n return {\n // NormalizedIsLayerTrait\n type: \"FRAME\",\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node),\n\n // NormalizedHasLayoutTrait\n layoutGrow: (node.inferredAutoLayout?.layoutGrow ?? node.layoutGrow) as 0 | 1 | undefined,\n layoutAlign: node.inferredAutoLayout?.layoutAlign ?? node.layoutAlign,\n layoutSizingHorizontal: node.layoutSizingHorizontal,\n layoutSizingVertical: node.layoutSizingVertical,\n absoluteBoundingBox: node.absoluteBoundingBox,\n relativeTransform: node.relativeTransform,\n layoutPositioning: node.layoutPositioning,\n minHeight: node.minHeight ?? undefined,\n minWidth: node.minWidth ?? undefined,\n maxHeight: node.maxHeight ?? undefined,\n maxWidth: node.maxWidth ?? undefined,\n\n // NormalizedHasGeometryTrait\n fills: [],\n fillStyleKey: undefined,\n strokes: [],\n strokeWeight: undefined,\n\n // NormalizedHasEffectsTrait\n effects: [],\n effectStyleKey: undefined,\n\n // NormalizedCornerTrait\n cornerRadius: undefined,\n rectangleCornerRadii: undefined,\n\n // NormalizedHasFramePropertiesTrait\n layoutMode: node.inferredAutoLayout?.layoutMode,\n layoutWrap: node.inferredAutoLayout?.layoutWrap,\n paddingLeft: node.inferredAutoLayout?.paddingLeft,\n paddingRight: node.inferredAutoLayout?.paddingRight,\n paddingTop: node.inferredAutoLayout?.paddingTop,\n paddingBottom: node.inferredAutoLayout?.paddingBottom,\n primaryAxisAlignItems: node.inferredAutoLayout?.primaryAxisAlignItems,\n counterAxisAlignItems: node.inferredAutoLayout?.counterAxisAlignItems,\n primaryAxisSizingMode: node.inferredAutoLayout?.primaryAxisSizingMode,\n counterAxisSizingMode: node.inferredAutoLayout?.counterAxisSizingMode,\n itemSpacing: node.inferredAutoLayout?.itemSpacing,\n counterAxisSpacing: node.inferredAutoLayout?.counterAxisSpacing ?? undefined,\n\n // NormalizedHasChildrenTrait\n children: await normalizeNodes(node.children),\n };\n }\n\n return normalizeNode;\n}\n","import type { ComponentPropertyDefinition } from \"@/codegen\";\n\nexport interface ComponentMetadata {\n name: string;\n key: string;\n componentPropertyDefinitions: Record<string, ComponentPropertyDefinition>;\n}\n","export interface IconData {\n name: string;\n type: \"monochrome\" | \"multicolor\";\n weight?: string;\n}\n","import type {\n LocalVariable,\n LocalVariableCollection,\n VariableAlias,\n VariableResolvedDataType,\n} from \"@figma/rest-api-spec\";\n\nexport type Variable = LocalVariable;\n\nexport type VariableCollection = LocalVariableCollection;\n\nexport type VariableType = VariableResolvedDataType;\n\nexport type VariableValue = Variable[\"valuesByMode\"][string];\n\nexport type VariableValueResolved = Exclude<VariableValue, VariableAlias>;\n\nexport type { VariableScope } from \"@figma/rest-api-spec\";\n","import type { Variable, VariableCollection } from \"./variable.interface\";\n\nexport interface VariableRepository {\n getVariableList(): Variable[];\n getVariableCollectionList(): VariableCollection[];\n findVariableByKey(key: string): Variable | undefined;\n findVariableById(id: string): Variable | undefined;\n findVariableByName(name: string): Variable | undefined;\n findVariableCollectionByKey(key: string): VariableCollection | undefined;\n findVariableCollectionById(id: string): VariableCollection | undefined;\n}\n\nexport function createStaticVariableRepository({\n variables,\n variableCollections,\n}: {\n variables: Record<string, Variable>;\n variableCollections: Record<string, VariableCollection>;\n}): VariableRepository {\n const variablesKeyMap = new Map<string, Variable>();\n const variablesIdMap = new Map<string, Variable>();\n const variablesNameMap = new Map<string, Variable>();\n const variableCollectionsKeyMap = new Map<string, VariableCollection>();\n const variableCollectionsIdMap = new Map<string, VariableCollection>();\n\n for (const variable of Object.values(variables)) {\n if (variable.remote) {\n continue;\n }\n\n variablesKeyMap.set(variable.key, variable);\n variablesIdMap.set(variable.id, variable);\n variablesNameMap.set(variable.name, variable);\n }\n\n for (const variableCollection of Object.values(variableCollections)) {\n if (variableCollection.remote) {\n continue;\n }\n\n variableCollectionsKeyMap.set(variableCollection.key, variableCollection);\n variableCollectionsIdMap.set(variableCollection.id, variableCollection);\n }\n\n const variablesList = [...variablesKeyMap.values()];\n const variableCollectionsList = [...variableCollectionsKeyMap.values()];\n\n return {\n getVariableList: () => variablesList,\n getVariableCollectionList: () => variableCollectionsList,\n findVariableByName: (name: string) => variablesNameMap.get(name),\n findVariableByKey: (key: string) => variablesKeyMap.get(key),\n findVariableById: (id: string) => variablesIdMap.get(id),\n findVariableCollectionByKey: (key: string) => variableCollectionsKeyMap.get(key),\n findVariableCollectionById: (id: string) => variableCollectionsIdMap.get(id),\n };\n}\n","import type { Style as FigmaStyle, StyleType as FigmaStyleType } from \"@figma/rest-api-spec\";\n\nexport type Style = FigmaStyle;\n\nexport type StyleType = FigmaStyleType;\n","import type { Style } from \"./style.interface\";\n\nexport interface StyleRepository {\n getAll(): Style[];\n getTextStyles(): Style[];\n getColorStyles(): Style[];\n findOneByKey(key: string): Style | undefined;\n findOneByName(name: string): Style | undefined;\n}\n\nexport function createStaticStyleRepository(styles: Style[]): StyleRepository {\n const stylesMap = new Map<string, Style>();\n const stylesNameMap = new Map<string, Style>();\n\n for (const style of styles) {\n stylesMap.set(style.key, style);\n stylesNameMap.set(style.name, style);\n }\n\n return {\n getAll: () => styles,\n getTextStyles: () => styles.filter((style) => style.styleType === \"TEXT\"),\n getColorStyles: () => styles.filter((style) => style.styleType === \"FILL\"),\n findOneByKey: (key) => stylesMap.get(key),\n findOneByName: (name) => stylesNameMap.get(name),\n };\n}\n","import type { IconData } from \"./icon.interface\";\n\nexport interface IconRepository {\n getOne(key: string): IconData;\n}\n\nexport function createStaticIconRepository(iconRecord: Record<string, IconData>) {\n return {\n getOne: (key: string) => iconRecord[key],\n };\n}\n","import type { IconData } from \"./icon.interface\";\nimport type { IconRepository } from \"./icon.repository\";\n\nexport interface IconService {\n isAvailable: (componentKey: string) => boolean;\n getOne: (componentKey: string) => IconData;\n}\n\nexport function createIconService({\n iconRepository,\n}: {\n iconRepository: IconRepository;\n}): IconService {\n function isAvailable(componentKey: string) {\n return iconRepository.getOne(componentKey) !== undefined;\n }\n\n function getOne(componentKey: string) {\n return iconRepository.getOne(componentKey);\n }\n\n return {\n isAvailable,\n getOne,\n };\n}\n","import type { StyleRepository } from \"./style.repository\";\n\nexport interface StyleService {\n getSlug: (id: string) => string[] | undefined;\n}\n\n// TODO: inferStyleName 추가해야 함, rest api에서 style value가 제공되지 않고 있어 보류\nexport function createStyleService({\n styleRepository,\n}: {\n styleRepository: StyleRepository;\n}): StyleService {\n function getName(id: string) {\n const style = styleRepository.findOneByKey(id);\n\n if (!style) {\n return undefined;\n }\n\n return style.name;\n }\n\n function getSlug(id: string): string[] | undefined {\n const name = getName(id);\n\n if (!name) {\n return undefined;\n }\n\n return name.split(\"/\");\n }\n\n return {\n getSlug,\n };\n}\n","import {\n isIdenticalVariableValue,\n isInsideScope,\n isVariableAlias,\n sanitizeVariableId,\n} from \"@/utils/figma-variable\";\nimport type { Variable, VariableScope, VariableValueResolved } from \"./variable.interface\";\nimport type { VariableRepository } from \"./variable.repository\";\n\nexport interface VariableService {\n getSlug: (id: string) => string[] | undefined;\n resolveValue: (variable: Variable, mode: string) => VariableValueResolved;\n infer: (value: VariableValueResolved, scope: VariableScope) => Variable | undefined;\n}\n\nexport interface VariableServiceDeps {\n variableRepository: VariableRepository;\n inferCompareFunction: (a: Variable, b: Variable) => number;\n}\n\nexport function createVariableService({\n variableRepository,\n inferCompareFunction,\n}: VariableServiceDeps): VariableService {\n const variables = variableRepository.getVariableList();\n\n // private\n function getName(key: string) {\n const sanitizedId = sanitizeVariableId(key);\n const variable = variableRepository.findVariableByKey(sanitizedId);\n\n if (!variable) {\n return undefined;\n }\n\n return variable.name;\n }\n\n function getDefaultModeId(variable: Variable) {\n const variableCollection = variableRepository.findVariableCollectionById(\n variable.variableCollectionId,\n );\n\n if (!variableCollection) {\n // Variable collection not found: ${variable.variableCollectionId}, falling back to variable.valuesByMode key\n return Object.keys(variable.valuesByMode)[0]!;\n }\n\n return variableCollection.defaultModeId;\n }\n\n // public\n function getSlug(key: string): string[] | undefined {\n const name = getName(key);\n\n if (!name) {\n return undefined;\n }\n\n return name.split(\"/\");\n }\n\n function resolveValue(variable: Variable, mode: string): VariableValueResolved {\n const value = variable.valuesByMode[mode];\n\n if (value === undefined) {\n throw new Error(`Variable value not found: ${variable.id} ${mode}`);\n }\n\n if (isVariableAlias(value)) {\n const resolvedVariable = variableRepository.findVariableById(value.id);\n\n if (!resolvedVariable) {\n throw new Error(`Variable not found: ${value.id}`);\n }\n\n return resolveValue(resolvedVariable, mode);\n }\n\n return value;\n }\n\n function infer(value: VariableValueResolved, scope: VariableScope) {\n // NOTE: We assume that the variable is in the default mode or value is equal between all modes for simplicity.\n const inferredVariables = variables.filter(\n (variable) =>\n isInsideScope(variable, scope) &&\n isIdenticalVariableValue(resolveValue(variable, getDefaultModeId(variable)), value),\n );\n\n const sortedVariables = inferredVariables.sort(inferCompareFunction);\n\n return sortedVariables[0];\n }\n\n return {\n getSlug,\n resolveValue,\n infer,\n };\n}\n","import type { ComponentMetadata } from \"./component.interface\";\n\nexport interface ComponentRepository {\n getOne(key: string): ComponentMetadata | undefined;\n}\n\nexport function createStaticComponentRepository(data: Record<string, ComponentMetadata>) {\n const componentRecord: Record<string, ComponentMetadata> = {};\n Object.values(data).forEach((component) => {\n componentRecord[component.key] = component;\n });\n\n return {\n getOne: (key: string) => componentRecord[key],\n };\n}\n","import { createStaticIconRepository } from \"./icon.repository\";\nimport { FIGMA_ICONS } from \"./data/__generated__/icons\";\nimport { FIGMA_STYLES } from \"./data/__generated__/styles\";\nimport { FIGMA_VARIABLE_COLLECTIONS } from \"./data/__generated__/variable-collections\";\nimport { FIGMA_VARIABLES } from \"./data/__generated__/variables\";\nimport * as FIGMA_COMPONENTS from \"./data/__generated__/component-sets\";\nimport { createStaticStyleRepository } from \"./style.repository\";\nimport { createStaticVariableRepository } from \"./variable.repository\";\nimport { createStaticComponentRepository } from \"./component.repository\";\n\nexport * from \"./icon.interface\";\nexport * from \"./icon.repository\";\nexport * from \"./icon.service\";\nexport * from \"./style.interface\";\nexport * from \"./style.repository\";\nexport * from \"./style.service\";\nexport * from \"./variable.interface\";\nexport * from \"./variable.repository\";\nexport * from \"./variable.service\";\nexport * from \"./component.interface\";\nexport * from \"./component.repository\";\n\nexport const styleRepository = createStaticStyleRepository(FIGMA_STYLES);\nexport const variableRepository = createStaticVariableRepository({\n variables: FIGMA_VARIABLES,\n variableCollections: FIGMA_VARIABLE_COLLECTIONS,\n});\nexport const iconRepository = createStaticIconRepository(FIGMA_ICONS);\nexport const componentRepository = createStaticComponentRepository(FIGMA_COMPONENTS);\n\nexport function getFigmaVariableKey(name: string) {\n return variableRepository.findVariableByName(name)?.key;\n}\n\nexport function getFigmaStyleKey(name: string) {\n return styleRepository.findOneByName(name)?.key;\n}\n\nexport function getFigmaColorVariableNames(scopes: Array<\"fg\" | \"bg\" | \"stroke\" | \"palette\">) {\n const variables = variableRepository.getVariableList();\n return variables\n .filter((variable) =>\n scopes.includes(variable.name.split(\"/\")[0] as \"fg\" | \"bg\" | \"stroke\" | \"palette\"),\n )\n .map((variable) => variable.name);\n}\n"],"names":[],"mappings":";;;;;;AACO;AACP;AACA;AACO;AACA;AACP;AACA;AACO;AACA;AACA;AACA;AACP;AACA;AACA;AACA;AACO;AACA;AACP;AACA;AACA;AACO;AACA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACO;AACA;AACA;AACP;AACA;AACO;AACP;AACA;AACO;AACP;AACA;AACO;AACP;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACO;AACP;AACA;AACO;AACP;AACA;AACA;AACA;AACO;;ACjFP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAGO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;;ACzBP;AACA;AACA;AACA;AACA;;AAEO;;ACLA;AACP;AACA;AACA;AACA;;ACLO;AACP;AACA;AACA;AACA;;ACHO;AACA;AACA;AACA;AACA;;ACJA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;;ACZO;AACA;;ACDA;AACP;AACA;AACA;AACA;AACA;AACA;AACO;;ACPA;AACP;AACA;AACO;AACP;AACA;;ACJO;AACP;AACA;AACA;AACO;AACP;AACA;;ACPO;AACP;AACA;AACO;AACP;AACA;;ACJO;AACP;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACO;;ACVA;AACP;AACA;AACO;AACP;AACA;;ACKO;AACA;AACA;AACP;AACA;AACO;AACP;AACA;AACO;AACA;AACA;;;"}
|