diff --git a/CHANGELOG.md b/CHANGELOG.md index 67b0b18..7d72f1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## 1.3.2 (2025-07-04) + ### 🩹 Fixes - plugins-runtime public package.json ([70fd69f](https://github.com/penpot/penpot-plugins/commit/70fd69f)) diff --git a/README.md b/README.md index 399aa37..736c3d4 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ A table listing the available plugins and their corresponding startup commands i | 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-tokens-plugin | Sandbox plugin to test tokens functionality | 4309 | npm run start:plugin:poc-tokens | http://localhost:4309/assets/manifest.json | ## Web Apps diff --git a/apps/poc-tokens-plugin/eslint.config.js b/apps/poc-tokens-plugin/eslint.config.js new file mode 100644 index 0000000..7aa90c2 --- /dev/null +++ b/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/apps/poc-tokens-plugin/project.json b/apps/poc-tokens-plugin/project.json new file mode 100644 index 0000000..701b411 --- /dev/null +++ b/apps/poc-tokens-plugin/project.json @@ -0,0 +1,78 @@ +{ + "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/apps/poc-tokens-plugin/src/app/app.component.css b/apps/poc-tokens-plugin/src/app/app.component.css new file mode 100644 index 0000000..4dfdae9 --- /dev/null +++ b/apps/poc-tokens-plugin/src/app/app.component.css @@ -0,0 +1,128 @@ +/* @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/apps/poc-tokens-plugin/src/app/app.component.html b/apps/poc-tokens-plugin/src/app/app.component.html new file mode 100644 index 0000000..8e32a94 --- /dev/null +++ b/apps/poc-tokens-plugin/src/app/app.component.html @@ -0,0 +1,116 @@ +
+

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/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",