mirror of
https://github.com/penpot/penpot.git
synced 2026-10-01 16:26:22 +00:00
* ✨ Auto link tokens when adding external libraries (provisional) * 🔧 Refactor tokens-lib initialization * 🔧 Add separated TokenStatus to store status apart of TokensLib * 🔧 Make all status operations use the new data structure * 🔧 Normalize status helper functions and access token sets by id * 🔧 Rename :tokens-file to :tokens-source * 🎉 Allow the user to choose the tokens-source of a file * 🎉 Make tokens library readonly when it's in an external file * 🎉 Show tokens in library summaries * 🎉 Show source info in sidebar * 🔧 Fix integration tests * 🐛 Propagate changes of token values in external library * 🎉 Layout updates * 🔧 Refactor tokens source calculations * 🔧 Add harder checks for nil or empty values in everything * 🐛 Fix some integration tests * 🔧 Add integration tests for tokens in external libs * 🔧 Validate and repair missing tokens status * 🎉 Make ui changes optional with config flag * 🐛 Propagate tokens after synchronizing components in ext library * 🐛 Propagate tokens after creating new instances * 🐛 Propagate tokens after synchronizing tokens in ext library * 🐛 Add a tokens source icon to libraries section (#11439) * 🐛 Add a tokens source icon to libraries section * 🐛 Fix ellipsis on library names * ♻️ Remove code under flag on legacy component * 🐛 Fix token theme name on inspect tab * 🎉 Add changes notification (#11476) * 🎉 Add changes notification * ♻️ Change fn names * 🐛 Fix tokens source label truncation and missing translations (#11533) * 🐛 Fix tokens source label truncation and missing translations The tokens source file name always showed, even for the current file, and long names wrapped onto a second line instead of truncating because the header used flex-wrap and overflow-wrap: break-word instead of single-line ellipsis. Show the source row unconditionally (it now displays "This file" when the source is the current file, matching the connected-library case), truncate the file name to one line with an ellipsis, and only attach a tooltip with the full name when the text is actually truncated. Replace the hardcoded UI strings with translated ones and add their English and Spanish entries. AI-assisted-by: claude-sonnet-5 * 🐛 Remove redundant effect dependency in tokens source file-name-truncated? was listed as a dependency of the with-effect that checks and observes label truncation, even though it isn't read inside the effect body. Since the effect itself flips that state via check-file-name-truncated, including it as a dependency caused the ResizeObserver to be needlessly disconnected and reconnected on every truncation change. AI-assisted-by: claude-sonnet-5 * 🐛 Fix small visual error * 🐛 Fix problem with plugins * 🐛 Fix playwright tests --------- Co-authored-by: Eva Marco <evamarcod@gmail.com> Co-authored-by: Eva Marco <eva.marco@kaleidos.net> Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
282 lines
7.0 KiB
TypeScript
282 lines
7.0 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 isEditableTokens = tokensCatalog.isEditableTokens;
|
|
|
|
penpot.ui.sendMessage({
|
|
source: 'penpot',
|
|
type: 'set-is-editable-tokens',
|
|
isEditableTokens,
|
|
});
|
|
|
|
const themes = tokensCatalog.themes;
|
|
|
|
const themesData = themes.map((theme) => {
|
|
const activeSets = theme.activeSets.map((set) => set.name).join(', ');
|
|
return {
|
|
id: theme.id,
|
|
group: theme.group,
|
|
name: theme.name,
|
|
active: theme.active,
|
|
activeSets: activeSets,
|
|
};
|
|
});
|
|
|
|
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,
|
|
valueString: JSON.stringify(token.value),
|
|
resolvedValueString: token.resolvedValueString,
|
|
};
|
|
}),
|
|
]);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
// Alternative way
|
|
//
|
|
// const selection = penpot.selection;
|
|
// if (token && selection) {
|
|
// token.applyToShapes(selection, properties);
|
|
// }
|
|
|
|
// Other alternative way
|
|
//
|
|
// const selection = penpot.selection;
|
|
// if (token && selection) {
|
|
// for (const shape of selection) {
|
|
// shape.applyToken(token, properties);
|
|
// }
|
|
// }
|
|
}
|