2024-04-08 04:43:30 -05:00
|
|
|
import * as React from 'react';
|
|
|
|
import { createRoot } from 'react-dom/client';
|
|
|
|
import slugify from 'slugify';
|
2022-10-11 08:55:08 -05:00
|
|
|
|
2024-04-08 04:43:30 -05:00
|
|
|
import { NodeData, TextData, TextDataChildren } from './interfaces';
|
|
|
|
import { PenpotFile, createFile } from './penpot';
|
|
|
|
import './ui.css';
|
2022-10-11 08:55:08 -05:00
|
|
|
|
2024-04-08 04:43:30 -05:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
2022-10-11 08:55:08 -05:00
|
|
|
declare function require(path: string): any;
|
|
|
|
|
2023-01-30 03:11:55 -05:00
|
|
|
// Open resources/gfonts.json and create a set of matched font names
|
|
|
|
const gfonts = new Set();
|
2024-04-08 04:43:30 -05:00
|
|
|
require('./gfonts.json').forEach((font: string) => gfonts.add(font));
|
2022-10-11 08:55:08 -05:00
|
|
|
|
2022-12-29 14:05:42 -05:00
|
|
|
type FigmaImageData = {
|
2024-04-08 04:43:30 -05:00
|
|
|
value: string;
|
|
|
|
width: number;
|
|
|
|
height: number;
|
|
|
|
};
|
2022-12-29 14:05:42 -05:00
|
|
|
|
2022-10-11 08:55:08 -05:00
|
|
|
type PenpotExporterState = {
|
2024-04-08 04:43:30 -05:00
|
|
|
missingFonts: Set<string>;
|
|
|
|
figmaRootNode: NodeData | null;
|
|
|
|
images: { [id: string]: FigmaImageData };
|
|
|
|
};
|
|
|
|
|
|
|
|
export default class PenpotExporter extends React.Component<unknown, PenpotExporterState> {
|
2022-10-11 08:55:08 -05:00
|
|
|
state: PenpotExporterState = {
|
2023-01-30 03:11:55 -05:00
|
|
|
missingFonts: new Set(),
|
2022-10-11 08:55:08 -05:00
|
|
|
figmaRootNode: null,
|
|
|
|
images: {}
|
|
|
|
};
|
|
|
|
|
|
|
|
componentDidMount = () => {
|
2024-04-08 04:43:30 -05:00
|
|
|
window.addEventListener('message', this.onMessage);
|
|
|
|
};
|
2023-01-30 03:11:55 -05:00
|
|
|
|
|
|
|
componentDidUpdate = () => {
|
|
|
|
this.setDimensions();
|
2024-04-08 04:43:30 -05:00
|
|
|
};
|
2023-01-30 03:11:55 -05:00
|
|
|
|
2024-04-08 04:43:30 -05:00
|
|
|
componentWillUnmount = () => {
|
2022-10-11 08:55:08 -05:00
|
|
|
window.removeEventListener('message', this.onMessage);
|
2024-04-08 04:43:30 -05:00
|
|
|
};
|
2022-10-11 08:55:08 -05:00
|
|
|
|
2024-04-08 04:43:30 -05:00
|
|
|
rgbToHex = (color: RGB) => {
|
2022-10-11 08:55:08 -05:00
|
|
|
const r = Math.round(255 * color.r);
|
|
|
|
const g = Math.round(255 * color.g);
|
|
|
|
const b = Math.round(255 * color.b);
|
|
|
|
const rgb = (r << 16) | (g << 8) | (b << 0);
|
|
|
|
return '#' + (0x1000000 + rgb).toString(16).slice(1);
|
2024-04-08 04:43:30 -05:00
|
|
|
};
|
2022-10-11 08:55:08 -05:00
|
|
|
|
2024-04-08 04:43:30 -05:00
|
|
|
translateSolidFill(fill: SolidPaint) {
|
2022-10-11 08:55:08 -05:00
|
|
|
return {
|
|
|
|
fillColor: this.rgbToHex(fill.color),
|
2024-04-08 04:43:30 -05:00
|
|
|
fillOpacity: fill.visible === false ? 0 : fill.opacity
|
|
|
|
};
|
2022-10-11 08:55:08 -05:00
|
|
|
}
|
|
|
|
|
2024-04-08 04:43:30 -05:00
|
|
|
translateGradientLinearFill(fill: GradientPaint /*, width: number, height: number*/) {
|
|
|
|
// const points = extractLinearGradientParamsFromTransform(width, height, fill.gradientTransform);
|
2022-10-11 08:55:08 -05:00
|
|
|
return {
|
|
|
|
fillColorGradient: {
|
2024-04-08 04:43:30 -05:00
|
|
|
type: Symbol.for('linear'),
|
|
|
|
width: 1,
|
|
|
|
// startX: points.start[0] / width,
|
|
|
|
// startY: points.start[1] / height,
|
|
|
|
// endX: points.end[0] / width,
|
|
|
|
// endY: points.end[1] / height,
|
|
|
|
stops: [
|
|
|
|
{
|
|
|
|
color: this.rgbToHex(fill.gradientStops[0].color),
|
|
|
|
offset: fill.gradientStops[0].position,
|
|
|
|
opacity: fill.gradientStops[0].color.a * (fill.opacity ?? 1)
|
|
|
|
},
|
|
|
|
{
|
|
|
|
color: this.rgbToHex(fill.gradientStops[1].color),
|
|
|
|
offset: fill.gradientStops[1].position,
|
|
|
|
opacity: fill.gradientStops[1].color.a * (fill.opacity ?? 1)
|
|
|
|
}
|
|
|
|
]
|
|
|
|
},
|
|
|
|
fillOpacity: fill.visible === false ? 0 : undefined
|
|
|
|
};
|
2022-10-11 08:55:08 -05:00
|
|
|
}
|
|
|
|
|
2024-04-08 04:43:30 -05:00
|
|
|
translateFill(fill: Paint /*, width: number, height: number*/) {
|
|
|
|
if (fill.type === 'SOLID') {
|
2022-10-11 08:55:08 -05:00
|
|
|
return this.translateSolidFill(fill);
|
2024-04-08 04:43:30 -05:00
|
|
|
} else if (fill.type === 'GRADIENT_LINEAR') {
|
|
|
|
return this.translateGradientLinearFill(fill /*, width, height*/);
|
2022-10-11 08:55:08 -05:00
|
|
|
} else {
|
2024-04-08 04:43:30 -05:00
|
|
|
console.error('Color type ' + fill.type + ' not supported yet');
|
2022-10-11 08:55:08 -05:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-08 04:43:30 -05:00
|
|
|
translateFills(fills: readonly Paint[] /*, width: number, height: number*/) {
|
|
|
|
const penpotFills = [];
|
2022-10-11 08:55:08 -05:00
|
|
|
let penpotFill = null;
|
2024-04-08 04:43:30 -05:00
|
|
|
for (const fill of fills) {
|
|
|
|
penpotFill = this.translateFill(fill /*, width, height*/);
|
2023-01-05 08:42:31 -05:00
|
|
|
|
2024-04-08 04:43:30 -05:00
|
|
|
if (penpotFill !== null) {
|
2022-10-11 08:55:08 -05:00
|
|
|
penpotFills.unshift(penpotFill);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return penpotFills;
|
|
|
|
}
|
|
|
|
|
2024-04-08 04:43:30 -05:00
|
|
|
addFontWarning(font: string) {
|
2023-01-30 03:11:55 -05:00
|
|
|
const newMissingFonts = this.state.missingFonts;
|
|
|
|
newMissingFonts.add(font);
|
|
|
|
|
2024-04-08 04:43:30 -05:00
|
|
|
this.setState(() => ({ missingFonts: newMissingFonts }));
|
2023-01-30 03:11:55 -05:00
|
|
|
}
|
|
|
|
|
2024-04-08 04:43:30 -05:00
|
|
|
createPenpotPage(file: PenpotFile, node: NodeData) {
|
2022-10-11 08:55:08 -05:00
|
|
|
file.addPage(node.name);
|
2024-04-08 04:43:30 -05:00
|
|
|
for (const child of node.children) {
|
2022-10-11 08:55:08 -05:00
|
|
|
this.createPenpotItem(file, child, 0, 0);
|
|
|
|
}
|
|
|
|
file.closePage();
|
|
|
|
}
|
|
|
|
|
2024-04-08 04:43:30 -05:00
|
|
|
createPenpotBoard(file: PenpotFile, node: NodeData, baseX: number, baseY: number) {
|
|
|
|
file.addArtboard({
|
|
|
|
name: node.name,
|
|
|
|
x: node.x + baseX,
|
|
|
|
y: node.y + baseY,
|
|
|
|
width: node.width,
|
|
|
|
height: node.height,
|
|
|
|
fills: this.translateFills(node.fills /*, node.width, node.height*/)
|
2023-01-05 08:42:31 -05:00
|
|
|
});
|
2024-04-08 04:43:30 -05:00
|
|
|
for (const child of node.children) {
|
2022-12-11 12:15:41 -05:00
|
|
|
this.createPenpotItem(file, child, node.x + baseX, node.y + baseY);
|
2022-10-11 08:55:08 -05:00
|
|
|
}
|
|
|
|
file.closeArtboard();
|
|
|
|
}
|
|
|
|
|
2024-04-08 04:43:30 -05:00
|
|
|
createPenpotGroup(file: PenpotFile, node: NodeData, baseX: number, baseY: number) {
|
|
|
|
file.addGroup({ name: node.name });
|
|
|
|
for (const child of node.children) {
|
2022-10-11 08:55:08 -05:00
|
|
|
this.createPenpotItem(file, child, baseX, baseY);
|
|
|
|
}
|
|
|
|
file.closeGroup();
|
|
|
|
}
|
|
|
|
|
2024-04-08 04:43:30 -05:00
|
|
|
createPenpotRectangle(file: PenpotFile, node: NodeData, baseX: number, baseY: number) {
|
|
|
|
file.createRect({
|
|
|
|
name: node.name,
|
|
|
|
x: node.x + baseX,
|
|
|
|
y: node.y + baseY,
|
|
|
|
width: node.width,
|
|
|
|
height: node.height,
|
|
|
|
fills: this.translateFills(node.fills /*, node.width, node.height*/)
|
|
|
|
});
|
2022-10-11 08:55:08 -05:00
|
|
|
}
|
|
|
|
|
2024-04-08 04:43:30 -05:00
|
|
|
createPenpotCircle(file: PenpotFile, node: NodeData, baseX: number, baseY: number) {
|
|
|
|
file.createCircle({
|
|
|
|
name: node.name,
|
|
|
|
x: node.x + baseX,
|
|
|
|
y: node.y + baseY,
|
|
|
|
width: node.width,
|
|
|
|
height: node.height,
|
|
|
|
fills: this.translateFills(node.fills /*, node.width, node.height*/)
|
|
|
|
});
|
2022-10-11 08:55:08 -05:00
|
|
|
}
|
|
|
|
|
2024-04-08 04:43:30 -05:00
|
|
|
translateHorizontalAlign(align: string) {
|
|
|
|
if (align === 'RIGHT') {
|
|
|
|
return Symbol.for('right');
|
2022-10-11 08:55:08 -05:00
|
|
|
}
|
2024-04-08 04:43:30 -05:00
|
|
|
if (align === 'CENTER') {
|
|
|
|
return Symbol.for('center');
|
2022-10-11 08:55:08 -05:00
|
|
|
}
|
2024-04-08 04:43:30 -05:00
|
|
|
return Symbol.for('left');
|
2022-10-11 08:55:08 -05:00
|
|
|
}
|
|
|
|
|
2024-04-08 04:43:30 -05:00
|
|
|
translateVerticalAlign(align: string) {
|
|
|
|
if (align === 'BOTTOM') {
|
|
|
|
return Symbol.for('bottom');
|
2022-10-11 08:55:08 -05:00
|
|
|
}
|
2024-04-08 04:43:30 -05:00
|
|
|
if (align === 'CENTER') {
|
|
|
|
return Symbol.for('center');
|
2022-10-11 08:55:08 -05:00
|
|
|
}
|
2024-04-08 04:43:30 -05:00
|
|
|
return Symbol.for('top');
|
2022-10-11 08:55:08 -05:00
|
|
|
}
|
|
|
|
|
2024-04-08 04:43:30 -05:00
|
|
|
translateFontStyle(style: string) {
|
|
|
|
return style.toLowerCase().replace(/\s/g, '');
|
2022-10-11 08:55:08 -05:00
|
|
|
}
|
|
|
|
|
2024-04-08 04:43:30 -05:00
|
|
|
getTextDecoration(node: TextData | TextDataChildren) {
|
2023-01-04 12:49:55 -05:00
|
|
|
const textDecoration = node.textDecoration;
|
2024-04-08 04:43:30 -05:00
|
|
|
if (textDecoration === 'STRIKETHROUGH') {
|
|
|
|
return 'line-through';
|
2023-01-04 12:49:55 -05:00
|
|
|
}
|
2024-04-08 04:43:30 -05:00
|
|
|
if (textDecoration === 'UNDERLINE') {
|
|
|
|
return 'underline';
|
2023-01-04 12:49:55 -05:00
|
|
|
}
|
2024-04-08 04:43:30 -05:00
|
|
|
return 'none';
|
2023-01-04 12:49:55 -05:00
|
|
|
}
|
|
|
|
|
2024-04-08 04:43:30 -05:00
|
|
|
getTextTransform(node: TextData | TextDataChildren) {
|
2023-01-04 12:49:55 -05:00
|
|
|
const textCase = node.textCase;
|
2024-04-08 04:43:30 -05:00
|
|
|
if (textCase === 'UPPER') {
|
|
|
|
return 'uppercase';
|
2023-01-04 12:49:55 -05:00
|
|
|
}
|
2024-04-08 04:43:30 -05:00
|
|
|
if (textCase === 'LOWER') {
|
|
|
|
return 'lowercase';
|
2023-01-04 12:49:55 -05:00
|
|
|
}
|
2024-04-08 04:43:30 -05:00
|
|
|
if (textCase === 'TITLE') {
|
|
|
|
return 'capitalize';
|
2023-01-04 12:49:55 -05:00
|
|
|
}
|
2024-04-08 04:43:30 -05:00
|
|
|
return 'none';
|
2023-01-04 12:49:55 -05:00
|
|
|
}
|
|
|
|
|
2024-04-08 04:43:30 -05:00
|
|
|
validateFont(fontName: FontName) {
|
2023-01-30 03:11:55 -05:00
|
|
|
const name = slugify(fontName.family.toLowerCase());
|
|
|
|
if (!gfonts.has(name)) {
|
|
|
|
this.addFontWarning(name);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-08 04:43:30 -05:00
|
|
|
createPenpotText(file: PenpotFile, node: TextData, baseX: number, baseY: number) {
|
|
|
|
const children = node.children.map(val => {
|
2023-01-30 03:11:55 -05:00
|
|
|
this.validateFont(val.fontName);
|
|
|
|
|
2022-10-14 02:47:05 -05:00
|
|
|
return {
|
|
|
|
lineHeight: val.lineHeight,
|
2024-04-08 04:43:30 -05:00
|
|
|
fontStyle: 'normal',
|
2022-10-14 02:47:05 -05:00
|
|
|
textAlign: this.translateHorizontalAlign(node.textAlignHorizontal),
|
2024-04-08 04:43:30 -05:00
|
|
|
fontId: 'gfont-' + slugify(val.fontName.family.toLowerCase()),
|
2022-10-14 02:47:05 -05:00
|
|
|
fontSize: val.fontSize.toString(),
|
|
|
|
fontWeight: val.fontWeight.toString(),
|
|
|
|
fontVariantId: this.translateFontStyle(val.fontName.style),
|
2023-01-04 12:49:55 -05:00
|
|
|
textDecoration: this.getTextDecoration(val),
|
|
|
|
textTransform: this.getTextTransform(val),
|
2022-10-14 02:47:05 -05:00
|
|
|
letterSpacing: val.letterSpacing,
|
2024-04-08 04:43:30 -05:00
|
|
|
fills: this.translateFills(val.fills /*, node.width, node.height*/),
|
2022-10-14 02:47:05 -05:00
|
|
|
fontFamily: val.fontName.family,
|
2024-04-08 04:43:30 -05:00
|
|
|
text: val.characters
|
|
|
|
};
|
|
|
|
});
|
2022-10-14 02:47:05 -05:00
|
|
|
|
2023-01-30 03:11:55 -05:00
|
|
|
this.validateFont(node.fontName);
|
|
|
|
|
2022-10-11 08:55:08 -05:00
|
|
|
file.createText({
|
|
|
|
name: node.name,
|
|
|
|
x: node.x + baseX,
|
|
|
|
y: node.y + baseY,
|
|
|
|
width: node.width,
|
|
|
|
height: node.height,
|
|
|
|
rotation: 0,
|
2024-04-08 04:43:30 -05:00
|
|
|
type: Symbol.for('text'),
|
2022-10-11 08:55:08 -05:00
|
|
|
content: {
|
2024-04-08 04:43:30 -05:00
|
|
|
type: 'root',
|
2022-10-11 08:55:08 -05:00
|
|
|
verticalAlign: this.translateVerticalAlign(node.textAlignVertical),
|
2024-04-08 04:43:30 -05:00
|
|
|
children: [
|
|
|
|
{
|
|
|
|
type: 'paragraph-set',
|
|
|
|
children: [
|
|
|
|
{
|
|
|
|
lineHeight: node.lineHeight,
|
|
|
|
fontStyle: 'normal',
|
|
|
|
children: children,
|
|
|
|
textTransform: this.getTextTransform(node),
|
|
|
|
textAlign: this.translateHorizontalAlign(node.textAlignHorizontal),
|
|
|
|
fontId: 'gfont-' + slugify(node.fontName.family.toLowerCase()),
|
|
|
|
fontSize: node.fontSize.toString(),
|
|
|
|
fontWeight: node.fontWeight.toString(),
|
|
|
|
type: 'paragraph',
|
|
|
|
textDecoration: this.getTextDecoration(node),
|
|
|
|
letterSpacing: node.letterSpacing,
|
|
|
|
fills: this.translateFills(node.fills /*, node.width, node.height*/),
|
|
|
|
fontFamily: node.fontName.family
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
]
|
2022-10-11 08:55:08 -05:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-04-08 04:43:30 -05:00
|
|
|
createPenpotImage(
|
|
|
|
file: PenpotFile,
|
|
|
|
node: NodeData,
|
|
|
|
baseX: number,
|
|
|
|
baseY: number,
|
|
|
|
image: FigmaImageData
|
|
|
|
) {
|
|
|
|
file.createImage({
|
|
|
|
name: node.name,
|
|
|
|
x: node.x + baseX,
|
|
|
|
y: node.y + baseY,
|
|
|
|
width: image.width,
|
|
|
|
height: image.height,
|
2022-10-11 08:55:08 -05:00
|
|
|
metadata: {
|
2022-12-29 14:05:42 -05:00
|
|
|
width: image.width,
|
|
|
|
height: image.height
|
2022-10-11 08:55:08 -05:00
|
|
|
},
|
2022-12-29 14:05:42 -05:00
|
|
|
dataUri: image.value
|
2022-10-11 08:55:08 -05:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-04-08 04:43:30 -05:00
|
|
|
calculateAdjustment(node: NodeData) {
|
2022-12-29 14:05:42 -05:00
|
|
|
// For each child, check whether the X or Y position is less than 0 and less than the
|
|
|
|
// current adjustment.
|
|
|
|
let adjustedX = 0;
|
|
|
|
let adjustedY = 0;
|
2024-04-08 04:43:30 -05:00
|
|
|
for (const child of node.children) {
|
|
|
|
if (child.x < adjustedX) {
|
2022-12-29 14:05:42 -05:00
|
|
|
adjustedX = child.x;
|
|
|
|
}
|
2024-04-08 04:43:30 -05:00
|
|
|
if (child.y < adjustedY) {
|
2022-12-29 14:05:42 -05:00
|
|
|
adjustedY = child.y;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return [adjustedX, adjustedY];
|
|
|
|
}
|
|
|
|
|
2024-04-08 04:43:30 -05:00
|
|
|
createPenpotItem(file: PenpotFile, node: NodeData, baseX: number, baseY: number) {
|
2022-12-29 14:05:42 -05:00
|
|
|
// We special-case images because an image in figma is a shape with one or many
|
|
|
|
// image fills. Given that handling images in Penpot is a bit different, we
|
|
|
|
// rasterize a figma shape with any image fills to a PNG and then add it as a single
|
|
|
|
// Penpot image. Implication is that any node that has an image fill will only be
|
|
|
|
// treated as an image, so we skip node type checks.
|
2024-04-08 04:43:30 -05:00
|
|
|
const hasImageFill = node.fills?.some((fill: Paint) => fill.type === 'IMAGE');
|
|
|
|
if (hasImageFill) {
|
2022-12-29 14:05:42 -05:00
|
|
|
// If the nested frames extended the bounds of the rasterized image, we need to
|
|
|
|
// account for this both in position on the canvas and the calculated width and
|
|
|
|
// height of the image.
|
|
|
|
const [adjustedX, adjustedY] = this.calculateAdjustment(node);
|
|
|
|
|
2024-04-08 04:43:30 -05:00
|
|
|
this.createPenpotImage(
|
|
|
|
file,
|
|
|
|
node,
|
|
|
|
baseX + adjustedX,
|
|
|
|
baseY + adjustedY,
|
|
|
|
this.state.images[node.id]
|
|
|
|
);
|
|
|
|
} else if (node.type == 'PAGE') {
|
2022-10-11 08:55:08 -05:00
|
|
|
this.createPenpotPage(file, node);
|
2024-04-08 04:43:30 -05:00
|
|
|
} else if (node.type == 'FRAME') {
|
2022-10-11 08:55:08 -05:00
|
|
|
this.createPenpotBoard(file, node, baseX, baseY);
|
2024-04-08 04:43:30 -05:00
|
|
|
} else if (node.type == 'GROUP') {
|
|
|
|
this.createPenpotGroup(file, node, baseX, baseY);
|
|
|
|
} else if (node.type == 'RECTANGLE') {
|
2022-12-29 14:05:42 -05:00
|
|
|
this.createPenpotRectangle(file, node, baseX, baseY);
|
2024-04-08 04:43:30 -05:00
|
|
|
} else if (node.type == 'ELLIPSE') {
|
2022-10-11 08:55:08 -05:00
|
|
|
this.createPenpotCircle(file, node, baseX, baseY);
|
2024-04-08 04:43:30 -05:00
|
|
|
} else if (node.type == 'TEXT') {
|
|
|
|
this.createPenpotText(file, node as unknown as TextData, baseX, baseY);
|
2022-10-11 08:55:08 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-08 04:43:30 -05:00
|
|
|
createPenpotFile() {
|
|
|
|
const node = this.state.figmaRootNode;
|
|
|
|
|
|
|
|
if (node === null) {
|
|
|
|
throw new Error('No Figma file data found');
|
|
|
|
}
|
|
|
|
|
|
|
|
const file = createFile(node.name);
|
|
|
|
for (const page of node.children) {
|
2022-10-11 08:55:08 -05:00
|
|
|
this.createPenpotItem(file, page, 0, 0);
|
|
|
|
}
|
|
|
|
return file;
|
|
|
|
}
|
|
|
|
|
|
|
|
onCreatePenpot = () => {
|
2024-04-08 06:20:13 -05:00
|
|
|
this.createPenpotFile().export();
|
2022-10-11 08:55:08 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
onCancel = () => {
|
2024-04-08 04:43:30 -05:00
|
|
|
parent.postMessage({ pluginMessage: { type: 'cancel' } }, '*');
|
2022-10-11 08:55:08 -05:00
|
|
|
};
|
|
|
|
|
2024-04-08 04:43:30 -05:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
onMessage = (event: any) => {
|
|
|
|
if (event.data.pluginMessage.type == 'FIGMAFILE') {
|
|
|
|
this.setState(() => ({
|
|
|
|
figmaRootNode: event.data.pluginMessage.data
|
|
|
|
}));
|
|
|
|
} else if (event.data.pluginMessage.type == 'IMAGE') {
|
2022-10-11 08:55:08 -05:00
|
|
|
const data = event.data.pluginMessage.data;
|
2022-12-29 14:05:42 -05:00
|
|
|
const image = document.createElement('img');
|
|
|
|
|
2024-04-08 04:43:30 -05:00
|
|
|
image.addEventListener('load', () => {
|
2022-12-29 14:05:42 -05:00
|
|
|
// Get byte array from response
|
2024-04-08 04:43:30 -05:00
|
|
|
this.setState(state => {
|
|
|
|
state.images[data.id] = {
|
|
|
|
value: data.value,
|
|
|
|
width: image.naturalWidth,
|
|
|
|
height: image.naturalHeight
|
|
|
|
};
|
|
|
|
return state;
|
|
|
|
});
|
2022-12-29 14:05:42 -05:00
|
|
|
});
|
|
|
|
image.src = data.value;
|
2022-10-11 08:55:08 -05:00
|
|
|
}
|
2024-04-08 04:43:30 -05:00
|
|
|
};
|
2022-10-11 08:55:08 -05:00
|
|
|
|
2023-01-30 03:11:55 -05:00
|
|
|
setDimensions = () => {
|
|
|
|
const isMissingFonts = this.state.missingFonts.size > 0;
|
|
|
|
|
|
|
|
let width = 300;
|
|
|
|
let height = 280;
|
|
|
|
|
|
|
|
if (isMissingFonts) {
|
2024-04-08 04:43:30 -05:00
|
|
|
height += this.state.missingFonts.size * 20;
|
2023-01-30 03:11:55 -05:00
|
|
|
width = 400;
|
|
|
|
}
|
|
|
|
|
2024-04-08 04:43:30 -05:00
|
|
|
parent.postMessage({ pluginMessage: { type: 'resize', width: width, height: height } }, '*');
|
|
|
|
};
|
2023-01-30 03:11:55 -05:00
|
|
|
|
|
|
|
renderFontWarnings = () => {
|
|
|
|
return (
|
2024-04-08 04:43:30 -05:00
|
|
|
<ul>
|
|
|
|
{Array.from(this.state.missingFonts).map(font => (
|
2023-01-30 03:11:55 -05:00
|
|
|
<li key={font}>{font}</li>
|
|
|
|
))}
|
|
|
|
</ul>
|
|
|
|
);
|
2024-04-08 04:43:30 -05:00
|
|
|
};
|
2022-10-11 08:55:08 -05:00
|
|
|
|
|
|
|
render() {
|
2023-01-30 03:11:55 -05:00
|
|
|
// Update the dimensions of the plugin window based on available data and selections
|
2022-10-11 08:55:08 -05:00
|
|
|
return (
|
|
|
|
<main>
|
|
|
|
<header>
|
2024-04-08 04:43:30 -05:00
|
|
|
<img src={require('./logo.svg')} />
|
2022-10-11 08:55:08 -05:00
|
|
|
<h2>Penpot Exporter</h2>
|
|
|
|
</header>
|
|
|
|
<section>
|
2024-04-08 04:43:30 -05:00
|
|
|
<div style={{ display: this.state.missingFonts.size > 0 ? 'inline' : 'none' }}>
|
|
|
|
<div id="missing-fonts">
|
|
|
|
{this.state.missingFonts.size} non-default font
|
|
|
|
{this.state.missingFonts.size > 1 ? 's' : ''}:{' '}
|
2023-01-30 03:11:55 -05:00
|
|
|
</div>
|
2024-04-08 04:43:30 -05:00
|
|
|
<small>Ensure fonts are installed in Penpot before importing.</small>
|
|
|
|
<div id="missing-fonts-list">{this.renderFontWarnings()}</div>
|
2023-01-30 03:11:55 -05:00
|
|
|
</div>
|
2022-10-11 08:55:08 -05:00
|
|
|
</section>
|
|
|
|
<footer>
|
|
|
|
<button className="brand" onClick={this.onCreatePenpot}>
|
|
|
|
Export
|
|
|
|
</button>
|
|
|
|
<button onClick={this.onCancel}>Cancel</button>
|
|
|
|
</footer>
|
|
|
|
</main>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-08 04:43:30 -05:00
|
|
|
createRoot(document.getElementById('penpot-export-page') as HTMLElement).render(
|
2022-10-11 08:55:08 -05:00
|
|
|
<React.StrictMode>
|
2024-04-08 04:43:30 -05:00
|
|
|
<PenpotExporter />
|
|
|
|
</React.StrictMode>
|
2022-10-11 08:55:08 -05:00
|
|
|
);
|