mirror of
https://github.com/penpot/penpot.git
synced 2026-11-01 23:50:38 +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>
296 lines
6.8 KiB
TypeScript
296 lines
6.8 KiB
TypeScript
import { Component } from '@angular/core';
|
|
import { toSignal } from '@angular/core/rxjs-interop';
|
|
import { fromEvent, map, filter, merge, of } from 'rxjs';
|
|
import { PluginMessageEvent, PluginUIEvent } from '../model';
|
|
|
|
type TokenTheme = {
|
|
id: string;
|
|
name: string;
|
|
group: string;
|
|
description: string;
|
|
active: boolean;
|
|
activeSets: string;
|
|
};
|
|
|
|
type TokenSet = {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
active: boolean;
|
|
};
|
|
|
|
type Token = {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
valueString: string;
|
|
resolvedValueString: 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 messages$ = fromEvent<MessageEvent<PluginMessageEvent>>(
|
|
window,
|
|
'message',
|
|
);
|
|
|
|
public initialTheme$ = of(
|
|
new URLSearchParams(window.location.search).get('theme'),
|
|
).pipe(filter((theme) => !!theme));
|
|
|
|
public theme = toSignal(
|
|
merge(
|
|
this.initialTheme$,
|
|
this.messages$.pipe(
|
|
filter((event) => event.data.type === 'theme'),
|
|
map((event) => {
|
|
return event.data.content;
|
|
}),
|
|
),
|
|
),
|
|
);
|
|
|
|
public isEditableTokens: boolean = false;
|
|
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);
|
|
} else if (event.data.type === 'set-is-editable-tokens') {
|
|
this.#setIsEditableTokens(event.data.isEditableTokens);
|
|
}
|
|
});
|
|
}
|
|
|
|
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,
|
|
// properties: ['borderRadiusTopRight'] // Uncomment to choose attribute to apply
|
|
}); // (incompatible attributes will have no effect)
|
|
}
|
|
}
|
|
|
|
#sendMessage(message: PluginUIEvent) {
|
|
parent.postMessage(message, '*');
|
|
}
|
|
|
|
#setIsEditableTokens(isEditableTokens: boolean) {
|
|
this.isEditableTokens = isEditableTokens;
|
|
}
|
|
|
|
#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);
|
|
}
|
|
}
|