Andrés Moya 5b5f22a8c6
🎉 Add tokens to Penpot Plugins API (#7756)
* 🎉 Add tokens to plugins API documentation

And add poc plugin example

* 📚 Document better the tokens value in plugins API

* 🔧 Refactor token validation schemas

* 🔧 Use automatic validation in token proxies

* 🔧 Use schemas to validate token creation

* 🔧 Use multi schema for token value

* 🔧 Use schema in token api methods

* 🐛 Fix review comments

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-02-09 14:18:31 +01:00

263 lines
6.5 KiB
TypeScript

import type { PluginMessageEvent, PluginUIEvent } from './model.js';
import {
TokenType,
TokenProperty,
TokenValueString,
} 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<PluginUIEvent>(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.properties);
}
});
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({ group: themeGroup, name: themeName });
if (theme) {
loadLibrary();
}
}
function addSet(setName: string) {
const tokensCatalog = penpot.library.local.tokens;
const set = tokensCatalog?.addSet({ name: 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({
type: tokenType as TokenType,
name: tokenName,
value: tokenValue as TokenValueString,
});
if (token) {
// TODO: remove this timeout when styleDictionary is replaced
// with tokenScript and the token validation is syncrhronous.
setTimeout(() => {
loadTokens(setId);
}, 0);
}
}
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;
// TODO: remove this timeout when styleDictionary is replaced
// with tokenScript and the token validation is syncrhronous.
setTimeout(() => {
loadTokens(setId);
}, 0);
}
}
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,
properties: TokenProperty[] | undefined,
) {
const tokensCatalog = penpot.library.local.tokens;
const set = tokensCatalog?.getSetById(setId);
const token = set?.getTokenById(tokenId);
if (token) {
token.applyToSelected(properties);
}
// Alternatve way
//
// const selection = penpot.selection;
// if (token && selection) {
// for (const shape of selection) {
// shape.applyToken(token, properties);
// }
// }
}