mirror of https://github.com/penpot/penpot.git
🎉 Import penpot-plugins repository
As commit 819a549e4928d2b1fa98e52bee82d59aec0f70d8
This commit is contained in:
parent
22a36d59d8
commit
ec1af4ad96
|
|
@ -0,0 +1,3 @@
|
||||||
|
E2E_LOGIN_EMAIL=""
|
||||||
|
E2E_LOGIN_PASSWORD=""
|
||||||
|
E2E_SCREENSHOTS= "false"
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# compiled output
|
||||||
|
dist
|
||||||
|
tmp
|
||||||
|
/out-tsc
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
|
||||||
|
# misc
|
||||||
|
/.sass-cache
|
||||||
|
/connect.lock
|
||||||
|
/coverage
|
||||||
|
/libpeerconnection.log
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
testem.log
|
||||||
|
/typings
|
||||||
|
|
||||||
|
# System Files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
.nx/cache
|
||||||
|
.nx/workspace-data
|
||||||
|
.env
|
||||||
|
|
||||||
|
.angular
|
||||||
|
|
||||||
|
**/assets/plugin.js
|
||||||
|
|
||||||
|
docs/api
|
||||||
|
|
||||||
|
|
||||||
|
apps/e2e/screenshots/*.png
|
||||||
|
|
||||||
|
vite.config.*.timestamp*
|
||||||
|
vitest.config.*.timestamp*
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
npx --no -- commitlint --edit $1
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [ -z "$HUSKY_HOOK" ] || [ "$HUSKY_HOOK" = "pre-commit" ]; then
|
||||||
|
npm run lint:affected
|
||||||
|
fi
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [ "$HUSKY_HOOK" = "pre-push" ]; then
|
||||||
|
npm run lint:affected
|
||||||
|
fi
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
v22.2.0
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
# Add files here to ignore them from prettier formatting
|
||||||
|
/dist
|
||||||
|
/coverage
|
||||||
|
/.nx/cache
|
||||||
|
.angular
|
||||||
|
|
||||||
|
/.nx/workspace-data
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
# a list of other known repositories we can talk to
|
||||||
|
uplinks:
|
||||||
|
npmjs:
|
||||||
|
url: https://registry.npmjs.org/
|
||||||
|
maxage: 60m
|
||||||
|
|
||||||
|
packages:
|
||||||
|
'**':
|
||||||
|
# give all users (including non-authenticated users) full access
|
||||||
|
# because it is a local registry
|
||||||
|
access: $all
|
||||||
|
publish: $all
|
||||||
|
unpublish: $all
|
||||||
|
|
||||||
|
# if package is not available locally, proxy requests to npm registry
|
||||||
|
proxy: npmjs
|
||||||
|
|
||||||
|
# log settings
|
||||||
|
logs:
|
||||||
|
type: stdout
|
||||||
|
format: pretty
|
||||||
|
level: warn
|
||||||
|
|
||||||
|
publish:
|
||||||
|
allow_offline: true # set offline to true to allow publish offline
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"nrwl.angular-console",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"firsttris.vscode-jest-runner"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"prettier.singleQuote": true
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,221 @@
|
||||||
|
## 1.3.2 (2025-07-04)
|
||||||
|
|
||||||
|
### 🩹 Fixes
|
||||||
|
|
||||||
|
- plugins-runtime public package.json ([70fd69f](https://github.com/penpot/penpot-plugins/commit/70fd69f))
|
||||||
|
|
||||||
|
### ❤️ Thank You
|
||||||
|
|
||||||
|
- Juanfran @juanfran
|
||||||
|
|
||||||
|
## 1.3.1 (2025-07-04)
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- plugins-runtime as npm library ([41c56b1](https://github.com/penpot/penpot-plugins/commit/41c56b1))
|
||||||
|
|
||||||
|
### 🩹 Fixes
|
||||||
|
|
||||||
|
- package-lock.json ([16b29f8](https://github.com/penpot/penpot-plugins/commit/16b29f8))
|
||||||
|
|
||||||
|
### ❤️ Thank You
|
||||||
|
|
||||||
|
- Juanfran @juanfran
|
||||||
|
|
||||||
|
## 1.3.0 (2025-06-25)
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- **plugin-types:** add skipChildren to exports ([b3373ba](https://github.com/penpot/penpot-plugins/commit/b3373ba))
|
||||||
|
- **plugins-runtime:** change plugins modal z-index ([c6a4a7d](https://github.com/penpot/penpot-plugins/commit/c6a4a7d))
|
||||||
|
- **plugins-runtime:** adds max resize to the screen size ([f2fe501](https://github.com/penpot/penpot-plugins/commit/f2fe501))
|
||||||
|
- **plugins-runtime:** adds localstorage wrapper API for plugins ([0006ca9](https://github.com/penpot/penpot-plugins/commit/0006ca9))
|
||||||
|
- **plugins-runtime:** add generateFontFaces method ([30e1d02](https://github.com/penpot/penpot-plugins/commit/30e1d02))
|
||||||
|
- **poc-state-plugins:** add some methods to the example ([b95961a](https://github.com/penpot/penpot-plugins/commit/b95961a))
|
||||||
|
- **poc-state-plugins:** example using the localstorage api ([b101523](https://github.com/penpot/penpot-plugins/commit/b101523))
|
||||||
|
|
||||||
|
### 🩹 Fixes
|
||||||
|
|
||||||
|
- **plugin-colors-to-tokens:** adapt to Penpot tokens metadata format ([3a1ff00](https://github.com/penpot/penpot-plugins/commit/3a1ff00))
|
||||||
|
- **plugin-colors-to-tokens:** avoid unvalid character in names ([dd0fd1a](https://github.com/penpot/penpot-plugins/commit/dd0fd1a))
|
||||||
|
- **plugin-types:** add missing board properties ([de4a2a0](https://github.com/penpot/penpot-plugins/commit/de4a2a0))
|
||||||
|
- **plugin-types:** fix problem with type ([9759964](https://github.com/penpot/penpot-plugins/commit/9759964))
|
||||||
|
- **plugins-runtime:** add allow-same-origin to iframe ([65d5351](https://github.com/penpot/penpot-plugins/commit/65d5351))
|
||||||
|
- **plugins-runtime:** fixes null checking issue ([6b5b562](https://github.com/penpot/penpot-plugins/commit/6b5b562))
|
||||||
|
- **plugins-runtime:** fix problem with resize modal position ([45dc41d](https://github.com/penpot/penpot-plugins/commit/45dc41d))
|
||||||
|
- **plugins-styles:** migrate to fonts css api v2 ([45a9ee9](https://github.com/penpot/penpot-plugins/commit/45a9ee9))
|
||||||
|
|
||||||
|
### ❤️ Thank You
|
||||||
|
|
||||||
|
- alonso.torres
|
||||||
|
- Martynas Barzda
|
||||||
|
- Xavier Julian
|
||||||
|
|
||||||
|
## 1.2.0 (2025-02-27)
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- upgrade nx & angular & prettier ([32de075](https://github.com/penpot/penpot-plugins/commit/32de075))
|
||||||
|
- add ui.resize & ui.size api ([815181d](https://github.com/penpot/penpot-plugins/commit/815181d))
|
||||||
|
- colors to tokens export plugin ([7f8a011](https://github.com/penpot/penpot-plugins/commit/7f8a011))
|
||||||
|
- transform color & opacity to rgba ([9a3e6e0](https://github.com/penpot/penpot-plugins/commit/9a3e6e0))
|
||||||
|
- **plugin-colors-to-tokens:** only rgba when the opacity is not 1 ([e922cf9](https://github.com/penpot/penpot-plugins/commit/e922cf9))
|
||||||
|
- **plugin-types:** deprecated fields in colors ([6adcc4c](https://github.com/penpot/penpot-plugins/commit/6adcc4c))
|
||||||
|
- **plugins-runtime:** add upload svg with images ([df925b5](https://github.com/penpot/penpot-plugins/commit/df925b5))
|
||||||
|
|
||||||
|
### 🩹 Fixes
|
||||||
|
|
||||||
|
- duplicated css ([19ca648](https://github.com/penpot/penpot-plugins/commit/19ca648))
|
||||||
|
- add error styles on invalid input ([1c29c34](https://github.com/penpot/penpot-plugins/commit/1c29c34))
|
||||||
|
- remove nonexistent api ([3837f1c](https://github.com/penpot/penpot-plugins/commit/3837f1c))
|
||||||
|
|
||||||
|
### ❤️ Thank You
|
||||||
|
|
||||||
|
- alonso.torres
|
||||||
|
- Juanfran @juanfran
|
||||||
|
- Michał Korczak
|
||||||
|
|
||||||
|
## 1.1.0 (2024-12-12)
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- updated doc links ([cb49dfb](https://github.com/penpot/penpot-plugins/commit/cb49dfb))
|
||||||
|
- **plugin-types:** add support for file history versions ([eab57d7](https://github.com/penpot/penpot-plugins/commit/eab57d7))
|
||||||
|
|
||||||
|
### 🩹 Fixes
|
||||||
|
|
||||||
|
- styles rename layers ([40e08f8](https://github.com/penpot/penpot-plugins/commit/40e08f8))
|
||||||
|
- **rename-layers:** i#8951 disable buttons when empty ([#8951](https://github.com/penpot/penpot-plugins/issues/8951))
|
||||||
|
|
||||||
|
### ❤️ Thank You
|
||||||
|
|
||||||
|
- alonso.torres
|
||||||
|
- María Valderrama @mavalroot
|
||||||
|
- Marina López @cocotime
|
||||||
|
|
||||||
|
# 1.0.0 (2024-10-25)
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- **plugins-runtime:** add close callback to load api ([aeddab7](https://github.com/penpot/penpot-plugins/commit/aeddab7))
|
||||||
|
- **runtime:** unload plugin ([b4d0463](https://github.com/penpot/penpot-plugins/commit/b4d0463))
|
||||||
|
|
||||||
|
### 🩹 Fixes
|
||||||
|
|
||||||
|
- search in icons plugin ([b4664a2](https://github.com/penpot/penpot-plugins/commit/b4664a2))
|
||||||
|
- **table-plugin:** i#8965 empty cell values when importing csv files ([#8965](https://github.com/penpot/penpot-plugins/issues/8965))
|
||||||
|
|
||||||
|
### ❤️ Thank You
|
||||||
|
|
||||||
|
- alonso.torres
|
||||||
|
- Juanfran @juanfran
|
||||||
|
- María Valderrama @mavalroot
|
||||||
|
- Marina López @cocotime
|
||||||
|
|
||||||
|
## 0.12.0 (2024-10-04)
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- e2e tests ([1371af9](https://github.com/penpot/penpot-plugins/commit/1371af9))
|
||||||
|
- add build to CI ([a434209](https://github.com/penpot/penpot-plugins/commit/a434209))
|
||||||
|
- **api-doc:** update readme ([99ff81d](https://github.com/penpot/penpot-plugins/commit/99ff81d))
|
||||||
|
- **docs:** add examples for new permissions ([2f0f7a6](https://github.com/penpot/penpot-plugins/commit/2f0f7a6))
|
||||||
|
- **e2e:** add screenshots ENV variable ([9292bf2](https://github.com/penpot/penpot-plugins/commit/9292bf2))
|
||||||
|
- **plugin-types:** add ruler guides and new zoom methods ([c8066be](https://github.com/penpot/penpot-plugins/commit/c8066be))
|
||||||
|
- **plugin-types:** add apis for comments ([e34e56c](https://github.com/penpot/penpot-plugins/commit/e34e56c))
|
||||||
|
- **plugin-types:** update comment related methods ([50bc7ba](https://github.com/penpot/penpot-plugins/commit/50bc7ba))
|
||||||
|
- **plugin-types:** removed old method and replaced with attributes ([1866299](https://github.com/penpot/penpot-plugins/commit/1866299))
|
||||||
|
- **plugins-runtime:** plugin live reload ([bbc77e4](https://github.com/penpot/penpot-plugins/commit/bbc77e4))
|
||||||
|
- **plugins-runtime:** adds new permissions `comment:read`, `comment:write` and `allow:downloads` ([5adbee2](https://github.com/penpot/penpot-plugins/commit/5adbee2))
|
||||||
|
- **plugins-runtime:** expose some public JS APIs to the plugins code ([22dfa92](https://github.com/penpot/penpot-plugins/commit/22dfa92))
|
||||||
|
- **poc-state-plugin:** add new functions to the plugin to test comments and rulers ([6adee11](https://github.com/penpot/penpot-plugins/commit/6adee11))
|
||||||
|
- **rename-layers:** final review - undo group ([2909bcc](https://github.com/penpot/penpot-plugins/commit/2909bcc))
|
||||||
|
- **runtime:** refactor plugin state ([16595c2](https://github.com/penpot/penpot-plugins/commit/16595c2))
|
||||||
|
- **runtime:** remove deprecated method ([ccc5f78](https://github.com/penpot/penpot-plugins/commit/ccc5f78))
|
||||||
|
- **table-plugin:** enhancement save config ([07af57d](https://github.com/penpot/penpot-plugins/commit/07af57d))
|
||||||
|
|
||||||
|
### 🩹 Fixes
|
||||||
|
|
||||||
|
- **e2e:** update dump params to shape model ([ade39ee](https://github.com/penpot/penpot-plugins/commit/ade39ee))
|
||||||
|
- **plugin-types:** optional path curves ([0ea57f1](https://github.com/penpot/penpot-plugins/commit/0ea57f1))
|
||||||
|
- **plugins-runtime:** clean pending timeouts ([8870dda](https://github.com/penpot/penpot-plugins/commit/8870dda))
|
||||||
|
- **plugins-runtime:** prevent plugin execution after close ([b65492a](https://github.com/penpot/penpot-plugins/commit/b65492a))
|
||||||
|
- **plugins-styles:** import svg inline ([567b0b5](https://github.com/penpot/penpot-plugins/commit/567b0b5))
|
||||||
|
- **runtime:** ses errorTrapping interferes with penpot error handler ([8c0e36d](https://github.com/penpot/penpot-plugins/commit/8c0e36d))
|
||||||
|
- **runtime:** prevent override Penpot objects ([120e9e5](https://github.com/penpot/penpot-plugins/commit/120e9e5))
|
||||||
|
|
||||||
|
### ❤️ Thank You
|
||||||
|
|
||||||
|
- alonso.torres
|
||||||
|
- Juanfran @juanfran
|
||||||
|
- María Valderrama @mavalroot
|
||||||
|
|
||||||
|
## 0.10.0 (2024-07-31)
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- change permissions names ([99126f8](https://github.com/penpot/penpot-plugins/commit/99126f8))
|
||||||
|
- stop offering icons in the style library ([5a219e9](https://github.com/penpot/penpot-plugins/commit/5a219e9))
|
||||||
|
- new publish script ([5114e78](https://github.com/penpot/penpot-plugins/commit/5114e78))
|
||||||
|
- init e2e test ([b0af705](https://github.com/penpot/penpot-plugins/commit/b0af705))
|
||||||
|
- **docs:** how api docs are generated ([e047977](https://github.com/penpot/penpot-plugins/commit/e047977))
|
||||||
|
- **docs:** basic css theme for typedoc ([0eac44d](https://github.com/penpot/penpot-plugins/commit/0eac44d))
|
||||||
|
- **plugin-types:** update API types ([bffa467](https://github.com/penpot/penpot-plugins/commit/bffa467))
|
||||||
|
- **plugin-types:** add pages info to the file ([b54edb3](https://github.com/penpot/penpot-plugins/commit/b54edb3))
|
||||||
|
- **plugin-types:** add parent reference to the shape ([2588778](https://github.com/penpot/penpot-plugins/commit/2588778))
|
||||||
|
- **plugin-types:** add root shape reference to the pages ([c712759](https://github.com/penpot/penpot-plugins/commit/c712759))
|
||||||
|
- **plugin-types:** add undo block operations to api ([1d3ad89](https://github.com/penpot/penpot-plugins/commit/1d3ad89))
|
||||||
|
- **plugins-runtime:** update selection ([f36fa23](https://github.com/penpot/penpot-plugins/commit/f36fa23))
|
||||||
|
- **plugins-runtime:** add new events 'contentsave' and 'shapechange', changed on/off signatures ([2b8a76b](https://github.com/penpot/penpot-plugins/commit/2b8a76b))
|
||||||
|
- **plugins-runtime:** add detach shape from component method ([ff488d4](https://github.com/penpot/penpot-plugins/commit/ff488d4))
|
||||||
|
- **plugins-runtime:** add API to access to prototypes ([a554775](https://github.com/penpot/penpot-plugins/commit/a554775))
|
||||||
|
- **plugins-runtime:** add method for pages ([9a9b33a](https://github.com/penpot/penpot-plugins/commit/9a9b33a))
|
||||||
|
- **plugins-types:** expose new attributes ([9ce45a2](https://github.com/penpot/penpot-plugins/commit/9ce45a2))
|
||||||
|
|
||||||
|
### 🩹 Fixes
|
||||||
|
|
||||||
|
- typo checkox > checkbox ([877a3f2](https://github.com/penpot/penpot-plugins/commit/877a3f2))
|
||||||
|
- avoid plugin location question ([b4c6165](https://github.com/penpot/penpot-plugins/commit/b4c6165))
|
||||||
|
- add files so no unexpected when creating new plugin ([ef5629a](https://github.com/penpot/penpot-plugins/commit/ef5629a))
|
||||||
|
- eslint migration to ESM docs ([249ea62](https://github.com/penpot/penpot-plugins/commit/249ea62))
|
||||||
|
- fix runtime version ([95afbf3](https://github.com/penpot/penpot-plugins/commit/95afbf3))
|
||||||
|
- horizontal scroll height on plugins modal ([08f989a](https://github.com/penpot/penpot-plugins/commit/08f989a))
|
||||||
|
- **contrast-plugin:** update colors when shape change ([8ce04d3](https://github.com/penpot/penpot-plugins/commit/8ce04d3))
|
||||||
|
- **docs:** add missing variant on destructive button ([9fa96e9](https://github.com/penpot/penpot-plugins/commit/9fa96e9))
|
||||||
|
- **plugin-types:** readonly PenpotShapeBase width & height ([415284f](https://github.com/penpot/penpot-plugins/commit/415284f))
|
||||||
|
- **plugins-runtime:** remove plugin event listener on close ([2138985](https://github.com/penpot/penpot-plugins/commit/2138985))
|
||||||
|
- **plugins-runtime:** fix problem with types in test ([17db173](https://github.com/penpot/penpot-plugins/commit/17db173))
|
||||||
|
- **styles:** input, button & select worksans font family ([1b9d3b2](https://github.com/penpot/penpot-plugins/commit/1b9d3b2))
|
||||||
|
|
||||||
|
### ❤️ Thank You
|
||||||
|
|
||||||
|
- alonso.torres
|
||||||
|
- Juanfran @juanfran
|
||||||
|
- María Valderrama @mavalroot
|
||||||
|
- Marina López @cocotime
|
||||||
|
- Xaviju
|
||||||
|
|
||||||
|
## 0.9.0 (2024-07-10)
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- change permissions names ([99126f8](https://github.com/penpot/penpot-plugins/commit/99126f8))
|
||||||
|
- stop offering icons in the style library ([5a219e9](https://github.com/penpot/penpot-plugins/commit/5a219e9))
|
||||||
|
- new publish script ([5114e78](https://github.com/penpot/penpot-plugins/commit/5114e78))
|
||||||
|
- **plugin-types:** update API types ([bffa467](https://github.com/penpot/penpot-plugins/commit/bffa467))
|
||||||
|
- **plugins-runtime:** update selection ([f36fa23](https://github.com/penpot/penpot-plugins/commit/f36fa23))
|
||||||
|
- **plugins-types:** expose new attributes ([9ce45a2](https://github.com/penpot/penpot-plugins/commit/9ce45a2))
|
||||||
|
|
||||||
|
### 🩹 Fixes
|
||||||
|
|
||||||
|
- typo checkox > checkbox ([877a3f2](https://github.com/penpot/penpot-plugins/commit/877a3f2))
|
||||||
|
- avoid plugin location question ([b4c6165](https://github.com/penpot/penpot-plugins/commit/b4c6165))
|
||||||
|
- fix runtime version ([2401a77](https://github.com/penpot/penpot-plugins/commit/2401a77))
|
||||||
|
- **styles:** input, button & select worksans font family ([1b9d3b2](https://github.com/penpot/penpot-plugins/commit/1b9d3b2))
|
||||||
|
|
||||||
|
### ❤️ Thank You
|
||||||
|
|
||||||
|
- alonso.torres
|
||||||
|
- Juanfran @juanfran
|
||||||
|
- Marina López @cocotime
|
||||||
|
- Xaviju @xaviju
|
||||||
|
|
@ -0,0 +1,134 @@
|
||||||
|
# Contributing Guide
|
||||||
|
|
||||||
|
Thank you for your interest in contributing to Penpot Plugins. This is a
|
||||||
|
generic guide that details how to contribute to Penpot Plugins in a way that
|
||||||
|
is efficient for everyone. If you want a specific documentation for
|
||||||
|
different parts of the platform, please refer to `docs/` directory.
|
||||||
|
|
||||||
|
## Reporting Bugs
|
||||||
|
|
||||||
|
We are using [GitHub Issues](https://github.com/penpot/penpot-plugins/issues)
|
||||||
|
for our public bugs. We keep a close eye on this and try to make it
|
||||||
|
clear when we have an internal fix in progress. Before filing a new
|
||||||
|
task, try to make sure your problem doesn't already exist.
|
||||||
|
|
||||||
|
If you found a bug, please report it, as far as possible with:
|
||||||
|
|
||||||
|
- a detailed explanation of steps to reproduce the error
|
||||||
|
- a browser and the browser version used
|
||||||
|
- a dev tools console exception stack trace (if it is available)
|
||||||
|
|
||||||
|
If you found a bug that you consider better discuss in private (for
|
||||||
|
example: security bugs), consider first send an email to
|
||||||
|
`support@penpot.app`.
|
||||||
|
|
||||||
|
**We don't have formal bug bounty program for security reports; this
|
||||||
|
is an open source application and your contribution will be recognized
|
||||||
|
in the changelog.**
|
||||||
|
|
||||||
|
## Pull requests
|
||||||
|
|
||||||
|
If you want propose a change or bug fix with the Pull-Request system
|
||||||
|
firstly you should carefully read the **DCO** section and format your
|
||||||
|
commits accordingly.
|
||||||
|
|
||||||
|
If you intend to fix a bug it's fine to submit a pull request right
|
||||||
|
away but we still recommend to file an issue detailing what you're
|
||||||
|
fixing. This is helpful in case we don't accept that specific fix but
|
||||||
|
want to keep track of the issue.
|
||||||
|
|
||||||
|
If you want to implement or start working in a new feature, please
|
||||||
|
open a **question** / **discussion** issue for it. No pull-request
|
||||||
|
will be accepted without previous chat about the changes,
|
||||||
|
independently if it is a new feature, already planned feature or small
|
||||||
|
quick win.
|
||||||
|
|
||||||
|
If is going to be your first pull request, You can learn how from this
|
||||||
|
free video series:
|
||||||
|
|
||||||
|
https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github
|
||||||
|
|
||||||
|
We will use the `easy fix` mark for tag for indicate issues that are
|
||||||
|
easy for beginners.
|
||||||
|
|
||||||
|
## Commit Guidelines
|
||||||
|
|
||||||
|
To maintain a clear and organized commit history in this repository, we adhere to the Conventional Commits specification. Conventional Commits provide a structured format for commit messages, making it easier to track changes, automate versioning, and improve readability.
|
||||||
|
|
||||||
|
Please familiarize yourself with the Conventional Commits rules by visiting the [official Conventional Commits website](https://www.conventionalcommits.org/en/v1.0.0/). This specification outlines how to structure your commit messages, including types, scopes, and descriptions.
|
||||||
|
|
||||||
|
## Code of conduct
|
||||||
|
|
||||||
|
As contributors and maintainers of this project, we pledge to respect
|
||||||
|
all people who contribute through reporting issues, posting feature
|
||||||
|
requests, updating documentation, submitting pull requests or patches,
|
||||||
|
and other activities.
|
||||||
|
|
||||||
|
We are committed to making participation in this project a
|
||||||
|
harassment-free experience for everyone, regardless of level of
|
||||||
|
experience, gender, gender identity and expression, sexual
|
||||||
|
orientation, disability, personal appearance, body size, race,
|
||||||
|
ethnicity, age, or religion.
|
||||||
|
|
||||||
|
Examples of unacceptable behavior by participants include the use of
|
||||||
|
sexual language or imagery, derogatory comments or personal attacks,
|
||||||
|
trolling, public or private harassment, insults, or other
|
||||||
|
unprofessional conduct.
|
||||||
|
|
||||||
|
Project maintainers have the right and responsibility to remove, edit,
|
||||||
|
or reject comments, commits, code, wiki edits, issues, and other
|
||||||
|
contributions that are not aligned to this Code of Conduct. Project
|
||||||
|
maintainers who do not follow the Code of Conduct may be removed from
|
||||||
|
the project team.
|
||||||
|
|
||||||
|
This code of conduct applies both within project spaces and in public
|
||||||
|
spaces when an individual is representing the project or its
|
||||||
|
community.
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior
|
||||||
|
may be reported by opening an issue or contacting one or more of the
|
||||||
|
project maintainers.
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the Contributor Covenant, version
|
||||||
|
1.1.0, available from http://contributor-covenant.org/version/1/1/0/
|
||||||
|
|
||||||
|
## Developer's Certificate of Origin (DCO)
|
||||||
|
|
||||||
|
By submitting code you are agree and can certify the below:
|
||||||
|
|
||||||
|
Developer's Certificate of Origin 1.1
|
||||||
|
|
||||||
|
By making a contribution to this project, I certify that:
|
||||||
|
|
||||||
|
(a) The contribution was created in whole or in part by me and I
|
||||||
|
have the right to submit it under the open source license
|
||||||
|
indicated in the file; or
|
||||||
|
|
||||||
|
(b) The contribution is based upon previous work that, to the best
|
||||||
|
of my knowledge, is covered under an appropriate open source
|
||||||
|
license and I have the right under that license to submit that
|
||||||
|
work with modifications, whether created in whole or in part
|
||||||
|
by me, under the same open source license (unless I am
|
||||||
|
permitted to submit under a different license), as indicated
|
||||||
|
in the file; or
|
||||||
|
|
||||||
|
(c) The contribution was provided directly to me by some other
|
||||||
|
person who certified (a), (b) or (c) and I have not modified
|
||||||
|
it.
|
||||||
|
|
||||||
|
(d) I understand and agree that this project and the contribution
|
||||||
|
are public and that a record of the contribution (including all
|
||||||
|
personal information I submit with it, including my sign-off) is
|
||||||
|
maintained indefinitely and may be redistributed consistent with
|
||||||
|
this project or the open source license(s) involved.
|
||||||
|
|
||||||
|
Then, all your code patches (**documentation are excluded**) should
|
||||||
|
contain a sign-off at the end of the patch/commit description body. It
|
||||||
|
can be automatically added on adding `-s` parameter to `git commit`.
|
||||||
|
|
||||||
|
This is an example of the aspect of the line:
|
||||||
|
|
||||||
|
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
|
||||||
|
|
||||||
|
Please, use your real name (sorry, no pseudonyms or anonymous
|
||||||
|
contributions are allowed).
|
||||||
|
|
@ -0,0 +1,382 @@
|
||||||
|
# Mozilla Public License Version 2.0
|
||||||
|
|
||||||
|
1. Definitions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
1.1. "Contributor"
|
||||||
|
means each individual or legal entity that creates, contributes to
|
||||||
|
the creation of, or owns Covered Software.
|
||||||
|
|
||||||
|
1.2. "Contributor Version"
|
||||||
|
means the combination of the Contributions of others (if any) used
|
||||||
|
by a Contributor and that particular Contributor's Contribution.
|
||||||
|
|
||||||
|
1.3. "Contribution"
|
||||||
|
means Covered Software of a particular Contributor.
|
||||||
|
|
||||||
|
1.4. "Covered Software"
|
||||||
|
means Source Code Form to which the initial Contributor has attached
|
||||||
|
the notice in Exhibit A, the Executable Form of such Source Code
|
||||||
|
Form, and Modifications of such Source Code Form, in each case
|
||||||
|
including portions thereof.
|
||||||
|
|
||||||
|
1.5. "Incompatible With Secondary Licenses"
|
||||||
|
means
|
||||||
|
|
||||||
|
(a) that the initial Contributor has attached the notice described
|
||||||
|
in Exhibit B to the Covered Software; or
|
||||||
|
|
||||||
|
(b) that the Covered Software was made available under the terms of
|
||||||
|
version 1.1 or earlier of the License, but not also under the
|
||||||
|
terms of a Secondary License.
|
||||||
|
|
||||||
|
1.6. "Executable Form"
|
||||||
|
means any form of the work other than Source Code Form.
|
||||||
|
|
||||||
|
1.7. "Larger Work"
|
||||||
|
means a work that combines Covered Software with other material, in
|
||||||
|
a separate file or files, that is not Covered Software.
|
||||||
|
|
||||||
|
1.8. "License"
|
||||||
|
means this document.
|
||||||
|
|
||||||
|
1.9. "Licensable"
|
||||||
|
means having the right to grant, to the maximum extent possible,
|
||||||
|
whether at the time of the initial grant or subsequently, any and
|
||||||
|
all of the rights conveyed by this License.
|
||||||
|
|
||||||
|
1.10. "Modifications"
|
||||||
|
means any of the following:
|
||||||
|
|
||||||
|
(a) any file in Source Code Form that results from an addition to,
|
||||||
|
deletion from, or modification of the contents of Covered
|
||||||
|
Software; or
|
||||||
|
|
||||||
|
(b) any new file in Source Code Form that contains any Covered
|
||||||
|
Software.
|
||||||
|
|
||||||
|
1.11. "Patent Claims" of a Contributor
|
||||||
|
means any patent claim(s), including without limitation, method,
|
||||||
|
process, and apparatus claims, in any patent Licensable by such
|
||||||
|
Contributor that would be infringed, but for the grant of the
|
||||||
|
License, by the making, using, selling, offering for sale, having
|
||||||
|
made, import, or transfer of either its Contributions or its
|
||||||
|
Contributor Version.
|
||||||
|
|
||||||
|
1.12. "Secondary License"
|
||||||
|
means either the GNU General Public License, Version 2.0, the GNU
|
||||||
|
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||||
|
Public License, Version 3.0, or any later versions of those
|
||||||
|
licenses.
|
||||||
|
|
||||||
|
1.13. "Source Code Form"
|
||||||
|
means the form of the work preferred for making modifications.
|
||||||
|
|
||||||
|
1.14. "You" (or "Your")
|
||||||
|
means an individual or a legal entity exercising rights under this
|
||||||
|
License. For legal entities, "You" includes any entity that
|
||||||
|
controls, is controlled by, or is under common control with You. For
|
||||||
|
purposes of this definition, "control" means (a) the power, direct
|
||||||
|
or indirect, to cause the direction or management of such entity,
|
||||||
|
whether by contract or otherwise, or (b) ownership of more than
|
||||||
|
fifty percent (50%) of the outstanding shares or beneficial
|
||||||
|
ownership of such entity.
|
||||||
|
|
||||||
|
2. License Grants and Conditions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
2.1. Grants
|
||||||
|
|
||||||
|
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||||
|
non-exclusive license:
|
||||||
|
|
||||||
|
(a) under intellectual property rights (other than patent or trademark)
|
||||||
|
Licensable by such Contributor to use, reproduce, make available,
|
||||||
|
modify, display, perform, distribute, and otherwise exploit its
|
||||||
|
Contributions, either on an unmodified basis, with Modifications, or
|
||||||
|
as part of a Larger Work; and
|
||||||
|
|
||||||
|
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||||
|
for sale, have made, import, and otherwise transfer either its
|
||||||
|
Contributions or its Contributor Version.
|
||||||
|
|
||||||
|
2.2. Effective Date
|
||||||
|
|
||||||
|
The licenses granted in Section 2.1 with respect to any Contribution
|
||||||
|
become effective for each Contribution on the date the Contributor first
|
||||||
|
distributes such Contribution.
|
||||||
|
|
||||||
|
2.3. Limitations on Grant Scope
|
||||||
|
|
||||||
|
The licenses granted in this Section 2 are the only rights granted under
|
||||||
|
this License. No additional rights or licenses will be implied from the
|
||||||
|
distribution or licensing of Covered Software under this License.
|
||||||
|
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||||
|
Contributor:
|
||||||
|
|
||||||
|
(a) for any code that a Contributor has removed from Covered Software;
|
||||||
|
or
|
||||||
|
|
||||||
|
(b) for infringements caused by: (i) Your and any other third party's
|
||||||
|
modifications of Covered Software, or (ii) the combination of its
|
||||||
|
Contributions with other software (except as part of its Contributor
|
||||||
|
Version); or
|
||||||
|
|
||||||
|
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||||
|
its Contributions.
|
||||||
|
|
||||||
|
This License does not grant any rights in the trademarks, service marks,
|
||||||
|
or logos of any Contributor (except as may be necessary to comply with
|
||||||
|
the notice requirements in Section 3.4).
|
||||||
|
|
||||||
|
2.4. Subsequent Licenses
|
||||||
|
|
||||||
|
No Contributor makes additional grants as a result of Your choice to
|
||||||
|
distribute the Covered Software under a subsequent version of this
|
||||||
|
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||||
|
permitted under the terms of Section 3.3).
|
||||||
|
|
||||||
|
2.5. Representation
|
||||||
|
|
||||||
|
Each Contributor represents that the Contributor believes its
|
||||||
|
Contributions are its original creation(s) or it has sufficient rights
|
||||||
|
to grant the rights to its Contributions conveyed by this License.
|
||||||
|
|
||||||
|
2.6. Fair Use
|
||||||
|
|
||||||
|
This License is not intended to limit any rights You have under
|
||||||
|
applicable copyright doctrines of fair use, fair dealing, or other
|
||||||
|
equivalents.
|
||||||
|
|
||||||
|
2.7. Conditions
|
||||||
|
|
||||||
|
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||||
|
in Section 2.1.
|
||||||
|
|
||||||
|
3. Responsibilities
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
3.1. Distribution of Source Form
|
||||||
|
|
||||||
|
All distribution of Covered Software in Source Code Form, including any
|
||||||
|
Modifications that You create or to which You contribute, must be under
|
||||||
|
the terms of this License. You must inform recipients that the Source
|
||||||
|
Code Form of the Covered Software is governed by the terms of this
|
||||||
|
License, and how they can obtain a copy of this License. You may not
|
||||||
|
attempt to alter or restrict the recipients' rights in the Source Code
|
||||||
|
Form.
|
||||||
|
|
||||||
|
3.2. Distribution of Executable Form
|
||||||
|
|
||||||
|
If You distribute Covered Software in Executable Form then:
|
||||||
|
|
||||||
|
(a) such Covered Software must also be made available in Source Code
|
||||||
|
Form, as described in Section 3.1, and You must inform recipients of
|
||||||
|
the Executable Form how they can obtain a copy of such Source Code
|
||||||
|
Form by reasonable means in a timely manner, at a charge no more
|
||||||
|
than the cost of distribution to the recipient; and
|
||||||
|
|
||||||
|
(b) You may distribute such Executable Form under the terms of this
|
||||||
|
License, or sublicense it under different terms, provided that the
|
||||||
|
license for the Executable Form does not attempt to limit or alter
|
||||||
|
the recipients' rights in the Source Code Form under this License.
|
||||||
|
|
||||||
|
3.3. Distribution of a Larger Work
|
||||||
|
|
||||||
|
You may create and distribute a Larger Work under terms of Your choice,
|
||||||
|
provided that You also comply with the requirements of this License for
|
||||||
|
the Covered Software. If the Larger Work is a combination of Covered
|
||||||
|
Software with a work governed by one or more Secondary Licenses, and the
|
||||||
|
Covered Software is not Incompatible With Secondary Licenses, this
|
||||||
|
License permits You to additionally distribute such Covered Software
|
||||||
|
under the terms of such Secondary License(s), so that the recipient of
|
||||||
|
the Larger Work may, at their option, further distribute the Covered
|
||||||
|
Software under the terms of either this License or such Secondary
|
||||||
|
License(s).
|
||||||
|
|
||||||
|
3.4. Notices
|
||||||
|
|
||||||
|
You may not remove or alter the substance of any license notices
|
||||||
|
(including copyright notices, patent notices, disclaimers of warranty,
|
||||||
|
or limitations of liability) contained within the Source Code Form of
|
||||||
|
the Covered Software, except that You may alter any license notices to
|
||||||
|
the extent required to remedy known factual inaccuracies.
|
||||||
|
|
||||||
|
3.5. Application of Additional Terms
|
||||||
|
|
||||||
|
You may choose to offer, and to charge a fee for, warranty, support,
|
||||||
|
indemnity or liability obligations to one or more recipients of Covered
|
||||||
|
Software. However, You may do so only on Your own behalf, and not on
|
||||||
|
behalf of any Contributor. You must make it absolutely clear that any
|
||||||
|
such warranty, support, indemnity, or liability obligation is offered by
|
||||||
|
You alone, and You hereby agree to indemnify every Contributor for any
|
||||||
|
liability incurred by such Contributor as a result of warranty, support,
|
||||||
|
indemnity or liability terms You offer. You may include additional
|
||||||
|
disclaimers of warranty and limitations of liability specific to any
|
||||||
|
jurisdiction.
|
||||||
|
|
||||||
|
4. Inability to Comply Due to Statute or Regulation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
If it is impossible for You to comply with any of the terms of this
|
||||||
|
License with respect to some or all of the Covered Software due to
|
||||||
|
statute, judicial order, or regulation then You must: (a) comply with
|
||||||
|
the terms of this License to the maximum extent possible; and (b)
|
||||||
|
describe the limitations and the code they affect. Such description must
|
||||||
|
be placed in a text file included with all distributions of the Covered
|
||||||
|
Software under this License. Except to the extent prohibited by statute
|
||||||
|
or regulation, such description must be sufficiently detailed for a
|
||||||
|
recipient of ordinary skill to be able to understand it.
|
||||||
|
|
||||||
|
5. Termination
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
5.1. The rights granted under this License will terminate automatically
|
||||||
|
if You fail to comply with any of its terms. However, if You become
|
||||||
|
compliant, then the rights granted under this License from a particular
|
||||||
|
Contributor are reinstated (a) provisionally, unless and until such
|
||||||
|
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||||
|
ongoing basis, if such Contributor fails to notify You of the
|
||||||
|
non-compliance by some reasonable means prior to 60 days after You have
|
||||||
|
come back into compliance. Moreover, Your grants from a particular
|
||||||
|
Contributor are reinstated on an ongoing basis if such Contributor
|
||||||
|
notifies You of the non-compliance by some reasonable means, this is the
|
||||||
|
first time You have received notice of non-compliance with this License
|
||||||
|
from such Contributor, and You become compliant prior to 30 days after
|
||||||
|
Your receipt of the notice.
|
||||||
|
|
||||||
|
5.2. If You initiate litigation against any entity by asserting a patent
|
||||||
|
infringement claim (excluding declaratory judgment actions,
|
||||||
|
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||||
|
directly or indirectly infringes any patent, then the rights granted to
|
||||||
|
You by any and all Contributors for the Covered Software under Section
|
||||||
|
2.1 of this License shall terminate.
|
||||||
|
|
||||||
|
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||||
|
end user license agreements (excluding distributors and resellers) which
|
||||||
|
have been validly granted by You or Your distributors under this License
|
||||||
|
prior to termination shall survive termination.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
- *
|
||||||
|
- 6. Disclaimer of Warranty \*
|
||||||
|
- ------------------------- \*
|
||||||
|
- *
|
||||||
|
- Covered Software is provided under this License on an "as is" \*
|
||||||
|
- basis, without warranty of any kind, either expressed, implied, or \*
|
||||||
|
- statutory, including, without limitation, warranties that the \*
|
||||||
|
- Covered Software is free of defects, merchantable, fit for a \*
|
||||||
|
- particular purpose or non-infringing. The entire risk as to the \*
|
||||||
|
- quality and performance of the Covered Software is with You. \*
|
||||||
|
- Should any Covered Software prove defective in any respect, You \*
|
||||||
|
- (not any Contributor) assume the cost of any necessary servicing, \*
|
||||||
|
- repair, or correction. This disclaimer of warranty constitutes an \*
|
||||||
|
- essential part of this License. No use of any Covered Software is \*
|
||||||
|
- authorized under this License except under this disclaimer. \*
|
||||||
|
- *
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
- *
|
||||||
|
- 7. Limitation of Liability \*
|
||||||
|
- -------------------------- \*
|
||||||
|
- *
|
||||||
|
- Under no circumstances and under no legal theory, whether tort \*
|
||||||
|
- (including negligence), contract, or otherwise, shall any \*
|
||||||
|
- Contributor, or anyone who distributes Covered Software as \*
|
||||||
|
- permitted above, be liable to You for any direct, indirect, \*
|
||||||
|
- special, incidental, or consequential damages of any character \*
|
||||||
|
- including, without limitation, damages for lost profits, loss of \*
|
||||||
|
- goodwill, work stoppage, computer failure or malfunction, or any \*
|
||||||
|
- and all other commercial damages or losses, even if such party \*
|
||||||
|
- shall have been informed of the possibility of such damages. This \*
|
||||||
|
- limitation of liability shall not apply to liability for death or \*
|
||||||
|
- personal injury resulting from such party's negligence to the \*
|
||||||
|
- extent applicable law prohibits such limitation. Some \*
|
||||||
|
- jurisdictions do not allow the exclusion or limitation of \*
|
||||||
|
- incidental or consequential damages, so this exclusion and \*
|
||||||
|
- limitation may not apply to You. \*
|
||||||
|
- *
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
8. Litigation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Any litigation relating to this License may be brought only in the
|
||||||
|
courts of a jurisdiction where the defendant maintains its principal
|
||||||
|
place of business and such litigation shall be governed by laws of that
|
||||||
|
jurisdiction, without reference to its conflict-of-law provisions.
|
||||||
|
Nothing in this Section shall prevent a party's ability to bring
|
||||||
|
cross-claims or counter-claims.
|
||||||
|
|
||||||
|
9. Miscellaneous
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
This License represents the complete agreement concerning the subject
|
||||||
|
matter hereof. If any provision of this License is held to be
|
||||||
|
unenforceable, such provision shall be reformed only to the extent
|
||||||
|
necessary to make it enforceable. Any law or regulation which provides
|
||||||
|
that the language of a contract shall be construed against the drafter
|
||||||
|
shall not be used to construe this License against a Contributor.
|
||||||
|
|
||||||
|
10. Versions of the License
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
10.1. New Versions
|
||||||
|
|
||||||
|
Mozilla Foundation is the license steward. Except as provided in Section
|
||||||
|
10.3, no one other than the license steward has the right to modify or
|
||||||
|
publish new versions of this License. Each version will be given a
|
||||||
|
distinguishing version number.
|
||||||
|
|
||||||
|
10.2. Effect of New Versions
|
||||||
|
|
||||||
|
You may distribute the Covered Software under the terms of the version
|
||||||
|
of the License under which You originally received the Covered Software,
|
||||||
|
or under the terms of any subsequent version published by the license
|
||||||
|
steward.
|
||||||
|
|
||||||
|
10.3. Modified Versions
|
||||||
|
|
||||||
|
If you create software not governed by this License, and you want to
|
||||||
|
create a new license for such software, you may create and use a
|
||||||
|
modified version of this License if you rename the license and remove
|
||||||
|
any references to the name of the license steward (except to note that
|
||||||
|
such modified license differs from this License).
|
||||||
|
|
||||||
|
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||||
|
Licenses
|
||||||
|
|
||||||
|
If You choose to distribute Source Code Form that is Incompatible With
|
||||||
|
Secondary Licenses under the terms of this version of the License, the
|
||||||
|
notice described in Exhibit B of this License must be attached.
|
||||||
|
|
||||||
|
## Exhibit A - Source Code Form License Notice
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
If it is not possible or desirable to put the notice in a particular
|
||||||
|
file, then You may include the notice in a location (such as a LICENSE
|
||||||
|
file in a relevant directory) where a recipient would be likely to look
|
||||||
|
for such a notice.
|
||||||
|
|
||||||
|
You may add additional accurate notices of copyright ownership.
|
||||||
|
|
||||||
|
## Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||||
|
|
||||||
|
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
|
defined by the Mozilla Public License, v. 2.0.
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
# Penpot Plugins
|
||||||
|
|
||||||
|
## What can you find here?
|
||||||
|
|
||||||
|
We've been working in an MVP to allow users to develop their own plugins and use the existing ones.
|
||||||
|
|
||||||
|
There are 2 important folders to keep an eye on: `apps` and `libs`.
|
||||||
|
|
||||||
|
In the `libs` folder you'll find:
|
||||||
|
|
||||||
|
- plugins-runtime: here you'll find the code that initializes the plugin and sets a few listeners to know when the penpot page/file/selection changes.
|
||||||
|
It has its own [README](libs/plugins-runtime/README.md).
|
||||||
|
- plugins-styles: basic css library with penpot styles in case you need help for styling your plugins.
|
||||||
|
|
||||||
|
In the `apps` folder you'll find some examples that use the libraries mentioned above.
|
||||||
|
|
||||||
|
- contrast-plugin: to run this example check <a href="#create-a-plugin-from-scratch-or-run-the-examples-from-the-apps-folder">Create a plugin from scratch</a>
|
||||||
|
|
||||||
|
- example-styles: to run this example you should run
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run start:styles-example
|
||||||
|
```
|
||||||
|
|
||||||
|
Open in your browser: `http://localhost:4202/`
|
||||||
|
|
||||||
|
## Run Penpot sample plugins
|
||||||
|
|
||||||
|
This guide will help you launch a Penpot plugin from the penpot-plugins repository. Before proceeding, ensure that you have Penpot running locally by following the [setup instructions](https://help.penpot.app/technical-guide/developer/devenv/).
|
||||||
|
|
||||||
|
In the terminal, navigate to the **penpot-plugins** repository and run `npm install` to install the required dependencies.
|
||||||
|
Then, run `npm start` to launch the plugins wrapper.
|
||||||
|
|
||||||
|
After installing the dependencies, choose a plugin to launch. You can either run one of the provided examples or create your own (see "Creating a plugin from scratch" below).
|
||||||
|
To launch a plugin, Open a new terminal tab and run the appropriate startup script for the chosen plugin.
|
||||||
|
|
||||||
|
For instance, to launch the Contrast plugin, use the following command:
|
||||||
|
|
||||||
|
```
|
||||||
|
// for the contrast plugin
|
||||||
|
npm run start:plugin:contrast
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, open in your browser the specific port. In this specific example would be `http://localhost:4302`
|
||||||
|
|
||||||
|
A table listing the available plugins and their corresponding startup commands is provided below.
|
||||||
|
|
||||||
|
## Sample plugins
|
||||||
|
|
||||||
|
| Plugin | Description | PORT | Start command | Manifest URL |
|
||||||
|
| ----------------------- | ----------------------------------------------------------- | ---- | ------------------------------------- | ------------------------------------------ |
|
||||||
|
| poc-state-plugin | Sandbox plugin to test new plugins api functionality | 4301 | npm run start:plugin:poc-state | http://localhost:4301/assets/manifest.json |
|
||||||
|
| contrast-plugin | Sample plugin that gives you color contrast information | 4302 | npm run start:plugin:contrast | http://localhost:4302/assets/manifest.json |
|
||||||
|
| icons-plugin | Tool to add icons from [Feather](https://feathericons.com/) | 4303 | npm run start:plugin:icons | http://localhost:4303/assets/manifest.json |
|
||||||
|
| lorem-ipsum-plugin | Generate Lorem ipsum text | 4304 | npm run start:plugin:loremipsum | http://localhost:4304/assets/manifest.json |
|
||||||
|
| create-palette-plugin | Creates a board with all the palette colors | 4305 | npm run start:plugin:palette | http://localhost:4305/assets/manifest.json |
|
||||||
|
| table-plugin | Create or import table | 4306 | npm run start:table-plugin | http://localhost:4306/assets/manifest.json |
|
||||||
|
| rename-layers-plugin | Rename layers in bulk | 4307 | npm run start:plugin:renamelayers | http://localhost:4307/assets/manifest.json |
|
||||||
|
| colors-to-tokens-plugin | Generate tokens JSON file | 4308 | npm run start:plugin:colors-to-tokens | http://localhost:4308/assets/manifest.json |
|
||||||
|
|
||||||
|
## Web Apps
|
||||||
|
|
||||||
|
| App | Description | PORT | Start command | URL |
|
||||||
|
| --------------- | ----------------------------------------------------------------- | ---- | -------------------------------- | ---------------------- |
|
||||||
|
| plugins-runtime | Runtime for the plugins subsystem | 4200 | npm run start:app:runtime | |
|
||||||
|
| example-styles | Showcase of some of the Penpot styles that can be used in plugins | 4201 | npm run start:app:styles-example | http://localhost:4201/ |
|
||||||
|
|
||||||
|
## Creating a plugin from scratch
|
||||||
|
|
||||||
|
If you want to create a new plugin, read the following [README](docs/create-plugin.md)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
```
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
Copyright (c) KALEIDOS INC
|
||||||
|
```
|
||||||
|
|
||||||
|
Penpot is a Kaleidos’ [open source project](https://kaleidos.net/)
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
import baseConfig from '../../eslint.config.js';
|
||||||
|
import { compat } from '../../eslint.base.config.js';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
...baseConfig,
|
||||||
|
...compat
|
||||||
|
.config({
|
||||||
|
extends: [
|
||||||
|
'plugin:@nx/angular',
|
||||||
|
'plugin:@angular-eslint/template/process-inline-templates',
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.map((config) => ({
|
||||||
|
...config,
|
||||||
|
files: ['**/*.ts'],
|
||||||
|
rules: {
|
||||||
|
'@angular-eslint/directive-selector': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
type: 'attribute',
|
||||||
|
prefix: 'app',
|
||||||
|
style: 'camelCase',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'@angular-eslint/component-selector': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
type: 'element',
|
||||||
|
prefix: 'app',
|
||||||
|
style: 'kebab-case',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
...compat
|
||||||
|
.config({ extends: ['plugin:@nx/angular-template'] })
|
||||||
|
.map((config) => ({
|
||||||
|
...config,
|
||||||
|
files: ['**/*.html'],
|
||||||
|
rules: {},
|
||||||
|
})),
|
||||||
|
{ ignores: ['**/assets/*.js'] },
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
project: './tsconfig.*?.json',
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
{
|
||||||
|
"name": "colors-to-tokens-plugin",
|
||||||
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||||
|
"projectType": "application",
|
||||||
|
"prefix": "app",
|
||||||
|
"sourceRoot": "apps/colors-to-tokens-plugin/src",
|
||||||
|
"tags": ["type:plugin"],
|
||||||
|
"targets": {
|
||||||
|
"build": {
|
||||||
|
"executor": "@angular-devkit/build-angular:application",
|
||||||
|
"outputs": ["{options.outputPath}"],
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist/apps/colors-to-tokens-plugin",
|
||||||
|
"index": "apps/colors-to-tokens-plugin/src/index.html",
|
||||||
|
"browser": "apps/colors-to-tokens-plugin/src/main.ts",
|
||||||
|
"polyfills": ["zone.js"],
|
||||||
|
"tsConfig": "apps/colors-to-tokens-plugin/tsconfig.app.json",
|
||||||
|
"assets": [
|
||||||
|
"apps/colors-to-tokens-plugin/src/favicon.ico",
|
||||||
|
"apps/colors-to-tokens-plugin/src/assets"
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"libs/plugins-styles/src/lib/styles.css",
|
||||||
|
"apps/colors-to-tokens-plugin/src/styles.css"
|
||||||
|
],
|
||||||
|
"scripts": [],
|
||||||
|
"optimization": {
|
||||||
|
"scripts": true,
|
||||||
|
"styles": true,
|
||||||
|
"fonts": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "500kb",
|
||||||
|
"maximumError": "1mb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "2kb",
|
||||||
|
"maximumError": "4kb"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputHashing": "all"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"optimization": false,
|
||||||
|
"extractLicenses": false,
|
||||||
|
"sourceMap": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "production",
|
||||||
|
"dependsOn": ["buildPlugin"]
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"executor": "@angular-devkit/build-angular:dev-server",
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"buildTarget": "colors-to-tokens-plugin:build:production"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"buildTarget": "colors-to-tokens-plugin:build:development",
|
||||||
|
"host": "0.0.0.0",
|
||||||
|
"port": 4308
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "development"
|
||||||
|
},
|
||||||
|
"extract-i18n": {
|
||||||
|
"executor": "@angular-devkit/build-angular:extract-i18n",
|
||||||
|
"options": {
|
||||||
|
"buildTarget": "colors-to-tokens-plugin:build"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-24);
|
||||||
|
padding-top: var(--spacing-36);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: var(--foreground-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
padding-bottom: var(--spacing-4);
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--accent-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title,
|
||||||
|
.description {
|
||||||
|
text-wrap: pretty;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-8);
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-btn {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-4);
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
app-svg {
|
||||||
|
--svg-stroke-color: var(--background-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.restart-btn {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-4);
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
app-svg {
|
||||||
|
--svg-stroke-color: var(--foreground-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
app-svg {
|
||||||
|
--svg-stroke-color: var(--accent-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Override default button appearance */
|
||||||
|
.download-btn[data-appearance='primary']:is(button):disabled {
|
||||||
|
color: var(--background-secondary);
|
||||||
|
background-color: var(--accent-primary-muted);
|
||||||
|
border: 2px solid var(--accent-primary-muted);
|
||||||
|
|
||||||
|
app-svg {
|
||||||
|
--svg-stroke-color: var(--background-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.success {
|
||||||
|
display: flex;
|
||||||
|
background-color: var(--success-950);
|
||||||
|
border-radius: var(--spacing-8);
|
||||||
|
border: 1px solid var(--success-500);
|
||||||
|
color: var(--app-white);
|
||||||
|
gap: var(--spacing-8);
|
||||||
|
padding: var(--spacing-8);
|
||||||
|
|
||||||
|
app-svg {
|
||||||
|
--svg-stroke-color: var(--success-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-note {
|
||||||
|
padding: 0 var(--spacing-8);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,183 @@
|
||||||
|
import { Component, effect, inject, linkedSignal } from '@angular/core';
|
||||||
|
import { toSignal } from '@angular/core/rxjs-interop';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import type {
|
||||||
|
PluginMessageEvent,
|
||||||
|
PluginUIEvent,
|
||||||
|
ThemePluginEvent,
|
||||||
|
SetColorsPluginEvent,
|
||||||
|
TokenFileExtraData,
|
||||||
|
} from '../model';
|
||||||
|
import { filter, fromEvent, map, merge, take } from 'rxjs';
|
||||||
|
import { transformToToken } from './utils/transform-to-token';
|
||||||
|
import { SvgComponent } from './components/svg.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
imports: [SvgComponent],
|
||||||
|
template: `
|
||||||
|
<h1 class="title title-m">Convert your colors assets to Design Tokens</h1>
|
||||||
|
<p class="description body-m">
|
||||||
|
A Penpot plugin to generate a JSON file with your color styles in a
|
||||||
|
<a target="_blank" href="https://tr.designtokens.org/format/"
|
||||||
|
>Design Token Standard format</a
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
@if (result()) {
|
||||||
|
<div class="success body-s">
|
||||||
|
<app-svg name="tick" />
|
||||||
|
Colors convertered to tokens successfully!
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="actions">
|
||||||
|
@if (result()) {
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-appearance="secondary"
|
||||||
|
class="restart-btn"
|
||||||
|
(click)="restart()"
|
||||||
|
>
|
||||||
|
<app-svg name="reload" />
|
||||||
|
Restart
|
||||||
|
</button>
|
||||||
|
} @else {
|
||||||
|
<button type="button" (click)="convert()" data-appearance="primary">
|
||||||
|
Convert colors
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
<button
|
||||||
|
(click)="handleDownload()"
|
||||||
|
class="download-btn"
|
||||||
|
type="button"
|
||||||
|
data-appearance="primary"
|
||||||
|
[attr.disabled]="result() ? null : true"
|
||||||
|
>
|
||||||
|
<app-svg name="download" />
|
||||||
|
Download
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- @if (result()) {
|
||||||
|
<p class="body-m download-note">
|
||||||
|
Now you can modify and import it (link to help center)
|
||||||
|
</p>
|
||||||
|
} -->
|
||||||
|
`,
|
||||||
|
styleUrl: './app.component.css',
|
||||||
|
host: {
|
||||||
|
'[attr.data-theme]': 'theme()',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export class AppComponent {
|
||||||
|
route = inject(ActivatedRoute);
|
||||||
|
messages$ = fromEvent<MessageEvent<PluginMessageEvent>>(window, 'message');
|
||||||
|
|
||||||
|
initialTheme$ = this.route.queryParamMap.pipe(
|
||||||
|
map((params) => params.get('theme')),
|
||||||
|
filter((theme) => !!theme),
|
||||||
|
take(1),
|
||||||
|
);
|
||||||
|
|
||||||
|
theme = toSignal(
|
||||||
|
merge(
|
||||||
|
this.initialTheme$,
|
||||||
|
this.messages$.pipe(
|
||||||
|
filter(
|
||||||
|
(event): event is MessageEvent<ThemePluginEvent> =>
|
||||||
|
event.data.type === 'theme',
|
||||||
|
),
|
||||||
|
map((event) => {
|
||||||
|
return event.data.content;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
#result = toSignal(
|
||||||
|
this.messages$.pipe(
|
||||||
|
filter(
|
||||||
|
(event): event is MessageEvent<SetColorsPluginEvent> =>
|
||||||
|
event.data.type === 'set-colors',
|
||||||
|
),
|
||||||
|
map((event) => {
|
||||||
|
if (event.data.colors) {
|
||||||
|
try {
|
||||||
|
const tokens = transformToToken(event.data.colors);
|
||||||
|
|
||||||
|
return {
|
||||||
|
tokens,
|
||||||
|
name: event.data.fileName,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
{
|
||||||
|
initialValue: null,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
result = linkedSignal(() => this.#result());
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
effect(() => {
|
||||||
|
if (this.result()) {
|
||||||
|
this.#sendMessage({
|
||||||
|
type: 'resize',
|
||||||
|
width: 410,
|
||||||
|
height: 340,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.#sendMessage({ type: 'reset' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#sendMessage(message: PluginUIEvent): void {
|
||||||
|
parent.postMessage(message, '*');
|
||||||
|
}
|
||||||
|
|
||||||
|
convert(): void {
|
||||||
|
this.#sendMessage({ type: 'get-colors' });
|
||||||
|
}
|
||||||
|
|
||||||
|
restart(): void {
|
||||||
|
this.result.set(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDownload() {
|
||||||
|
const fileTokens = this.#result();
|
||||||
|
if (!fileTokens) return;
|
||||||
|
|
||||||
|
const extraData: TokenFileExtraData = {
|
||||||
|
$themes: [],
|
||||||
|
$metadata: {
|
||||||
|
activeThemes: [],
|
||||||
|
tokenSetOrder: [],
|
||||||
|
activeSets: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const tokensStructure = {
|
||||||
|
...fileTokens.tokens,
|
||||||
|
...extraData,
|
||||||
|
};
|
||||||
|
|
||||||
|
const blob = new Blob([JSON.stringify(tokensStructure)], {
|
||||||
|
type: 'text/json',
|
||||||
|
});
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = fileTokens.name + '-tokens.json';
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { ApplicationConfig } from '@angular/core';
|
||||||
|
import { provideRouter } from '@angular/router';
|
||||||
|
|
||||||
|
export const appConfig: ApplicationConfig = {
|
||||||
|
providers: [provideRouter([])],
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
--svg-stroke-color: transparent;
|
||||||
|
--svg-fill-color: transparent;
|
||||||
|
|
||||||
|
inline-size: var(--spacing-16);
|
||||||
|
block-size: var(--spacing-16);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
stroke: var(--svg-stroke-color);
|
||||||
|
fill: var(--svg-fill-color);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { Component, input } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-svg',
|
||||||
|
template: `
|
||||||
|
@switch (name()) {
|
||||||
|
@case ('tick') {
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="1154.667 712.01 14.666 11.333"
|
||||||
|
>
|
||||||
|
<path d="m1167.333 714.01-7.333 7.333-3.333-3.333" />
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
d="m1167.333 714.01-7.333 7.333-3.333-3.333"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
@case ('download') {
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="859 710.01 16 16"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
d="M873 720.01v2.667a1.335 1.335 0 0 1-1.333 1.333h-9.334a1.335 1.335 0 0 1-1.333-1.333v-2.667m2.667-3.333L867 720.01m0 0 3.333-3.333M867 720.01v-8"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
@case ('reload') {
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path d="M2.4 8a6 6 0 1 1 1.758 4.242M2.4 8l2.1-2zm0 0L1 5.5z"></path>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
styleUrl: './svg.component.css',
|
||||||
|
})
|
||||||
|
export class SvgComponent {
|
||||||
|
name = input.required<'tick' | 'download' | 'reload'>();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,498 @@
|
||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`transform colors to tokens 1`] = `
|
||||||
|
{
|
||||||
|
"colors": {
|
||||||
|
"blue": {
|
||||||
|
"050": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#ebf8ff",
|
||||||
|
},
|
||||||
|
"100": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#bee3f8",
|
||||||
|
},
|
||||||
|
"650": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#2a4365",
|
||||||
|
},
|
||||||
|
"700": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#2c5282",
|
||||||
|
},
|
||||||
|
"900": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#1a365d",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"gray": {
|
||||||
|
"100": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#edf2f7",
|
||||||
|
},
|
||||||
|
"200": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#e2e8f0",
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#a0aec0",
|
||||||
|
},
|
||||||
|
"600": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#4a5568",
|
||||||
|
},
|
||||||
|
"700": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#2d3748",
|
||||||
|
},
|
||||||
|
"800": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#1a202c",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"green": {
|
||||||
|
"050": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#f0fff4",
|
||||||
|
},
|
||||||
|
"100": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#c6f6d5",
|
||||||
|
},
|
||||||
|
"300": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#68d391",
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#38a169",
|
||||||
|
},
|
||||||
|
"600": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#2f855a",
|
||||||
|
},
|
||||||
|
"700": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#276749",
|
||||||
|
},
|
||||||
|
"800": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#22543d",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"pink": {
|
||||||
|
"100": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#fed7e2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"purple": {
|
||||||
|
"100": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#e9d8fd",
|
||||||
|
},
|
||||||
|
"300": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#b794f4",
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#805ad5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"red": {
|
||||||
|
"100": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#FED7D7",
|
||||||
|
},
|
||||||
|
"300": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#FC8181",
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#e53e3e",
|
||||||
|
},
|
||||||
|
"600": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#c53030",
|
||||||
|
},
|
||||||
|
"800": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#822727",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"shadow": {
|
||||||
|
"dark": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "rgba(0, 0, 0, 0.69803923)",
|
||||||
|
},
|
||||||
|
"light": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "rgba(0, 0, 0, 0.16078432)",
|
||||||
|
},
|
||||||
|
"mid": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "rgba(0, 0, 0, 0.6)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"white": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#ffffff",
|
||||||
|
},
|
||||||
|
"yellow": {
|
||||||
|
"050": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#fffff0",
|
||||||
|
},
|
||||||
|
"100": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#fefcbf",
|
||||||
|
},
|
||||||
|
"200": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#faf089",
|
||||||
|
},
|
||||||
|
"700": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#975a16",
|
||||||
|
},
|
||||||
|
"800": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#744210",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ui/darkmode": {
|
||||||
|
"dm": {
|
||||||
|
"background": {
|
||||||
|
"blue": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#1a365d",
|
||||||
|
},
|
||||||
|
"green": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#1c4532",
|
||||||
|
},
|
||||||
|
"yellow": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#5f370e",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"button": {
|
||||||
|
"blue": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#2b6cb0",
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#4a5568",
|
||||||
|
},
|
||||||
|
"green": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#2f855a",
|
||||||
|
},
|
||||||
|
"yellow": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#975a16",
|
||||||
|
},
|
||||||
|
"yellow[DONTUSE]": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#fefcbf",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"card": {
|
||||||
|
"background": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#2d3748",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"chart": {
|
||||||
|
"accent": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#9f7aea",
|
||||||
|
"alt": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#ecc94b",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"background": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#4a5568",
|
||||||
|
},
|
||||||
|
"green": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#38a169",
|
||||||
|
},
|
||||||
|
"red": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#e53e3e",
|
||||||
|
},
|
||||||
|
"yellow": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#ecc94b",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"background": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#1a202c",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#1a202c",
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"default": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#e2e8f0",
|
||||||
|
},
|
||||||
|
"secondary": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#718096",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"input": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#4a5568",
|
||||||
|
},
|
||||||
|
"label": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#a0aec0",
|
||||||
|
},
|
||||||
|
"sidebar": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#171923",
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"blue": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#63b3ed",
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#a0aec0",
|
||||||
|
},
|
||||||
|
"emphasis": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#edf2f7",
|
||||||
|
},
|
||||||
|
"green": {
|
||||||
|
"default": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#68d391",
|
||||||
|
},
|
||||||
|
"emphasis": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#9ae6b4",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"red": {
|
||||||
|
"default": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#f56565",
|
||||||
|
},
|
||||||
|
"emphasis": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#FC8181",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"yellow": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#faf089",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ui/lightmode": {
|
||||||
|
"lm": {
|
||||||
|
"axis": {
|
||||||
|
"line": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#a0aec0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"background": {
|
||||||
|
"blue": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#ebf8ff",
|
||||||
|
},
|
||||||
|
"green": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#f0fff4",
|
||||||
|
},
|
||||||
|
"yellow": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#fffff0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"bar": {
|
||||||
|
"line": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "rgba(160, 174, 192, 0.4)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"button": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#E2E8F0",
|
||||||
|
"active": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#a0aec0",
|
||||||
|
},
|
||||||
|
"blue": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#bee3f8",
|
||||||
|
},
|
||||||
|
"green": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#c6f6d5",
|
||||||
|
},
|
||||||
|
"yellow": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#fefcbf",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"card": {
|
||||||
|
"background": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#ffffff",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"chart": {
|
||||||
|
"accent": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#b794f4",
|
||||||
|
"alt": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#faf089",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"background": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#E2E8F0",
|
||||||
|
},
|
||||||
|
"green": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#68d391",
|
||||||
|
},
|
||||||
|
"red": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#FC8181",
|
||||||
|
},
|
||||||
|
"yellow": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#faf089",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"background": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#EDF2F7",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#e2e8f0",
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"blue": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#2a4365",
|
||||||
|
},
|
||||||
|
"blue1": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#bee3f8",
|
||||||
|
},
|
||||||
|
"blue2": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#0000ff",
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#A0AEC0",
|
||||||
|
},
|
||||||
|
"green": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#276749",
|
||||||
|
},
|
||||||
|
"red": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#c53030",
|
||||||
|
},
|
||||||
|
"secondary": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#e2e8f0",
|
||||||
|
},
|
||||||
|
"yellow": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#744210",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"input": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#EDF2F7",
|
||||||
|
},
|
||||||
|
"label": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#4a5568",
|
||||||
|
},
|
||||||
|
"placeholder": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#718096",
|
||||||
|
},
|
||||||
|
"shadow": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "rgba(0, 0, 0, 0.2)",
|
||||||
|
},
|
||||||
|
"sidebar": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#1a202c",
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"blue": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#2a4365",
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#2d3748",
|
||||||
|
},
|
||||||
|
"emphasis": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#000000",
|
||||||
|
},
|
||||||
|
"green": {
|
||||||
|
"default": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#22543d",
|
||||||
|
},
|
||||||
|
"emphasis": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#38a169",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"red": {
|
||||||
|
"default": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#822727",
|
||||||
|
},
|
||||||
|
"emphasis": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#e53e3e",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"secondary": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#718096",
|
||||||
|
},
|
||||||
|
"yellow": {
|
||||||
|
"$type": "color",
|
||||||
|
"$value": "#744210",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
@ -0,0 +1,654 @@
|
||||||
|
import { expect, test } from 'vitest';
|
||||||
|
import { transformToToken } from './transform-to-token';
|
||||||
|
|
||||||
|
const initColors = [
|
||||||
|
{
|
||||||
|
name: 'dm chart yellow',
|
||||||
|
color: '#ecc94b',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / dark mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dm text blue',
|
||||||
|
color: '#63b3ed',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / dark mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'green 700',
|
||||||
|
color: '#276749',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'colors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dm text green emphasis',
|
||||||
|
color: '#9ae6b4',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / dark mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lm icon blue',
|
||||||
|
color: '#2a4365',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / light mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lm card background',
|
||||||
|
color: '#ffffff',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / light mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'gray 600',
|
||||||
|
color: '#4a5568',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'colors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'yellow 200',
|
||||||
|
color: '#faf089',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'colors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lm button',
|
||||||
|
color: '#E2E8F0',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / light mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'green 300',
|
||||||
|
color: '#68d391',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'colors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dm chart red',
|
||||||
|
color: '#e53e3e',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / dark mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'gray 400',
|
||||||
|
color: '#a0aec0',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'colors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lm dashboard background',
|
||||||
|
color: '#EDF2F7',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / light mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lm text yellow',
|
||||||
|
color: '#744210',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / light mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'pink 100',
|
||||||
|
color: '#fed7e2',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'colors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lm text secondary',
|
||||||
|
color: '#718096',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / light mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'blue 900',
|
||||||
|
color: '#1a365d',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'colors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'green 800',
|
||||||
|
color: '#22543d',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'colors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'red 300',
|
||||||
|
color: '#FC8181',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'colors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lm button yellow',
|
||||||
|
color: '#fefcbf',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / light mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lm text red emphasis',
|
||||||
|
color: '#e53e3e',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / light mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dm chart green',
|
||||||
|
color: '#38a169',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / dark mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lm placeholder',
|
||||||
|
color: '#718096',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / light mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'purple 500',
|
||||||
|
color: '#805ad5',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'colors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lm chart yellow',
|
||||||
|
color: '#faf089',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / light mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'shadow light',
|
||||||
|
color: '#000000',
|
||||||
|
opacity: 0.16078432,
|
||||||
|
path: 'colors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dm footer',
|
||||||
|
color: '#1a202c',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / dark mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'red 800',
|
||||||
|
color: '#822727',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'colors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lm text blue',
|
||||||
|
color: '#2a4365',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / light mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'purple 300',
|
||||||
|
color: '#b794f4',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'colors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'purple 100',
|
||||||
|
color: '#e9d8fd',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'colors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dm background blue',
|
||||||
|
color: '#1a365d',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / dark mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dm text red default',
|
||||||
|
color: '#f56565',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / dark mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'gray 700',
|
||||||
|
color: '#2d3748',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'colors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lm button blue',
|
||||||
|
color: '#bee3f8',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / light mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dm icon default',
|
||||||
|
color: '#e2e8f0',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / dark mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'green 600',
|
||||||
|
color: '#2f855a',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'colors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'yellow 100',
|
||||||
|
color: '#fefcbf',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'colors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'blue 100',
|
||||||
|
color: '#bee3f8',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'colors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dm background yellow',
|
||||||
|
color: '#5f370e',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / dark mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'blue 650',
|
||||||
|
color: '#2a4365',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'colors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dm text green default',
|
||||||
|
color: '#68d391',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / dark mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lm bar line',
|
||||||
|
color: '#a0aec0',
|
||||||
|
opacity: 0.4,
|
||||||
|
path: 'ui / light mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dm background green',
|
||||||
|
color: '#1c4532',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / dark mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dm button blue',
|
||||||
|
color: '#2b6cb0',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / dark mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'gray 100',
|
||||||
|
color: '#edf2f7',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'colors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dm button yellow',
|
||||||
|
color: '#975a16',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / dark mode',
|
||||||
|
},
|
||||||
|
/* 3 different blue colors in the same path */
|
||||||
|
{
|
||||||
|
name: 'lm icon blue',
|
||||||
|
color: '#bee3f8',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / light mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lm icon blue',
|
||||||
|
color: '#0000ff',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / light mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lm text default',
|
||||||
|
color: '#2d3748',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / light mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lm icon red',
|
||||||
|
color: '#c53030',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / light mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lm text green default',
|
||||||
|
color: '#22543d',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / light mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lm icon green',
|
||||||
|
color: '#276749',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / light mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'shadow dark',
|
||||||
|
color: '#000000',
|
||||||
|
opacity: 0.69803923,
|
||||||
|
path: 'colors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lm chart background',
|
||||||
|
color: '#E2E8F0',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / light mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'yellow 050',
|
||||||
|
color: '#fffff0',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'colors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'green 100',
|
||||||
|
color: '#c6f6d5',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'colors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lm sidebar',
|
||||||
|
color: '#1a202c',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / light mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dm label',
|
||||||
|
color: '#a0aec0',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / dark mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'green 050',
|
||||||
|
color: '#f0fff4',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'colors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dm button green',
|
||||||
|
color: '#2f855a',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / dark mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lm label',
|
||||||
|
color: '#4a5568',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / light mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lm button active',
|
||||||
|
color: '#a0aec0',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / light mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dm icon secondary',
|
||||||
|
color: '#718096',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / dark mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dm chart background',
|
||||||
|
color: '#4a5568',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / dark mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lm axis line',
|
||||||
|
color: '#a0aec0',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / light mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dm button yellow[DONTUSE]',
|
||||||
|
color: '#fefcbf',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / dark mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dm sidebar',
|
||||||
|
color: '#171923',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / dark mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dm text emphasis',
|
||||||
|
color: '#edf2f7',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / dark mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lm chart green',
|
||||||
|
color: '#68d391',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / light mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lm background blue',
|
||||||
|
color: '#ebf8ff',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / light mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'red 100',
|
||||||
|
color: '#FED7D7',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'colors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dm text default',
|
||||||
|
color: '#a0aec0',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / dark mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'red 500',
|
||||||
|
color: '#e53e3e',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'colors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'yellow 800',
|
||||||
|
color: '#744210',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'colors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'blue 050',
|
||||||
|
color: '#ebf8ff',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'colors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'gray 800',
|
||||||
|
color: '#1a202c',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'colors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lm background yellow',
|
||||||
|
color: '#fffff0',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / light mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dm chart accent',
|
||||||
|
color: '#9f7aea',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / dark mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lm shadow',
|
||||||
|
color: '#000000',
|
||||||
|
opacity: 0.2,
|
||||||
|
path: 'ui / light mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dm text red emphasis',
|
||||||
|
color: '#FC8181',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / dark mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dm input',
|
||||||
|
color: '#4a5568',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / dark mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dm chart accent alt',
|
||||||
|
color: '#ecc94b',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / dark mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lm button green',
|
||||||
|
color: '#c6f6d5',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / light mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'red 600',
|
||||||
|
color: '#c53030',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'colors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lm icon secondary',
|
||||||
|
color: '#e2e8f0',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / light mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dm text yellow',
|
||||||
|
color: '#faf089',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / dark mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'green 500',
|
||||||
|
color: '#38a169',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'colors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'shadow mid',
|
||||||
|
color: '#000000',
|
||||||
|
opacity: 0.6,
|
||||||
|
path: 'colors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lm text green emphasis',
|
||||||
|
color: '#38a169',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / light mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'blue 700',
|
||||||
|
color: '#2c5282',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'colors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lm text red default',
|
||||||
|
color: '#822727',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / light mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lm footer',
|
||||||
|
color: '#e2e8f0',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / light mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lm text emphasis',
|
||||||
|
color: '#000000',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / light mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dm card background',
|
||||||
|
color: '#2d3748',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / dark mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lm chart accent',
|
||||||
|
color: '#b794f4',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / light mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'white',
|
||||||
|
color: '#ffffff',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'colors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dm button default',
|
||||||
|
color: '#4a5568',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / dark mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lm input',
|
||||||
|
color: '#EDF2F7',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / light mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lm chart accent alt',
|
||||||
|
color: '#faf089',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / light mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'gray 200',
|
||||||
|
color: '#e2e8f0',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'colors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lm icon yellow',
|
||||||
|
color: '#744210',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / light mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dm dashboard background',
|
||||||
|
color: '#1a202c',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / dark mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lm icon default',
|
||||||
|
color: '#A0AEC0',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / light mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'yellow 700',
|
||||||
|
color: '#975a16',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'colors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lm background green',
|
||||||
|
color: '#f0fff4',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / light mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lm chart red',
|
||||||
|
color: '#FC8181',
|
||||||
|
opacity: 1,
|
||||||
|
path: 'ui / light mode',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
test('transform colors to tokens', () => {
|
||||||
|
const result = transformToToken(initColors);
|
||||||
|
|
||||||
|
expect(result).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { LibraryColor } from '@penpot/plugin-types';
|
||||||
|
import { TokenStructure } from '../../model';
|
||||||
|
|
||||||
|
function transformToRgba({
|
||||||
|
color,
|
||||||
|
opacity,
|
||||||
|
}: Required<Pick<LibraryColor, 'color' | 'opacity'>>) {
|
||||||
|
color = color.slice(1);
|
||||||
|
|
||||||
|
const r = parseInt(color.substring(0, 2), 16);
|
||||||
|
const g = parseInt(color.substring(2, 4), 16);
|
||||||
|
const b = parseInt(color.substring(4, 6), 16);
|
||||||
|
|
||||||
|
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Color extends LibraryColor {
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transformToToken(colors: LibraryColor[]) {
|
||||||
|
const result: TokenStructure = {};
|
||||||
|
|
||||||
|
colors
|
||||||
|
.filter((data): data is Color => !!data.color)
|
||||||
|
.forEach((data) => {
|
||||||
|
const currentOpacity = data.opacity ?? 1;
|
||||||
|
const value =
|
||||||
|
currentOpacity === 1
|
||||||
|
? data.color
|
||||||
|
: transformToRgba({
|
||||||
|
opacity: currentOpacity,
|
||||||
|
color: data.color,
|
||||||
|
});
|
||||||
|
|
||||||
|
const names: string[] = data.name.replace(/[#{}$]/g, '').split(' ');
|
||||||
|
const key: string =
|
||||||
|
data.path.replace(' \\/ ', '/').replace(/ /g, '') || 'global';
|
||||||
|
|
||||||
|
if (!result[key]) {
|
||||||
|
result[key] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = [key, ...names];
|
||||||
|
let acc = result;
|
||||||
|
|
||||||
|
props.forEach((prop, index) => {
|
||||||
|
if (!acc[prop]) {
|
||||||
|
acc[prop] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index === props.length - 1) {
|
||||||
|
let propIndex = 1;
|
||||||
|
const initialProp = prop;
|
||||||
|
|
||||||
|
while (acc[prop]?.$value) {
|
||||||
|
prop = `${initialProp}${propIndex}`;
|
||||||
|
propIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
acc[prop] = {
|
||||||
|
$value: value,
|
||||||
|
$type: 'color',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
acc = acc[prop] as TokenStructure;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 4.5 KiB |
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"name": "Colors to Tokens",
|
||||||
|
"description": "Generate a design tokens file from a list of colors",
|
||||||
|
"code": "/assets/plugin.js",
|
||||||
|
"icon": "/assets/icon.png",
|
||||||
|
"permissions": ["content:read", "library:read", "allow:downloads"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>colors-to-tokens-plugin</title>
|
||||||
|
<base href="/" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<app-root></app-root>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { bootstrapApplication } from '@angular/platform-browser';
|
||||||
|
import { appConfig } from './app/app.config';
|
||||||
|
import { AppComponent } from './app/app.component';
|
||||||
|
|
||||||
|
bootstrapApplication(AppComponent, appConfig).catch((err) =>
|
||||||
|
console.error(err),
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { LibraryColor } from '@penpot/plugin-types';
|
||||||
|
|
||||||
|
export interface Token {
|
||||||
|
$value: string;
|
||||||
|
$type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TokenFileExtraData {
|
||||||
|
$themes: [];
|
||||||
|
$metadata: TokenFileMetada;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TokenFileMetada {
|
||||||
|
activeThemes: [];
|
||||||
|
tokenSetOrder: [];
|
||||||
|
activeSets: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TokenStructure = {
|
||||||
|
[key: string]: Token | TokenStructure;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface GETColorsPluginUIEvent {
|
||||||
|
type: 'get-colors';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResetPluginUIEvent {
|
||||||
|
type: 'reset';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResizePluginUIEvent {
|
||||||
|
type: 'resize';
|
||||||
|
height: number;
|
||||||
|
width: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PluginUIEvent =
|
||||||
|
| GETColorsPluginUIEvent
|
||||||
|
| ResizePluginUIEvent
|
||||||
|
| ResetPluginUIEvent;
|
||||||
|
|
||||||
|
export interface ThemePluginEvent {
|
||||||
|
type: 'theme';
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SetColorsPluginEvent {
|
||||||
|
type: 'set-colors';
|
||||||
|
colors: LibraryColor[] | null;
|
||||||
|
fileName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PluginMessageEvent = ThemePluginEvent | SetColorsPluginEvent;
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
import type { PluginMessageEvent, PluginUIEvent } from './model.js';
|
||||||
|
|
||||||
|
const defaultSize = {
|
||||||
|
width: 410,
|
||||||
|
height: 280,
|
||||||
|
};
|
||||||
|
|
||||||
|
penpot.ui.open('COLORS TO TOKENS', `?theme=${penpot.theme}`, {
|
||||||
|
width: defaultSize.width,
|
||||||
|
height: defaultSize.height,
|
||||||
|
});
|
||||||
|
|
||||||
|
penpot.on('themechange', (theme) => {
|
||||||
|
sendMessage({ type: 'theme', content: theme });
|
||||||
|
});
|
||||||
|
|
||||||
|
penpot.ui.onMessage<PluginUIEvent>((message) => {
|
||||||
|
if (message.type === 'get-colors') {
|
||||||
|
const colors = penpot.library.local.colors.filter(
|
||||||
|
(color) => !color.gradient,
|
||||||
|
);
|
||||||
|
|
||||||
|
const fileName = penpot.currentFile?.name ?? 'Untitled';
|
||||||
|
|
||||||
|
sendMessage({
|
||||||
|
type: 'set-colors',
|
||||||
|
colors,
|
||||||
|
fileName,
|
||||||
|
});
|
||||||
|
} else if (message.type === 'resize') {
|
||||||
|
if (
|
||||||
|
penpot.ui.size?.width === defaultSize.width &&
|
||||||
|
penpot.ui.size?.height === defaultSize.height
|
||||||
|
) {
|
||||||
|
resize(message.width, message.height);
|
||||||
|
}
|
||||||
|
} else if (message.type === 'reset') {
|
||||||
|
resize(defaultSize.width, defaultSize.height);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function resize(width: number, height: number) {
|
||||||
|
if ('resize' in penpot.ui) {
|
||||||
|
(penpot as any).ui.resize(width, height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendMessage(message: PluginMessageEvent) {
|
||||||
|
penpot.ui.sendMessage(message);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../dist/out-tsc",
|
||||||
|
"types": []
|
||||||
|
},
|
||||||
|
"files": ["src/main.ts"],
|
||||||
|
"include": ["src/**/*.d.ts"],
|
||||||
|
"exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2022",
|
||||||
|
"useDefineForClassFields": false,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"files": [],
|
||||||
|
"include": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.app.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.editor.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.plugin.json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"angularCompilerOptions": {
|
||||||
|
"enableI18nLegacyMessageIdFormat": false,
|
||||||
|
"strictInjectionParameters": true,
|
||||||
|
"strictInputAccessModifiers": true,
|
||||||
|
"strictTemplates": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": []
|
||||||
|
},
|
||||||
|
"files": ["src/plugin.ts"],
|
||||||
|
"include": ["../../libs/plugin-types/index.d.ts"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../dist/out-tsc",
|
||||||
|
"types": [
|
||||||
|
"vitest/globals",
|
||||||
|
"vitest/importMeta",
|
||||||
|
"vite/client",
|
||||||
|
"node",
|
||||||
|
"vitest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"vite.config.ts",
|
||||||
|
"vitest.config.ts",
|
||||||
|
"src/**/*.test.ts",
|
||||||
|
"src/**/*.spec.ts",
|
||||||
|
"src/**/*.test.tsx",
|
||||||
|
"src/**/*.spec.tsx",
|
||||||
|
"src/**/*.test.js",
|
||||||
|
"src/**/*.spec.js",
|
||||||
|
"src/**/*.test.jsx",
|
||||||
|
"src/**/*.spec.jsx",
|
||||||
|
"src/**/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
/// <reference types='vitest' />
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
root: __dirname,
|
||||||
|
cacheDir: '../node_modules/.vite/colors-to-tokens-plugin',
|
||||||
|
test: {
|
||||||
|
watch: false,
|
||||||
|
globals: true,
|
||||||
|
cache: {
|
||||||
|
dir: '../node_modules/.vitest',
|
||||||
|
},
|
||||||
|
environment: 'jsdom',
|
||||||
|
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
||||||
|
reporters: ['default'],
|
||||||
|
coverage: {
|
||||||
|
reportsDirectory: '../coverage/colors-to-tokens-plugin',
|
||||||
|
provider: 'v8',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
import baseConfig from '../../eslint.config.js';
|
||||||
|
import { compat } from '../../eslint.base.config.js';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
...baseConfig,
|
||||||
|
...compat
|
||||||
|
.config({
|
||||||
|
extends: [
|
||||||
|
'plugin:@nx/angular',
|
||||||
|
'plugin:@angular-eslint/template/process-inline-templates',
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.map((config) => ({
|
||||||
|
...config,
|
||||||
|
files: ['**/*.ts'],
|
||||||
|
rules: {
|
||||||
|
'@angular-eslint/directive-selector': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
type: 'attribute',
|
||||||
|
prefix: 'app',
|
||||||
|
style: 'camelCase',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'@angular-eslint/component-selector': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
type: 'element',
|
||||||
|
prefix: 'app',
|
||||||
|
style: 'kebab-case',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
...compat
|
||||||
|
.config({ extends: ['plugin:@nx/angular-template'] })
|
||||||
|
.map((config) => ({
|
||||||
|
...config,
|
||||||
|
files: ['**/*.html'],
|
||||||
|
rules: {},
|
||||||
|
})),
|
||||||
|
{ ignores: ['**/assets/*.js'] },
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
project: './tsconfig.*?.json',
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
{
|
||||||
|
"name": "contrast-plugin",
|
||||||
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||||
|
"projectType": "application",
|
||||||
|
"prefix": "app",
|
||||||
|
"sourceRoot": "apps/contrast-plugin/src",
|
||||||
|
"tags": ["type:plugin"],
|
||||||
|
"targets": {
|
||||||
|
"build": {
|
||||||
|
"executor": "@angular-devkit/build-angular:application",
|
||||||
|
"outputs": ["{options.outputPath}"],
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist/apps/contrast-plugin",
|
||||||
|
"index": "apps/contrast-plugin/src/index.html",
|
||||||
|
"browser": "apps/contrast-plugin/src/main.ts",
|
||||||
|
"polyfills": ["zone.js"],
|
||||||
|
"tsConfig": "apps/contrast-plugin/tsconfig.app.json",
|
||||||
|
"assets": [
|
||||||
|
"apps/contrast-plugin/src/favicon.ico",
|
||||||
|
"apps/contrast-plugin/src/assets"
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"libs/plugins-styles/src/lib/styles.css",
|
||||||
|
"apps/contrast-plugin/src/styles.css"
|
||||||
|
],
|
||||||
|
"scripts": [],
|
||||||
|
"optimization": {
|
||||||
|
"scripts": true,
|
||||||
|
"styles": true,
|
||||||
|
"fonts": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "500kb",
|
||||||
|
"maximumError": "1mb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "2kb",
|
||||||
|
"maximumError": "4kb"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputHashing": "all"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"optimization": false,
|
||||||
|
"extractLicenses": false,
|
||||||
|
"sourceMap": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "production",
|
||||||
|
"dependsOn": ["buildPlugin"]
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"executor": "@angular-devkit/build-angular:dev-server",
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"buildTarget": "contrast-plugin:build:production"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"buildTarget": "contrast-plugin:build:development",
|
||||||
|
"host": "0.0.0.0",
|
||||||
|
"port": 4302
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "development"
|
||||||
|
},
|
||||||
|
"extract-i18n": {
|
||||||
|
"executor": "@angular-devkit/build-angular:extract-i18n",
|
||||||
|
"options": {
|
||||||
|
"buildTarget": "contrast-plugin:build"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
.wrapper {
|
||||||
|
padding-block-start: var(--spacing-24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bold {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contrast-preview {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-8);
|
||||||
|
padding-block-end: var(--spacing-20);
|
||||||
|
border-block-end: 2px solid var(--background-quaternary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-box {
|
||||||
|
block-size: 66px;
|
||||||
|
border: 1px solid var(--db-quaternary);
|
||||||
|
border-radius: var(--spacing-8);
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
var(--color1) 0%,
|
||||||
|
var(--color1) 50%,
|
||||||
|
var(--color2) 50%,
|
||||||
|
var(--color2) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-colors {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contrast-ratio {
|
||||||
|
padding-block: var(--spacing-24);
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: var(--foreground-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.contrast-results {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contrast-result {
|
||||||
|
.title {
|
||||||
|
margin-block-end: var(--spacing-4);
|
||||||
|
}
|
||||||
|
.list {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
inline-size: 42px;
|
||||||
|
block-size: 32px;
|
||||||
|
color: var(--app-white);
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: var(--spacing-8);
|
||||||
|
|
||||||
|
&.good {
|
||||||
|
background-color: var(--success-950);
|
||||||
|
border-color: var(--success-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.fail {
|
||||||
|
background-color: var(--error-950);
|
||||||
|
border-color: var(--error-700);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:host[data-theme='light'] {
|
||||||
|
.tag {
|
||||||
|
color: var(--app-black);
|
||||||
|
&.good {
|
||||||
|
background-color: #a7e8d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.fail {
|
||||||
|
background-color: var(--error-200);
|
||||||
|
border-color: var(--error-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-box {
|
||||||
|
border: 1px solid #d0d3d6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,236 @@
|
||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
computed,
|
||||||
|
inject,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { toSignal } from '@angular/core/rxjs-interop';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import type {
|
||||||
|
PluginMessageEvent,
|
||||||
|
PluginUIEvent,
|
||||||
|
ThemePluginEvent,
|
||||||
|
} from '../model';
|
||||||
|
import { filter, fromEvent, map, merge, take } from 'rxjs';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Shape } from '@penpot/plugin-types';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
imports: [CommonModule],
|
||||||
|
selector: 'app-root',
|
||||||
|
template: `
|
||||||
|
<div class="wrapper body-s">
|
||||||
|
@if (selection().length === 0) {
|
||||||
|
<p class="empty-preview">
|
||||||
|
Select two filled shapes to calculate the color contrast between them.
|
||||||
|
</p>
|
||||||
|
} @else if (selection().length === 1) {
|
||||||
|
<p class="empty-preview">
|
||||||
|
Select <span class="bold">one more</span> filled shape to calculate
|
||||||
|
the color contrast between the selected colors.
|
||||||
|
</p>
|
||||||
|
} @else if (selection().length >= 2) {
|
||||||
|
<div class="contrast-preview">
|
||||||
|
<p>Selected colors:</p>
|
||||||
|
<div class="color-box"></div>
|
||||||
|
<ul class="select-colors">
|
||||||
|
<li>
|
||||||
|
{{ color1() }}
|
||||||
|
</li>
|
||||||
|
<li>{{ color2() }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<p class="contrast-ratio">
|
||||||
|
Contrast ratio: <span>{{ result() }} : 1</span>
|
||||||
|
</p>
|
||||||
|
<div class="contrast-results">
|
||||||
|
<div class="contrast-result">
|
||||||
|
<p class="title">Normal text:</p>
|
||||||
|
<ul class="list">
|
||||||
|
<li
|
||||||
|
class="tag"
|
||||||
|
[ngClass]="
|
||||||
|
result() >= contrastStandards.AA.normal ? 'good' : 'fail'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
AA
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
class="tag"
|
||||||
|
[ngClass]="
|
||||||
|
result() >= contrastStandards.AAA.normal ? 'good' : 'fail'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
AAA
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="contrast-result">
|
||||||
|
<p class="title">
|
||||||
|
Large text
|
||||||
|
<span class="body-xs">(starting from 19px bold or 24px):</span>
|
||||||
|
</p>
|
||||||
|
<ul class="list">
|
||||||
|
<li
|
||||||
|
class="tag"
|
||||||
|
[ngClass]="
|
||||||
|
result() >= contrastStandards.AA.large ? 'good' : 'fail'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
AA
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
class="tag"
|
||||||
|
[ngClass]="
|
||||||
|
result() >= contrastStandards.AAA.large ? 'good' : 'fail'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
AAA
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="contrast-result">
|
||||||
|
<p class="title">
|
||||||
|
Graphics
|
||||||
|
<span class="body-xs">(such as form input borders):</span>
|
||||||
|
</p>
|
||||||
|
<ul class="list">
|
||||||
|
<li
|
||||||
|
class="tag"
|
||||||
|
[ngClass]="
|
||||||
|
result() >= contrastStandards.graphics ? 'good' : 'fail'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
AA
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
styleUrl: './app.component.css',
|
||||||
|
host: {
|
||||||
|
'[attr.data-theme]': 'theme()',
|
||||||
|
'[style.--color1]': 'color1()',
|
||||||
|
'[style.--color2]': 'color2()',
|
||||||
|
},
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class AppComponent {
|
||||||
|
#route = inject(ActivatedRoute);
|
||||||
|
#messages$ = fromEvent<MessageEvent<PluginMessageEvent>>(window, 'message');
|
||||||
|
|
||||||
|
#initialTheme$ = this.#route.queryParamMap.pipe(
|
||||||
|
map((params) => params.get('theme')),
|
||||||
|
filter((theme) => !!theme),
|
||||||
|
take(1),
|
||||||
|
);
|
||||||
|
|
||||||
|
selection = toSignal(
|
||||||
|
this.#messages$.pipe(
|
||||||
|
filter(
|
||||||
|
(event) =>
|
||||||
|
event.data.type === 'init' || event.data.type === 'selection',
|
||||||
|
),
|
||||||
|
map((event) => {
|
||||||
|
if (event.data.type === 'init') {
|
||||||
|
return event.data.content.selection;
|
||||||
|
} else if (event.data.type === 'selection') {
|
||||||
|
return event.data.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}),
|
||||||
|
map((shapes) => {
|
||||||
|
return shapes
|
||||||
|
.map((shape) => this.#getShapeColor(shape))
|
||||||
|
.filter((color): color is string => !!color);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
{
|
||||||
|
initialValue: [],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
theme = toSignal(
|
||||||
|
merge(
|
||||||
|
this.#initialTheme$,
|
||||||
|
this.#messages$.pipe(
|
||||||
|
map((event) => event.data),
|
||||||
|
filter((data): data is ThemePluginEvent => data.type === 'theme'),
|
||||||
|
map((data) => {
|
||||||
|
return data.content;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
color1 = computed(() => {
|
||||||
|
return this.selection().at(-2);
|
||||||
|
});
|
||||||
|
|
||||||
|
color2 = computed(() => {
|
||||||
|
return this.selection().at(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
result = computed<number>(() => {
|
||||||
|
const color1 = this.color1();
|
||||||
|
const color2 = this.color2();
|
||||||
|
|
||||||
|
if (!color1 || !color2) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lum1 = this.#getLuminosity(color1) + 0.05;
|
||||||
|
const lum2 = this.#getLuminosity(color2) + 0.05;
|
||||||
|
|
||||||
|
const result = lum1 > lum2 ? lum1 / lum2 : lum2 / lum1;
|
||||||
|
|
||||||
|
return Number(result.toFixed(2));
|
||||||
|
});
|
||||||
|
|
||||||
|
contrastStandards = {
|
||||||
|
AA: {
|
||||||
|
normal: 4.5,
|
||||||
|
large: 3,
|
||||||
|
},
|
||||||
|
AAA: {
|
||||||
|
normal: 7,
|
||||||
|
large: 4.5,
|
||||||
|
},
|
||||||
|
graphics: 3,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.#sendMessage({ type: 'ready' });
|
||||||
|
}
|
||||||
|
|
||||||
|
#getLuminosity(color: string) {
|
||||||
|
const rgb = this.#hexToRgb(color);
|
||||||
|
const a = rgb.map((v) => {
|
||||||
|
v /= 255;
|
||||||
|
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
||||||
|
});
|
||||||
|
return 0.2126 * a[0] + 0.7152 * a[1] + 0.0722 * a[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
#hexToRgb(hex: string) {
|
||||||
|
const r = parseInt(hex.slice(1, 3), 16);
|
||||||
|
const g = parseInt(hex.slice(3, 5), 16);
|
||||||
|
const b = parseInt(hex.slice(5, 7), 16);
|
||||||
|
return [r, g, b];
|
||||||
|
}
|
||||||
|
|
||||||
|
#getShapeColor(shape?: Shape): string | undefined {
|
||||||
|
const fills = shape?.fills;
|
||||||
|
if (fills && fills !== 'mixed') {
|
||||||
|
return fills?.[0]?.fillColor ?? shape?.strokes?.[0]?.strokeColor;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sendMessage(message: PluginUIEvent) {
|
||||||
|
parent.postMessage(message, '*');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { ApplicationConfig } from '@angular/core';
|
||||||
|
import { provideRouter } from '@angular/router';
|
||||||
|
|
||||||
|
export const appConfig: ApplicationConfig = {
|
||||||
|
providers: [provideRouter([])],
|
||||||
|
};
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.5 KiB |
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"name": "Contrast",
|
||||||
|
"description": "Measure contrast plugin",
|
||||||
|
"code": "/assets/plugin.js",
|
||||||
|
"icon": "/assets/icon.png",
|
||||||
|
"permissions": ["content:read"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>contrast-plugin</title>
|
||||||
|
<base href="/" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<app-root></app-root>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { bootstrapApplication } from '@angular/platform-browser';
|
||||||
|
import { appConfig } from './app/app.config';
|
||||||
|
import { AppComponent } from './app/app.component';
|
||||||
|
|
||||||
|
bootstrapApplication(AppComponent, appConfig).catch((err) =>
|
||||||
|
console.error(err),
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { Shape } from '@penpot/plugin-types';
|
||||||
|
|
||||||
|
export interface InitPluginUIEvent {
|
||||||
|
type: 'ready';
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PluginUIEvent = InitPluginUIEvent;
|
||||||
|
|
||||||
|
export interface InitPluginEvent {
|
||||||
|
type: 'init';
|
||||||
|
content: {
|
||||||
|
theme: string;
|
||||||
|
selection: Shape[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export interface SelectionPluginEvent {
|
||||||
|
type: 'selection';
|
||||||
|
content: Shape[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ThemePluginEvent {
|
||||||
|
type: 'theme';
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PluginMessageEvent =
|
||||||
|
| InitPluginEvent
|
||||||
|
| SelectionPluginEvent
|
||||||
|
| ThemePluginEvent;
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
import type { PluginMessageEvent, PluginUIEvent } from './model.js';
|
||||||
|
|
||||||
|
penpot.ui.open('CONTRAST PLUGIN', `?theme=${penpot.theme}`, {
|
||||||
|
width: 285,
|
||||||
|
height: 525,
|
||||||
|
});
|
||||||
|
|
||||||
|
penpot.ui.onMessage<PluginUIEvent>((message) => {
|
||||||
|
if (message.type === 'ready') {
|
||||||
|
sendMessage({
|
||||||
|
type: 'init',
|
||||||
|
content: {
|
||||||
|
theme: penpot.theme,
|
||||||
|
selection: penpot.selection,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
initEvents();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
penpot.on('selectionchange', () => {
|
||||||
|
const shapes = penpot.selection;
|
||||||
|
sendMessage({ type: 'selection', content: shapes });
|
||||||
|
|
||||||
|
initEvents();
|
||||||
|
});
|
||||||
|
|
||||||
|
let listeners: symbol[] = [];
|
||||||
|
|
||||||
|
function initEvents() {
|
||||||
|
listeners.forEach((listener) => {
|
||||||
|
penpot.off(listener);
|
||||||
|
});
|
||||||
|
|
||||||
|
listeners = penpot.selection.map((shape) => {
|
||||||
|
return penpot.on(
|
||||||
|
'shapechange',
|
||||||
|
() => {
|
||||||
|
const shapes = penpot.selection;
|
||||||
|
sendMessage({ type: 'selection', content: shapes });
|
||||||
|
},
|
||||||
|
{ shapeId: shape.id },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
penpot.on('themechange', () => {
|
||||||
|
const theme = penpot.theme;
|
||||||
|
sendMessage({ type: 'theme', content: theme });
|
||||||
|
});
|
||||||
|
|
||||||
|
function sendMessage(message: PluginMessageEvent) {
|
||||||
|
penpot.ui.sendMessage(message);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../dist/out-tsc",
|
||||||
|
"types": []
|
||||||
|
},
|
||||||
|
"files": ["src/main.ts"],
|
||||||
|
"include": ["src/**/*.d.ts"],
|
||||||
|
"exclude": ["src/**/*.test.ts", "src/**/*.spec.ts"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2022",
|
||||||
|
"useDefineForClassFields": false,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"files": [],
|
||||||
|
"include": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.app.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.editor.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.plugin.json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"angularCompilerOptions": {
|
||||||
|
"enableI18nLegacyMessageIdFormat": false,
|
||||||
|
"strictInjectionParameters": true,
|
||||||
|
"strictInputAccessModifiers": true,
|
||||||
|
"strictTemplates": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": []
|
||||||
|
},
|
||||||
|
"files": ["src/plugin.ts"],
|
||||||
|
"include": ["../../libs/plugin-types/index.d.ts"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
/// <reference types='vitest' />
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
root: __dirname,
|
||||||
|
cacheDir: '../node_modules/.vite/contrast-plugin',
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
cache: {
|
||||||
|
dir: '../node_modules/.vitest',
|
||||||
|
},
|
||||||
|
environment: 'jsdom',
|
||||||
|
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
||||||
|
reporters: ['default'],
|
||||||
|
coverage: {
|
||||||
|
reportsDirectory: '../coverage/contrast-plugin',
|
||||||
|
provider: 'v8',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"presets": ["@nx/js/babel"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"jsc": {
|
||||||
|
"parser": {
|
||||||
|
"syntax": "typescript"
|
||||||
|
},
|
||||||
|
"target": "es2016"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
import baseConfig from '../../eslint.config.js';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
...baseConfig,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
project: './tsconfig.*?.json',
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
||||||
|
rules: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.ts', '**/*.tsx'],
|
||||||
|
rules: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.js', '**/*.jsx'],
|
||||||
|
rules: {},
|
||||||
|
},
|
||||||
|
{ ignores: ['vite.config.ts'] },
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en"></html>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"name": "create-palette-plugin",
|
||||||
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||||
|
"projectType": "application",
|
||||||
|
"sourceRoot": "apps/create-palette-plugin/src",
|
||||||
|
"tags": ["type:plugin"],
|
||||||
|
"targets": {}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"name": "Create Palette from library",
|
||||||
|
"description": "Create a board with all the colors in the local library",
|
||||||
|
"code": "/plugin.js",
|
||||||
|
"icon": "/assets/icon.png",
|
||||||
|
"permissions": ["content:read", "content:write", "library:read"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
main();
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
createPalette();
|
||||||
|
penpot.closePlugin();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPalette() {
|
||||||
|
const colors = penpot.library.local.colors.sort((a, b) =>
|
||||||
|
a.name.toLowerCase() > b.name.toLowerCase()
|
||||||
|
? 1
|
||||||
|
: a.name.toLowerCase() < b.name.toLowerCase()
|
||||||
|
? -1
|
||||||
|
: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
const cols = 4;
|
||||||
|
const rows = Math.ceil(colors.length / cols);
|
||||||
|
|
||||||
|
const width = cols * 200 + Math.max(0, cols - 1) * 10 + 20;
|
||||||
|
const height = rows * 100 + Math.max(0, rows - 1) * 10 + 20;
|
||||||
|
|
||||||
|
const board = penpot.createBoard();
|
||||||
|
board.name = 'Palette';
|
||||||
|
|
||||||
|
const viewport = penpot.viewport;
|
||||||
|
board.x = viewport.center.x - width / 2;
|
||||||
|
board.y = viewport.center.y - height / 2;
|
||||||
|
|
||||||
|
if (colors.length === 0) {
|
||||||
|
// NO colors return
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
board.resize(width, height);
|
||||||
|
board.borderRadius = 8;
|
||||||
|
|
||||||
|
// create grid
|
||||||
|
const grid = board.addGridLayout();
|
||||||
|
|
||||||
|
for (let i = 0; i < rows; i++) {
|
||||||
|
grid.addRow('flex', 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < cols; i++) {
|
||||||
|
grid.addColumn('flex', 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
grid.alignItems = 'center';
|
||||||
|
grid.justifyItems = 'start';
|
||||||
|
grid.justifyContent = 'stretch';
|
||||||
|
grid.alignContent = 'stretch';
|
||||||
|
grid.rowGap = 10;
|
||||||
|
grid.columnGap = 10;
|
||||||
|
grid.verticalPadding = 10;
|
||||||
|
grid.horizontalPadding = 10;
|
||||||
|
|
||||||
|
grid.horizontalSizing = 'auto';
|
||||||
|
|
||||||
|
// create text
|
||||||
|
for (let row = 0; row < rows; row++) {
|
||||||
|
for (let col = 0; col < cols; col++) {
|
||||||
|
const i = row * cols + col;
|
||||||
|
const color = colors[i];
|
||||||
|
|
||||||
|
if (i >= colors.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const board = penpot.createBoard();
|
||||||
|
grid.appendChild(board, row + 1, col + 1);
|
||||||
|
board.fills = [color.asFill()];
|
||||||
|
board.strokes = [
|
||||||
|
{ strokeColor: '#000000', strokeOpacity: 0.3, strokeStyle: 'solid' },
|
||||||
|
];
|
||||||
|
|
||||||
|
if (board.layoutChild) {
|
||||||
|
board.layoutChild.horizontalSizing = 'fill';
|
||||||
|
board.layoutChild.verticalSizing = 'fill';
|
||||||
|
}
|
||||||
|
|
||||||
|
const flex = board.addFlexLayout();
|
||||||
|
flex.alignItems = 'center';
|
||||||
|
flex.justifyContent = 'center';
|
||||||
|
flex.verticalPadding = 8;
|
||||||
|
flex.horizontalPadding = 8;
|
||||||
|
|
||||||
|
const text = penpot.createText(color.name);
|
||||||
|
text.fontWeight = 'bold';
|
||||||
|
text.fontVariantId = 'bold';
|
||||||
|
text.growType = 'auto-width';
|
||||||
|
text.strokes = [
|
||||||
|
{
|
||||||
|
strokeColor: '#FFFFFF',
|
||||||
|
strokeWidth: 1,
|
||||||
|
strokeAlignment: 'outer',
|
||||||
|
strokeOpacity: 0.5,
|
||||||
|
strokeStyle: 'solid',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
board.appendChild(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../dist/out-tsc",
|
||||||
|
"types": ["node"]
|
||||||
|
},
|
||||||
|
"exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"],
|
||||||
|
"include": ["src/**/*.ts", "../../libs/plugin-types/index.d.ts"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"files": [],
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ESNext", "DOM"],
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"strict": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"types": ["vite/client"]
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.app.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.spec.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../dist/out-tsc",
|
||||||
|
"types": [
|
||||||
|
"vitest/globals",
|
||||||
|
"vitest/importMeta",
|
||||||
|
"vite/client",
|
||||||
|
"node",
|
||||||
|
"vitest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"vite.config.ts",
|
||||||
|
"vitest.config.ts",
|
||||||
|
"src/**/*.test.ts",
|
||||||
|
"src/**/*.spec.ts",
|
||||||
|
"src/**/*.test.tsx",
|
||||||
|
"src/**/*.spec.tsx",
|
||||||
|
"src/**/*.test.js",
|
||||||
|
"src/**/*.spec.js",
|
||||||
|
"src/**/*.test.jsx",
|
||||||
|
"src/**/*.spec.jsx",
|
||||||
|
"src/**/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
/// <reference types='vitest' />
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
root: __dirname,
|
||||||
|
cacheDir: '../../node_modules/.vite/apps/create-palette-plugin',
|
||||||
|
|
||||||
|
server: {
|
||||||
|
port: 4305,
|
||||||
|
host: '0.0.0.0',
|
||||||
|
},
|
||||||
|
|
||||||
|
preview: {
|
||||||
|
port: 4305,
|
||||||
|
host: '0.0.0.0',
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: [nxViteTsPaths()],
|
||||||
|
|
||||||
|
// Uncomment this if you are using workers.
|
||||||
|
// worker: {
|
||||||
|
// plugins: [ nxViteTsPaths() ],
|
||||||
|
// },
|
||||||
|
|
||||||
|
build: {
|
||||||
|
outDir: '../../dist/apps/create-palette-plugin',
|
||||||
|
reportCompressedSize: true,
|
||||||
|
commonjsOptions: {
|
||||||
|
transformMixedEsModules: true,
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
input: {
|
||||||
|
plugin: 'src/plugin.ts',
|
||||||
|
index: './index.html',
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
entryFileNames: '[name].js',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
cache: {
|
||||||
|
dir: '../../node_modules/.vitest',
|
||||||
|
},
|
||||||
|
environment: 'jsdom',
|
||||||
|
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
||||||
|
|
||||||
|
reporters: ['default'],
|
||||||
|
coverage: {
|
||||||
|
reportsDirectory: '../../coverage/apps/create-palette-plugin',
|
||||||
|
provider: 'v8',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
import baseConfig from '../../eslint.config.js';
|
||||||
|
import typescriptEslintParser from '@typescript-eslint/parser';
|
||||||
|
import globals from 'globals';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
...baseConfig,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
parser: typescriptEslintParser,
|
||||||
|
parserOptions: { project: './apps/e2e/tsconfig.json' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
||||||
|
rules: {},
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
...globals.node,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.ts', '**/*.tsx'],
|
||||||
|
rules: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.js', '**/*.jsx'],
|
||||||
|
rules: {},
|
||||||
|
},
|
||||||
|
{ ignores: ['vite.config.ts'] },
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"name": "e2e",
|
||||||
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||||
|
"projectType": "application",
|
||||||
|
"implicitDependencies": [],
|
||||||
|
"tags": ["type:e2e"],
|
||||||
|
"targets": {}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,17 @@
|
||||||
|
export interface FileRpc {
|
||||||
|
'~:name': string;
|
||||||
|
'~:revn': number;
|
||||||
|
'~:id': string;
|
||||||
|
'~:is-shared': boolean;
|
||||||
|
'~:version': number;
|
||||||
|
'~:project-id': string;
|
||||||
|
'~:data': {
|
||||||
|
'~:pages': string[];
|
||||||
|
'~:objects': string[];
|
||||||
|
'~:styles': string[];
|
||||||
|
'~:components': string[];
|
||||||
|
'~:styles-v2': string[];
|
||||||
|
'~:components-v2': string[];
|
||||||
|
'~:features': string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
export interface Shape {
|
||||||
|
id: string;
|
||||||
|
frameId?: string;
|
||||||
|
parentId?: string;
|
||||||
|
shapes?: string[];
|
||||||
|
layoutGridCells?: Shape[];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
import componentLibrary from './plugins/component-library';
|
||||||
|
import testingPlugin from './plugins/create-board-text-rect';
|
||||||
|
import flex from './plugins/create-flexlayout';
|
||||||
|
import grid from './plugins/create-gridlayout';
|
||||||
|
import rulerGuides from './plugins/create-ruler-guides';
|
||||||
|
import createText from './plugins/create-text';
|
||||||
|
import group from './plugins/group';
|
||||||
|
import insertSvg from './plugins/insert-svg';
|
||||||
|
import pluginData from './plugins/plugin-data';
|
||||||
|
import comments from './plugins/create-comments';
|
||||||
|
import { Agent } from './utils/agent';
|
||||||
|
|
||||||
|
describe('Plugins', () => {
|
||||||
|
it('create board - text - rectable', async () => {
|
||||||
|
const agent = await Agent();
|
||||||
|
const result = await agent.runCode(testingPlugin.toString(), {
|
||||||
|
screenshot: 'create-board-text-rect',
|
||||||
|
});
|
||||||
|
expect(result).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('create flex layout', async () => {
|
||||||
|
const agent = await Agent();
|
||||||
|
const result = await agent.runCode(flex.toString(), {
|
||||||
|
screenshot: 'create-flexlayout',
|
||||||
|
});
|
||||||
|
expect(result).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('create grid layout', async () => {
|
||||||
|
const agent = await Agent();
|
||||||
|
const result = await agent.runCode(grid.toString(), {
|
||||||
|
screenshot: 'create-gridlayout',
|
||||||
|
});
|
||||||
|
expect(result).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('group and ungroup', async () => {
|
||||||
|
const agent = await Agent();
|
||||||
|
const result = await agent.runCode(group.toString(), {
|
||||||
|
screenshot: 'group-ungroup',
|
||||||
|
});
|
||||||
|
expect(result).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('insert svg', async () => {
|
||||||
|
const agent = await Agent();
|
||||||
|
const result = await agent.runCode(insertSvg.toString(), {
|
||||||
|
screenshot: 'insert-svg',
|
||||||
|
});
|
||||||
|
expect(result).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('plugin data', async () => {
|
||||||
|
const agent = await Agent();
|
||||||
|
const result = await agent.runCode(pluginData.toString());
|
||||||
|
expect(result).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('component library', async () => {
|
||||||
|
const agent = await Agent();
|
||||||
|
const result = await agent.runCode(componentLibrary.toString(), {
|
||||||
|
screenshot: 'component-library',
|
||||||
|
});
|
||||||
|
expect(result).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('text and textrange', async () => {
|
||||||
|
const agent = await Agent();
|
||||||
|
const result = await agent.runCode(createText.toString(), {
|
||||||
|
screenshot: 'create-text',
|
||||||
|
});
|
||||||
|
expect(result).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ruler guides', async () => {
|
||||||
|
const agent = await Agent();
|
||||||
|
const result = await agent.runCode(rulerGuides.toString(), {
|
||||||
|
screenshot: 'create-ruler-guides',
|
||||||
|
});
|
||||||
|
expect(result).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('comments', async () => {
|
||||||
|
const agent = await Agent();
|
||||||
|
const result = await agent.runCode(comments.toString(), {
|
||||||
|
screenshot: 'create-comments',
|
||||||
|
avoidSavedStatus: true,
|
||||||
|
});
|
||||||
|
expect(result).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
export default function () {
|
||||||
|
const rectangle = penpot.createRectangle();
|
||||||
|
rectangle.x = penpot.viewport.center.x;
|
||||||
|
rectangle.y = penpot.viewport.center.y;
|
||||||
|
|
||||||
|
const shape = penpot.currentPage?.getShapeById(rectangle.id);
|
||||||
|
if (shape) {
|
||||||
|
penpot.library.local.createComponent([shape]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
import type { Board, Rectangle, Text } from '@penpot/plugin-types';
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
function createText(text: string): Text | undefined {
|
||||||
|
const textNode = penpot.createText(text);
|
||||||
|
|
||||||
|
if (!textNode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
textNode.x = penpot.viewport.center.x;
|
||||||
|
textNode.y = penpot.viewport.center.y;
|
||||||
|
|
||||||
|
return textNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createRectangle(): Rectangle {
|
||||||
|
const rectangle = penpot.createRectangle();
|
||||||
|
|
||||||
|
rectangle.setPluginData('customKey', 'customValue');
|
||||||
|
|
||||||
|
rectangle.x = penpot.viewport.center.x;
|
||||||
|
rectangle.y = penpot.viewport.center.y;
|
||||||
|
|
||||||
|
rectangle.resize(200, 200);
|
||||||
|
|
||||||
|
return rectangle;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createBoard(): Board {
|
||||||
|
const board = penpot.createBoard();
|
||||||
|
|
||||||
|
board.name = 'Board name';
|
||||||
|
|
||||||
|
board.x = penpot.viewport.center.x;
|
||||||
|
board.y = penpot.viewport.center.y;
|
||||||
|
|
||||||
|
board.borderRadius = 8;
|
||||||
|
|
||||||
|
board.resize(300, 300);
|
||||||
|
|
||||||
|
const text = penpot.createText('Hello from board');
|
||||||
|
|
||||||
|
if (!text) {
|
||||||
|
throw new Error('Could not create text');
|
||||||
|
}
|
||||||
|
|
||||||
|
text.x = 10;
|
||||||
|
text.y = 10;
|
||||||
|
board.appendChild(text);
|
||||||
|
|
||||||
|
return board;
|
||||||
|
}
|
||||||
|
|
||||||
|
createBoard();
|
||||||
|
createRectangle();
|
||||||
|
createText('Hello from plugin');
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
export default function () {
|
||||||
|
async function createComment() {
|
||||||
|
const page = penpot.currentPage;
|
||||||
|
|
||||||
|
if (page) {
|
||||||
|
await page.addCommentThread('Hello world!', {
|
||||||
|
x: penpot.viewport.center.x,
|
||||||
|
y: penpot.viewport.center.y,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function replyComment() {
|
||||||
|
const page = penpot.currentPage;
|
||||||
|
|
||||||
|
if (page) {
|
||||||
|
const comments = await page.findCommentThreads({
|
||||||
|
onlyYours: true,
|
||||||
|
showResolved: false,
|
||||||
|
});
|
||||||
|
await comments[0].reply('This is a reply.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteComment() {
|
||||||
|
const page = penpot.currentPage;
|
||||||
|
|
||||||
|
if (page) {
|
||||||
|
const commentThreads = await page.findCommentThreads({
|
||||||
|
onlyYours: true,
|
||||||
|
showResolved: false,
|
||||||
|
});
|
||||||
|
await page.removeCommentThread(commentThreads[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createComment();
|
||||||
|
replyComment();
|
||||||
|
deleteComment();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
export default function () {
|
||||||
|
function createFlexLayout(): void {
|
||||||
|
const board = penpot.createBoard();
|
||||||
|
board.horizontalSizing = 'auto';
|
||||||
|
board.verticalSizing = 'auto';
|
||||||
|
|
||||||
|
board.x = penpot.viewport.center.x;
|
||||||
|
board.y = penpot.viewport.center.y;
|
||||||
|
|
||||||
|
const flex = board.addFlexLayout();
|
||||||
|
|
||||||
|
flex.dir = 'column';
|
||||||
|
flex.wrap = 'wrap';
|
||||||
|
flex.alignItems = 'center';
|
||||||
|
flex.justifyContent = 'center';
|
||||||
|
flex.verticalPadding = 5;
|
||||||
|
flex.horizontalPadding = 5;
|
||||||
|
flex.horizontalSizing = 'fill';
|
||||||
|
flex.verticalSizing = 'fill';
|
||||||
|
|
||||||
|
board.appendChild(penpot.createRectangle());
|
||||||
|
board.appendChild(penpot.createEllipse());
|
||||||
|
}
|
||||||
|
|
||||||
|
createFlexLayout();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
export default function () {
|
||||||
|
function createGridLayout(): void {
|
||||||
|
const board = penpot.createBoard();
|
||||||
|
board.x = penpot.viewport.center.x;
|
||||||
|
board.y = penpot.viewport.center.y;
|
||||||
|
|
||||||
|
const grid = board.addGridLayout();
|
||||||
|
|
||||||
|
grid.addRow('flex', 1);
|
||||||
|
grid.addRow('flex', 1);
|
||||||
|
grid.addColumn('flex', 1);
|
||||||
|
grid.addColumn('flex', 1);
|
||||||
|
|
||||||
|
grid.alignItems = 'center';
|
||||||
|
grid.justifyItems = 'start';
|
||||||
|
grid.justifyContent = 'space-between';
|
||||||
|
grid.alignContent = 'stretch';
|
||||||
|
grid.rowGap = 10;
|
||||||
|
grid.columnGap = 10;
|
||||||
|
grid.verticalPadding = 5;
|
||||||
|
grid.horizontalPadding = 5;
|
||||||
|
grid.horizontalSizing = 'auto';
|
||||||
|
grid.verticalSizing = 'auto';
|
||||||
|
}
|
||||||
|
|
||||||
|
createGridLayout();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
export default function () {
|
||||||
|
function createRulerGuides(): void {
|
||||||
|
const page = penpot.currentPage;
|
||||||
|
|
||||||
|
if (page) {
|
||||||
|
page.addRulerGuide('horizontal', penpot.viewport.center.x);
|
||||||
|
page.addRulerGuide('vertical', penpot.viewport.center.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeRulerGuides(): void {
|
||||||
|
const page = penpot.currentPage;
|
||||||
|
|
||||||
|
if (page) {
|
||||||
|
page.removeRulerGuide(page.rulerGuides[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createRulerGuides();
|
||||||
|
removeRulerGuides();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
export default function () {
|
||||||
|
function createText(): void {
|
||||||
|
const text = penpot.createText('Hello World!');
|
||||||
|
|
||||||
|
if (text) {
|
||||||
|
text.x = penpot.viewport.center.x;
|
||||||
|
text.y = penpot.viewport.center.y;
|
||||||
|
text.growType = 'auto-width';
|
||||||
|
text.textTransform = 'uppercase';
|
||||||
|
text.textDecoration = 'underline';
|
||||||
|
text.fontId = 'gfont-work-sans';
|
||||||
|
text.fontStyle = 'italic';
|
||||||
|
text.fontSize = '20';
|
||||||
|
text.fontWeight = '500';
|
||||||
|
|
||||||
|
const textRange = text.getRange(0, 5);
|
||||||
|
textRange.fontSize = '40';
|
||||||
|
textRange.fills = [{ fillColor: '#ff6fe0', fillOpacity: 1 }];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createText();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
export default function () {
|
||||||
|
function group() {
|
||||||
|
const selected = penpot.selection;
|
||||||
|
|
||||||
|
if (selected.length && !penpot.utils.types.isGroup(selected[0])) {
|
||||||
|
return penpot.group(selected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ungroup() {
|
||||||
|
const selected = penpot.selection;
|
||||||
|
|
||||||
|
if (selected.length && penpot.utils.types.isGroup(selected[0])) {
|
||||||
|
return penpot.ungroup(selected[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rectangle = penpot.createRectangle();
|
||||||
|
rectangle.x = penpot.viewport.center.x;
|
||||||
|
rectangle.y = penpot.viewport.center.y;
|
||||||
|
const rectangle2 = penpot.createRectangle();
|
||||||
|
rectangle2.x = penpot.viewport.center.x + 100;
|
||||||
|
rectangle2.y = penpot.viewport.center.y + 100;
|
||||||
|
|
||||||
|
penpot.selection = [rectangle, rectangle2];
|
||||||
|
|
||||||
|
group();
|
||||||
|
ungroup();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
export default function () {
|
||||||
|
function insertSvg(svg: string) {
|
||||||
|
const icon = penpot.createShapeFromSvg(svg);
|
||||||
|
|
||||||
|
if (icon) {
|
||||||
|
icon.name = 'Test icon';
|
||||||
|
icon.x = penpot.viewport.center.x;
|
||||||
|
icon.y = penpot.viewport.center.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
const svg = `
|
||||||
|
<svg width="300" height="130" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="200" height="100" x="10" y="10" rx="20" ry="20" fill="blue" />
|
||||||
|
Sorry, your browser does not support inline SVG.
|
||||||
|
</svg>`;
|
||||||
|
|
||||||
|
insertSvg(svg);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
export default function () {
|
||||||
|
const rectangle = penpot.createRectangle();
|
||||||
|
|
||||||
|
rectangle?.setPluginData('testData', 'test');
|
||||||
|
return rectangle?.getPluginData('testData');
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,167 @@
|
||||||
|
import puppeteer from 'puppeteer';
|
||||||
|
import { PenpotApi } from './api';
|
||||||
|
import { getFileUrl } from './get-file-url';
|
||||||
|
import { idObjectToArray } from './clean-id';
|
||||||
|
import { Shape } from '../models/shape.model';
|
||||||
|
|
||||||
|
const screenshotsEnable = process.env['E2E_SCREENSHOTS'] === 'true';
|
||||||
|
|
||||||
|
function replaceIds(shapes: Shape[]) {
|
||||||
|
let id = 1;
|
||||||
|
|
||||||
|
const getId = () => {
|
||||||
|
return String(id++);
|
||||||
|
};
|
||||||
|
|
||||||
|
function replaceChildrenId(id: string, newId: string) {
|
||||||
|
for (const node of shapes) {
|
||||||
|
if (node.parentId === id) {
|
||||||
|
node.parentId = newId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.frameId === id) {
|
||||||
|
node.frameId = newId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.shapes) {
|
||||||
|
node.shapes = node.shapes?.map((shapeId) => {
|
||||||
|
return shapeId === id ? newId : shapeId;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.layoutGridCells) {
|
||||||
|
node.layoutGridCells = idObjectToArray(node.layoutGridCells, newId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const node of shapes) {
|
||||||
|
const previousId = node.id;
|
||||||
|
|
||||||
|
node.id = getId();
|
||||||
|
|
||||||
|
replaceChildrenId(previousId, node.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function Agent() {
|
||||||
|
console.log('Initializing Penpot API...');
|
||||||
|
const penpotApi = await PenpotApi();
|
||||||
|
|
||||||
|
console.log('Creating file...');
|
||||||
|
const file = await penpotApi.createFile();
|
||||||
|
console.log('File created with id:', file['~:id']);
|
||||||
|
|
||||||
|
const fileUrl = getFileUrl(file);
|
||||||
|
console.log('File URL:', fileUrl);
|
||||||
|
|
||||||
|
console.log('Launching browser...');
|
||||||
|
const browser = await puppeteer.launch({});
|
||||||
|
const page = await browser.newPage();
|
||||||
|
|
||||||
|
await page.setViewport({ width: 1920, height: 1080 });
|
||||||
|
|
||||||
|
console.log('Setting authentication cookie...');
|
||||||
|
page.setCookie({
|
||||||
|
name: 'auth-token',
|
||||||
|
value: penpotApi.getAuth().split('=')[1],
|
||||||
|
domain: 'localhost',
|
||||||
|
path: '/',
|
||||||
|
expires: (Date.now() + 3600 * 1000) / 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Navigating to file URL...');
|
||||||
|
await page.goto(fileUrl);
|
||||||
|
await page.waitForSelector('[data-testid="viewport"]');
|
||||||
|
console.log('Page loaded and viewport selector found.');
|
||||||
|
|
||||||
|
page
|
||||||
|
.on('console', async (message) => {
|
||||||
|
console.log(`${message.type()} ${message.text()}`);
|
||||||
|
})
|
||||||
|
.on('pageerror', (message) => {
|
||||||
|
console.error('Page error:', message);
|
||||||
|
});
|
||||||
|
|
||||||
|
const finish = async () => {
|
||||||
|
console.log('Deleting file and closing browser...');
|
||||||
|
await penpotApi.deleteFile(file['~:id']);
|
||||||
|
await browser.close();
|
||||||
|
console.log('Clean up done.');
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
async runCode(
|
||||||
|
code: string,
|
||||||
|
options: {
|
||||||
|
screenshot?: string;
|
||||||
|
autoFinish?: boolean;
|
||||||
|
avoidSavedStatus?: boolean;
|
||||||
|
} = {
|
||||||
|
screenshot: '',
|
||||||
|
autoFinish: true,
|
||||||
|
avoidSavedStatus: false,
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const autoFinish = options.autoFinish ?? true;
|
||||||
|
|
||||||
|
console.log('Running plugin code...');
|
||||||
|
await page.evaluate((testingPlugin) => {
|
||||||
|
(globalThis as any).ɵloadPlugin({
|
||||||
|
pluginId: 'TEST',
|
||||||
|
name: 'Test',
|
||||||
|
code: `
|
||||||
|
(${testingPlugin})();
|
||||||
|
`,
|
||||||
|
icon: '',
|
||||||
|
description: '',
|
||||||
|
permissions: ['content:read', 'content:write'],
|
||||||
|
});
|
||||||
|
}, code);
|
||||||
|
|
||||||
|
if (!options.avoidSavedStatus) {
|
||||||
|
console.log('Waiting for save status...');
|
||||||
|
await page.waitForSelector(
|
||||||
|
'.main_ui_workspace_right_header__saved-status',
|
||||||
|
{
|
||||||
|
timeout: 10000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
console.log('Save status found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.screenshot && screenshotsEnable) {
|
||||||
|
console.log('Taking screenshot:', options.screenshot);
|
||||||
|
await page.screenshot({
|
||||||
|
path: 'screenshots/' + options.screenshot + '.png',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
page.once('console', async (msg) => {
|
||||||
|
const args = (await Promise.all(
|
||||||
|
msg.args().map((arg) => arg.jsonValue()),
|
||||||
|
)) as Record<string, unknown>[];
|
||||||
|
|
||||||
|
const result = Object.values(args[1]) as Shape[];
|
||||||
|
|
||||||
|
replaceIds(result);
|
||||||
|
console.log('IDs replaced in result.');
|
||||||
|
|
||||||
|
resolve(result);
|
||||||
|
|
||||||
|
if (autoFinish) {
|
||||||
|
console.log('Auto finish enabled. Cleaning up...');
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Evaluating debug.dump_objects...');
|
||||||
|
page.evaluate(`
|
||||||
|
debug.dump_objects();
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
finish,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
import { FileRpc } from '../models/file-rpc.model';
|
||||||
|
|
||||||
|
const apiUrl = 'http://localhost:3449';
|
||||||
|
|
||||||
|
export async function PenpotApi() {
|
||||||
|
if (!process.env['E2E_LOGIN_EMAIL']) {
|
||||||
|
throw new Error('E2E_LOGIN_EMAIL not set');
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultLoginRequest = await fetch(
|
||||||
|
`${apiUrl}/api/rpc/command/login-with-password`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/transit+json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
'~:email': process.env['E2E_LOGIN_EMAIL'],
|
||||||
|
'~:password': process.env['E2E_LOGIN_PASSWORD'],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const loginData = await resultLoginRequest.json();
|
||||||
|
const authToken = resultLoginRequest.headers
|
||||||
|
.get('set-cookie')
|
||||||
|
?.split(';')
|
||||||
|
.at(0);
|
||||||
|
|
||||||
|
if (!authToken) {
|
||||||
|
throw new Error('Login failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
getAuth: () => authToken,
|
||||||
|
createFile: async () => {
|
||||||
|
const createFileRequest = await fetch(
|
||||||
|
`${apiUrl}/api/rpc/command/create-file`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/transit+json',
|
||||||
|
cookie: authToken,
|
||||||
|
credentials: 'include',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
'~:name': `test file ${new Date().toISOString()}`,
|
||||||
|
'~:project-id': loginData['~:default-project-id'],
|
||||||
|
'~:features': {
|
||||||
|
'~#set': [
|
||||||
|
'fdata/objects-map',
|
||||||
|
'fdata/pointer-map',
|
||||||
|
'fdata/shape-data-type',
|
||||||
|
'components/v2',
|
||||||
|
'styles/v2',
|
||||||
|
'layout/grid',
|
||||||
|
'plugins/runtime',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return (await createFileRequest.json()) as FileRpc;
|
||||||
|
},
|
||||||
|
deleteFile: async (fileId: string) => {
|
||||||
|
const deleteFileRequest = await fetch(
|
||||||
|
`${apiUrl}/api/rpc/command/delete-file`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/transit+json',
|
||||||
|
cookie: authToken,
|
||||||
|
credentials: 'include',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
'~:id': fileId,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return deleteFileRequest;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { Shape } from '../models/shape.model';
|
||||||
|
|
||||||
|
export function cleanId(id: string) {
|
||||||
|
return id.replace('~u', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function idObjectToArray(obj: Shape[], newId: string) {
|
||||||
|
return Object.values(obj).map((item) => {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
id: newId,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { FileRpc } from '../models/file-rpc.model';
|
||||||
|
import { cleanId } from './clean-id';
|
||||||
|
|
||||||
|
export function getFileUrl(file: FileRpc) {
|
||||||
|
const projectId = cleanId(file['~:project-id']);
|
||||||
|
const fileId = cleanId(file['~:id']);
|
||||||
|
const pageId = cleanId(file['~:data']['~:pages'][0]);
|
||||||
|
|
||||||
|
return `http://localhost:3449/#/workspace/${projectId}/${fileId}?page-id=${pageId}`;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"useDefineForClassFields": false,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"types": [
|
||||||
|
"vitest/globals",
|
||||||
|
"vitest/importMeta",
|
||||||
|
"vite/client",
|
||||||
|
"node",
|
||||||
|
"vitest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"vite.config.ts",
|
||||||
|
"vitest.config.ts",
|
||||||
|
"src/**/*.ts",
|
||||||
|
"../../libs/plugin-types/index.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
/// <reference types='vitest' />
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
root: __dirname,
|
||||||
|
cacheDir: '../../node_modules/.vite/e2e',
|
||||||
|
test: {
|
||||||
|
testTimeout: 20000,
|
||||||
|
watch: false,
|
||||||
|
globals: true,
|
||||||
|
cache: {
|
||||||
|
dir: '../node_modules/.vitest',
|
||||||
|
},
|
||||||
|
environment: 'happy-dom',
|
||||||
|
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
||||||
|
reporters: ['default'],
|
||||||
|
coverage: {
|
||||||
|
reportsDirectory: '../coverage/e2e',
|
||||||
|
provider: 'v8',
|
||||||
|
},
|
||||||
|
setupFiles: ['dotenv/config'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"presets": ["@nx/js/babel"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"jsc": {
|
||||||
|
"parser": {
|
||||||
|
"syntax": "typescript"
|
||||||
|
},
|
||||||
|
"target": "es2016"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
import baseConfig from '../../eslint.config.js';
|
||||||
|
import globals from 'globals';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
...baseConfig,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
project: './tsconfig.*?.json',
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
||||||
|
rules: {},
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.ts', '**/*.tsx'],
|
||||||
|
rules: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.js', '**/*.jsx'],
|
||||||
|
rules: {},
|
||||||
|
},
|
||||||
|
{ ignores: ['vite.config.ts'] },
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Penpot plugin styles</title>
|
||||||
|
<base href="/" />
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
|
<link rel="stylesheet" href="/src/styles.css" />
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<app-root></app-root>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/xml.min.js"></script>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"name": "example-styles",
|
||||||
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||||
|
"projectType": "application",
|
||||||
|
"sourceRoot": "apps/example-styles/src",
|
||||||
|
"tags": ["type:app"],
|
||||||
|
"targets": {}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 7.5 KiB |
|
|
@ -0,0 +1,123 @@
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
background: var(--app-black);
|
||||||
|
color: var(--app-white);
|
||||||
|
margin: 0 var(--spacing-20);
|
||||||
|
}
|
||||||
|
|
||||||
|
app-root {
|
||||||
|
display: block;
|
||||||
|
padding-block: var(--spacing-16);
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
border-bottom: 1px solid var(--app-white);
|
||||||
|
padding-block: var(--spacing-32);
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
padding-block-end: 0;
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-group {
|
||||||
|
border-radius: var(--spacing-20);
|
||||||
|
margin-block-end: var(--spacing-20);
|
||||||
|
padding: var(--spacing-32);
|
||||||
|
|
||||||
|
&[data-theme='dark'] {
|
||||||
|
border: 1px solid var(--db-quaternary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-theme='light'] {
|
||||||
|
border: 1px solid var(--lb-quaternary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.component-section {
|
||||||
|
margin-block-end: var(--spacing-40);
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-block-end: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-margin {
|
||||||
|
margin-block-end: var(--spacing-16);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* COLOR */
|
||||||
|
.color-section {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: calc(var(--spacing-40) * 2);
|
||||||
|
list-style: none;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-block-end: var(--spacing-16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-preview {
|
||||||
|
block-size: var(--spacing-36);
|
||||||
|
border: 1px solid #8f9da3;
|
||||||
|
border-radius: var(--spacing-4);
|
||||||
|
display: block;
|
||||||
|
inline-size: var(--spacing-36);
|
||||||
|
margin-inline-start: var(--spacing-16);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SPACING */
|
||||||
|
.spacing-group {
|
||||||
|
display: grid;
|
||||||
|
margin-block-end: var(--spacing-16);
|
||||||
|
grid-template-columns: calc(var(--spacing-4) * 30) 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spacing-label {
|
||||||
|
margin-inline-end: var(--spacing-24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spacing-preview {
|
||||||
|
background-color: var(--error-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* INPUT / BUTTONS */
|
||||||
|
.inputs-list,
|
||||||
|
.button-list {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-20);
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-block-end: var(--spacing-20);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ICON */
|
||||||
|
.icons-section {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-section {
|
||||||
|
.form-group {
|
||||||
|
min-inline-size: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SWITCH */
|
||||||
|
.switch-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-8);
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-12);
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,36 @@
|
||||||
|
import html from './app.element.html?raw';
|
||||||
|
import 'plugins-styles/lib/styles.css';
|
||||||
|
import './app.element.css';
|
||||||
|
|
||||||
|
export class AppElement extends HTMLElement {
|
||||||
|
public static observedAttributes = [];
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.innerHTML = html;
|
||||||
|
|
||||||
|
Array.from(this.querySelectorAll('template')).forEach((el: HTMLElement) => {
|
||||||
|
const pre = document.createElement('pre');
|
||||||
|
const code = document.createElement('code');
|
||||||
|
code.classList.add('language-html');
|
||||||
|
const removeLineIndentation = el.innerHTML.replaceAll(
|
||||||
|
this.getIndentationSize(el.innerHTML),
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
|
||||||
|
code.textContent = removeLineIndentation;
|
||||||
|
|
||||||
|
pre.appendChild(code);
|
||||||
|
|
||||||
|
el.parentNode?.appendChild(pre);
|
||||||
|
el.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
(window as any).hljs.highlightAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
getIndentationSize(str: string) {
|
||||||
|
const size = str.length - str.trimStart().length;
|
||||||
|
return ' '.repeat(size - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define('app-root', AppElement);
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue