mirror of
https://github.com/penpot/penpot-exporter-figma-plugin.git
synced 2024-12-22 05:33:02 -05:00
updated penpot lib
This commit is contained in:
commit
b9f64f07f7
26 changed files with 5476 additions and 6030 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,2 +1,6 @@
|
||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
|
ui-src/.env
|
||||||
|
|
||||||
|
# Sentry Config File
|
||||||
|
.env.sentry-build-plugin
|
||||||
|
|
48
CHANGELOG.md
48
CHANGELOG.md
|
@ -1,5 +1,53 @@
|
||||||
# penpot-exporter
|
# penpot-exporter
|
||||||
|
|
||||||
|
## 0.12.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#236](https://github.com/penpot/penpot-exporter-figma-plugin/pull/236)
|
||||||
|
[`3da80b4`](https://github.com/penpot/penpot-exporter-figma-plugin/commit/3da80b4c266cf21e3123f8bf8a80bf2318c48c38)
|
||||||
|
Thanks [@Cenadros](https://github.com/Cenadros)! - Fixed transformed shapes when flipped
|
||||||
|
horizontally/vertically
|
||||||
|
|
||||||
|
## 0.12.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- [#228](https://github.com/penpot/penpot-exporter-figma-plugin/pull/228)
|
||||||
|
[`a079f16`](https://github.com/penpot/penpot-exporter-figma-plugin/commit/a079f168df4f0d3cbd15ea58097f6763380d72a4)
|
||||||
|
Thanks [@Cenadros](https://github.com/Cenadros)! - Added basic analytics and error tracking using
|
||||||
|
MixPanel and Sentry
|
||||||
|
|
||||||
|
## 0.11.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- [#229](https://github.com/penpot/penpot-exporter-figma-plugin/pull/229)
|
||||||
|
[`f77bc46`](https://github.com/penpot/penpot-exporter-figma-plugin/commit/f77bc463acdb9c12ca45f0ac7e908761eef454e9)
|
||||||
|
Thanks [@Cenadros](https://github.com/Cenadros)! - Basic Error Management
|
||||||
|
|
||||||
|
## 0.10.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#225](https://github.com/penpot/penpot-exporter-figma-plugin/pull/225)
|
||||||
|
[`2d0b63d`](https://github.com/penpot/penpot-exporter-figma-plugin/commit/2d0b63d5cd0579d1c2aef0694ed6624edc288fb2)
|
||||||
|
Thanks [@jordisala1991](https://github.com/jordisala1991)! - Fix vector network error on invalid
|
||||||
|
access to the property
|
||||||
|
|
||||||
|
## 0.10.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#221](https://github.com/penpot/penpot-exporter-figma-plugin/pull/221)
|
||||||
|
[`638817a`](https://github.com/penpot/penpot-exporter-figma-plugin/commit/638817a1d6f5b4a21b266d73d797b677ce2ebac7)
|
||||||
|
Thanks [@Cenadros](https://github.com/Cenadros)! - Figma typings update
|
||||||
|
|
||||||
|
- [#220](https://github.com/penpot/penpot-exporter-figma-plugin/pull/220)
|
||||||
|
[`c95d442`](https://github.com/penpot/penpot-exporter-figma-plugin/commit/c95d442e74b2e59ab8873dab808f9f95cdfb4021)
|
||||||
|
Thanks [@Cenadros](https://github.com/Cenadros)! - Fixed issue where big vectors are making the
|
||||||
|
plugin crash
|
||||||
|
|
||||||
## 0.10.0
|
## 0.10.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
<a href="https://penpot.app/"><b>Penpot Website</b></a> •
|
<a href="https://penpot.app/"><b>Penpot Website</b></a> •
|
||||||
<a href="https://community.penpot.app/t/figma-to-penpot-export-plugin/5554"><b>Export Figma to Penpot (Penpot community)</b></a> •
|
<a href="https://community.penpot.app/t/figma-to-penpot-export-plugin/5554"><b>Export Figma to Penpot (Penpot community)</b></a> •
|
||||||
<a href="https://community.penpot.app/"><b>Penpot Community</b></a> •
|
<a href="https://community.penpot.app/"><b>Penpot Community</b></a> •
|
||||||
<a href="https://www.figma.com/community/plugin/1219369440655168734/penpot-exporter"><b>Plugin in Figma community</b></a>
|
<a href="https://www.figma.com/community/plugin/1219369440655168734/penpot-exporter"><b>Plugin in Figma community</b></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
@ -96,6 +96,10 @@ then selecting "Download ZIP". Extract the ZIP file to a location on your comput
|
||||||
3. Once you are in the correct folder, you can run the `npm install` command to install the
|
3. Once you are in the correct folder, you can run the `npm install` command to install the
|
||||||
dependencies, and then the `npm run build` command to build the plugin.
|
dependencies, and then the `npm run build` command to build the plugin.
|
||||||
|
|
||||||
|
#### Building for production:
|
||||||
|
|
||||||
|
Follow the same steps as above, but instead of running `npm run build`, run `npm run build:prod`.
|
||||||
|
|
||||||
### Add to Figma
|
### Add to Figma
|
||||||
|
|
||||||
`Figma menu` > `Plugins` > `Development` > `Import plugin from manifest…` To add the plugin to
|
`Figma menu` > `Plugins` > `Development` > `Import plugin from manifest…` To add the plugin to
|
||||||
|
|
|
@ -5,6 +5,13 @@
|
||||||
"main": "dist/code.js",
|
"main": "dist/code.js",
|
||||||
"ui": "dist/index.html",
|
"ui": "dist/index.html",
|
||||||
"editorType": ["figma"],
|
"editorType": ["figma"],
|
||||||
"networkAccess": { "allowedDomains": ["none"] },
|
"networkAccess": {
|
||||||
|
"allowedDomains": [
|
||||||
|
"https://o4508183201316864.ingest.de.sentry.io",
|
||||||
|
"https://api-js.mixpanel.com"
|
||||||
|
],
|
||||||
|
"reasoning": "We use Sentry and Mixpanel to monitor the performance of the plugin and get information about errors to continue improving the experience."
|
||||||
|
},
|
||||||
|
"permissions": ["currentuser"],
|
||||||
"documentAccess": "dynamic-page"
|
"documentAccess": "dynamic-page"
|
||||||
}
|
}
|
||||||
|
|
2261
package-lock.json
generated
2261
package-lock.json
generated
File diff suppressed because it is too large
Load diff
12
package.json
12
package.json
|
@ -1,13 +1,14 @@
|
||||||
{
|
{
|
||||||
"name": "penpot-exporter",
|
"name": "penpot-exporter",
|
||||||
"version": "0.10.0",
|
"version": "0.12.1",
|
||||||
"description": "Penpot exporter",
|
"description": "Penpot exporter",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "concurrently -n widget,iframe 'npm:build:main' 'npm:build:ui'",
|
"build": "concurrently -n widget,iframe 'npm:build:main' 'npm:build:ui -- --mode development'",
|
||||||
|
"build:prod": "concurrently -n widget,iframe 'npm:build:main' 'npm:build:ui -- --mode production'",
|
||||||
"build:main": "esbuild plugin-src/code.ts --bundle --outfile=dist/code.js --target=es2016 --minify",
|
"build:main": "esbuild plugin-src/code.ts --bundle --outfile=dist/code.js --target=es2016 --minify",
|
||||||
"build:ui": "vite build",
|
"build:ui": "vite build",
|
||||||
"build:watch": "concurrently -n widget,iframe 'npm:build:main -- --watch' 'npm:build:ui -- --watch'",
|
"build:watch": "concurrently -n widget,iframe 'npm:build:main -- --watch' 'npm:build:ui -- --watch --mode development'",
|
||||||
"lint": "concurrently 'npm:lint:*'",
|
"lint": "concurrently 'npm:lint:*'",
|
||||||
"lint:eslint": "eslint .",
|
"lint:eslint": "eslint .",
|
||||||
"lint:stylelint": "stylelint ui-src/**.css",
|
"lint:stylelint": "stylelint ui-src/**.css",
|
||||||
|
@ -23,9 +24,12 @@
|
||||||
"license": "MPL2.0",
|
"license": "MPL2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@create-figma-plugin/ui": "^3.2",
|
"@create-figma-plugin/ui": "^3.2",
|
||||||
|
"@sentry/react": "^8.34",
|
||||||
|
"@sentry/vite-plugin": "^2.22",
|
||||||
"base64-js": "^1.5",
|
"base64-js": "^1.5",
|
||||||
"classnames": "^2.5",
|
"classnames": "^2.5",
|
||||||
"lru-cache": "^11.0",
|
"lru-cache": "^11.0",
|
||||||
|
"mixpanel-figma": "^2.0",
|
||||||
"preact": "^10.23",
|
"preact": "^10.23",
|
||||||
"react-hook-form": "^7.52",
|
"react-hook-form": "^7.52",
|
||||||
"romans": "^2.0",
|
"romans": "^2.0",
|
||||||
|
@ -37,7 +41,7 @@
|
||||||
"@changesets/changelog-github": "^0.5",
|
"@changesets/changelog-github": "^0.5",
|
||||||
"@changesets/cli": "^2.27",
|
"@changesets/cli": "^2.27",
|
||||||
"@figma/eslint-plugin-figma-plugins": "^0.15",
|
"@figma/eslint-plugin-figma-plugins": "^0.15",
|
||||||
"@figma/plugin-typings": "^1.98",
|
"@figma/plugin-typings": "^1.100",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^4.3",
|
"@trivago/prettier-plugin-sort-imports": "^4.3",
|
||||||
"@types/svg-path-parser": "^1.1",
|
"@types/svg-path-parser": "^1.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.18",
|
"@typescript-eslint/eslint-plugin": "^7.18",
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { getUserData } from '@plugin/getUserData';
|
||||||
|
|
||||||
import { findAllTextNodes } from './findAllTextnodes';
|
import { findAllTextNodes } from './findAllTextnodes';
|
||||||
import { handleExportMessage } from './handleExportMessage';
|
import { handleExportMessage } from './handleExportMessage';
|
||||||
import { registerChange } from './registerChange';
|
import { registerChange } from './registerChange';
|
||||||
|
@ -9,6 +11,7 @@ figma.showUI(__html__, { themeColors: true, width: BASE_WIDTH, height: BASE_HEIG
|
||||||
|
|
||||||
figma.ui.onmessage = message => {
|
figma.ui.onmessage = message => {
|
||||||
if (message.type === 'ready') {
|
if (message.type === 'ready') {
|
||||||
|
getUserData();
|
||||||
findAllTextNodes();
|
findAllTextNodes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
13
plugin-src/getUserData.ts
Normal file
13
plugin-src/getUserData.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
export const getUserData = async () => {
|
||||||
|
const user = figma.currentUser;
|
||||||
|
if (user) {
|
||||||
|
figma.ui.postMessage({
|
||||||
|
type: 'USER_DATA',
|
||||||
|
data: {
|
||||||
|
userId: user.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.warn('Could not get user data');
|
||||||
|
}
|
||||||
|
};
|
|
@ -8,6 +8,6 @@ export const registerComponentProperties = (node: ComponentSetNode | ComponentNo
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error registering component properties', node, error);
|
console.warn('Could not register component properties', node, error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { translateRotation, translateZeroRotation } from '@plugin/translators';
|
import { translateRotation, translateZeroRotation } from '@plugin/translators';
|
||||||
import { applyInverseRotation, getRotation, hasRotation } from '@plugin/utils';
|
import { applyInverseRotation, getRotation, isTransformed } from '@plugin/utils';
|
||||||
|
|
||||||
import { ShapeBaseAttributes, ShapeGeomAttributes } from '@ui/lib/types/shapes/shape';
|
import { ShapeBaseAttributes, ShapeGeomAttributes } from '@ui/lib/types/shapes/shape';
|
||||||
|
|
||||||
|
@ -8,10 +8,6 @@ export const transformRotation = (
|
||||||
): Pick<ShapeBaseAttributes, 'transform' | 'transformInverse' | 'rotation'> => {
|
): Pick<ShapeBaseAttributes, 'transform' | 'transformInverse' | 'rotation'> => {
|
||||||
const rotation = getRotation(node.absoluteTransform);
|
const rotation = getRotation(node.absoluteTransform);
|
||||||
|
|
||||||
if (!hasRotation(rotation)) {
|
|
||||||
return translateZeroRotation();
|
|
||||||
}
|
|
||||||
|
|
||||||
return translateRotation(node.absoluteTransform, rotation);
|
return translateRotation(node.absoluteTransform, rotation);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -23,7 +19,10 @@ export const transformRotationAndPosition = (
|
||||||
const y = node.absoluteTransform[1][2];
|
const y = node.absoluteTransform[1][2];
|
||||||
const rotation = getRotation(node.absoluteTransform);
|
const rotation = getRotation(node.absoluteTransform);
|
||||||
|
|
||||||
if (!hasRotation(rotation) || !node.absoluteBoundingBox) {
|
if (
|
||||||
|
!node.absoluteBoundingBox ||
|
||||||
|
!isTransformed(node.absoluteTransform, node.absoluteBoundingBox)
|
||||||
|
) {
|
||||||
return {
|
return {
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
|
|
|
@ -14,16 +14,21 @@ import { translateCommands, translateWindingRule } from '@plugin/translators/vec
|
||||||
import { PathShape } from '@ui/lib/types/shapes/pathShape';
|
import { PathShape } from '@ui/lib/types/shapes/pathShape';
|
||||||
|
|
||||||
export const transformVectorPaths = (node: VectorNode): PathShape[] => {
|
export const transformVectorPaths = (node: VectorNode): PathShape[] => {
|
||||||
|
let regions: readonly VectorRegion[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
regions = node.vectorNetwork?.regions ?? [];
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Could not access the vector network', node, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const strokeLength = node.strokes.length;
|
||||||
|
|
||||||
const pathShapes = node.vectorPaths
|
const pathShapes = node.vectorPaths
|
||||||
.filter((vectorPath, index) => {
|
.filter((vectorPath, index) => {
|
||||||
return (
|
return nodeHasFills(node, vectorPath, regions[index]) || strokeLength > 0;
|
||||||
nodeHasFills(node, vectorPath, (node.vectorNetwork.regions ?? [])[index]) ||
|
|
||||||
node.strokes.length > 0
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.map((vectorPath, index) =>
|
.map((vectorPath, index) => transformVectorPath(node, vectorPath, regions[index]));
|
||||||
transformVectorPath(node, vectorPath, (node.vectorNetwork.regions ?? [])[index])
|
|
||||||
);
|
|
||||||
|
|
||||||
const geometryShapes = node.fillGeometry
|
const geometryShapes = node.fillGeometry
|
||||||
.filter(
|
.filter(
|
||||||
|
|
|
@ -62,7 +62,7 @@ export const transformSceneNode = async (node: SceneNode): Promise<PenpotNode |
|
||||||
}
|
}
|
||||||
|
|
||||||
if (penpotNode === undefined) {
|
if (penpotNode === undefined) {
|
||||||
console.error(`Unsupported node type: ${node.type}`);
|
console.warn(`Unsupported node type: ${node.type}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return penpotNode;
|
return penpotNode;
|
||||||
|
|
|
@ -20,7 +20,7 @@ export const translateFill = (fill: Paint): Fill | undefined => {
|
||||||
return translateImageFill(fill);
|
return translateImageFill(fill);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error(`Unsupported fill type: ${fill.type}`);
|
console.warn(`Unsupported fill type: ${fill.type}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const translateFills = (
|
export const translateFills = (
|
||||||
|
@ -60,5 +60,5 @@ export const translatePageFill = (fill: Paint): string | undefined => {
|
||||||
return rgbToHex(fill.color);
|
return rgbToHex(fill.color);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error(`Unsupported page fill type: ${fill.type}`);
|
console.warn(`Unsupported page fill type: ${fill.type}`);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
import { Command } from 'svg-path-parser';
|
import { Command } from 'svg-path-parser';
|
||||||
|
|
||||||
import { getRotation, hasRotation } from '@plugin/utils';
|
import { isTransformed } from '@plugin/utils';
|
||||||
|
|
||||||
import { translateNonRotatedCommands } from '.';
|
import { translateNonRotatedCommands } from '.';
|
||||||
import { translateRotatedCommands } from './translateRotatedCommands';
|
import { translateRotatedCommands } from './translateRotatedCommands';
|
||||||
|
|
||||||
export const translateCommands = (node: LayoutMixin, commands: Command[]) => {
|
export const translateCommands = (node: LayoutMixin, commands: Command[]) => {
|
||||||
const rotation = getRotation(node.absoluteTransform);
|
if (node.absoluteBoundingBox && isTransformed(node.absoluteTransform, node.absoluteBoundingBox)) {
|
||||||
|
|
||||||
if (hasRotation(rotation) && node.absoluteBoundingBox) {
|
|
||||||
return translateRotatedCommands(commands, node.absoluteTransform, node.absoluteBoundingBox);
|
return translateRotatedCommands(commands, node.absoluteTransform, node.absoluteBoundingBox);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import { ClosePath, CurveTo, Segment } from '@ui/lib/types/shapes/pathShape';
|
import { ClosePath, CurveTo, Segment } from '@ui/lib/types/shapes/pathShape';
|
||||||
import { Point } from '@ui/lib/types/utils/point';
|
import { Point } from '@ui/lib/types/utils/point';
|
||||||
|
|
||||||
const ROTATION_TOLERANCE = 0.000001;
|
|
||||||
|
|
||||||
export const applyRotation = (point: Point, transform: Transform, boundingBox: Rect): Point => {
|
export const applyRotation = (point: Point, transform: Transform, boundingBox: Rect): Point => {
|
||||||
const centerPoint = calculateCenter(boundingBox);
|
const centerPoint = calculateCenter(boundingBox);
|
||||||
|
|
||||||
|
@ -61,7 +59,13 @@ export const applyInverseRotation = (
|
||||||
export const getRotation = (transform: Transform): number =>
|
export const getRotation = (transform: Transform): number =>
|
||||||
Math.acos(transform[0][0]) * (180 / Math.PI);
|
Math.acos(transform[0][0]) * (180 / Math.PI);
|
||||||
|
|
||||||
export const hasRotation = (rotation: number): boolean => rotation > ROTATION_TOLERANCE;
|
export const isTransformed = (transform: Transform, boundingBox: Rect | null): boolean => {
|
||||||
|
if (!boundingBox) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return transform[0][2] !== boundingBox.x || transform[1][2] !== boundingBox.y;
|
||||||
|
};
|
||||||
|
|
||||||
const inverseMatrix = (matrix: Transform): Transform => [
|
const inverseMatrix = (matrix: Transform): Transform => [
|
||||||
[matrix[0][0], matrix[1][0], matrix[0][2]],
|
[matrix[0][0], matrix[1][0], matrix[0][2]],
|
||||||
|
|
2
ui-src/.env.example
Normal file
2
ui-src/.env.example
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
VITE_SENTRY_DSN=
|
||||||
|
VITE_MIXPANEL_TOKEN=
|
40
ui-src/components/LibraryError.tsx
Normal file
40
ui-src/components/LibraryError.tsx
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import { Banner, Button, IconWarning32, Link } from '@create-figma-plugin/ui';
|
||||||
|
|
||||||
|
import { useFigmaContext } from '@ui/context';
|
||||||
|
|
||||||
|
import { Stack } from './Stack';
|
||||||
|
|
||||||
|
export const LibraryError = () => {
|
||||||
|
const { reload, cancel, error } = useFigmaContext();
|
||||||
|
|
||||||
|
if (!error) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack space="small">
|
||||||
|
<Stack space="xsmall">
|
||||||
|
<Banner icon={<IconWarning32 />} variant="warning">
|
||||||
|
Oops! It looks like there was an <b>error generating the export file</b>.
|
||||||
|
</Banner>
|
||||||
|
<span>
|
||||||
|
Please open an issue in our{' '}
|
||||||
|
<Link
|
||||||
|
href="https://github.com/penpot/penpot-exporter-figma-plugin/issues"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
Github repository
|
||||||
|
</Link>
|
||||||
|
, and we'll be happy to assist you!
|
||||||
|
</span>
|
||||||
|
<Stack space="xsmall" direction="row">
|
||||||
|
<Button onClick={reload} fullWidth>
|
||||||
|
Reload
|
||||||
|
</Button>
|
||||||
|
<Button secondary onClick={cancel} fullWidth>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,5 +1,6 @@
|
||||||
import { LoadingIndicator } from '@create-figma-plugin/ui';
|
import { LoadingIndicator } from '@create-figma-plugin/ui';
|
||||||
|
|
||||||
|
import { LibraryError } from '@ui/components/LibraryError';
|
||||||
import { useFigmaContext } from '@ui/context';
|
import { useFigmaContext } from '@ui/context';
|
||||||
|
|
||||||
import { ExportForm } from './ExportForm';
|
import { ExportForm } from './ExportForm';
|
||||||
|
@ -7,7 +8,7 @@ import { ExporterProgress } from './ExporterProgress';
|
||||||
import { PluginReload } from './PluginReload';
|
import { PluginReload } from './PluginReload';
|
||||||
|
|
||||||
export const PenpotExporter = () => {
|
export const PenpotExporter = () => {
|
||||||
const { loading, needsReload, exporting } = useFigmaContext();
|
const { loading, needsReload, exporting, error } = useFigmaContext();
|
||||||
|
|
||||||
if (loading) return <LoadingIndicator />;
|
if (loading) return <LoadingIndicator />;
|
||||||
|
|
||||||
|
@ -15,5 +16,7 @@ export const PenpotExporter = () => {
|
||||||
|
|
||||||
if (needsReload) return <PluginReload />;
|
if (needsReload) return <PluginReload />;
|
||||||
|
|
||||||
|
if (error) return <LibraryError />;
|
||||||
|
|
||||||
return <ExportForm />;
|
return <ExportForm />;
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,7 +11,9 @@ type PluginMessage =
|
||||||
| ProgressStepMessage
|
| ProgressStepMessage
|
||||||
| ProgressCurrentItemMessage
|
| ProgressCurrentItemMessage
|
||||||
| ProgressTotalItemsMessage
|
| ProgressTotalItemsMessage
|
||||||
| ProgressProcessedItemsMessage;
|
| ProgressProcessedItemsMessage
|
||||||
|
| ErrorMessage
|
||||||
|
| UserDataMessage;
|
||||||
|
|
||||||
type PenpotDocumentMessage = {
|
type PenpotDocumentMessage = {
|
||||||
type: 'PENPOT_DOCUMENT';
|
type: 'PENPOT_DOCUMENT';
|
||||||
|
@ -47,6 +49,18 @@ type ProgressProcessedItemsMessage = {
|
||||||
data: number;
|
data: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ErrorMessage = {
|
||||||
|
type: 'ERROR';
|
||||||
|
data: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type UserDataMessage = {
|
||||||
|
type: 'USER_DATA';
|
||||||
|
data: {
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const sendMessage = (pluginMessage: PluginMessage) => {
|
export const sendMessage = (pluginMessage: PluginMessage) => {
|
||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
new MessageEvent<MessageData>('message', {
|
new MessageEvent<MessageData>('message', {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { FormValues } from '@ui/components/ExportForm';
|
import { FormValues } from '@ui/components/ExportForm';
|
||||||
|
import { identify, track } from '@ui/metrics/mixpanel';
|
||||||
import { parse } from '@ui/parser';
|
import { parse } from '@ui/parser';
|
||||||
|
|
||||||
import { MessageData, sendMessage } from '.';
|
import { MessageData, sendMessage } from '.';
|
||||||
|
@ -10,6 +11,7 @@ export type UseFigmaHook = {
|
||||||
needsReload: boolean;
|
needsReload: boolean;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
exporting: boolean;
|
exporting: boolean;
|
||||||
|
error: boolean;
|
||||||
step: Steps | undefined;
|
step: Steps | undefined;
|
||||||
currentItem: string | undefined;
|
currentItem: string | undefined;
|
||||||
totalItems: number;
|
totalItems: number;
|
||||||
|
@ -38,6 +40,7 @@ export const useFigma = (): UseFigmaHook => {
|
||||||
const [needsReload, setNeedsReload] = useState(false);
|
const [needsReload, setNeedsReload] = useState(false);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [exporting, setExporting] = useState(false);
|
const [exporting, setExporting] = useState(false);
|
||||||
|
const [error, setError] = useState(false);
|
||||||
|
|
||||||
const [step, setStep] = useState<Steps>();
|
const [step, setStep] = useState<Steps>();
|
||||||
const [currentItem, setCurrentItem] = useState<string | undefined>();
|
const [currentItem, setCurrentItem] = useState<string | undefined>();
|
||||||
|
@ -54,6 +57,11 @@ export const useFigma = (): UseFigmaHook => {
|
||||||
const { pluginMessage } = event.data;
|
const { pluginMessage } = event.data;
|
||||||
|
|
||||||
switch (pluginMessage.type) {
|
switch (pluginMessage.type) {
|
||||||
|
case 'USER_DATA': {
|
||||||
|
identify({ userId: pluginMessage.data.userId });
|
||||||
|
track('Plugin Loaded');
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'PENPOT_DOCUMENT': {
|
case 'PENPOT_DOCUMENT': {
|
||||||
const file = await parse(pluginMessage.data);
|
const file = await parse(pluginMessage.data);
|
||||||
|
|
||||||
|
@ -62,9 +70,20 @@ export const useFigma = (): UseFigmaHook => {
|
||||||
data: 'exporting'
|
data: 'exporting'
|
||||||
});
|
});
|
||||||
|
|
||||||
const blob = await file.export();
|
const blob = await file.export().catch(error => {
|
||||||
|
sendMessage({
|
||||||
|
type: 'ERROR',
|
||||||
|
data: error.message
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
download(blob, `${pluginMessage.data.name}.zip`);
|
if (blob) {
|
||||||
|
download(blob, `${pluginMessage.data.name}.zip`);
|
||||||
|
|
||||||
|
// get size of the file in Mb rounded to 2 decimal places
|
||||||
|
const size = Math.round((blob.size / 1024 / 1024) * 100) / 100;
|
||||||
|
track('File Exported', { 'Exported File Size': size + ' Mb' });
|
||||||
|
}
|
||||||
|
|
||||||
setExporting(false);
|
setExporting(false);
|
||||||
setStep(undefined);
|
setStep(undefined);
|
||||||
|
@ -98,6 +117,13 @@ export const useFigma = (): UseFigmaHook => {
|
||||||
setProcessedItems(pluginMessage.data);
|
setProcessedItems(pluginMessage.data);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'ERROR': {
|
||||||
|
setError(true);
|
||||||
|
setLoading(false);
|
||||||
|
setExporting(false);
|
||||||
|
track('Error', { 'Error Message': pluginMessage.data });
|
||||||
|
throw new Error(pluginMessage.data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -113,6 +139,7 @@ export const useFigma = (): UseFigmaHook => {
|
||||||
|
|
||||||
const reload = () => {
|
const reload = () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
setError(false);
|
||||||
postMessage('reload');
|
postMessage('reload');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -143,6 +170,7 @@ export const useFigma = (): UseFigmaHook => {
|
||||||
needsReload,
|
needsReload,
|
||||||
loading,
|
loading,
|
||||||
exporting,
|
exporting,
|
||||||
|
error,
|
||||||
step,
|
step,
|
||||||
currentItem,
|
currentItem,
|
||||||
totalItems,
|
totalItems,
|
||||||
|
|
8917
ui-src/lib/penpot.js
8917
ui-src/lib/penpot.js
File diff suppressed because one or more lines are too long
|
@ -2,10 +2,16 @@ import 'node_modules/@create-figma-plugin/ui/lib/css/base.css';
|
||||||
import { StrictMode } from 'react';
|
import { StrictMode } from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
|
import { initializeMixpanel } from '@ui/metrics/mixpanel';
|
||||||
|
import { initializeSentry } from '@ui/metrics/sentry';
|
||||||
|
|
||||||
import { App } from './App';
|
import { App } from './App';
|
||||||
import './main.css';
|
import './main.css';
|
||||||
import './reset.css';
|
import './reset.css';
|
||||||
|
|
||||||
|
initializeMixpanel();
|
||||||
|
initializeSentry();
|
||||||
|
|
||||||
createRoot(document.getElementById('root') as HTMLElement).render(
|
createRoot(document.getElementById('root') as HTMLElement).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App />
|
<App />
|
||||||
|
|
34
ui-src/metrics/mixpanel.ts
Normal file
34
ui-src/metrics/mixpanel.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import mixpanel from 'mixpanel-figma';
|
||||||
|
|
||||||
|
export const track = (name: string, opts = {}) => {
|
||||||
|
if (import.meta.env.VITE_MIXPANEL_TOKEN && import.meta.env.PROD) {
|
||||||
|
opts = {
|
||||||
|
...opts,
|
||||||
|
'Plugin Version': APP_VERSION
|
||||||
|
};
|
||||||
|
mixpanel.track(name, opts);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const identify = ({ userId }: { userId: string }) => {
|
||||||
|
if (import.meta.env.VITE_MIXPANEL_TOKEN && import.meta.env.PROD) {
|
||||||
|
mixpanel.identify(userId);
|
||||||
|
|
||||||
|
mixpanel.people.set({
|
||||||
|
'USER_ID': userId,
|
||||||
|
'Plugin Version': APP_VERSION
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initializeMixpanel = () => {
|
||||||
|
if (import.meta.env.VITE_MIXPANEL_TOKEN && import.meta.env.PROD) {
|
||||||
|
mixpanel.init(import.meta.env.VITE_MIXPANEL_TOKEN, {
|
||||||
|
disable_cookie: true,
|
||||||
|
disable_persistence: true,
|
||||||
|
opt_out_tracking_by_default: true,
|
||||||
|
ip: false,
|
||||||
|
track_pageview: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
16
ui-src/metrics/sentry.ts
Normal file
16
ui-src/metrics/sentry.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import * as Sentry from '@sentry/react';
|
||||||
|
|
||||||
|
export const initializeSentry = () => {
|
||||||
|
if (import.meta.env.VITE_SENTRY_DSN && import.meta.env.PROD) {
|
||||||
|
Sentry.init({
|
||||||
|
dsn: import.meta.env.VITE_SENTRY_DSN,
|
||||||
|
integrations: [Sentry.browserTracingIntegration(), Sentry.replayIntegration()],
|
||||||
|
release: `penpot-exporter@${APP_VERSION}`,
|
||||||
|
// Tracing
|
||||||
|
tracesSampleRate: 1.0, // Capture 100% of the transactions
|
||||||
|
// Session Replay
|
||||||
|
replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production.
|
||||||
|
replaysOnErrorSampleRate: 1.0 // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur.
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
8
ui-src/vite-env.d.ts
vendored
8
ui-src/vite-env.d.ts
vendored
|
@ -1,2 +1,10 @@
|
||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
/// <reference types="vite-plugin-svgr/client" />
|
/// <reference types="vite-plugin-svgr/client" />
|
||||||
|
declare module ViteEnv {
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
VITE_SENTRY_DSN: string;
|
||||||
|
VITE_MIXPANEL_TOKEN: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare const APP_VERSION: string;
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
|
import { sentryVitePlugin } from '@sentry/vite-plugin';
|
||||||
import react from '@vitejs/plugin-react-swc';
|
import react from '@vitejs/plugin-react-swc';
|
||||||
|
import * as process from 'node:process';
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import { viteSingleFile } from 'vite-plugin-singlefile';
|
import { viteSingleFile } from 'vite-plugin-singlefile';
|
||||||
import svgr from 'vite-plugin-svgr';
|
import svgr from 'vite-plugin-svgr';
|
||||||
|
@ -6,7 +8,16 @@ import tsconfigPaths from 'vite-tsconfig-paths';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
root: './ui-src',
|
root: './ui-src',
|
||||||
plugins: [svgr(), react(), viteSingleFile({ removeViteModuleLoader: true }), tsconfigPaths()],
|
plugins: [
|
||||||
|
svgr(),
|
||||||
|
react(),
|
||||||
|
viteSingleFile({ removeViteModuleLoader: true }),
|
||||||
|
tsconfigPaths(),
|
||||||
|
sentryVitePlugin({
|
||||||
|
org: 'runroom-sl',
|
||||||
|
project: 'penpot-exporter'
|
||||||
|
})
|
||||||
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'react': 'preact/compat',
|
'react': 'preact/compat',
|
||||||
|
@ -19,8 +30,13 @@ export default defineConfig({
|
||||||
target: 'esnext',
|
target: 'esnext',
|
||||||
reportCompressedSize: false,
|
reportCompressedSize: false,
|
||||||
outDir: '../dist',
|
outDir: '../dist',
|
||||||
|
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
external: ['!../css/base.css']
|
external: ['!../css/base.css']
|
||||||
}
|
},
|
||||||
|
sourcemap: true
|
||||||
|
},
|
||||||
|
define: {
|
||||||
|
APP_VERSION: JSON.stringify(process.env.npm_package_version)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue