diff --git a/plugins/README.md b/plugins/README.md
index 399aa37943..22f4556166 100644
--- a/plugins/README.md
+++ b/plugins/README.md
@@ -19,7 +19,7 @@ In the `apps` folder you'll find some examples that use the libraries mentioned
- example-styles: to run this example you should run
```
-npm run start:styles-example
+pnpm run start:styles-example
```
Open in your browser: `http://localhost:4202/`
@@ -28,8 +28,8 @@ Open in your browser: `http://localhost:4202/`
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.
+In the terminal, navigate to the **penpot-plugins** repository and run `pnpm install` to install the required dependencies.
+Then, run `pnpm run 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.
@@ -38,7 +38,7 @@ For instance, to launch the Contrast plugin, use the following command:
```
// for the contrast plugin
-npm run start:plugin:contrast
+pnpm run start:plugin:contrast
```
Finally, open in your browser the specific port. In this specific example would be `http://localhost:4302`
@@ -49,21 +49,22 @@ A table listing the available plugins and their corresponding startup commands i
| 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 |
+| poc-state-plugin | Sandbox plugin to test new plugins api functionality | 4301 | pnpm run start:plugin:poc-state | http://localhost:4301/assets/manifest.json |
+| contrast-plugin | Sample plugin that gives you color contrast information | 4302 | pnpm run start:plugin:contrast | http://localhost:4302/assets/manifest.json |
+| icons-plugin | Tool to add icons from [Feather](https://feathericons.com/) | 4303 | pnpm run start:plugin:icons | http://localhost:4303/assets/manifest.json |
+| lorem-ipsum-plugin | Generate Lorem ipsum text | 4304 | pnpm run start:plugin:loremipsum | http://localhost:4304/assets/manifest.json |
+| create-palette-plugin | Creates a board with all the palette colors | 4305 | pnpm run start:plugin:palette | http://localhost:4305/assets/manifest.json |
+| table-plugin | Create or import table | 4306 | pnpm run start:table-plugin | http://localhost:4306/assets/manifest.json |
+| rename-layers-plugin | Rename layers in bulk | 4307 | pnpm run start:plugin:renamelayers | http://localhost:4307/assets/manifest.json |
+| colors-to-tokens-plugin | Generate tokens JSON file | 4308 | pnpm run start:plugin:colors-to-tokens | http://localhost:4308/assets/manifest.json |
+| poc-tokens-plugin | Sandbox plugin to test tokens functionality | 4309 | pnpm run start:plugin:poc-tokens | http://localhost:4309/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/ |
+| plugins-runtime | Runtime for the plugins subsystem | 4200 | pnpm run start:app:runtime | |
+| example-styles | Showcase of some of the Penpot styles that can be used in plugins | 4201 | pnpm run start:app:styles-example | http://localhost:4201/ |
## Creating a plugin from scratch
diff --git a/plugins/apps/poc-tokens-plugin/eslint.config.js b/plugins/apps/poc-tokens-plugin/eslint.config.js
new file mode 100644
index 0000000000..7aa90c2ab0
--- /dev/null
+++ b/plugins/apps/poc-tokens-plugin/eslint.config.js
@@ -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,
+ },
+ },
+ },
+];
diff --git a/plugins/apps/poc-tokens-plugin/project.json b/plugins/apps/poc-tokens-plugin/project.json
new file mode 100644
index 0000000000..c2bc7cad81
--- /dev/null
+++ b/plugins/apps/poc-tokens-plugin/project.json
@@ -0,0 +1,79 @@
+{
+ "name": "poc-tokens-plugin",
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "prefix": "app",
+ "sourceRoot": "apps/poc-tokens-plugin/src",
+ "tags": ["type:plugin"],
+ "targets": {
+ "build": {
+ "executor": "@angular-devkit/build-angular:application",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/apps/poc-tokens-plugin",
+ "index": "apps/poc-tokens-plugin/src/index.html",
+ "browser": "apps/poc-tokens-plugin/src/main.ts",
+ "polyfills": ["zone.js"],
+ "tsConfig": "apps/poc-tokens-plugin/tsconfig.app.json",
+ "assets": [
+ "apps/poc-tokens-plugin/src/favicon.ico",
+ "apps/poc-tokens-plugin/src/assets"
+ ],
+ "styles": [
+ "libs/plugins-styles/src/lib/styles.css",
+ "apps/poc-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": "poc-tokens-plugin:build:production"
+ },
+ "development": {
+ "buildTarget": "poc-tokens-plugin:build:development",
+ "port": 4309,
+ "host": "0.0.0.0"
+ }
+ },
+ "defaultConfiguration": "development"
+ },
+ "extract-i18n": {
+ "executor": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "buildTarget": "poc-tokens-plugin:build"
+ }
+ }
+ }
+}
diff --git a/plugins/apps/poc-tokens-plugin/src/app/app.component.css b/plugins/apps/poc-tokens-plugin/src/app/app.component.css
new file mode 100644
index 0000000000..9a64100273
--- /dev/null
+++ b/plugins/apps/poc-tokens-plugin/src/app/app.component.css
@@ -0,0 +1,127 @@
+/* @import "@penpot/plugin-styles/styles.css"; */
+
+.container {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+}
+
+.title-l {
+ margin: var(--spacing-16) 0;
+}
+
+.columns {
+ display: grid;
+ grid-template-columns: 50% 50%;
+ flex-grow: 1;
+ margin-block-end: var(--spacing-16);
+}
+
+.panels {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+ padding: 0 var(--spacing-8);
+}
+
+.panel {
+ padding: var(--spacing-8);
+ display: flex;
+ flex-basis: 0;
+ flex-grow: 1;
+ flex-direction: column;
+ overflow: auto;
+}
+
+.panel:not(:first-child) {
+ border-block-start: 1px solid var(--df-secondary);
+ padding-block-start: var(--spacing-16);
+}
+
+.panel-heading,
+.token-group {
+ display: flex;
+ flex-direction: row;
+ padding-inline-end: var(--spacing-8);
+}
+
+.panel-heading p,
+.token-group span {
+ flex-grow: 1;
+}
+
+.panel-heading button,
+.token-group button {
+ background: none;
+ padding: var(--spacing-4) calc(var(--spacing-12) / 2);
+}
+
+.panel-heading button:focus,
+.token-group button:focus {
+ padding: calc(var(--spacing-4) - 2px) calc(var(--spacing-12) / 2 - 2px);
+}
+
+.panel-item button {
+ opacity: 0;
+ margin-inline-end: var(--spacing-8);
+ padding: var(--spacing-4) calc(var(--spacing-12) / 2);
+}
+
+.panel-item button:hover {
+ opacity: 1;
+}
+
+.panel-item button:focus {
+ opacity: 1;
+ padding: calc(var(--spacing-4) - 2px) calc(var(--spacing-12) / 2 - 2px);
+}
+
+.panel ul {
+ /* flex-grow: 1; */
+ overflow-y: auto;
+ padding-inline-end: var(--spacing-8);
+}
+
+.panel-item {
+ display: flex;
+ flex-direction: row;
+}
+
+.panel-item span {
+ flex-grow: 1;
+}
+
+.set-item {
+ cursor: pointer;
+}
+
+.set-item.selected {
+ background-color: var(--db-quaternary);
+}
+
+.set-item:hover {
+ color: var(--da-primary);
+ background-color: var(--db-secondary);
+}
+
+.token-group:not(:first-child) {
+ margin-top: var(--spacing-8);
+}
+
+.token-group {
+ border-block-end: 1px solid var(--df-secondary);
+ text-transform: capitalize;
+}
+
+.token-item {
+ cursor: pointer;
+}
+
+.token-item:hover {
+ color: var(--da-primary);
+}
+
+.buttons {
+ display: flex;
+ flex-direction: row-reverse;
+}
diff --git a/plugins/apps/poc-tokens-plugin/src/app/app.component.html b/plugins/apps/poc-tokens-plugin/src/app/app.component.html
new file mode 100644
index 0000000000..5be709cb57
--- /dev/null
+++ b/plugins/apps/poc-tokens-plugin/src/app/app.component.html
@@ -0,0 +1,144 @@
+
+
Design tokens plugin POC
+
+
+
+
+
+
THEMES
+
+
+
+
+ @for (theme of themes; track theme.id) {
+
+ {{ theme.group }} / {{ theme.name }}
+
+
+
+
+
+
+ }
+
+
+
+
+
+
SETS
+
+
+
+
+ @for (set of sets; track set.id) {
+
+
+ {{ set.name }}
+
+
+
+
+
+
+
+ }
+
+
+
+
+
+
TOKENS
+
+
+ @for (group of tokenGroups; track group[0]) {
+
+ {{ group[0] }}
+
+
+ @for (token of group[1]; track token.id) {
+
+ {{ token.name }}
+
+
+
+ }
+ }
+
+
+
+
+
+
+
+
+
diff --git a/plugins/apps/poc-tokens-plugin/src/app/app.component.ts b/plugins/apps/poc-tokens-plugin/src/app/app.component.ts
new file mode 100644
index 0000000000..63995292f6
--- /dev/null
+++ b/plugins/apps/poc-tokens-plugin/src/app/app.component.ts
@@ -0,0 +1,290 @@
+import { Component, inject } from '@angular/core';
+import { toSignal } from '@angular/core/rxjs-interop';
+import { ActivatedRoute } from '@angular/router';
+import { fromEvent, map, filter, take, merge } from 'rxjs';
+import { PluginMessageEvent, PluginUIEvent } from '../model';
+
+type TokenTheme = {
+ id: string;
+ name: string;
+ group: string;
+ description: string;
+ active: boolean;
+};
+
+type TokenSet = {
+ id: string;
+ name: string;
+ description: string;
+ active: boolean;
+};
+
+type Token = {
+ id: string;
+ name: string;
+ description: string;
+};
+
+type TokensGroup = [string, Token[]];
+
+@Component({
+ selector: 'app-root',
+ templateUrl: './app.component.html',
+ styleUrl: './app.component.css',
+ host: {
+ '[attr.data-theme]': 'theme()',
+ },
+})
+export class AppComponent {
+ public route = inject(ActivatedRoute);
+
+ public messages$ = fromEvent>(
+ window,
+ 'message',
+ );
+
+ public initialTheme$ = this.route.queryParamMap.pipe(
+ map((params) => params.get('theme')),
+ filter((theme) => !!theme),
+ take(1),
+ );
+
+ public theme = toSignal(
+ merge(
+ this.initialTheme$,
+ this.messages$.pipe(
+ filter((event) => event.data.type === 'theme'),
+ map((event) => {
+ return event.data.content;
+ }),
+ ),
+ ),
+ );
+
+ public themes: TokenTheme[] = [];
+ public sets: TokenSet[] = [];
+ public tokenGroups: TokensGroup[] = [];
+ public currentSetId: string | undefined = undefined;
+
+ constructor() {
+ window.addEventListener('message', (event) => {
+ if (event.data.type === 'set-themes') {
+ this.#setThemes(event.data.themesData);
+ } else if (event.data.type === 'set-sets') {
+ this.#setSets(event.data.setsData);
+ } else if (event.data.type === 'set-tokens') {
+ this.#setTokens(event.data.tokenGroupsData);
+ }
+ });
+ }
+
+ loadLibrary() {
+ this.#sendMessage({ type: 'load-library' });
+ }
+
+ loadTokens(setId: string) {
+ this.currentSetId = setId;
+ this.#sendMessage({ type: 'load-tokens', setId });
+ }
+
+ addTheme() {
+ this.#sendMessage({
+ type: 'add-theme',
+ themeGroup: this.#randomString(),
+ themeName: this.#randomString(),
+ });
+ }
+
+ addSet() {
+ this.#sendMessage({ type: 'add-set', setName: this.#randomString() });
+ }
+
+ addToken(tokenType: string) {
+ let tokenValue;
+ switch (tokenType) {
+ case 'borderRadius':
+ tokenValue = 25;
+ break;
+ case 'shadow':
+ tokenValue = [
+ {
+ color: '#123456',
+ inset: 'false',
+ offsetX: '6',
+ offsetY: '6',
+ spread: '0',
+ blur: '4',
+ },
+ ];
+ break;
+ case 'color':
+ tokenValue = '#fabada';
+ break;
+ case 'dimension':
+ tokenValue = 100;
+ break;
+ case 'fontFamilies':
+ tokenValue = ['Source Sans Pro', 'Sans serif'];
+ break;
+ case 'fontSizes':
+ tokenValue = 24;
+ break;
+ case 'fontWeights':
+ tokenValue = 'bold';
+ break;
+ case 'letterSpacing':
+ tokenValue = 0.5;
+ break;
+ case 'number':
+ tokenValue = 33;
+ break;
+ case 'opacity':
+ tokenValue = 0.6;
+ break;
+ case 'rotation':
+ tokenValue = 45;
+ break;
+ case 'sizing':
+ tokenValue = 200;
+ break;
+ case 'spacing':
+ tokenValue = 16;
+ break;
+ case 'borderWidth':
+ tokenValue = 3;
+ break;
+ case 'textCase':
+ tokenValue = 'lowercase';
+ break;
+ case 'textDecoration':
+ tokenValue = 'underline';
+ break;
+ case 'typography':
+ tokenValue = {
+ fontFamilies: ['Acme', 'Arial', 'Sans Serif'],
+ fontSizes: '36',
+ letterSpacing: '0.8',
+ textCase: 'uppercase',
+ textDecoration: 'none',
+ fontWeights: '600',
+ lineHeight: '1.5',
+ };
+ break;
+ }
+
+ if (this.currentSetId && tokenValue) {
+ this.#sendMessage({
+ type: 'add-token',
+ setId: this.currentSetId,
+ tokenType,
+ tokenName: this.#randomString(),
+ tokenValue,
+ });
+ } else {
+ console.log('Invalid token type');
+ }
+ }
+
+ renameTheme(themeId: string, themeName: string) {
+ const newName = prompt('Rename theme', themeName);
+ if (newName && newName !== '') {
+ this.#sendMessage({ type: 'rename-theme', themeId, newName });
+ }
+ }
+
+ renameSet(setId: string, setName: string) {
+ const newName = prompt('Rename set', setName);
+ if (newName && newName !== '') {
+ this.#sendMessage({ type: 'rename-set', setId, newName });
+ }
+ }
+
+ renameToken(tokenId: string, tokenName: string) {
+ const newName = prompt('Rename token', tokenName);
+ if (this.currentSetId && newName && newName !== '') {
+ this.#sendMessage({
+ type: 'rename-token',
+ setId: this.currentSetId,
+ tokenId,
+ newName,
+ });
+ }
+ }
+
+ deleteTheme(themeId: string) {
+ this.#sendMessage({ type: 'delete-theme', themeId });
+ }
+
+ deleteSet(setId: string) {
+ this.#sendMessage({ type: 'delete-set', setId });
+ }
+
+ deleteToken(tokenId: string) {
+ if (this.currentSetId) {
+ this.#sendMessage({
+ type: 'delete-token',
+ setId: this.currentSetId,
+ tokenId,
+ });
+ }
+ }
+
+ isThemeActive(themeId: string) {
+ for (const theme of this.themes) {
+ if (theme.id === themeId) {
+ return theme.active;
+ }
+ }
+ return false;
+ }
+
+ toggleTheme(themeId: string) {
+ this.#sendMessage({ type: 'toggle-theme', themeId });
+ }
+
+ isSetActive(setId: string) {
+ for (const set of this.sets) {
+ if (set.id === setId) {
+ return set.active;
+ }
+ }
+ return false;
+ }
+
+ toggleSet(setId: string) {
+ this.#sendMessage({ type: 'toggle-set', setId });
+ }
+
+ applyToken(tokenId: string) {
+ if (this.currentSetId) {
+ this.#sendMessage({
+ type: 'apply-token',
+ setId: this.currentSetId,
+ tokenId,
+ // attributes: ['stroke-color'] // Uncomment to choose attribute to apply
+ }); // (incompatible attributes will have no effect)
+ }
+ }
+
+ #sendMessage(message: PluginUIEvent) {
+ parent.postMessage(message, '*');
+ }
+
+ #setThemes(themes: TokenTheme[]) {
+ this.themes = themes;
+ }
+
+ #setSets(sets: TokenSet[]) {
+ this.sets = sets;
+ }
+
+ #setTokens(tokenGroups: TokensGroup[]) {
+ this.tokenGroups = tokenGroups;
+ }
+
+ #randomString() {
+ // Generate a big random number and convert it to string using base 36
+ // (the number of letters in the ascii alphabet)
+ return Math.floor(Math.random() * Date.now()).toString(36);
+ }
+}
diff --git a/plugins/apps/poc-tokens-plugin/src/app/app.config.ts b/plugins/apps/poc-tokens-plugin/src/app/app.config.ts
new file mode 100644
index 0000000000..fb93f472fd
--- /dev/null
+++ b/plugins/apps/poc-tokens-plugin/src/app/app.config.ts
@@ -0,0 +1,11 @@
+import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
+import { provideRouter } from '@angular/router';
+
+import { routes } from './app.routes';
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ provideZoneChangeDetection({ eventCoalescing: true }),
+ provideRouter(routes),
+ ],
+};
diff --git a/plugins/apps/poc-tokens-plugin/src/app/app.routes.ts b/plugins/apps/poc-tokens-plugin/src/app/app.routes.ts
new file mode 100644
index 0000000000..dc39edb5f2
--- /dev/null
+++ b/plugins/apps/poc-tokens-plugin/src/app/app.routes.ts
@@ -0,0 +1,3 @@
+import { Routes } from '@angular/router';
+
+export const routes: Routes = [];
diff --git a/plugins/apps/poc-tokens-plugin/src/assets/CORS b/plugins/apps/poc-tokens-plugin/src/assets/CORS
new file mode 100644
index 0000000000..72e8ffc0db
--- /dev/null
+++ b/plugins/apps/poc-tokens-plugin/src/assets/CORS
@@ -0,0 +1 @@
+*
diff --git a/plugins/apps/poc-tokens-plugin/src/assets/_headers b/plugins/apps/poc-tokens-plugin/src/assets/_headers
new file mode 100644
index 0000000000..c776a4792b
--- /dev/null
+++ b/plugins/apps/poc-tokens-plugin/src/assets/_headers
@@ -0,0 +1,2 @@
+/*
+ Access-Control-Allow-Origin: *
diff --git a/plugins/apps/poc-tokens-plugin/src/assets/favicon.ico b/plugins/apps/poc-tokens-plugin/src/assets/favicon.ico
new file mode 100644
index 0000000000..fc5e208af4
Binary files /dev/null and b/plugins/apps/poc-tokens-plugin/src/assets/favicon.ico differ
diff --git a/plugins/apps/poc-tokens-plugin/src/assets/icon.png b/plugins/apps/poc-tokens-plugin/src/assets/icon.png
new file mode 100644
index 0000000000..cf045fb5e6
Binary files /dev/null and b/plugins/apps/poc-tokens-plugin/src/assets/icon.png differ
diff --git a/plugins/apps/poc-tokens-plugin/src/assets/manifest.json b/plugins/apps/poc-tokens-plugin/src/assets/manifest.json
new file mode 100644
index 0000000000..540167f2f9
--- /dev/null
+++ b/plugins/apps/poc-tokens-plugin/src/assets/manifest.json
@@ -0,0 +1,14 @@
+{
+ "name": "Design tokens plugin POC",
+ "description": "This is a plugin to try Design Tokens in Penpot API",
+ "code": "/assets/plugin.js",
+ "permissions": [
+ "page:read",
+ "content:read",
+ "file:read",
+ "selection:read",
+ "content:write",
+ "library:read",
+ "library:write"
+ ]
+}
diff --git a/plugins/apps/poc-tokens-plugin/src/index.html b/plugins/apps/poc-tokens-plugin/src/index.html
new file mode 100644
index 0000000000..c285210e33
--- /dev/null
+++ b/plugins/apps/poc-tokens-plugin/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ Angular example plugin
+
+
+
+
+
+
+
+
diff --git a/plugins/apps/poc-tokens-plugin/src/main.ts b/plugins/apps/poc-tokens-plugin/src/main.ts
new file mode 100644
index 0000000000..8882c4517f
--- /dev/null
+++ b/plugins/apps/poc-tokens-plugin/src/main.ts
@@ -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),
+);
diff --git a/plugins/apps/poc-tokens-plugin/src/model.ts b/plugins/apps/poc-tokens-plugin/src/model.ts
new file mode 100644
index 0000000000..8f76935892
--- /dev/null
+++ b/plugins/apps/poc-tokens-plugin/src/model.ts
@@ -0,0 +1,112 @@
+import { TokenProperty } from '@penpot/plugin-types';
+
+/**
+ * This file contains the typescript interfaces for the plugin events.
+ */
+
+// Events sent from the ui to the plugin
+
+export interface LoadLibraryEvent {
+ type: 'load-library';
+}
+
+export interface LoadTokensEvent {
+ type: 'load-tokens';
+ setId: string;
+}
+
+export interface AddThemeEvent {
+ type: 'add-theme';
+ themeGroup: string;
+ themeName: string;
+}
+
+export interface AddSetEvent {
+ type: 'add-set';
+ setName: string;
+}
+
+export interface AddTokenEvent {
+ type: 'add-token';
+ setId: string;
+ tokenType: string;
+ tokenName: string;
+ tokenValue: unknown;
+}
+
+export interface RenameThemeEvent {
+ type: 'rename-theme';
+ themeId: string;
+ newName: string;
+}
+
+export interface RenameSetEvent {
+ type: 'rename-set';
+ setId: string;
+ newName: string;
+}
+
+export interface RenameTokenEvent {
+ type: 'rename-token';
+ setId: string;
+ tokenId: string;
+ newName: string;
+}
+
+export interface DeleteThemeEvent {
+ type: 'delete-theme';
+ themeId: string;
+}
+
+export interface DeleteSetEvent {
+ type: 'delete-set';
+ setId: string;
+}
+
+export interface DeleteTokenEvent {
+ type: 'delete-token';
+ setId: string;
+ tokenId: string;
+}
+
+export interface ToggleThemeEvent {
+ type: 'toggle-theme';
+ themeId: string;
+}
+
+export interface ToggleSetEvent {
+ type: 'toggle-set';
+ setId: string;
+}
+
+export interface ApplyTokenEvent {
+ type: 'apply-token';
+ setId: string;
+ tokenId: string;
+ attributes?: TokenProperty[];
+}
+
+export type PluginUIEvent =
+ | LoadLibraryEvent
+ | LoadTokensEvent
+ | AddThemeEvent
+ | AddSetEvent
+ | AddTokenEvent
+ | RenameThemeEvent
+ | RenameSetEvent
+ | RenameTokenEvent
+ | DeleteThemeEvent
+ | DeleteSetEvent
+ | DeleteTokenEvent
+ | ToggleThemeEvent
+ | ToggleSetEvent
+ | ApplyTokenEvent;
+
+// Events sent from the plugin to the ui
+
+export interface ThemePluginEvent {
+ type: 'theme';
+ content: string;
+}
+
+export type PluginMessageEvent = ThemePluginEvent;
diff --git a/plugins/apps/poc-tokens-plugin/src/plugin.ts b/plugins/apps/poc-tokens-plugin/src/plugin.ts
new file mode 100644
index 0000000000..cccf6f678e
--- /dev/null
+++ b/plugins/apps/poc-tokens-plugin/src/plugin.ts
@@ -0,0 +1,246 @@
+import type { PluginMessageEvent, PluginUIEvent } from './model.js';
+import { TokenType, TokenProperty } from '@penpot/plugin-types';
+
+penpot.ui.open('Design Tokens test', `?theme=${penpot.theme}`, {
+ width: 1000,
+ height: 800,
+});
+
+penpot.on('themechange', (theme) => {
+ sendMessage({ type: 'theme', content: theme });
+});
+
+penpot.ui.onMessage(async (message) => {
+ if (message.type === 'load-library') {
+ loadLibrary();
+ } else if (message.type === 'load-tokens') {
+ loadTokens(message.setId);
+ } else if (message.type === 'add-theme') {
+ addTheme(message.themeGroup, message.themeName);
+ } else if (message.type === 'add-set') {
+ addSet(message.setName);
+ } else if (message.type === 'add-token') {
+ addToken(
+ message.setId,
+ message.tokenType,
+ message.tokenName,
+ message.tokenValue,
+ );
+ } else if (message.type === 'rename-theme') {
+ renameTheme(message.themeId, message.newName);
+ } else if (message.type === 'rename-set') {
+ renameSet(message.setId, message.newName);
+ } else if (message.type === 'rename-token') {
+ renameToken(message.setId, message.tokenId, message.newName);
+ } else if (message.type === 'delete-theme') {
+ deleteTheme(message.themeId);
+ } else if (message.type === 'delete-set') {
+ deleteSet(message.setId);
+ } else if (message.type === 'delete-token') {
+ deleteToken(message.setId, message.tokenId);
+ } else if (message.type === 'toggle-theme') {
+ toggleTheme(message.themeId);
+ } else if (message.type === 'toggle-set') {
+ toggleSet(message.setId);
+ } else if (message.type === 'apply-token') {
+ applyToken(message.setId, message.tokenId, message.attributes);
+ }
+});
+
+function sendMessage(message: PluginMessageEvent) {
+ penpot.ui.sendMessage(message);
+}
+
+function loadLibrary() {
+ const tokensCatalog = penpot.library.local.tokens;
+
+ const themes = tokensCatalog.themes;
+
+ const themesData = themes.map((theme) => {
+ return {
+ id: theme.id,
+ group: theme.group,
+ name: theme.name,
+ active: theme.active,
+ };
+ });
+
+ penpot.ui.sendMessage({
+ source: 'penpot',
+ type: 'set-themes',
+ themesData,
+ });
+
+ const sets = tokensCatalog.sets;
+
+ const setsData = sets.map((set) => {
+ return {
+ id: set.id,
+ name: set.name,
+ active: set.active,
+ };
+ });
+
+ penpot.ui.sendMessage({
+ source: 'penpot',
+ type: 'set-sets',
+ setsData,
+ });
+}
+
+function loadTokens(setId: string) {
+ const tokensCatalog = penpot.library.local.tokens;
+ const set = tokensCatalog?.getSetById(setId);
+ const tokensByType = set?.tokensByType;
+
+ const tokenGroupsData = [];
+ if (tokensByType) {
+ for (const group of tokensByType) {
+ const type = group[0];
+ const tokens = group[1];
+ tokenGroupsData.push([
+ type,
+ tokens.map((token) => {
+ return {
+ id: token.id,
+ name: token.name,
+ description: token.description,
+ };
+ }),
+ ]);
+ }
+
+ penpot.ui.sendMessage({
+ source: 'penpot',
+ type: 'set-tokens',
+ tokenGroupsData,
+ });
+ }
+}
+
+function addTheme(themeGroup: string, themeName: string) {
+ const tokensCatalog = penpot.library.local.tokens;
+ const theme = tokensCatalog?.addTheme(themeGroup, themeName);
+ if (theme) {
+ loadLibrary();
+ }
+}
+
+function addSet(setName: string) {
+ const tokensCatalog = penpot.library.local.tokens;
+ const set = tokensCatalog?.addSet(setName);
+ if (set) {
+ loadLibrary();
+ }
+}
+
+function addToken(
+ setId: string,
+ tokenType: string,
+ tokenName: string,
+ tokenValue: unknown,
+) {
+ const tokensCatalog = penpot.library.local.tokens;
+ const set = tokensCatalog?.getSetById(setId);
+ const token = set?.addToken(tokenType as TokenType, tokenName, tokenValue);
+ if (token) {
+ loadTokens(setId);
+ }
+}
+
+function renameTheme(themeId: string, newName: string) {
+ const tokensCatalog = penpot.library.local.tokens;
+ const theme = tokensCatalog?.getThemeById(themeId);
+ if (theme) {
+ theme.name = newName;
+ loadLibrary();
+ }
+}
+
+function renameSet(setId: string, newName: string) {
+ const tokensCatalog = penpot.library.local.tokens;
+ const set = tokensCatalog?.getSetById(setId);
+ if (set) {
+ set.name = newName;
+ loadLibrary();
+ }
+}
+
+function renameToken(setId: string, tokenId: string, newName: string) {
+ const tokensCatalog = penpot.library.local.tokens;
+ const set = tokensCatalog?.getSetById(setId);
+ const token = set?.getTokenById(tokenId);
+ if (token) {
+ token.name = newName;
+ loadTokens(setId);
+ }
+}
+
+function deleteTheme(themeId: string) {
+ const tokensCatalog = penpot.library.local.tokens;
+ const theme = tokensCatalog?.getThemeById(themeId);
+ if (theme) {
+ theme.remove();
+ loadLibrary();
+ }
+}
+
+function deleteSet(setId: string) {
+ const tokensCatalog = penpot.library.local.tokens;
+ const set = tokensCatalog?.getSetById(setId);
+ if (set) {
+ set.remove();
+ loadLibrary();
+ }
+}
+
+function deleteToken(setId: string, tokenId: string) {
+ const tokensCatalog = penpot.library.local.tokens;
+ const set = tokensCatalog?.getSetById(setId);
+ const token = set?.getTokenById(tokenId);
+ if (token) {
+ token.remove();
+ loadTokens(setId);
+ }
+}
+
+function toggleTheme(themeId: string) {
+ const tokensCatalog = penpot.library.local.tokens;
+ const theme = tokensCatalog?.getThemeById(themeId);
+ if (theme) {
+ theme.toggleActive();
+ loadLibrary();
+ }
+}
+
+function toggleSet(setId: string) {
+ const tokensCatalog = penpot.library.local.tokens;
+ const set = tokensCatalog?.getSetById(setId);
+ if (set) {
+ set.toggleActive();
+ loadLibrary();
+ }
+}
+
+function applyToken(
+ setId: string,
+ tokenId: string,
+ attributes: TokenProperty[] | undefined,
+) {
+ const tokensCatalog = penpot.library.local.tokens;
+ const set = tokensCatalog?.getSetById(setId);
+ const token = set?.getTokenById(tokenId);
+
+ if (token) {
+ token.applyToSelected(attributes);
+ }
+
+ // Alternatve way
+ //
+ // const selection = penpot.selection;
+ // if (token && selection) {
+ // for (const shape of selection) {
+ // shape.applyToken(token, attributes);
+ // }
+ // }
+}
diff --git a/plugins/apps/poc-tokens-plugin/src/styles.css b/plugins/apps/poc-tokens-plugin/src/styles.css
new file mode 100644
index 0000000000..007341e2f7
--- /dev/null
+++ b/plugins/apps/poc-tokens-plugin/src/styles.css
@@ -0,0 +1,23 @@
+/* @import "@penpot/plugin-styles/styles.css"; */
+
+html {
+ height: 100%;
+}
+
+body {
+ height: 100%;
+ line-height: 1.5;
+ padding: 10px;
+}
+
+ul {
+ margin-block-start: var(--spacing-12);
+}
+
+.title-l {
+ text-align: center;
+}
+
+.headline-l {
+ margin-block-start: var(--spacing-8);
+}
diff --git a/plugins/apps/poc-tokens-plugin/tsconfig.app.json b/plugins/apps/poc-tokens-plugin/tsconfig.app.json
new file mode 100644
index 0000000000..936913d9af
--- /dev/null
+++ b/plugins/apps/poc-tokens-plugin/tsconfig.app.json
@@ -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"]
+}
diff --git a/plugins/apps/poc-tokens-plugin/tsconfig.editor.json b/plugins/apps/poc-tokens-plugin/tsconfig.editor.json
new file mode 100644
index 0000000000..b927bb69fc
--- /dev/null
+++ b/plugins/apps/poc-tokens-plugin/tsconfig.editor.json
@@ -0,0 +1,7 @@
+{
+ "extends": "./tsconfig.json",
+ "include": ["src/**/*.ts"],
+ "compilerOptions": {
+ "types": ["node"]
+ }
+}
diff --git a/plugins/apps/poc-tokens-plugin/tsconfig.json b/plugins/apps/poc-tokens-plugin/tsconfig.json
new file mode 100644
index 0000000000..4c48587cfe
--- /dev/null
+++ b/plugins/apps/poc-tokens-plugin/tsconfig.json
@@ -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
+ }
+}
diff --git a/plugins/apps/poc-tokens-plugin/tsconfig.plugin.json b/plugins/apps/poc-tokens-plugin/tsconfig.plugin.json
new file mode 100644
index 0000000000..961987f7a1
--- /dev/null
+++ b/plugins/apps/poc-tokens-plugin/tsconfig.plugin.json
@@ -0,0 +1,8 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "types": []
+ },
+ "files": ["src/plugin.ts"],
+ "include": ["../../libs/plugin-types/index.d.ts"]
+}
diff --git a/plugins/libs/plugin-types/index.d.ts b/plugins/libs/plugin-types/index.d.ts
index ff56cb2490..57b29ff591 100644
--- a/plugins/libs/plugin-types/index.d.ts
+++ b/plugins/libs/plugin-types/index.d.ts
@@ -2,10 +2,8 @@
* These are methods and properties available on the `penpot` global object.
*
*/
-export interface Penpot extends Omit<
- Context,
- 'addListener' | 'removeListener'
-> {
+export interface Penpot
+ extends Omit {
ui: {
/**
* Opens the plugin UI. It is possible to develop a plugin without interface (see Palette color example) but if you need, the way to open this UI is using `penpot.ui.open`.
@@ -2533,6 +2531,13 @@ export interface Library extends PluginData {
*/
readonly components: LibraryComponent[];
+ /**
+ * A catalog of Design Tokens in the library.
+ *
+ * See `TokenCatalog` type to see usage.
+ */
+ readonly tokens: TokenCatalog;
+
/**
* Creates a new color element in the library.
* @return Returns a new `LibraryColor` object representing the created color element.
@@ -2800,9 +2805,9 @@ export interface LibraryTypography extends LibraryElement {
fontId: string;
/**
- * The font family of the typography element.
+ * The font families of the typography element.
*/
- fontFamily: string;
+ fontFamilies: string;
/**
* The unique identifier of the font variant used in the typography element.
@@ -3728,6 +3733,17 @@ export interface ShapeBase extends PluginData {
*/
setParentIndex(index: number): void;
+ /**
+ * The design tokens applied to this shape.
+ * It's a map property name -> token name.
+ *
+ * NOTE that the tokens application is by name and not by id. If there exist
+ * several tokens with the same name in different sets, the actual token applied
+ * and the value set to the attributes will depend on which sets are active
+ * (and will change if different sets or themes are activated later).
+ */
+ readonly tokens: { [property: string]: string };
+
/**
* @return Returns true if the current shape is inside a component instance
*/
@@ -3892,6 +3908,19 @@ export interface ShapeBase extends PluginData {
*/
removeInteraction(interaction: Interaction): void;
+ /**
+ * Applies one design token to one or more properties of the shape.
+ * @param token is the Token to apply
+ * @param properties an optional list of property names. If omitted, the
+ * default properties will be applied.
+ *
+ * NOTE that the tokens application is by name and not by id. If there exist
+ * several tokens with the same name in different sets, the actual token applied
+ * and the value set to the attributes will depend on which sets are active
+ * (and will change if different sets or themes are activated later).
+ */
+ applyToken(token: Token, properties: TokenProperty[] | undefined): void;
+
/**
* Creates a clone of the shape.
* @return Returns a new instance of the shape with identical properties.
@@ -4279,6 +4308,1033 @@ export type TrackType = 'flex' | 'fixed' | 'percent' | 'auto';
*/
export type Trigger = 'click' | 'mouse-enter' | 'mouse-leave' | 'after-delay';
+/**
+ * Represents the base properties and methods of a Design Token in Penpot, shared by
+ * all token types.
+ */
+export interface TokenBase {
+ /**
+ * The unique identifier for this token, used only internally inside Penpot.
+ * This one is not exported or synced with external Design Token sources.
+ */
+ readonly id: string;
+
+ /**
+ * The name of the token. It may include a group path separated by `.`.
+ */
+ name: string;
+
+ /**
+ * An optional description text.
+ */
+ description: string;
+
+ /**
+ * Adds to the set that contains this Token a new one equal to this one
+ * but with a new id.
+ */
+ duplicate(): Token;
+
+ /**
+ * Removes this token from the catalog.
+ *
+ * It will NOT be unapplied from any shape, since there may be other tokens
+ * with the same name.
+ */
+ remove(): void;
+
+ /**
+ * Applies this token to one or more properties of the given shapes.
+ * @param shapes is an array of shapes to apply it.
+ * @param properties an optional list of property names. If omitted, the
+ * default properties will be applied.
+ *
+ * NOTE that the tokens application is by name and not by id. If there exist
+ * several tokens with the same name in different sets, the actual token applied
+ * and the value set to the attributes will depend on which sets are active
+ * (and will change if different sets or themes are activated later).
+ */
+ applyToShapes(shapes: Shape[], properties: TokenProperty[] | undefined): void;
+
+ /**
+ * Applies this token to the currently selected shapes.
+ *
+ * Parameters and warnings are the same as above.
+ */
+ applyToSelected(properties: TokenProperty[] | undefined): void;
+}
+
+/**
+ * Represents a token of type BorderRadius.
+ * This interface extends `TokenBase` and specifies the data type of the value.
+ */
+export interface TokenBorderRadius extends TokenBase {
+ /**
+ * The type of the token.
+ */
+ readonly type: 'borderRadius';
+
+ /**
+ * The value as defined in the token itself.
+ * It's a positive number or a reference.
+ */
+ value: string;
+
+ /**
+ * The value calculated by finding all tokens with the same name in active sets
+ * and resolving the references.
+ *
+ * It's a positive number, or undefined if no value has been found in active sets.
+ */
+ readonly resolvedValue: number | undefined;
+}
+
+/*
+ * The value of a TokenShadow in its composite form.
+ */
+export interface TokenShadowValue {
+ /**
+ * The color as a string (e.g. "#FF5733").
+ */
+ color: string;
+
+ /**
+ * If the shadow is inset or drop.
+ */
+ inset: boolean;
+
+ /**
+ * The horizontal offset of the shadow in pixels.
+ */
+ offsetX: number;
+
+ /**
+ * The vertical offset of the shadow in pixels.
+ */
+ offsetY: number;
+
+ /**
+ * The spread distance of the shadow in pixels.
+ */
+ spread: number;
+
+ /**
+ * The amount of blur to apply to the shadow.
+ */
+ blur: number;
+}
+
+/*
+ * The value of a TokenShadow in its composite of strings form.
+ */
+export interface TokenShadowValueString {
+ /**
+ * The color as a string (e.g. "#FF5733"), or a reference
+ * to a color token.
+ */
+ color: string;
+
+ /**
+ * If the shadow is inset or drop, or a reference of a
+ * boolean token.
+ */
+ inset: string;
+
+ /**
+ * The horizontal offset of the shadow in pixels, or a reference
+ * to a number token.
+ */
+ offsetX: string;
+
+ /**
+ * The vertical offset of the shadow in pixels, or a reference
+ * to a number token.
+ */
+ offsetY: string;
+
+ /**
+ * The spread distance of the shadow in pixels, or a reference
+ * to a number token.
+ */
+ spread: string;
+
+ /**
+ * The amount of blur to apply to the shadow, or a reference
+ * to a number token.
+ */
+ blur: string;
+}
+
+/**
+ * Represents a token of type Shadow.
+ * This interface extends `TokenBase` and specifies the data type of the value.
+ */
+export interface TokenShadow extends TokenBase {
+ /**
+ * The type of the token.
+ */
+ readonly type: 'shadow';
+
+ /**
+ * The value as defined in the token itself.
+ * It may be a string with a reference to other token, or else
+ * an array of TokenShadowValueString.
+ */
+ value: string | TokenShadowValueString[];
+
+ /**
+ * The value calculated by finding all tokens with the same name in active sets
+ * and resolving the references.
+ *
+ * It's an array of TokenShadowValue, or undefined if no value has been found
+ * in active sets.
+ */
+ readonly resolvedValue: TokenShadowValue[] | undefined;
+}
+
+/**
+ * Represents a token of type Color.
+ * This interface extends `TokenBase` and specifies the data type of the value.
+ */
+export interface TokenColor extends TokenBase {
+ /**
+ * The type of the token.
+ */
+ readonly type: 'color';
+
+ /**
+ * The value as defined in the token itself.
+ * It's a rgb color or a reference.
+ */
+ value: string;
+
+ /**
+ * The value as defined in the token itself.
+ * It's a rgb color or a reference.
+ */
+ readonly resolvedValue: string | undefined;
+}
+
+/**
+ * Represents a token of type Dimension.
+ * This interface extends `TokenBase` and specifies the data type of the value.
+ */
+export interface TokenDimension extends TokenBase {
+ /**
+ * The type of the token.
+ */
+ readonly type: 'dimension';
+
+ /**
+ * The value as defined in the token itself.
+ * It's a positive number or a reference.
+ */
+ value: string;
+
+ /**
+ * The value calculated by finding all tokens with the same name in active sets
+ * and resolving the references.
+ *
+ * It's a positive number, or undefined if no value has been found in active sets.
+ */
+ readonly resolvedValue: number | undefined;
+}
+
+/**
+ * Represents a token of type FontFamilies.
+ * This interface extends `TokenBase` and specifies the data type of the value.
+ */
+export interface TokenFontFamilies extends TokenBase {
+ /**
+ * The type of the token.
+ */
+ readonly type: 'fontFamilies';
+
+ /**
+ * The value as defined in the token itself.
+ * It may be a string with a reference to other token, or else
+ * an array of strings with one or more font families (each family
+ * is an item in the array).
+ */
+ value: string | string[];
+
+ /**
+ * The value calculated by finding all tokens with the same name in active sets
+ * and resolving the references.
+ *
+ * It's an array of strings with one or more font families,
+ * or undefined if no value has been found in active sets.
+ */
+ readonly resolvedValue: string[] | undefined;
+}
+
+/**
+ * Represents a token of type FontSizes.
+ * This interface extends `TokenBase` and specifies the data type of the value.
+ */
+export interface TokenFontSizes extends TokenBase {
+ /**
+ * The type of the token.
+ */
+ readonly type: 'fontSizes';
+
+ /**
+ * The value as defined in the token itself.
+ * It's a positive number or a reference.
+ */
+ value: string;
+
+ /**
+ * The value calculated by finding all tokens with the same name in active sets
+ * and resolving the references.
+ *
+ * It's a positive number, or undefined if no value has been found in active sets.
+ */
+ readonly resolvedValue: number | undefined;
+}
+
+/**
+ * Represents a token of type FontWeights.
+ * This interface extends `TokenBase` and specifies the data type of the value.
+ */
+export interface TokenFontWeights extends TokenBase {
+ /**
+ * The type of the token.
+ */
+ readonly type: 'fontWeights';
+
+ /**
+ * The value as defined in the token itself.
+ * It's a weight string or a reference.
+ */
+ value: string;
+
+ /**
+ * The value calculated by finding all tokens with the same name in active sets
+ * and resolving the references.
+ *
+ * It's a weight string ("bold", "strong", etc.), or undefined if no value has
+ * been found in active sets.
+ */
+ readonly resolvedValue: string | undefined;
+}
+
+/**
+ * Represents a token of type LetterSpacing.
+ * This interface extends `TokenBase` and specifies the data type of the value.
+ */
+export interface TokenLetterSpacing extends TokenBase {
+ /**
+ * The type of the token.
+ */
+ readonly type: 'letterSpacing';
+
+ /**
+ * The value as defined in the token itself.
+ * It's a number or a reference.
+ */
+ value: string;
+
+ /**
+ * The value calculated by finding all tokens with the same name in active sets
+ * and resolving the references.
+ *
+ * It's a number, or undefined if no value has been found in active sets.
+ */
+ readonly resolvedValue: number | undefined;
+}
+
+/**
+ * Represents a token of type Number.
+ * This interface extends `TokenBase` and specifies the data type of the value.
+ */
+export interface TokenNumber extends TokenBase {
+ /**
+ * The type of the token.
+ */
+ readonly type: 'number';
+
+ /**
+ * The value as defined in the token itself.
+ * It's a number or a reference.
+ */
+ value: string;
+
+ /**
+ * The value calculated by finding all tokens with the same name in active sets
+ * and resolving the references.
+ *
+ * It's a number, or undefined if no value has been found in active sets.
+ */
+ readonly resolvedValue: number | undefined;
+}
+
+/**
+ * Represents a token of type Opacity.
+ * This interface extends `TokenBase` and specifies the data type of the value.
+ */
+export interface TokenOpacity extends TokenBase {
+ /**
+ * The type of the token.
+ */
+ readonly type: 'opacity';
+
+ /**
+ * The value as defined in the token itself.
+ * It's a number between 0 and 1 or a reference.
+ */
+ value: string;
+
+ /**
+ * The value calculated by finding all tokens with the same name in active sets
+ * and resolving the references.
+ *
+ * It's a number between 0 and 1, or undefined if no value has been found
+ * in active sets.
+ */
+ readonly resolvedValue: number | undefined;
+}
+
+/**
+ * Represents a token of type Rotation.
+ * This interface extends `TokenBase` and specifies the data type of the value.
+ */
+export interface TokenRotation extends TokenBase {
+ /**
+ * The type of the token.
+ */
+ readonly type: 'rotation';
+
+ /**
+ * The value as defined in the token itself.
+ * It's a number in degrees or a reference.
+ */
+ value: string;
+
+ /**
+ * The value calculated by finding all tokens with the same name in active sets
+ * and resolving the references.
+ *
+ * It's a number in degrees, or undefined if no value has been found
+ * in active sets.
+ */
+ readonly resolvedValue: number | undefined;
+}
+
+/**
+ * Represents a token of type Sizing.
+ * This interface extends `TokenBase` and specifies the data type of the value.
+ */
+export interface TokenSizing extends TokenBase {
+ /**
+ * The type of the token.
+ */
+ readonly type: 'sizing';
+
+ /**
+ * The value as defined in the token itself.
+ * It's a number or a reference.
+ */
+ value: string;
+
+ /**
+ * The value calculated by finding all tokens with the same name in active sets
+ * and resolving the references.
+ *
+ * It's a number, or undefined if no value has been found in active sets.
+ */
+ readonly resolvedValue: number | undefined;
+}
+
+/**
+ * Represents a token of type Spacing.
+ * This interface extends `TokenBase` and specifies the data type of the value.
+ */
+export interface TokenSpacing extends TokenBase {
+ /**
+ * The type of the token.
+ */
+ readonly type: 'spacing';
+
+ /**
+ * The value as defined in the token itself.
+ * It's a number or a reference.
+ */
+ value: string;
+
+ /**
+ * The value calculated by finding all tokens with the same name in active sets
+ * and resolving the references.
+ *
+ * It's a number, or undefined if no value has been found in active sets.
+ */
+ readonly resolvedValue: number | undefined;
+}
+
+/**
+ * Represents a token of type BorderWidth.
+ * This interface extends `TokenBase` and specifies the data type of the value.
+ */
+export interface TokenBorderWidth extends TokenBase {
+ /**
+ * The type of the token.
+ */
+ readonly type: 'borderWidth';
+
+ /**
+ * The value as defined in the token itself.
+ * It's a positive number or a reference.
+ */
+ value: string;
+
+ /**
+ * The value calculated by finding all tokens with the same name in active sets
+ * and resolving the references.
+ *
+ * It's a positive number, or undefined if no value has been found in active sets.
+ */
+ readonly resolvedValue: number | undefined;
+}
+
+/**
+ * Represents a token of type TextCase.
+ * This interface extends `TokenBase` and specifies the data type of the value.
+ */
+export interface TokenTextCase extends TokenBase {
+ /**
+ * The type of the token.
+ */
+ readonly type: 'textCase';
+
+ /**
+ * The value as defined in the token itself.
+ * It's a case string or a reference.
+ */
+ value: string;
+
+ /**
+ * The value calculated by finding all tokens with the same name in active sets
+ * and resolving the references.
+ *
+ * It's a case string ("none", "uppercase", "lowercase", "capitalize"), or
+ * undefined if no value has been found in active sets.
+ */
+ readonly resolvedValue: string | undefined;
+}
+
+/**
+ * Represents a token of type Decoration.
+ * This interface extends `TokenBase` and specifies the data type of the value.
+ */
+export interface TokenTextDecoration extends TokenBase {
+ /**
+ * The type of the token.
+ */
+ readonly type: 'textDecoration';
+
+ /**
+ * The value as defined in the token itself.
+ * It's a decoration string or a reference.
+ */
+ value: string;
+
+ /**
+ * The value calculated by finding all tokens with the same name in active sets
+ * and resolving the references.
+ *
+ * It's a decoration string, or undefined if no value has been found
+ * in active sets.
+ */
+ readonly resolvedValue: string | undefined;
+}
+
+/*
+ * The value of a TokenTypography in its composite form.
+ */
+export interface TokenTypographyValue {
+ /**
+ * The letter spacing, as a number.
+ */
+ letterSpacing: number;
+
+ /**
+ * The list of font families.
+ */
+ fontFamilies: string[];
+
+ /**
+ * The font size, as a positive number.
+ */
+ fontSizes: number;
+
+ /**
+ * The font weight, as a weight string ("bold", "strong", etc.).
+ */
+ fontWeights: string;
+
+ /**
+ * The line height, as a number.
+ */
+ lineHeight: number;
+
+ /**
+ * The text case as a string ("none", "uppercase", "lowercase" "capitalize").
+ */
+ textCase: string;
+
+ /**
+ * The text decoration as a string ("none", "underline", "strike-through").
+ */
+ textDecoration: string;
+}
+
+/*
+ * The value of a TokenTypography in its composite of strings form.
+ */
+export interface TokenTypographyValueString {
+ /**
+ * The letter spacing, as a number, or a reference to a TokenLetterSpacing.
+ */
+ letterSpacing: string;
+
+ /**
+ * The list of font families, or a reference to a TokenFontFamilies.
+ */
+ fontFamilies: string | string[];
+
+ /**
+ * The font size, as a positive number, or a reference to a TokenFontSizes.
+ */
+ fontSizes: string;
+
+ /**
+ * The font weight, as a weight string ("bold", "strong", etc.), or a
+ * reference to a TokenFontWeights.
+ */
+ fontWeight: string;
+
+ /**
+ * The line height, as a number. Note that there not exists an individual
+ * token type line height, only part of a Typography token. If you need to
+ * put here a reference, use a NumberToken.
+ */
+ lineHeight: string;
+
+ /**
+ * The text case as a string ("none", "uppercase", "lowercase" "capitalize"),
+ * or a reference to a TokenTextCase.
+ */
+ textCase: string;
+
+ /**
+ * The text decoration as a string ("none", "underline", "strike-through"),
+ * or a reference to a TokenTextDecoration.
+ */
+ textDecoration: string;
+}
+
+/**
+ * Represents a token of type Typography.
+ * This interface extends `TokenBase` and specifies the data type of the value.
+ */
+export interface TokenTypography extends TokenBase {
+ /**
+ * The type of the token.
+ */
+ readonly type: 'typography';
+
+ /**
+ * The value as defined in the token itself.
+ * It may be a string with a reference to other token, or a
+ * TokenTypographyValueString.
+ */
+ value: string | TokenTypographyValueString;
+
+ /**
+ * The value calculated by finding all tokens with the same name in active sets
+ * and resolving the references.
+ *
+ * It's a TokenTypographyValue, or undefined if no value has been found
+ * in active sets.
+ */
+ readonly resolvedValue: TokenTypographyValue[] | undefined;
+}
+
+/**
+ * The supported Design Tokens in Penpot.
+ */
+export type Token =
+ | TokenBorderRadius
+ | TokenShadow
+ | TokenColor
+ | TokenDimension
+ | TokenFontFamilies
+ | TokenFontSizes
+ | TokenFontWeights
+ | TokenLetterSpacing
+ | TokenNumber
+ | TokenOpacity
+ | TokenRotation
+ | TokenSizing
+ | TokenSpacing
+ | TokenBorderWidth
+ | TokenTextCase
+ | TokenTextDecoration
+ | TokenTypography;
+
+/**
+ * The collection of all tokens in a Penpot file's library.
+ *
+ * Tokens are contained in sets, that can be marked as active
+ * or inactive to control the resolved value of the tokens.
+ *
+ * The active status of sets can be handled by presets named
+ * Themes.
+ */
+export interface TokenCatalog {
+ /**
+ * The list of themes in this catalog, in creation order.
+ */
+ readonly themes: TokenTheme[];
+
+ /**
+ * The list of sets in this catalog, in the order defined
+ * by the user. The order is important because then same token name
+ * exists in several active sets, the latter has precedence.
+ */
+ readonly sets: TokenSet[];
+
+ /**
+ * Creates a new TokenTheme and adds it to the catalog.
+ * @param group The group name of the theme (can be empty string).
+ * @param name The name of the theme (required)
+ * @return Returns the created TokenTheme.
+ */
+ addTheme(group: string, name: string): TokenTheme;
+
+ /**
+ * Creates a new TokenSet and adds it to the catalog.
+ * @param name The name of the set (required). It may contain
+ * a group path, separated by `/`.
+ * @return Returns the created TokenSet.
+ */
+ addSet(name: string): TokenSet;
+
+ /**
+ * Retrieves a theme.
+ * @param id the id of the theme.
+ * @returns Returns the theme or undefined if not found.
+ */
+ getThemeById(id: string): TokenTheme | undefined;
+
+ /**
+ * Retrieves a set.
+ * @param id the id of the set.
+ * @returns Returns the set or undefined if not found.
+ */
+ getSetById(id: string): TokenSet | undefined;
+}
+
+/**
+ * A collection of Design Tokens.
+ *
+ * Inside a set, tokens have an unique name, that will designate
+ * what token to use if the name is applied to a shape and this
+ * set is active.
+ */
+export interface TokenSet {
+ /**
+ * The unique identifier for this set, used only internally inside Penpot.
+ * This one is not exported or synced with external Design Token sources.
+ */
+ readonly id: string;
+
+ /**
+ * The name of the set. It may include a group path separated by `/`.
+ */
+ name: string;
+
+ /**
+ * Indicates if the set is currently active.
+ */
+ active: boolean;
+
+ /**
+ * The tokens contained in this set, in alphabetical order.
+ */
+ readonly tokens: Token[];
+
+ /**
+ * The tokens contained in this set, grouped by type.
+ */
+ readonly tokensByType: [string, Token[]][];
+
+ /**
+ * Toggles the active status of this set.
+ */
+ toggleActive(): void;
+
+ /**
+ * Retrieves a token.
+ * @param id the id of the token.
+ * @returns Returns the token or undefined if not found.
+ */
+ getTokenById(id: string): Token | undefined;
+
+ /**
+ * Creates a new Token and adds it to the set.
+ * @param type Thetype of token.
+ * @param name The name of the token (required). It may contain
+ * a group path, separated by `.`.
+ * @return Returns the created Token.
+ */
+ addToken(type: TokenType, name: string, value: unknown): Token;
+
+ /**
+ * Adds to the catalog a new TokenSet equal to this one but with a new id.
+ */
+ duplicate(): TokenSet;
+
+ /**
+ * Removes this set from the catalog.
+ */
+ remove(): void;
+}
+
+/**
+ * A preset of active TokenSets.
+ *
+ * A theme contains a list of references to TokenSets. When the theme
+ * is activated, it sets are activated too. This will not deactivate
+ * sets that are _not_ in this theme, because they may have been
+ * activated by other themes.
+ *
+ * Themes may be gruped. At any time only one of the themes in a group
+ * may be active. But there may be active themes in other groups. This
+ * allows to define multiple "axis" for theming (e.g. color scheme,
+ * density or brand).
+ *
+ * When a TokenSet is activated or deactivated directly, all themes
+ * are disabled (indicating that now there is a "custom" manual theme
+ * active).
+ */
+export interface TokenTheme {
+ /**
+ * The unique identifier for this theme, used only internally inside Penpot.
+ * This one is not exported or synced with external Design Token sources.
+ */
+ readonly id: string;
+
+ /**
+ * Optional identifier that may exists if the theme was imported from an
+ * external tool that uses ids in the json file.
+ */
+ readonly externalId: string | undefined;
+
+ /**
+ * The group name of the theme. Can be empt string.
+ */
+ group: string;
+
+ /**
+ * The name of the theme.
+ */
+ name: string;
+
+ /**
+ * Indicates if the theme is currently active.
+ */
+ active: boolean;
+
+ /**
+ * Toggles the active status of this theme.
+ */
+ toggleActive(): void;
+
+ /**
+ * The sets that will be activated if this theme is activated.
+ */
+ activeSets: TokenSet[];
+
+ /**
+ * Adds a set to the list of the theme.
+ */
+ addSet(tokenSet: TokenSet): void;
+
+ /**
+ * Removes a set from the list of the theme.
+ */
+ removeSet(tokenSet: TokenSet): void;
+
+ /**
+ * Adds to the catalog a new TokenTheme equal to this one but with a new id.
+ */
+ duplicate(): TokenTheme;
+
+ /**
+ * Removes this theme from the catalog.
+ */
+ remove(): void;
+}
+
+/**
+ * The properties that a BorderRadius token can be applied to.
+ */
+type TokenBorderRadiusProps = 'r1' | 'r2' | 'r3' | 'r4';
+
+/**
+ * The properties that a Shadow token can be applied to.
+ */
+type TokenShadowProps = 'shadow';
+
+/**
+ * The properties that a Color token can be applied to.
+ */
+type TokenColorProps = 'fill' | 'stroke';
+
+/**
+ * The properties that a Dimension token can be applied to.
+ */
+type TokenDimensionProps =
+ // Axis
+ | 'x'
+ | 'y'
+
+ // Stroke width
+ | 'stroke-width';
+
+/**
+ * The properties that a FontFamilies token can be applied to.
+ */
+type TokenFontFamiliesProps = 'font-families';
+
+/**
+ * The properties that a FontSizes token can be applied to.
+ */
+type TokenFontSizesProps = 'font-size';
+
+/**
+ * The properties that a FontWeight token can be applied to.
+ */
+type TokenFontWeightProps = 'font-weight';
+
+/**
+ * The properties that a LetterSpacing token can be applied to.
+ */
+type TokenLetterSpacingProps = 'letter-spacing';
+
+/**
+ * The properties that a Number token can be applied to.
+ */
+type TokenNumberProps = 'rotation' | 'line-height';
+
+/**
+ * The properties that an Opacity token can be applied to.
+ */
+type TokenOpacityProps = 'opacity';
+
+/**
+ * The properties that a Sizing token can be applied to.
+ */
+type TokenSizingProps =
+ // Size
+ | 'width'
+ | 'height'
+
+ // Layout
+ | 'layout-item-min-w'
+ | 'layout-item-max-w'
+ | 'layout-item-min-h'
+ | 'layout-item-max-h';
+
+/**
+ * The properties that a Spacing token can be applied to.
+ */
+type TokenSpacingProps =
+ // Spacing / Gap
+ | 'row-gap'
+ | 'column-gap'
+
+ // Spacing / Padding
+ | 'p1'
+ | 'p2'
+ | 'p3'
+ | 'p4'
+
+ // Spacing / Margin
+ | 'm1'
+ | 'm2'
+ | 'm3'
+ | 'm4';
+
+/**
+ * The properties that a BorderWidth token can be applied to.
+ */
+type TokenBorderWidthProps = 'stroke-width';
+
+/**
+ * The properties that a TextCase token can be applied to.
+ */
+type TokenTextCaseProps = 'text-case';
+
+/**
+ * The properties that a TextDecoration token can be applied to.
+ */
+type TokenTextDecorationProps = 'text-decoration';
+
+/**
+ * The properties that a Typography token can be applied to.
+ */
+type TokenTypographyProps = 'typography';
+
+/**
+ * All the properties that a token can be applied to.
+ * Not always correspond to Shape properties. For example,
+ * `fill` property applies to `fillColor` of the first fill
+ * of the shape.
+ *
+ */
+export type TokenProperty =
+ | 'all'
+ | TokenBorderRadiusProps
+ | TokenShadowProps
+ | TokenColorProps
+ | TokenDimensionProps
+ | TokenFontFamiliesProps
+ | TokenFontSizesProps
+ | TokenFontWeightProps
+ | TokenLetterSpacingProps
+ | TokenNumberProps
+ | TokenOpacityProps
+ | TokenSizingProps
+ | TokenSpacingProps
+ | TokenBorderWidthProps
+ | TokenTextCaseProps
+ | TokenTextDecorationProps
+ | TokenTypographyProps;
+
+/**
+ * The supported types of Design Tokens in Penpot.
+ */
+export type TokenType =
+ | 'borderRadius'
+ | 'shadow'
+ | 'color'
+ | 'dimension'
+ | 'fontFamilies'
+ | 'fontSizes'
+ | 'fontWeights'
+ | 'letterSpacing'
+ | 'number'
+ | 'opacity'
+ | 'rotation'
+ | 'sizing'
+ | 'spacing'
+ | 'borderWidth'
+ | 'textCase'
+ | 'textDecoration'
+ | 'typography';
+
/**
* Represents a user in Penpot.
*/
diff --git a/plugins/package.json b/plugins/package.json
index 722feae2a5..50a9e29f7b 100644
--- a/plugins/package.json
+++ b/plugins/package.json
@@ -16,6 +16,7 @@
"start:plugin:table": "nx run table-plugin:init",
"start:plugin:renamelayers": "nx run rename-layers-plugin:init",
"start:plugin:colors-to-tokens": "nx run colors-to-tokens-plugin:init",
+ "start:plugin:poc-tokens": "nx run poc-tokens-plugin:init",
"build": "nx build plugins-runtime --emptyOutDir=true",
"build:plugins": "nx run-many -t build --parallel -p tag:type:plugin --exclude=poc-state-plugin",
"build:styles-example": "nx run example-styles:build",