mirror of
https://github.com/penpot/penpot-exporter-figma-plugin.git
synced 2024-12-22 13:43:03 -05:00
Use Fill Styles to generate fills more efficiently (#180)
* first commit * change esbuild * changeset
This commit is contained in:
parent
4e5d01adb3
commit
672567614b
23 changed files with 137 additions and 29 deletions
5
.changeset/pretty-cats-clap.md
Normal file
5
.changeset/pretty-cats-clap.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
"penpot-exporter": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Use Fill Styles to optimize fills transformations
|
27
plugin-src/StyleLibrary.ts
Normal file
27
plugin-src/StyleLibrary.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { Fill } from '@ui/lib/types/utils/fill';
|
||||||
|
|
||||||
|
class StyleLibrary {
|
||||||
|
private styles: Map<string, Fill[]> = new Map();
|
||||||
|
|
||||||
|
public register(id: string, styles: Fill[]) {
|
||||||
|
this.styles.set(id, styles);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get(id: string): Fill[] | undefined {
|
||||||
|
return this.styles.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public has(id: string): boolean {
|
||||||
|
return this.styles.has(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public all(): Record<string, Fill[]> {
|
||||||
|
return Object.fromEntries(this.styles.entries());
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(styles: Record<string, Fill[]>): void {
|
||||||
|
this.styles = new Map(Object.entries(styles));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const styleLibrary = new StyleLibrary();
|
|
@ -1,11 +1,53 @@
|
||||||
import { translateFills } from '@plugin/translators/fills';
|
import { translateFillStyle, translateFills } from '@plugin/translators/fills';
|
||||||
|
import { StyleTextSegment } from '@plugin/translators/text/paragraph';
|
||||||
|
|
||||||
import { ShapeAttributes } from '@ui/lib/types/shapes/shape';
|
import { ShapeAttributes } from '@ui/lib/types/shapes/shape';
|
||||||
|
import { TextStyle } from '@ui/lib/types/shapes/textShape';
|
||||||
|
|
||||||
export const transformFills = (
|
export const transformFills = (
|
||||||
node: MinimalFillsMixin & DimensionAndPositionMixin
|
node:
|
||||||
): Pick<ShapeAttributes, 'fills'> => {
|
| (MinimalFillsMixin & DimensionAndPositionMixin)
|
||||||
|
| VectorRegion
|
||||||
|
| VectorNode
|
||||||
|
| StyleTextSegment
|
||||||
|
): Pick<ShapeAttributes, 'fills' | 'fillStyleId'> | Pick<TextStyle, 'fills' | 'fillStyleId'> => {
|
||||||
|
if (hasFillStyle(node)) {
|
||||||
|
return {
|
||||||
|
fills: [],
|
||||||
|
fillStyleId: translateFillStyle(node.fillStyleId, node.fills)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fills: translateFills(node.fills)
|
fills: translateFills(node.fills)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const transformVectorFills = (
|
||||||
|
node: VectorNode,
|
||||||
|
vectorPath: VectorPath,
|
||||||
|
vectorRegion: VectorRegion | undefined
|
||||||
|
): Pick<ShapeAttributes, 'fills' | 'fillStyleId'> => {
|
||||||
|
if (vectorPath.windingRule === 'NONE') {
|
||||||
|
return {
|
||||||
|
fills: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const fillsNode = vectorRegion?.fills ? vectorRegion : node;
|
||||||
|
return transformFills(fillsNode);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasFillStyle = (
|
||||||
|
node:
|
||||||
|
| (MinimalFillsMixin & DimensionAndPositionMixin)
|
||||||
|
| VectorRegion
|
||||||
|
| VectorNode
|
||||||
|
| StyleTextSegment
|
||||||
|
): boolean => {
|
||||||
|
return (
|
||||||
|
node.fillStyleId !== figma.mixed &&
|
||||||
|
node.fillStyleId !== undefined &&
|
||||||
|
node.fillStyleId.length > 0
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -15,7 +15,8 @@ export const transformText = (node: TextNode): TextAttributes & Pick<TextShape,
|
||||||
'textDecoration',
|
'textDecoration',
|
||||||
'indentation',
|
'indentation',
|
||||||
'listOptions',
|
'listOptions',
|
||||||
'fills'
|
'fills',
|
||||||
|
'fillStyleId'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -6,9 +6,9 @@ import {
|
||||||
transformLayoutAttributes,
|
transformLayoutAttributes,
|
||||||
transformProportion,
|
transformProportion,
|
||||||
transformSceneNode,
|
transformSceneNode,
|
||||||
transformStrokesFromVector
|
transformStrokesFromVector,
|
||||||
|
transformVectorFills
|
||||||
} from '@plugin/transformers/partials';
|
} from '@plugin/transformers/partials';
|
||||||
import { translateFills } from '@plugin/translators/fills';
|
|
||||||
import { translateCommands, translateWindingRule } from '@plugin/translators/vectors';
|
import { translateCommands, translateWindingRule } from '@plugin/translators/vectors';
|
||||||
|
|
||||||
import { PathShape } from '@ui/lib/types/shapes/pathShape';
|
import { PathShape } from '@ui/lib/types/shapes/pathShape';
|
||||||
|
@ -66,13 +66,12 @@ const transformVectorPath = (
|
||||||
type: 'path',
|
type: 'path',
|
||||||
name: 'svg-path',
|
name: 'svg-path',
|
||||||
content: translateCommands(node, normalizedPaths),
|
content: translateCommands(node, normalizedPaths),
|
||||||
fills:
|
|
||||||
vectorPath.windingRule === 'NONE' ? [] : translateFills(vectorRegion?.fills ?? node.fills),
|
|
||||||
svgAttrs: {
|
svgAttrs: {
|
||||||
fillRule: translateWindingRule(vectorPath.windingRule)
|
fillRule: translateWindingRule(vectorPath.windingRule)
|
||||||
},
|
},
|
||||||
constraintsH: 'scale',
|
constraintsH: 'scale',
|
||||||
constraintsV: 'scale',
|
constraintsV: 'scale',
|
||||||
|
...transformVectorFills(node, vectorPath, vectorRegion),
|
||||||
...transformStrokesFromVector(node, normalizedPaths, vectorRegion),
|
...transformStrokesFromVector(node, normalizedPaths, vectorRegion),
|
||||||
...transformEffects(node),
|
...transformEffects(node),
|
||||||
...transformSceneNode(node),
|
...transformSceneNode(node),
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { componentsLibrary } from '@plugin/ComponentLibrary';
|
import { componentsLibrary } from '@plugin/ComponentLibrary';
|
||||||
import { imagesLibrary } from '@plugin/ImageLibrary';
|
import { imagesLibrary } from '@plugin/ImageLibrary';
|
||||||
import { remoteComponentLibrary } from '@plugin/RemoteComponentLibrary';
|
import { remoteComponentLibrary } from '@plugin/RemoteComponentLibrary';
|
||||||
|
import { styleLibrary } from '@plugin/StyleLibrary';
|
||||||
import { translateRemoteChildren } from '@plugin/translators';
|
import { translateRemoteChildren } from '@plugin/translators';
|
||||||
import { sleep } from '@plugin/utils';
|
import { sleep } from '@plugin/utils';
|
||||||
|
|
||||||
|
@ -86,6 +87,7 @@ export const transformDocumentNode = async (node: DocumentNode): Promise<PenpotD
|
||||||
name: node.name,
|
name: node.name,
|
||||||
children,
|
children,
|
||||||
components: componentsLibrary.all(),
|
components: componentsLibrary.all(),
|
||||||
images: await downloadImages()
|
images: await downloadImages(),
|
||||||
|
styles: styleLibrary.all()
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { styleLibrary } from '@plugin/StyleLibrary';
|
||||||
import { translateImageFill, translateSolidFill } from '@plugin/translators/fills';
|
import { translateImageFill, translateSolidFill } from '@plugin/translators/fills';
|
||||||
import {
|
import {
|
||||||
translateGradientLinearFill,
|
translateGradientLinearFill,
|
||||||
|
@ -41,6 +42,19 @@ export const translateFills = (
|
||||||
return penpotFills;
|
return penpotFills;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const translateFillStyle = (
|
||||||
|
fillStyleId: string | typeof figma.mixed | undefined,
|
||||||
|
fills: readonly Paint[] | typeof figma.mixed | undefined
|
||||||
|
): string | undefined => {
|
||||||
|
if (fillStyleId === figma.mixed || fillStyleId === undefined) return;
|
||||||
|
|
||||||
|
if (!styleLibrary.has(fillStyleId)) {
|
||||||
|
styleLibrary.register(fillStyleId, translateFills(fills));
|
||||||
|
}
|
||||||
|
|
||||||
|
return fillStyleId;
|
||||||
|
};
|
||||||
|
|
||||||
export const translatePageFill = (fill: Paint): string | undefined => {
|
export const translatePageFill = (fill: Paint): string | undefined => {
|
||||||
switch (fill.type) {
|
switch (fill.type) {
|
||||||
case 'SOLID':
|
case 'SOLID':
|
||||||
|
|
|
@ -17,6 +17,7 @@ export type StyleTextSegment = Pick<
|
||||||
| 'indentation'
|
| 'indentation'
|
||||||
| 'listOptions'
|
| 'listOptions'
|
||||||
| 'fills'
|
| 'fills'
|
||||||
|
| 'fillStyleId'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
type PartialTranslation = {
|
type PartialTranslation = {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { translateFills } from '@plugin/translators/fills';
|
import { transformFills } from '@plugin/transformers/partials';
|
||||||
import { translateFontId } from '@plugin/translators/text/font';
|
import { translateFontId } from '@plugin/translators/text/font';
|
||||||
import { StyleTextSegment, translateParagraphProperties } from '@plugin/translators/text/paragraph';
|
import { StyleTextSegment, translateParagraphProperties } from '@plugin/translators/text/paragraph';
|
||||||
import {
|
import {
|
||||||
|
@ -41,8 +41,8 @@ export const transformTextStyle = (node: TextNode, segment: StyleTextSegment): T
|
||||||
|
|
||||||
const translateStyleTextSegment = (node: TextNode, segment: StyleTextSegment): PenpotTextNode => {
|
const translateStyleTextSegment = (node: TextNode, segment: StyleTextSegment): PenpotTextNode => {
|
||||||
return {
|
return {
|
||||||
fills: translateFills(segment.fills),
|
|
||||||
text: segment.characters,
|
text: segment.characters,
|
||||||
...transformTextStyle(node, segment)
|
...transformTextStyle(node, segment),
|
||||||
|
...transformFills(segment)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"extends": "../tsconfig.base.json",
|
"extends": "../tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2017",
|
"target": "ES2019",
|
||||||
"lib": ["ES2017"],
|
"lib": ["ES2019"],
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"typeRoots": ["../node_modules/@figma"],
|
"typeRoots": ["../node_modules/@figma"],
|
||||||
"moduleResolution": "Node",
|
"moduleResolution": "Node",
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { GroupShape } from '@ui/lib/types/shapes/groupShape';
|
||||||
import { PathShape } from '@ui/lib/types/shapes/pathShape';
|
import { PathShape } from '@ui/lib/types/shapes/pathShape';
|
||||||
import { RectShape } from '@ui/lib/types/shapes/rectShape';
|
import { RectShape } from '@ui/lib/types/shapes/rectShape';
|
||||||
import { TextShape } from '@ui/lib/types/shapes/textShape';
|
import { TextShape } from '@ui/lib/types/shapes/textShape';
|
||||||
|
import { Color } from '@ui/lib/types/utils/color';
|
||||||
import { Uuid } from '@ui/lib/types/utils/uuid';
|
import { Uuid } from '@ui/lib/types/utils/uuid';
|
||||||
|
|
||||||
export interface PenpotFile {
|
export interface PenpotFile {
|
||||||
|
@ -22,9 +23,9 @@ export interface PenpotFile {
|
||||||
createCircle(circle: CircleShape): Uuid;
|
createCircle(circle: CircleShape): Uuid;
|
||||||
createPath(path: PathShape): Uuid;
|
createPath(path: PathShape): Uuid;
|
||||||
createText(options: TextShape): Uuid;
|
createText(options: TextShape): Uuid;
|
||||||
// addLibraryColor(color: any): void;
|
addLibraryColor(color: Color): void;
|
||||||
// updateLibraryColor(color: any): void;
|
updateLibraryColor(color: Color): void;
|
||||||
// deleteLibraryColor(color: any): void;
|
deleteLibraryColor(color: Color): void;
|
||||||
// addLibraryTypography(typography: any): void;
|
// addLibraryTypography(typography: any): void;
|
||||||
// deleteLibraryTypography(typography: any): void;
|
// deleteLibraryTypography(typography: any): void;
|
||||||
startComponent(component: ComponentShape): Uuid;
|
startComponent(component: ComponentShape): Uuid;
|
||||||
|
|
|
@ -51,6 +51,7 @@ export type ShapeAttributes = {
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
maskedGroup?: boolean;
|
maskedGroup?: boolean;
|
||||||
fills?: Fill[];
|
fills?: Fill[];
|
||||||
|
fillStyleId?: string; // @TODO: move to any other place
|
||||||
hideFillOnExport?: boolean;
|
hideFillOnExport?: boolean;
|
||||||
proportion?: number;
|
proportion?: number;
|
||||||
proportionLock?: boolean;
|
proportionLock?: boolean;
|
||||||
|
|
|
@ -60,6 +60,7 @@ export type TextStyle = FontId & {
|
||||||
textAlign?: TextHorizontalAlign;
|
textAlign?: TextHorizontalAlign;
|
||||||
textDirection?: 'ltr' | 'rtl' | 'auto';
|
textDirection?: 'ltr' | 'rtl' | 'auto';
|
||||||
fills?: Fill[];
|
fills?: Fill[];
|
||||||
|
fillStyleId?: string; // @TODO: move to any other place
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FontId = {
|
export type FontId = {
|
||||||
|
|
|
@ -14,7 +14,7 @@ export const createArtboard = (
|
||||||
|
|
||||||
shape.id = id;
|
shape.id = id;
|
||||||
shape.shapeRef ??= parseFigmaId(file, figmaRelatedId, true);
|
shape.shapeRef ??= parseFigmaId(file, figmaRelatedId, true);
|
||||||
shape.fills = symbolFills(shape.fills);
|
shape.fills = symbolFills(shape.fillStyleId, shape.fills);
|
||||||
shape.strokes = symbolStrokes(shape.strokes);
|
shape.strokes = symbolStrokes(shape.strokes);
|
||||||
|
|
||||||
file.addArtboard(shape);
|
file.addArtboard(shape);
|
||||||
|
|
|
@ -11,7 +11,7 @@ export const createBool = (
|
||||||
) => {
|
) => {
|
||||||
shape.id = parseFigmaId(file, figmaId);
|
shape.id = parseFigmaId(file, figmaId);
|
||||||
shape.shapeRef = parseFigmaId(file, figmaRelatedId, true);
|
shape.shapeRef = parseFigmaId(file, figmaRelatedId, true);
|
||||||
shape.fills = symbolFills(shape.fills);
|
shape.fills = symbolFills(shape.fillStyleId, shape.fills);
|
||||||
shape.strokes = symbolStrokes(shape.strokes);
|
shape.strokes = symbolStrokes(shape.strokes);
|
||||||
shape.boolType = symbolBoolType(shape.boolType);
|
shape.boolType = symbolBoolType(shape.boolType);
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ export const createCircle = (
|
||||||
) => {
|
) => {
|
||||||
shape.id = parseFigmaId(file, figmaId);
|
shape.id = parseFigmaId(file, figmaId);
|
||||||
shape.shapeRef = parseFigmaId(file, figmaRelatedId, true);
|
shape.shapeRef = parseFigmaId(file, figmaRelatedId, true);
|
||||||
shape.fills = symbolFills(shape.fills);
|
shape.fills = symbolFills(shape.fillStyleId, shape.fills);
|
||||||
shape.strokes = symbolStrokes(shape.strokes);
|
shape.strokes = symbolStrokes(shape.strokes);
|
||||||
|
|
||||||
file.createCircle(shape);
|
file.createCircle(shape);
|
||||||
|
|
|
@ -43,7 +43,7 @@ const createComponentLibrary = async (file: PenpotFile, uiComponent: UiComponent
|
||||||
|
|
||||||
const { children = [], ...shape } = component;
|
const { children = [], ...shape } = component;
|
||||||
|
|
||||||
shape.fills = symbolFills(shape.fills);
|
shape.fills = symbolFills(shape.fillStyleId, shape.fills);
|
||||||
shape.strokes = symbolStrokes(shape.strokes);
|
shape.strokes = symbolStrokes(shape.strokes);
|
||||||
shape.id = uiComponent.componentId;
|
shape.id = uiComponent.componentId;
|
||||||
shape.componentId = uiComponent.componentId;
|
shape.componentId = uiComponent.componentId;
|
||||||
|
|
|
@ -9,7 +9,7 @@ export const createPath = (
|
||||||
) => {
|
) => {
|
||||||
shape.id = parseFigmaId(file, figmaId);
|
shape.id = parseFigmaId(file, figmaId);
|
||||||
shape.shapeRef = parseFigmaId(file, figmaRelatedId, true);
|
shape.shapeRef = parseFigmaId(file, figmaRelatedId, true);
|
||||||
shape.fills = symbolFills(shape.fills);
|
shape.fills = symbolFills(shape.fillStyleId, shape.fills);
|
||||||
shape.strokes = symbolStrokes(shape.strokes);
|
shape.strokes = symbolStrokes(shape.strokes);
|
||||||
shape.content = symbolPathContent(shape.content);
|
shape.content = symbolPathContent(shape.content);
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ export const createRectangle = (
|
||||||
) => {
|
) => {
|
||||||
shape.id = parseFigmaId(file, figmaId);
|
shape.id = parseFigmaId(file, figmaId);
|
||||||
shape.shapeRef = parseFigmaId(file, figmaRelatedId, true);
|
shape.shapeRef = parseFigmaId(file, figmaRelatedId, true);
|
||||||
shape.fills = symbolFills(shape.fills);
|
shape.fills = symbolFills(shape.fillStyleId, shape.fills);
|
||||||
shape.strokes = symbolStrokes(shape.strokes);
|
shape.strokes = symbolStrokes(shape.strokes);
|
||||||
|
|
||||||
file.createRect(shape);
|
file.createRect(shape);
|
||||||
|
|
|
@ -21,10 +21,10 @@ const parseContent = (content: TextContent | undefined): TextContent | undefined
|
||||||
content.children?.forEach(paragraphSet => {
|
content.children?.forEach(paragraphSet => {
|
||||||
paragraphSet.children.forEach(paragraph => {
|
paragraphSet.children.forEach(paragraph => {
|
||||||
paragraph.children.forEach(textNode => {
|
paragraph.children.forEach(textNode => {
|
||||||
textNode.fills = symbolFills(textNode.fills);
|
textNode.fills = symbolFills(textNode.fillStyleId, textNode.fills);
|
||||||
});
|
});
|
||||||
|
|
||||||
paragraph.fills = symbolFills(paragraph.fills);
|
paragraph.fills = symbolFills(paragraph.fillStyleId, paragraph.fills);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
|
import { styleLibrary } from '@plugin/StyleLibrary';
|
||||||
|
|
||||||
import { Fill } from '@ui/lib/types/utils/fill';
|
import { Fill } from '@ui/lib/types/utils/fill';
|
||||||
import { ImageColor, PartialImageColor } from '@ui/lib/types/utils/imageColor';
|
import { ImageColor, PartialImageColor } from '@ui/lib/types/utils/imageColor';
|
||||||
import { uiImages } from '@ui/parser/libraries';
|
import { uiImages } from '@ui/parser/libraries';
|
||||||
|
|
||||||
export const symbolFills = (fills?: Fill[]): Fill[] | undefined => {
|
export const symbolFills = (fillStyleId?: string, fills?: Fill[]): Fill[] | undefined => {
|
||||||
if (!fills) return;
|
const nodeFills = fillStyleId ? styleLibrary.get(fillStyleId) : fills;
|
||||||
|
|
||||||
return fills.map(fill => {
|
if (!nodeFills) return;
|
||||||
|
|
||||||
|
return nodeFills.map(fill => {
|
||||||
if (fill.fillImage) {
|
if (fill.fillImage) {
|
||||||
fill.fillImage = symbolFillImage(fill.fillImage);
|
fill.fillImage = symbolFillImage(fill.fillImage);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { componentsLibrary } from '@plugin/ComponentLibrary';
|
import { componentsLibrary } from '@plugin/ComponentLibrary';
|
||||||
|
import { styleLibrary } from '@plugin/StyleLibrary';
|
||||||
// @TODO: Direct import on purpose, to avoid problems with the tsc linting
|
// @TODO: Direct import on purpose, to avoid problems with the tsc linting
|
||||||
import { sleep } from '@plugin/utils/sleep';
|
import { sleep } from '@plugin/utils/sleep';
|
||||||
|
|
||||||
|
@ -40,8 +41,15 @@ const optimizeImages = async (images: Record<string, Uint8Array>) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const parse = async ({ name, children = [], components, images }: PenpotDocument) => {
|
export const parse = async ({
|
||||||
|
name,
|
||||||
|
children = [],
|
||||||
|
components,
|
||||||
|
images,
|
||||||
|
styles
|
||||||
|
}: PenpotDocument) => {
|
||||||
componentsLibrary.init(components);
|
componentsLibrary.init(components);
|
||||||
|
styleLibrary.init(styles);
|
||||||
|
|
||||||
await optimizeImages(images);
|
await optimizeImages(images);
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import { PenpotPage } from '@ui/lib/types/penpotPage';
|
import { PenpotPage } from '@ui/lib/types/penpotPage';
|
||||||
import { ComponentShape } from '@ui/lib/types/shapes/componentShape';
|
import { ComponentShape } from '@ui/lib/types/shapes/componentShape';
|
||||||
|
import { Fill } from '@ui/lib/types/utils/fill';
|
||||||
|
|
||||||
export type PenpotDocument = {
|
export type PenpotDocument = {
|
||||||
name: string;
|
name: string;
|
||||||
children?: PenpotPage[];
|
children?: PenpotPage[];
|
||||||
components: Record<string, ComponentShape>;
|
components: Record<string, ComponentShape>;
|
||||||
images: Record<string, Uint8Array>;
|
images: Record<string, Uint8Array>;
|
||||||
|
styles: Record<string, Fill[]>;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue