+
diff --git a/apps/poc-tokens-plugin/src/app/app.component.ts b/apps/poc-tokens-plugin/src/app/app.component.ts
new file mode 100644
index 0000000..d97ac57
--- /dev/null
+++ b/apps/poc-tokens-plugin/src/app/app.component.ts
@@ -0,0 +1,280 @@
+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 = {
+ offsetX: 6,
+ offsetY: 6,
+ blur: 4,
+ spread: 0,
+ color: '#000000',
+ inset: false,
+ }
+ 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 '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 'fontWeights':
+ tokenValue = 'bold';
+ break;
+ case 'typography':
+ tokenValue = {
+ fontFamilies: 'Acme',
+ fontSizes: 36,
+ letterSpacing: 0.8,
+ textCase: 'none',
+ textDecoration: 'none',
+ fontWeights: 600,
+ };
+ 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/apps/poc-tokens-plugin/src/app/app.config.ts b/apps/poc-tokens-plugin/src/app/app.config.ts
new file mode 100644
index 0000000..a1e7d6f
--- /dev/null
+++ b/apps/poc-tokens-plugin/src/app/app.config.ts
@@ -0,0 +1,8 @@
+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/apps/poc-tokens-plugin/src/app/app.routes.ts b/apps/poc-tokens-plugin/src/app/app.routes.ts
new file mode 100644
index 0000000..dc39edb
--- /dev/null
+++ b/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/apps/poc-tokens-plugin/src/assets/CORS b/apps/poc-tokens-plugin/src/assets/CORS
new file mode 100644
index 0000000..72e8ffc
--- /dev/null
+++ b/apps/poc-tokens-plugin/src/assets/CORS
@@ -0,0 +1 @@
+*
diff --git a/apps/poc-tokens-plugin/src/assets/_headers b/apps/poc-tokens-plugin/src/assets/_headers
new file mode 100644
index 0000000..c776a47
--- /dev/null
+++ b/apps/poc-tokens-plugin/src/assets/_headers
@@ -0,0 +1,2 @@
+/*
+ Access-Control-Allow-Origin: *
diff --git a/apps/poc-tokens-plugin/src/assets/favicon.ico b/apps/poc-tokens-plugin/src/assets/favicon.ico
new file mode 100644
index 0000000..fc5e208
Binary files /dev/null and b/apps/poc-tokens-plugin/src/assets/favicon.ico differ
diff --git a/apps/poc-tokens-plugin/src/assets/icon.png b/apps/poc-tokens-plugin/src/assets/icon.png
new file mode 100644
index 0000000..cf045fb
Binary files /dev/null and b/apps/poc-tokens-plugin/src/assets/icon.png differ
diff --git a/apps/poc-tokens-plugin/src/assets/manifest.json b/apps/poc-tokens-plugin/src/assets/manifest.json
new file mode 100644
index 0000000..b4099c7
--- /dev/null
+++ b/apps/poc-tokens-plugin/src/assets/manifest.json
@@ -0,0 +1,7 @@
+{
+ "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/apps/poc-tokens-plugin/src/index.html b/apps/poc-tokens-plugin/src/index.html
new file mode 100644
index 0000000..4cc3bad
--- /dev/null
+++ b/apps/poc-tokens-plugin/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ Angular example plugin
+
+
+
+
+
+
+
+
diff --git a/apps/poc-tokens-plugin/src/main.ts b/apps/poc-tokens-plugin/src/main.ts
new file mode 100644
index 0000000..35b00f3
--- /dev/null
+++ b/apps/poc-tokens-plugin/src/main.ts
@@ -0,0 +1,6 @@
+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/apps/poc-tokens-plugin/src/model.ts b/apps/poc-tokens-plugin/src/model.ts
new file mode 100644
index 0000000..8f76935
--- /dev/null
+++ b/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/apps/poc-tokens-plugin/src/plugin.ts b/apps/poc-tokens-plugin/src/plugin.ts
new file mode 100644
index 0000000..a719626
--- /dev/null
+++ b/apps/poc-tokens-plugin/src/plugin.ts
@@ -0,0 +1,234 @@
+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/apps/poc-tokens-plugin/src/styles.css b/apps/poc-tokens-plugin/src/styles.css
new file mode 100644
index 0000000..a6d2af3
--- /dev/null
+++ b/apps/poc-tokens-plugin/src/styles.css
@@ -0,0 +1,24 @@
+/* @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/apps/poc-tokens-plugin/tsconfig.app.json b/apps/poc-tokens-plugin/tsconfig.app.json
new file mode 100644
index 0000000..936913d
--- /dev/null
+++ b/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/apps/poc-tokens-plugin/tsconfig.editor.json b/apps/poc-tokens-plugin/tsconfig.editor.json
new file mode 100644
index 0000000..b927bb6
--- /dev/null
+++ b/apps/poc-tokens-plugin/tsconfig.editor.json
@@ -0,0 +1,7 @@
+{
+ "extends": "./tsconfig.json",
+ "include": ["src/**/*.ts"],
+ "compilerOptions": {
+ "types": ["node"]
+ }
+}
diff --git a/apps/poc-tokens-plugin/tsconfig.json b/apps/poc-tokens-plugin/tsconfig.json
new file mode 100644
index 0000000..4c48587
--- /dev/null
+++ b/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/apps/poc-tokens-plugin/tsconfig.plugin.json b/apps/poc-tokens-plugin/tsconfig.plugin.json
new file mode 100644
index 0000000..4d286ac
--- /dev/null
+++ b/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/libs/plugin-types/index.d.ts b/libs/plugin-types/index.d.ts
index 7fd89f4..c0ced90 100644
--- a/libs/plugin-types/index.d.ts
+++ b/libs/plugin-types/index.d.ts
@@ -2499,6 +2499,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.
@@ -3681,6 +3688,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
*/
@@ -3845,6 +3863,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.
@@ -4232,6 +4263,806 @@ 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: 'border-radius';
+
+ /**
+ * 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 of undefined if no set is active.
+ */
+ readonly resolvedValue: number | 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 of undefined if no set is active.
+ */
+ readonly resolvedValue: number | undefined;
+}
+
+/**
+ * Represents a token of type FontFamily.
+ * This interface extends `TokenBase` and specifies the data type of the value.
+ */
+export interface TokenFontFamily extends TokenBase {
+ /**
+ * The type of the token.
+ */
+ readonly type: 'font-family';
+
+ /**
+ * The value as defined in the token itself.
+ * It's a string with one or more font families, separated
+ * by commas, 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 string with one or more font families, separated
+ * by commas, of undefined if no set is active.
+ */
+ readonly resolvedValue: string | undefined;
+}
+
+/**
+ * Represents a token of type FontSize.
+ * This interface extends `TokenBase` and specifies the data type of the value.
+ */
+export interface TokenFontSize extends TokenBase {
+ /**
+ * The type of the token.
+ */
+ readonly type: 'font-size';
+
+ /**
+ * 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 of undefined if no set is active.
+ */
+ readonly resolvedValue: number | undefined;
+}
+
+/**
+ * Represents a token of type FontWeight.
+ * This interface extends `TokenBase` and specifies the data type of the value.
+ */
+export interface TokenFontWeight extends TokenBase {
+ /**
+ * The type of the token.
+ */
+ readonly type: 'font-weight';
+
+ /**
+ * 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 of undefined if no set is active.
+ */
+ readonly resolvedValue: string | undefined;
+}
+
+/**
+ * Represents a token of type FontLetterSpacing.
+ * This interface extends `TokenBase` and specifies the data type of the value.
+ */
+export interface TokenFontLetterSpacing extends TokenBase {
+ /**
+ * The type of the token.
+ */
+ readonly type: 'letter-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 of undefined if no set is active.
+ */
+ readonly resolvedValue: string | 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: 'letter-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 of undefined if no set is active.
+ */
+ 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 of undefined if no set is active.
+ */
+ 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 of undefined if no set is active.
+ */
+ 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 of undefined if no set is active.
+ */
+ 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 of undefined if no set is active.
+ */
+ readonly resolvedValue: number | undefined;
+}
+
+/**
+ * Represents a token of type StrokeWidth.
+ * This interface extends `TokenBase` and specifies the data type of the value.
+ */
+export interface TokenStrokeWidth extends TokenBase {
+ /**
+ * The type of the token.
+ */
+ readonly type: 'stroke-width';
+
+ /**
+ * 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 of undefined if no set is active.
+ */
+ 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: 'text-case';
+
+ /**
+ * 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 of undefined if no set is active.
+ */
+ 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: 'text-decoration';
+
+ /**
+ * 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 of undefined if no set is active.
+ */
+ readonly resolvedValue: string | undefined;
+}
+
+/**
+ * The supported Design Tokens in Penpot.
+ */
+export type Token =
+ TokenBorderRadius |
+ TokenColor |
+ TokenDimension |
+ TokenFontSize |
+ TokenOpacity |
+ TokenRotation |
+ TokenSizing |
+ TokenSpacing |
+ TokenStrokeWidth;
+
+/**
+ * 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 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 FontFamily token can be applied to.
+ */
+type TokenFontFamilyProps =
+ 'font-family';
+
+/**
+ * The properties that a FontSize token can be applied to.
+ */
+type TokenFontSizeProps =
+ '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 TokenFontLetterSpacingProps =
+ '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 StrokeWidth token can be applied to.
+ */
+type TokenStrokeWidthProps =
+ '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';
+
+/**
+ * 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 |
+ TokenColorProps |
+ TokenDimensionProps |
+ TokenFontFamilyProps |
+ TokenFontSizeProps |
+ TokenFontWeightProps |
+ TokenFontLetterSpacingProps |
+ TokenNumberProps |
+ TokenOpacityProps |
+ TokenSizingProps |
+ TokenSpacingProps |
+ TokenStrokeWidthProps |
+ TokenTextCaseProps |
+ TokenTextDecorationProps;
+
+/**
+ * The supported types of Design Tokens in Penpot.
+ */
+export type TokenType =
+ 'border-radius' |
+ 'color' |
+ 'dimension' |
+ 'font-family' |
+ 'font-size' |
+ 'font-weight' |
+ 'letter-spacing' |
+ 'letter-number' |
+ 'opacity' |
+ 'rotation' |
+ 'sizing' |
+ 'spacing' |
+ 'stroke-width' |
+ 'text-case' |
+ 'text-decoration';
+
/**
* Represents a user in Penpot.
*/
diff --git a/package-lock.json b/package-lock.json
index f96d167..c937d2e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12217,9 +12217,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001695",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001695.tgz",
- "integrity": "sha512-vHyLade6wTgI2u1ec3WQBxv+2BrTERV28UXQu9LO6lZ9pYeMk34vjXFLOxo1A4UBA8XTL4njRQZdno/yYaSmWw==",
+ "version": "1.0.30001753",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001753.tgz",
+ "integrity": "sha512-Bj5H35MD/ebaOV4iDLqPEtiliTN29qkGtEHCwawWn4cYm+bPJM2NsaP30vtZcnERClMzp52J4+aw2UNbK4o+zw==",
"dev": true,
"funding": [
{
@@ -12234,7 +12234,8 @@
"type": "github",
"url": "https://github.com/sponsors/ai"
}
- ]
+ ],
+ "license": "CC-BY-4.0"
},
"node_modules/ccount": {
"version": "2.0.1",
diff --git a/package.json b/package.json
index 4399a9a..0ddbd0e 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,7 @@
"start:plugin:table": "npx nx run table-plugin:init",
"start:plugin:renamelayers": "npx nx run rename-layers-plugin:init",
"start:plugin:colors-to-tokens": "npx nx run colors-to-tokens-plugin:init",
+ "start:plugin:poc-tokens": "npx nx run poc-tokens-plugin:init",
"build": "npx nx build plugins-runtime --emptyOutDir=true",
"build:plugins": "npx nx run-many -t build --parallel -p tag:type:plugin --exclude=poc-state-plugin",
"build:styles-example": "npx nx run example-styles:build",