mirror of
https://github.com/penpot/penpot.git
synced 2025-12-29 17:39:13 -05:00
Compare commits
4 Commits
develop
...
plugins-in
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72ee0ba409 | ||
|
|
56d610ddfc | ||
|
|
7895b8579b | ||
|
|
4564a43bc4 |
45
.github/workflows/tests.yml
vendored
45
.github/workflows/tests.yml
vendored
@@ -51,6 +51,49 @@ jobs:
|
||||
run: |
|
||||
./scripts/test
|
||||
|
||||
test-plugins:
|
||||
name: Plugins Runtime Linter & Tests
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
id: setup-node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
|
||||
- name: Install deps
|
||||
working-directory: ./plugins
|
||||
run: npm ci
|
||||
shell: bash
|
||||
|
||||
- name: Run Lint
|
||||
working-directory: ./plugins
|
||||
run: npm run lint
|
||||
|
||||
- name: Run Format Check
|
||||
working-directory: ./plugins
|
||||
run: npm run format:check
|
||||
|
||||
- name: Run Test
|
||||
working-directory: ./plugins
|
||||
run: npm run test
|
||||
|
||||
- name: Build runtime
|
||||
working-directory: ./plugins
|
||||
run: npm run build
|
||||
|
||||
- name: Build plugins
|
||||
working-directory: ./plugins
|
||||
run: npm run build:plugins
|
||||
|
||||
- name: Build styles
|
||||
working-directory: ./plugins
|
||||
run: npm run build:styles-example
|
||||
|
||||
test-frontend:
|
||||
name: "Frontend Tests"
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -67,6 +110,8 @@ jobs:
|
||||
|
||||
- name: Component Tests
|
||||
working-directory: ./frontend
|
||||
env:
|
||||
VITEST_BROWSER_TIMEOUT: 120000
|
||||
run: |
|
||||
./scripts/test-components
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
/** @type { import('@storybook/react-vite').StorybookConfig } */
|
||||
const config = {
|
||||
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
|
||||
@@ -5,18 +7,38 @@ const config = {
|
||||
addons: [
|
||||
"@storybook/addon-themes",
|
||||
"@storybook/addon-docs",
|
||||
"@storybook/addon-vitest"
|
||||
"@storybook/addon-vitest",
|
||||
],
|
||||
core: {
|
||||
builder: "@storybook/builder-vite",
|
||||
options: {
|
||||
viteConfigPath: "../vite.config.js",
|
||||
},
|
||||
},
|
||||
framework: {
|
||||
name: "@storybook/react-vite",
|
||||
options: {},
|
||||
options: {
|
||||
// fastRefresh: false,
|
||||
}
|
||||
},
|
||||
docs: {},
|
||||
|
||||
async viteFinal(config) {
|
||||
return defineConfig({
|
||||
...config,
|
||||
plugins: [
|
||||
...(config.plugins ?? []),
|
||||
{
|
||||
name: 'force-full-reload-always',
|
||||
apply: 'serve',
|
||||
enforce: 'post',
|
||||
|
||||
handleHotUpdate(ctx) {
|
||||
ctx.server.ws.send({
|
||||
type: 'full-reload',
|
||||
path: '*',
|
||||
});
|
||||
|
||||
// returning [] tells Vite: “no modules handled”
|
||||
return [];
|
||||
},
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
};
|
||||
export default config;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { withThemeByClassName } from "@storybook/addon-themes";
|
||||
|
||||
|
||||
import Components from "@target/components";
|
||||
import translations from "@public/translation.en.js";
|
||||
Components.setDefaultTranslations(translations);
|
||||
|
||||
@@ -58,15 +58,16 @@
|
||||
"@penpot/plugins-runtime": "1.3.2",
|
||||
"@penpot/svgo": "penpot/svgo#v3.2",
|
||||
"@penpot/text-editor": "portal:./text-editor",
|
||||
"@playwright/test": "1.52.0",
|
||||
"@storybook/addon-docs": "10.0.4",
|
||||
"@storybook/addon-themes": "10.0.4",
|
||||
"@storybook/addon-vitest": "10.0.4",
|
||||
"@storybook/react-vite": "10.0.4",
|
||||
"@playwright/test": "1.57.0",
|
||||
"@storybook/addon-docs": "10.1.11",
|
||||
"@storybook/addon-themes": "10.1.11",
|
||||
"@storybook/addon-vitest": "10.1.11",
|
||||
"@storybook/react-vite": "10.1.11",
|
||||
"@tokens-studio/sd-transforms": "1.2.11",
|
||||
"@types/node": "^22.15.21",
|
||||
"@vitest/browser": "3.2.4",
|
||||
"@vitest/coverage-v8": "3.2.4",
|
||||
"@types/node": "^22.19.3",
|
||||
"@vitest/browser": "4.0.16",
|
||||
"@vitest/browser-playwright": "^4.0.16",
|
||||
"@vitest/coverage-v8": "4.0.16",
|
||||
"@zip.js/zip.js": "patch:@zip.js/zip.js@npm%3A2.7.60#~/.yarn/patches/@zip.js-zip.js-npm-2.7.60-b6b814410b.patch",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"compression": "^1.8.1",
|
||||
@@ -80,7 +81,7 @@
|
||||
"gettext-parser": "^8.0.0",
|
||||
"highlight.js": "^11.10.0",
|
||||
"js-beautify": "^1.15.4",
|
||||
"jsdom": "^27.0.0",
|
||||
"jsdom": "^27.4.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"map-stream": "0.0.7",
|
||||
@@ -109,15 +110,16 @@
|
||||
"sass-embedded": "^1.89.0",
|
||||
"sax": "^1.4.1",
|
||||
"source-map-support": "^0.5.21",
|
||||
"storybook": "10.0.4",
|
||||
"storybook": "10.1.11",
|
||||
"style-dictionary": "5.0.0-rc.1",
|
||||
"svg-sprite": "^2.0.4",
|
||||
"tdigest": "^0.1.2",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"typescript": "^5.9.2",
|
||||
"ua-parser-js": "2.0.5",
|
||||
"vite": "^6.3.5",
|
||||
"vitest": "^3.2.0",
|
||||
"vite": "^7.3.0",
|
||||
"vitest": "^4.0.16",
|
||||
"wait-on": "^9.0.3",
|
||||
"wasm-pack": "^0.13.1",
|
||||
"watcher": "^2.3.1",
|
||||
"workerpool": "^9.3.2",
|
||||
|
||||
@@ -7,7 +7,4 @@ yarn install;
|
||||
|
||||
yarn run playwright install chromium --with-deps;
|
||||
yarn run build:storybook
|
||||
|
||||
exec npx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \
|
||||
"npx http-server storybook-static --port 6006 --silent" \
|
||||
"npx wait-on tcp:6006 && yarn test:storybook"
|
||||
yarn run test:storybook
|
||||
|
||||
@@ -121,24 +121,22 @@
|
||||
:storybook
|
||||
{:target :esm
|
||||
:output-dir "target/storybook/"
|
||||
:devtools {:enabled false}
|
||||
:devtools {:enabled false
|
||||
:console-support false}
|
||||
:js-options
|
||||
{:js-provider :import
|
||||
:entry-keys ["module" "browser" "main"]
|
||||
:export-conditions ["module" "import", "browser" "require" "default"]}
|
||||
|
||||
:modules
|
||||
{:base
|
||||
{:entries []}
|
||||
|
||||
:components
|
||||
{:components
|
||||
{:exports {default app.main.ui.ds/default
|
||||
helpers app.main.ui.ds.helpers/default}
|
||||
:prepend-js ";(globalThis.goog.provide = globalThis.goog.constructNamespace_);(globalThis.goog.require = globalThis.goog.module.get);"
|
||||
:depends-on #{:base}}}
|
||||
:depends-on #{}}}
|
||||
|
||||
:compiler-options
|
||||
{:output-feature-set :es2020
|
||||
{:output-feature-set :es-next
|
||||
:output-wrapper false
|
||||
:warnings {:fn-deprecated false}}}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ import { defineConfig } from "vite";
|
||||
import { configDefaults } from "vitest/config";
|
||||
import { resolve } from "path";
|
||||
|
||||
import { playwright } from '@vitest/browser-playwright'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
@@ -32,11 +34,15 @@ export default defineConfig({
|
||||
browser: {
|
||||
enabled: true,
|
||||
headless: true,
|
||||
provider: "playwright",
|
||||
instances: [
|
||||
{
|
||||
browser: "chromium",
|
||||
provider: playwright({
|
||||
launchOptions: {
|
||||
slowMo: 100,
|
||||
timeout: 160000,
|
||||
},
|
||||
actionTimeout: 5000,
|
||||
}),
|
||||
instances: [
|
||||
{browser: "chromium"},
|
||||
],
|
||||
},
|
||||
setupFiles: [".storybook/vitest.setup.ts"],
|
||||
|
||||
1432
frontend/yarn.lock
1432
frontend/yarn.lock
File diff suppressed because it is too large
Load Diff
3
plugins/.env.example
Normal file
3
plugins/.env.example
Normal file
@@ -0,0 +1,3 @@
|
||||
E2E_LOGIN_EMAIL=""
|
||||
E2E_LOGIN_PASSWORD=""
|
||||
E2E_SCREENSHOTS= "false"
|
||||
55
plugins/.gitignore
vendored
Normal file
55
plugins/.gitignore
vendored
Normal file
@@ -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*
|
||||
1
plugins/.husky/commit-msg
Normal file
1
plugins/.husky/commit-msg
Normal file
@@ -0,0 +1 @@
|
||||
npx --no -- commitlint --edit $1
|
||||
5
plugins/.husky/pre-commit
Normal file
5
plugins/.husky/pre-commit
Normal file
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -z "$HUSKY_HOOK" ] || [ "$HUSKY_HOOK" = "pre-commit" ]; then
|
||||
npm run lint:affected
|
||||
fi
|
||||
5
plugins/.husky/pre-push
Normal file
5
plugins/.husky/pre-push
Normal file
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ "$HUSKY_HOOK" = "pre-push" ]; then
|
||||
npm run lint:affected
|
||||
fi
|
||||
7
plugins/.prettierignore
Normal file
7
plugins/.prettierignore
Normal file
@@ -0,0 +1,7 @@
|
||||
# Add files here to ignore them from prettier formatting
|
||||
/dist
|
||||
/coverage
|
||||
/.nx/cache
|
||||
.angular
|
||||
|
||||
/.nx/workspace-data
|
||||
3
plugins/.prettierrc
Normal file
3
plugins/.prettierrc
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"singleQuote": true
|
||||
}
|
||||
25
plugins/.verdaccio/config.yml
Normal file
25
plugins/.verdaccio/config.yml
Normal file
@@ -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
|
||||
8
plugins/.vscode/extensions.json
vendored
Normal file
8
plugins/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"nrwl.angular-console",
|
||||
"esbenp.prettier-vscode",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"firsttris.vscode-jest-runner"
|
||||
]
|
||||
}
|
||||
3
plugins/.vscode/settings.json
vendored
Normal file
3
plugins/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"prettier.singleQuote": true
|
||||
}
|
||||
221
plugins/CHANGELOG.md
Normal file
221
plugins/CHANGELOG.md
Normal file
@@ -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
|
||||
134
plugins/CONTRIBUTING.md
Normal file
134
plugins/CONTRIBUTING.md
Normal file
@@ -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).
|
||||
382
plugins/LICENSE
Normal file
382
plugins/LICENSE
Normal file
@@ -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.
|
||||
82
plugins/README.md
Normal file
82
plugins/README.md
Normal file
@@ -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/)
|
||||
51
plugins/apps/colors-to-tokens-plugin/eslint.config.js
Normal file
51
plugins/apps/colors-to-tokens-plugin/eslint.config.js
Normal file
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
79
plugins/apps/colors-to-tokens-plugin/project.json
Normal file
79
plugins/apps/colors-to-tokens-plugin/project.json
Normal file
@@ -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;
|
||||
}
|
||||
183
plugins/apps/colors-to-tokens-plugin/src/app/app.component.ts
Normal file
183
plugins/apps/colors-to-tokens-plugin/src/app/app.component.ts
Normal file
@@ -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;
|
||||
}
|
||||
BIN
plugins/apps/colors-to-tokens-plugin/src/assets/icon.png
Normal file
BIN
plugins/apps/colors-to-tokens-plugin/src/assets/icon.png
Normal file
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"]
|
||||
}
|
||||
12
plugins/apps/colors-to-tokens-plugin/src/index.html
Normal file
12
plugins/apps/colors-to-tokens-plugin/src/index.html
Normal file
@@ -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>
|
||||
7
plugins/apps/colors-to-tokens-plugin/src/main.ts
Normal file
7
plugins/apps/colors-to-tokens-plugin/src/main.ts
Normal file
@@ -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),
|
||||
);
|
||||
53
plugins/apps/colors-to-tokens-plugin/src/model.ts
Normal file
53
plugins/apps/colors-to-tokens-plugin/src/model.ts
Normal file
@@ -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;
|
||||
50
plugins/apps/colors-to-tokens-plugin/src/plugin.ts
Normal file
50
plugins/apps/colors-to-tokens-plugin/src/plugin.ts
Normal file
@@ -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
plugins/apps/colors-to-tokens-plugin/src/styles.css
Normal file
0
plugins/apps/colors-to-tokens-plugin/src/styles.css
Normal file
10
plugins/apps/colors-to-tokens-plugin/tsconfig.app.json
Normal file
10
plugins/apps/colors-to-tokens-plugin/tsconfig.app.json
Normal file
@@ -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": []
|
||||
}
|
||||
}
|
||||
33
plugins/apps/colors-to-tokens-plugin/tsconfig.json
Normal file
33
plugins/apps/colors-to-tokens-plugin/tsconfig.json
Normal file
@@ -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"]
|
||||
}
|
||||
26
plugins/apps/colors-to-tokens-plugin/tsconfig.spec.json
Normal file
26
plugins/apps/colors-to-tokens-plugin/tsconfig.spec.json
Normal file
@@ -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"
|
||||
]
|
||||
}
|
||||
21
plugins/apps/colors-to-tokens-plugin/vite.config.ts
Normal file
21
plugins/apps/colors-to-tokens-plugin/vite.config.ts
Normal file
@@ -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',
|
||||
},
|
||||
},
|
||||
});
|
||||
51
plugins/apps/contrast-plugin/eslint.config.js
Normal file
51
plugins/apps/contrast-plugin/eslint.config.js
Normal file
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
79
plugins/apps/contrast-plugin/project.json
Normal file
79
plugins/apps/contrast-plugin/project.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
97
plugins/apps/contrast-plugin/src/app/app.component.css
Normal file
97
plugins/apps/contrast-plugin/src/app/app.component.css
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
236
plugins/apps/contrast-plugin/src/app/app.component.ts
Normal file
236
plugins/apps/contrast-plugin/src/app/app.component.ts
Normal file
@@ -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, '*');
|
||||
}
|
||||
}
|
||||
6
plugins/apps/contrast-plugin/src/app/app.config.ts
Normal file
6
plugins/apps/contrast-plugin/src/app/app.config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { ApplicationConfig } from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [provideRouter([])],
|
||||
};
|
||||
BIN
plugins/apps/contrast-plugin/src/assets/icon.png
Normal file
BIN
plugins/apps/contrast-plugin/src/assets/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.5 KiB |
7
plugins/apps/contrast-plugin/src/assets/manifest.json
Normal file
7
plugins/apps/contrast-plugin/src/assets/manifest.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "Contrast",
|
||||
"description": "Measure contrast plugin",
|
||||
"code": "/assets/plugin.js",
|
||||
"icon": "/assets/icon.png",
|
||||
"permissions": ["content:read"]
|
||||
}
|
||||
12
plugins/apps/contrast-plugin/src/index.html
Normal file
12
plugins/apps/contrast-plugin/src/index.html
Normal file
@@ -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>
|
||||
7
plugins/apps/contrast-plugin/src/main.ts
Normal file
7
plugins/apps/contrast-plugin/src/main.ts
Normal file
@@ -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),
|
||||
);
|
||||
29
plugins/apps/contrast-plugin/src/model.ts
Normal file
29
plugins/apps/contrast-plugin/src/model.ts
Normal file
@@ -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;
|
||||
55
plugins/apps/contrast-plugin/src/plugin.ts
Normal file
55
plugins/apps/contrast-plugin/src/plugin.ts
Normal file
@@ -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
plugins/apps/contrast-plugin/src/styles.css
Normal file
0
plugins/apps/contrast-plugin/src/styles.css
Normal file
10
plugins/apps/contrast-plugin/tsconfig.app.json
Normal file
10
plugins/apps/contrast-plugin/tsconfig.app.json
Normal file
@@ -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"]
|
||||
}
|
||||
7
plugins/apps/contrast-plugin/tsconfig.editor.json
Normal file
7
plugins/apps/contrast-plugin/tsconfig.editor.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": ["src/**/*.ts"],
|
||||
"compilerOptions": {
|
||||
"types": []
|
||||
}
|
||||
}
|
||||
33
plugins/apps/contrast-plugin/tsconfig.json
Normal file
33
plugins/apps/contrast-plugin/tsconfig.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
8
plugins/apps/contrast-plugin/tsconfig.plugin.json
Normal file
8
plugins/apps/contrast-plugin/tsconfig.plugin.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"types": []
|
||||
},
|
||||
"files": ["src/plugin.ts"],
|
||||
"include": ["../../libs/plugin-types/index.d.ts"]
|
||||
}
|
||||
20
plugins/apps/contrast-plugin/vite.config.ts
Normal file
20
plugins/apps/contrast-plugin/vite.config.ts
Normal file
@@ -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',
|
||||
},
|
||||
},
|
||||
});
|
||||
3
plugins/apps/create-palette-plugin/.babelrc
Normal file
3
plugins/apps/create-palette-plugin/.babelrc
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"presets": ["@nx/js/babel"]
|
||||
}
|
||||
8
plugins/apps/create-palette-plugin/.swcrc
Normal file
8
plugins/apps/create-palette-plugin/.swcrc
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"jsc": {
|
||||
"parser": {
|
||||
"syntax": "typescript"
|
||||
},
|
||||
"target": "es2016"
|
||||
}
|
||||
}
|
||||
26
plugins/apps/create-palette-plugin/eslint.config.js
Normal file
26
plugins/apps/create-palette-plugin/eslint.config.js
Normal file
@@ -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'] },
|
||||
];
|
||||
2
plugins/apps/create-palette-plugin/index.html
Normal file
2
plugins/apps/create-palette-plugin/index.html
Normal file
@@ -0,0 +1,2 @@
|
||||
<!doctype html>
|
||||
<html lang="en"></html>
|
||||
8
plugins/apps/create-palette-plugin/project.json
Normal file
8
plugins/apps/create-palette-plugin/project.json
Normal file
@@ -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": {}
|
||||
}
|
||||
BIN
plugins/apps/create-palette-plugin/public/assets/icon.png
Normal file
BIN
plugins/apps/create-palette-plugin/public/assets/icon.png
Normal file
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"]
|
||||
}
|
||||
104
plugins/apps/create-palette-plugin/src/plugin.ts
Normal file
104
plugins/apps/create-palette-plugin/src/plugin.ts
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
9
plugins/apps/create-palette-plugin/tsconfig.app.json
Normal file
9
plugins/apps/create-palette-plugin/tsconfig.app.json
Normal file
@@ -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"]
|
||||
}
|
||||
30
plugins/apps/create-palette-plugin/tsconfig.json
Normal file
30
plugins/apps/create-palette-plugin/tsconfig.json
Normal file
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
26
plugins/apps/create-palette-plugin/tsconfig.spec.json
Normal file
26
plugins/apps/create-palette-plugin/tsconfig.spec.json
Normal file
@@ -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"
|
||||
]
|
||||
}
|
||||
58
plugins/apps/create-palette-plugin/vite.config.ts
Normal file
58
plugins/apps/create-palette-plugin/vite.config.ts
Normal file
@@ -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',
|
||||
},
|
||||
},
|
||||
});
|
||||
32
plugins/apps/e2e/eslint.config.js
Normal file
32
plugins/apps/e2e/eslint.config.js
Normal file
@@ -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'] },
|
||||
];
|
||||
8
plugins/apps/e2e/project.json
Normal file
8
plugins/apps/e2e/project.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "e2e",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"projectType": "application",
|
||||
"implicitDependencies": [],
|
||||
"tags": ["type:e2e"],
|
||||
"targets": {}
|
||||
}
|
||||
0
plugins/apps/e2e/screenshots/.gitkeep
Normal file
0
plugins/apps/e2e/screenshots/.gitkeep
Normal file
2421
plugins/apps/e2e/src/__snapshots__/plugins.spec.ts.snap
Normal file
2421
plugins/apps/e2e/src/__snapshots__/plugins.spec.ts.snap
Normal file
File diff suppressed because it is too large
Load Diff
17
plugins/apps/e2e/src/models/file-rpc.model.ts
Normal file
17
plugins/apps/e2e/src/models/file-rpc.model.ts
Normal file
@@ -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[];
|
||||
};
|
||||
}
|
||||
7
plugins/apps/e2e/src/models/shape.model.ts
Normal file
7
plugins/apps/e2e/src/models/shape.model.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface Shape {
|
||||
id: string;
|
||||
frameId?: string;
|
||||
parentId?: string;
|
||||
shapes?: string[];
|
||||
layoutGridCells?: Shape[];
|
||||
}
|
||||
92
plugins/apps/e2e/src/plugins.spec.ts
Normal file
92
plugins/apps/e2e/src/plugins.spec.ts
Normal file
@@ -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();
|
||||
});
|
||||
});
|
||||
10
plugins/apps/e2e/src/plugins/component-library.ts
Normal file
10
plugins/apps/e2e/src/plugins/component-library.ts
Normal file
@@ -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]);
|
||||
}
|
||||
}
|
||||
58
plugins/apps/e2e/src/plugins/create-board-text-rect.ts
Normal file
58
plugins/apps/e2e/src/plugins/create-board-text-rect.ts
Normal file
@@ -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');
|
||||
}
|
||||
40
plugins/apps/e2e/src/plugins/create-comments.ts
Normal file
40
plugins/apps/e2e/src/plugins/create-comments.ts
Normal file
@@ -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();
|
||||
}
|
||||
26
plugins/apps/e2e/src/plugins/create-flexlayout.ts
Normal file
26
plugins/apps/e2e/src/plugins/create-flexlayout.ts
Normal file
@@ -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();
|
||||
}
|
||||
27
plugins/apps/e2e/src/plugins/create-gridlayout.ts
Normal file
27
plugins/apps/e2e/src/plugins/create-gridlayout.ts
Normal file
@@ -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();
|
||||
}
|
||||
21
plugins/apps/e2e/src/plugins/create-ruler-guides.ts
Normal file
21
plugins/apps/e2e/src/plugins/create-ruler-guides.ts
Normal file
@@ -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();
|
||||
}
|
||||
23
plugins/apps/e2e/src/plugins/create-text.ts
Normal file
23
plugins/apps/e2e/src/plugins/create-text.ts
Normal file
@@ -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();
|
||||
}
|
||||
29
plugins/apps/e2e/src/plugins/group.ts
Normal file
29
plugins/apps/e2e/src/plugins/group.ts
Normal file
@@ -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();
|
||||
}
|
||||
21
plugins/apps/e2e/src/plugins/insert-svg.ts
Normal file
21
plugins/apps/e2e/src/plugins/insert-svg.ts
Normal file
@@ -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);
|
||||
}
|
||||
6
plugins/apps/e2e/src/plugins/plugin-data.ts
Normal file
6
plugins/apps/e2e/src/plugins/plugin-data.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export default function () {
|
||||
const rectangle = penpot.createRectangle();
|
||||
|
||||
rectangle?.setPluginData('testData', 'test');
|
||||
return rectangle?.getPluginData('testData');
|
||||
}
|
||||
167
plugins/apps/e2e/src/utils/agent.ts
Normal file
167
plugins/apps/e2e/src/utils/agent.ts
Normal file
@@ -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,
|
||||
};
|
||||
}
|
||||
85
plugins/apps/e2e/src/utils/api.ts
Normal file
85
plugins/apps/e2e/src/utils/api.ts
Normal file
@@ -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;
|
||||
},
|
||||
};
|
||||
}
|
||||
14
plugins/apps/e2e/src/utils/clean-id.ts
Normal file
14
plugins/apps/e2e/src/utils/clean-id.ts
Normal file
@@ -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,
|
||||
};
|
||||
});
|
||||
}
|
||||
10
plugins/apps/e2e/src/utils/get-file-url.ts
Normal file
10
plugins/apps/e2e/src/utils/get-file-url.ts
Normal file
@@ -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}`;
|
||||
}
|
||||
27
plugins/apps/e2e/tsconfig.json
Normal file
27
plugins/apps/e2e/tsconfig.json
Normal file
@@ -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"
|
||||
]
|
||||
}
|
||||
23
plugins/apps/e2e/vite.config.ts
Normal file
23
plugins/apps/e2e/vite.config.ts
Normal file
@@ -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'],
|
||||
},
|
||||
});
|
||||
3
plugins/apps/example-styles/.babelrc
Normal file
3
plugins/apps/example-styles/.babelrc
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"presets": ["@nx/js/babel"]
|
||||
}
|
||||
8
plugins/apps/example-styles/.swcrc
Normal file
8
plugins/apps/example-styles/.swcrc
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"jsc": {
|
||||
"parser": {
|
||||
"syntax": "typescript"
|
||||
},
|
||||
"target": "es2016"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user