diff --git a/.changeset/strange-gorillas-knock.md b/.changeset/strange-gorillas-knock.md new file mode 100644 index 0000000..e9186d9 --- /dev/null +++ b/.changeset/strange-gorillas-knock.md @@ -0,0 +1,5 @@ +--- +"penpot-exporter": minor +--- + +Support for color libraries diff --git a/plugin-src/StyleLibrary.ts b/plugin-src/StyleLibrary.ts index 56ef624..1cfe112 100644 --- a/plugin-src/StyleLibrary.ts +++ b/plugin-src/StyleLibrary.ts @@ -1,13 +1,11 @@ -import { Fill } from '@ui/lib/types/utils/fill'; - class StyleLibrary { - private styles: Map = new Map(); + private styles: Map = new Map(); - public register(id: string, styles: Fill[]) { + public register(id: string, styles?: PaintStyle | undefined) { this.styles.set(id, styles); } - public get(id: string): Fill[] | undefined { + public get(id: string): PaintStyle | undefined { return this.styles.get(id); } @@ -15,13 +13,9 @@ class StyleLibrary { return this.styles.has(id); } - public all(): Record { + public all(): Record { return Object.fromEntries(this.styles.entries()); } - - public init(styles: Record): void { - this.styles = new Map(Object.entries(styles)); - } } export const styleLibrary = new StyleLibrary(); diff --git a/plugin-src/transformers/partials/transformFills.ts b/plugin-src/transformers/partials/transformFills.ts index b71882f..66af40a 100644 --- a/plugin-src/transformers/partials/transformFills.ts +++ b/plugin-src/transformers/partials/transformFills.ts @@ -1,4 +1,4 @@ -import { translateFillStyle, translateFills } from '@plugin/translators/fills'; +import { translateFillStyleId, translateFills } from '@plugin/translators/fills'; import { StyleTextSegment } from '@plugin/translators/text/paragraph'; import { ShapeAttributes } from '@ui/lib/types/shapes/shape'; @@ -14,7 +14,7 @@ export const transformFills = ( if (hasFillStyle(node)) { return { fills: [], - fillStyleId: translateFillStyle(node.fillStyleId, node.fills) + fillStyleId: translateFillStyleId(node.fillStyleId) }; } diff --git a/plugin-src/transformers/transformDocumentNode.ts b/plugin-src/transformers/transformDocumentNode.ts index edcd577..d1452df 100644 --- a/plugin-src/transformers/transformDocumentNode.ts +++ b/plugin-src/transformers/transformDocumentNode.ts @@ -3,9 +3,11 @@ import { imagesLibrary } from '@plugin/ImageLibrary'; import { remoteComponentLibrary } from '@plugin/RemoteComponentLibrary'; import { styleLibrary } from '@plugin/StyleLibrary'; import { translateRemoteChildren } from '@plugin/translators'; +import { translatePaintStyles } from '@plugin/translators/fills'; import { sleep } from '@plugin/utils'; import { PenpotPage } from '@ui/lib/types/penpotPage'; +import { FillStyle } from '@ui/lib/types/utils/fill'; import { PenpotDocument } from '@ui/types'; import { transformPageNode } from '.'; @@ -48,6 +50,47 @@ const downloadImages = async (): Promise> => { return images; }; +const getFillStyles = async (): Promise> => { + const stylesToFetch = Object.entries(styleLibrary.all()); + const styles: Record = {}; + + if (stylesToFetch.length === 0) return styles; + + let currentStyle = 1; + + figma.ui.postMessage({ + type: 'PROGRESS_TOTAL_ITEMS', + data: stylesToFetch.length + }); + + figma.ui.postMessage({ + type: 'PROGRESS_STEP', + data: 'fills' + }); + + for (const [styleId, paintStyle] of stylesToFetch) { + const figmaStyle = paintStyle ?? (await figma.getStyleByIdAsync(styleId)); + if (figmaStyle && isPaintStyle(figmaStyle)) { + styles[styleId] = translatePaintStyles(figmaStyle); + } + + figma.ui.postMessage({ + type: 'PROGRESS_PROCESSED_ITEMS', + data: currentStyle++ + }); + + await sleep(0); + } + + await sleep(20); + + return styles; +}; + +const isPaintStyle = (style: BaseStyle): style is PaintStyle => { + return style.type === 'PAINT'; +}; + const processPages = async (node: DocumentNode): Promise => { const children = []; let currentPage = 1; @@ -74,6 +117,11 @@ const processPages = async (node: DocumentNode): Promise => { }; export const transformDocumentNode = async (node: DocumentNode): Promise => { + const localPaintStyles = await figma.getLocalPaintStylesAsync(); + localPaintStyles.forEach(style => { + styleLibrary.register(style.id, style); + }); + const children = await processPages(node); if (remoteComponentLibrary.remaining() > 0) { @@ -83,11 +131,15 @@ export const transformDocumentNode = async (node: DocumentNode): Promise { if (fillStyleId === figma.mixed || fillStyleId === undefined) return; if (!styleLibrary.has(fillStyleId)) { - styleLibrary.register(fillStyleId, translateFills(fills)); + styleLibrary.register(fillStyleId); } return fillStyleId; diff --git a/plugin-src/translators/fills/translatePaintStyles.ts b/plugin-src/translators/fills/translatePaintStyles.ts new file mode 100644 index 0000000..03effc7 --- /dev/null +++ b/plugin-src/translators/fills/translatePaintStyles.ts @@ -0,0 +1,35 @@ +import { translateFill } from '@plugin/translators/fills/translateFills'; + +import { FillStyle } from '@ui/lib/types/utils/fill'; + +export const translatePaintStyles = (figmaStyle: PaintStyle): FillStyle => { + const fillStyle: FillStyle = { + name: figmaStyle.name, + fills: [], + colors: [] + }; + + const colorName = (figmaStyle: PaintStyle, index: number): string => { + // @TODO: Think something better + return figmaStyle.paints.length > 1 ? `Color ${index + 1}` : figmaStyle.name; + }; + + let index = 0; + const path = + (figmaStyle.remote ? 'Remote / ' : '') + (figmaStyle.paints.length > 1 ? figmaStyle.name : ''); + + for (const fill of figmaStyle.paints) { + const penpotFill = translateFill(fill); + + if (penpotFill) { + fillStyle.fills.unshift(penpotFill); + fillStyle.colors.unshift({ + path, + name: colorName(figmaStyle, index) + }); + } + index++; + } + + return fillStyle; +}; diff --git a/ui-src/components/ExporterProgress.tsx b/ui-src/components/ExporterProgress.tsx index a142b91..bc9e64a 100644 --- a/ui-src/components/ExporterProgress.tsx +++ b/ui-src/components/ExporterProgress.tsx @@ -29,6 +29,15 @@ const stepMessages: Record = { total: 'pages built 🏗️', current: 'Currently processing layer' }, + fills: { + total: 'color libraries fetched 🎨' + }, + format: { + total: 'formatting color libraries 🎨' + }, + libraries: { + total: 'color libraries built 🎨' + }, components: { total: 'components built 🏗️', current: 'Currently processing layer' @@ -60,7 +69,10 @@ const StepProgress = (): JSX.Element | null => { case 'images': case 'optimization': case 'building': + case 'fills': case 'components': + case 'format': + case 'libraries': return ( <> {processedItems} of {totalItems} {stepMessages[step].total} diff --git a/ui-src/context/useFigma.ts b/ui-src/context/useFigma.ts index f9c6664..462f545 100644 --- a/ui-src/context/useFigma.ts +++ b/ui-src/context/useFigma.ts @@ -26,7 +26,10 @@ export type Steps = | 'optimization' | 'building' | 'components' - | 'exporting'; + | 'exporting' + | 'fills' + | 'format' + | 'libraries'; export const useFigma = (): UseFigmaHook => { const [missingFonts, setMissingFonts] = useState(); diff --git a/ui-src/lib/types/utils/color.ts b/ui-src/lib/types/utils/color.ts index 0545174..2a68c78 100644 --- a/ui-src/lib/types/utils/color.ts +++ b/ui-src/lib/types/utils/color.ts @@ -1,5 +1,5 @@ import { Gradient } from './gradient'; -import { ImageColor } from './imageColor'; +import { ImageColor, PartialImageColor } from './imageColor'; import { Uuid } from './uuid'; export type Color = { @@ -13,5 +13,5 @@ export type Color = { refId?: Uuid; refFile?: Uuid; gradient?: Gradient; - image?: ImageColor; + image?: ImageColor | PartialImageColor; // @TODO: move to any other place }; diff --git a/ui-src/lib/types/utils/fill.ts b/ui-src/lib/types/utils/fill.ts index 2169156..5af7ed3 100644 --- a/ui-src/lib/types/utils/fill.ts +++ b/ui-src/lib/types/utils/fill.ts @@ -1,3 +1,5 @@ +import { Color } from '@ui/lib/types/utils/color'; + import { Gradient } from './gradient'; import { ImageColor, PartialImageColor } from './imageColor'; import { Uuid } from './uuid'; @@ -10,3 +12,9 @@ export type Fill = { fillColorRefId?: Uuid; fillImage?: ImageColor | PartialImageColor; // @TODO: move to any other place }; + +export type FillStyle = { + name: string; + fills: Fill[]; + colors: Color[]; +}; diff --git a/ui-src/parser/creators/createFile.ts b/ui-src/parser/creators/buildFile.ts similarity index 72% rename from ui-src/parser/creators/createFile.ts rename to ui-src/parser/creators/buildFile.ts index f391f87..73d69e0 100644 --- a/ui-src/parser/creators/createFile.ts +++ b/ui-src/parser/creators/buildFile.ts @@ -1,14 +1,13 @@ import { sleep } from '@plugin/utils/sleep'; import { sendMessage } from '@ui/context'; -import { createFile as createPenpotFile } from '@ui/lib/penpot'; +import { PenpotFile } from '@ui/lib/types/penpotFile'; import { PenpotPage } from '@ui/lib/types/penpotPage'; import { idLibrary } from '@ui/parser'; -import { createComponentsLibrary, createPage } from '@ui/parser/creators'; +import { createColorsLibrary, createComponentsLibrary, createPage } from '@ui/parser/creators'; import { uiComponents } from '@ui/parser/libraries'; -export const createFile = async (name: string, children: PenpotPage[]) => { - const file = createPenpotFile(name); +export const buildFile = async (file: PenpotFile, children: PenpotPage[]) => { let pagesBuilt = 1; uiComponents.init(); @@ -35,6 +34,8 @@ export const createFile = async (name: string, children: PenpotPage[]) => { await sleep(0); } + await createColorsLibrary(file); + await createComponentsLibrary(file); return file; diff --git a/ui-src/parser/creators/createColorsLibrary.ts b/ui-src/parser/creators/createColorsLibrary.ts new file mode 100644 index 0000000..25c96ad --- /dev/null +++ b/ui-src/parser/creators/createColorsLibrary.ts @@ -0,0 +1,41 @@ +import { sleep } from '@plugin/utils/sleep'; + +import { sendMessage } from '@ui/context'; +import { PenpotFile } from '@ui/lib/types/penpotFile'; +import { uiColorLibraries } from '@ui/parser/libraries/UiColorLibraries'; + +export const createColorsLibrary = async (file: PenpotFile) => { + let librariesBuilt = 1; + const libraries = uiColorLibraries.all(); + + sendMessage({ + type: 'PROGRESS_TOTAL_ITEMS', + data: libraries.length + }); + + sendMessage({ + type: 'PROGRESS_STEP', + data: 'libraries' + }); + + for (const library of libraries) { + for (let index = 0; index < library.fills.length; index++) { + file.addLibraryColor({ + ...library.colors[index], + id: library.fills[index].fillColorRefId, + refFile: library.fills[index].fillColorRefFile, + color: library.fills[index].fillColor, + opacity: library.fills[index].fillOpacity, + image: library.fills[index].fillImage, + gradient: library.fills[index].fillColorGradient + }); + + sendMessage({ + type: 'PROGRESS_PROCESSED_ITEMS', + data: librariesBuilt++ + }); + + await sleep(0); + } + } +}; diff --git a/ui-src/parser/creators/index.ts b/ui-src/parser/creators/index.ts index 87a4c49..ece31ef 100644 --- a/ui-src/parser/creators/index.ts +++ b/ui-src/parser/creators/index.ts @@ -1,10 +1,11 @@ export * from './createArtboard'; export * from './createBool'; export * from './createCircle'; +export * from './createColorsLibrary'; export * from './createComponent'; export * from './createComponentInstance'; export * from './createComponentsLibrary'; -export * from './createFile'; +export * from './buildFile'; export * from './createGroup'; export * from './createItems'; export * from './createPage'; diff --git a/ui-src/parser/creators/symbols/symbolFills.ts b/ui-src/parser/creators/symbols/symbolFills.ts index 03c6e84..46b26ec 100644 --- a/ui-src/parser/creators/symbols/symbolFills.ts +++ b/ui-src/parser/creators/symbols/symbolFills.ts @@ -1,11 +1,10 @@ -import { styleLibrary } from '@plugin/StyleLibrary'; - import { Fill } from '@ui/lib/types/utils/fill'; import { ImageColor, PartialImageColor } from '@ui/lib/types/utils/imageColor'; import { uiImages } from '@ui/parser/libraries'; +import { uiColorLibraries } from '@ui/parser/libraries/UiColorLibraries'; export const symbolFills = (fillStyleId?: string, fills?: Fill[]): Fill[] | undefined => { - const nodeFills = fillStyleId ? styleLibrary.get(fillStyleId) : fills; + const nodeFills = fillStyleId ? uiColorLibraries.get(fillStyleId)?.fills : fills; if (!nodeFills) return; diff --git a/ui-src/parser/libraries/UiColorLibraries.ts b/ui-src/parser/libraries/UiColorLibraries.ts new file mode 100644 index 0000000..76b5ee5 --- /dev/null +++ b/ui-src/parser/libraries/UiColorLibraries.ts @@ -0,0 +1,19 @@ +import { FillStyle } from '@ui/lib/types/utils/fill'; + +class UiColorLibraries { + private libraries: Map = new Map(); + + public register(id: string, fillStyle: FillStyle) { + this.libraries.set(id, fillStyle); + } + + public get(id: string): FillStyle | undefined { + return this.libraries.get(id); + } + + public all(): FillStyle[] { + return Array.from(this.libraries.values()); + } +} + +export const uiColorLibraries = new UiColorLibraries(); diff --git a/ui-src/parser/libraries/index.ts b/ui-src/parser/libraries/index.ts index fe59ab7..588c5f3 100644 --- a/ui-src/parser/libraries/index.ts +++ b/ui-src/parser/libraries/index.ts @@ -1,2 +1,3 @@ export * from './UiComponents'; export * from './UiImages'; +export * from './UiColorLibraries'; diff --git a/ui-src/parser/parse.ts b/ui-src/parser/parse.ts index 4413738..7e4a2fb 100644 --- a/ui-src/parser/parse.ts +++ b/ui-src/parser/parse.ts @@ -1,11 +1,14 @@ import { componentsLibrary } from '@plugin/ComponentLibrary'; -import { styleLibrary } from '@plugin/StyleLibrary'; // @TODO: Direct import on purpose, to avoid problems with the tsc linting import { sleep } from '@plugin/utils/sleep'; import { sendMessage } from '@ui/context'; -import { createFile } from '@ui/parser/creators'; +import { createFile } from '@ui/lib/penpot'; +import { PenpotFile } from '@ui/lib/types/penpotFile'; +import { FillStyle } from '@ui/lib/types/utils/fill'; +import { buildFile } from '@ui/parser/creators'; import { uiImages } from '@ui/parser/libraries'; +import { uiColorLibraries } from '@ui/parser/libraries'; import { PenpotDocument } from '@ui/types'; import { parseImage } from '.'; @@ -41,6 +44,43 @@ const optimizeImages = async (images: Record) => { } }; +const prepareColorLibraries = async (file: PenpotFile, styles: Record) => { + const stylesToRegister = Object.entries(styles); + + if (stylesToRegister.length === 0) return; + + let stylesRegistered = 1; + + sendMessage({ + type: 'PROGRESS_TOTAL_ITEMS', + data: stylesToRegister.length + }); + + sendMessage({ + type: 'PROGRESS_STEP', + data: 'format' + }); + + for (const [key, fillStyle] of stylesToRegister) { + for (let index = 0; index < fillStyle.fills.length; index++) { + const colorId = file.newId(); + fillStyle.fills[index].fillColorRefId = colorId; + fillStyle.fills[index].fillColorRefFile = file.getId(); + fillStyle.colors[index].id = colorId; + fillStyle.colors[index].refFile = file.getId(); + } + + uiColorLibraries.register(key, fillStyle); + + sendMessage({ + type: 'PROGRESS_PROCESSED_ITEMS', + data: stylesRegistered++ + }); + + await sleep(0); + } +}; + export const parse = async ({ name, children = [], @@ -49,9 +89,11 @@ export const parse = async ({ styles }: PenpotDocument) => { componentsLibrary.init(components); - styleLibrary.init(styles); + + const file = createFile(name); await optimizeImages(images); + await prepareColorLibraries(file, styles); - return createFile(name, children); + return buildFile(file, children); }; diff --git a/ui-src/types/penpotDocument.ts b/ui-src/types/penpotDocument.ts index b0cbadb..6a5e9c1 100644 --- a/ui-src/types/penpotDocument.ts +++ b/ui-src/types/penpotDocument.ts @@ -1,11 +1,11 @@ import { PenpotPage } from '@ui/lib/types/penpotPage'; import { ComponentShape } from '@ui/lib/types/shapes/componentShape'; -import { Fill } from '@ui/lib/types/utils/fill'; +import { FillStyle } from '@ui/lib/types/utils/fill'; export type PenpotDocument = { name: string; children?: PenpotPage[]; components: Record; images: Record; - styles: Record; + styles: Record; };