From 8e794696112eda5bf4fff9b49e883b46e9e31c83 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 7 Jan 2026 13:13:35 +0100 Subject: [PATCH] :tada: Add tokens to plugins API documentation And add poc plugin example --- plugins/README.md | 29 +- .../apps/poc-tokens-plugin/eslint.config.js | 51 + plugins/apps/poc-tokens-plugin/project.json | 79 ++ .../src/app/app.component.css | 127 ++ .../src/app/app.component.html | 144 +++ .../src/app/app.component.ts | 290 +++++ .../poc-tokens-plugin/src/app/app.config.ts | 11 + .../poc-tokens-plugin/src/app/app.routes.ts | 3 + .../apps/poc-tokens-plugin/src/assets/CORS | 1 + .../poc-tokens-plugin/src/assets/_headers | 2 + .../poc-tokens-plugin/src/assets/favicon.ico | Bin 0 -> 15406 bytes .../poc-tokens-plugin/src/assets/icon.png | Bin 0 -> 35196 bytes .../src/assets/manifest.json | 14 + plugins/apps/poc-tokens-plugin/src/index.html | 13 + plugins/apps/poc-tokens-plugin/src/main.ts | 7 + plugins/apps/poc-tokens-plugin/src/model.ts | 112 ++ plugins/apps/poc-tokens-plugin/src/plugin.ts | 246 ++++ plugins/apps/poc-tokens-plugin/src/styles.css | 23 + .../apps/poc-tokens-plugin/tsconfig.app.json | 10 + .../poc-tokens-plugin/tsconfig.editor.json | 7 + plugins/apps/poc-tokens-plugin/tsconfig.json | 33 + .../poc-tokens-plugin/tsconfig.plugin.json | 8 + plugins/libs/plugin-types/index.d.ts | 1068 ++++++++++++++++- plugins/package.json | 1 + 24 files changed, 2259 insertions(+), 20 deletions(-) create mode 100644 plugins/apps/poc-tokens-plugin/eslint.config.js create mode 100644 plugins/apps/poc-tokens-plugin/project.json create mode 100644 plugins/apps/poc-tokens-plugin/src/app/app.component.css create mode 100644 plugins/apps/poc-tokens-plugin/src/app/app.component.html create mode 100644 plugins/apps/poc-tokens-plugin/src/app/app.component.ts create mode 100644 plugins/apps/poc-tokens-plugin/src/app/app.config.ts create mode 100644 plugins/apps/poc-tokens-plugin/src/app/app.routes.ts create mode 100644 plugins/apps/poc-tokens-plugin/src/assets/CORS create mode 100644 plugins/apps/poc-tokens-plugin/src/assets/_headers create mode 100644 plugins/apps/poc-tokens-plugin/src/assets/favicon.ico create mode 100644 plugins/apps/poc-tokens-plugin/src/assets/icon.png create mode 100644 plugins/apps/poc-tokens-plugin/src/assets/manifest.json create mode 100644 plugins/apps/poc-tokens-plugin/src/index.html create mode 100644 plugins/apps/poc-tokens-plugin/src/main.ts create mode 100644 plugins/apps/poc-tokens-plugin/src/model.ts create mode 100644 plugins/apps/poc-tokens-plugin/src/plugin.ts create mode 100644 plugins/apps/poc-tokens-plugin/src/styles.css create mode 100644 plugins/apps/poc-tokens-plugin/tsconfig.app.json create mode 100644 plugins/apps/poc-tokens-plugin/tsconfig.editor.json create mode 100644 plugins/apps/poc-tokens-plugin/tsconfig.json create mode 100644 plugins/apps/poc-tokens-plugin/tsconfig.plugin.json diff --git a/plugins/README.md b/plugins/README.md index 399aa37943..22f4556166 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -19,7 +19,7 @@ In the `apps` folder you'll find some examples that use the libraries mentioned - example-styles: to run this example you should run ``` -npm run start:styles-example +pnpm run start:styles-example ``` Open in your browser: `http://localhost:4202/` @@ -28,8 +28,8 @@ Open in your browser: `http://localhost:4202/` This guide will help you launch a Penpot plugin from the penpot-plugins repository. Before proceeding, ensure that you have Penpot running locally by following the [setup instructions](https://help.penpot.app/technical-guide/developer/devenv/). -In the terminal, navigate to the **penpot-plugins** repository and run `npm install` to install the required dependencies. -Then, run `npm start` to launch the plugins wrapper. +In the terminal, navigate to the **penpot-plugins** repository and run `pnpm install` to install the required dependencies. +Then, run `pnpm run start` to launch the plugins wrapper. After installing the dependencies, choose a plugin to launch. You can either run one of the provided examples or create your own (see "Creating a plugin from scratch" below). To launch a plugin, Open a new terminal tab and run the appropriate startup script for the chosen plugin. @@ -38,7 +38,7 @@ For instance, to launch the Contrast plugin, use the following command: ``` // for the contrast plugin -npm run start:plugin:contrast +pnpm run start:plugin:contrast ``` Finally, open in your browser the specific port. In this specific example would be `http://localhost:4302` @@ -49,21 +49,22 @@ A table listing the available plugins and their corresponding startup commands i | Plugin | Description | PORT | Start command | Manifest URL | | ----------------------- | ----------------------------------------------------------- | ---- | ------------------------------------- | ------------------------------------------ | -| poc-state-plugin | Sandbox plugin to test new plugins api functionality | 4301 | npm run start:plugin:poc-state | http://localhost:4301/assets/manifest.json | -| contrast-plugin | Sample plugin that gives you color contrast information | 4302 | npm run start:plugin:contrast | http://localhost:4302/assets/manifest.json | -| icons-plugin | Tool to add icons from [Feather](https://feathericons.com/) | 4303 | npm run start:plugin:icons | http://localhost:4303/assets/manifest.json | -| lorem-ipsum-plugin | Generate Lorem ipsum text | 4304 | npm run start:plugin:loremipsum | http://localhost:4304/assets/manifest.json | -| create-palette-plugin | Creates a board with all the palette colors | 4305 | npm run start:plugin:palette | http://localhost:4305/assets/manifest.json | -| 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-state-plugin | Sandbox plugin to test new plugins api functionality | 4301 | pnpm run start:plugin:poc-state | http://localhost:4301/assets/manifest.json | +| contrast-plugin | Sample plugin that gives you color contrast information | 4302 | pnpm run start:plugin:contrast | http://localhost:4302/assets/manifest.json | +| icons-plugin | Tool to add icons from [Feather](https://feathericons.com/) | 4303 | pnpm run start:plugin:icons | http://localhost:4303/assets/manifest.json | +| lorem-ipsum-plugin | Generate Lorem ipsum text | 4304 | pnpm run start:plugin:loremipsum | http://localhost:4304/assets/manifest.json | +| create-palette-plugin | Creates a board with all the palette colors | 4305 | pnpm run start:plugin:palette | http://localhost:4305/assets/manifest.json | +| table-plugin | Create or import table | 4306 | pnpm run start:table-plugin | http://localhost:4306/assets/manifest.json | +| rename-layers-plugin | Rename layers in bulk | 4307 | pnpm run start:plugin:renamelayers | http://localhost:4307/assets/manifest.json | +| colors-to-tokens-plugin | Generate tokens JSON file | 4308 | pnpm run start:plugin:colors-to-tokens | http://localhost:4308/assets/manifest.json | +| poc-tokens-plugin | Sandbox plugin to test tokens functionality | 4309 | pnpm run start:plugin:poc-tokens | http://localhost:4309/assets/manifest.json | ## Web Apps | App | Description | PORT | Start command | URL | | --------------- | ----------------------------------------------------------------- | ---- | -------------------------------- | ---------------------- | -| plugins-runtime | Runtime for the plugins subsystem | 4200 | npm run start:app:runtime | | -| example-styles | Showcase of some of the Penpot styles that can be used in plugins | 4201 | npm run start:app:styles-example | http://localhost:4201/ | +| plugins-runtime | Runtime for the plugins subsystem | 4200 | pnpm run start:app:runtime | | +| example-styles | Showcase of some of the Penpot styles that can be used in plugins | 4201 | pnpm run start:app:styles-example | http://localhost:4201/ | ## Creating a plugin from scratch diff --git a/plugins/apps/poc-tokens-plugin/eslint.config.js b/plugins/apps/poc-tokens-plugin/eslint.config.js new file mode 100644 index 0000000000..7aa90c2ab0 --- /dev/null +++ b/plugins/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/plugins/apps/poc-tokens-plugin/project.json b/plugins/apps/poc-tokens-plugin/project.json new file mode 100644 index 0000000000..c2bc7cad81 --- /dev/null +++ b/plugins/apps/poc-tokens-plugin/project.json @@ -0,0 +1,79 @@ +{ + "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/plugins/apps/poc-tokens-plugin/src/app/app.component.css b/plugins/apps/poc-tokens-plugin/src/app/app.component.css new file mode 100644 index 0000000000..9a64100273 --- /dev/null +++ b/plugins/apps/poc-tokens-plugin/src/app/app.component.css @@ -0,0 +1,127 @@ +/* @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/plugins/apps/poc-tokens-plugin/src/app/app.component.html b/plugins/apps/poc-tokens-plugin/src/app/app.component.html new file mode 100644 index 0000000000..5be709cb57 --- /dev/null +++ b/plugins/apps/poc-tokens-plugin/src/app/app.component.html @@ -0,0 +1,144 @@ +
+

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/plugins/apps/poc-tokens-plugin/src/app/app.component.ts b/plugins/apps/poc-tokens-plugin/src/app/app.component.ts new file mode 100644 index 0000000000..63995292f6 --- /dev/null +++ b/plugins/apps/poc-tokens-plugin/src/app/app.component.ts @@ -0,0 +1,290 @@ +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 = [ + { + 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, + // 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/plugins/apps/poc-tokens-plugin/src/app/app.config.ts b/plugins/apps/poc-tokens-plugin/src/app/app.config.ts new file mode 100644 index 0000000000..fb93f472fd --- /dev/null +++ b/plugins/apps/poc-tokens-plugin/src/app/app.config.ts @@ -0,0 +1,11 @@ +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/plugins/apps/poc-tokens-plugin/src/app/app.routes.ts b/plugins/apps/poc-tokens-plugin/src/app/app.routes.ts new file mode 100644 index 0000000000..dc39edb5f2 --- /dev/null +++ b/plugins/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/plugins/apps/poc-tokens-plugin/src/assets/CORS b/plugins/apps/poc-tokens-plugin/src/assets/CORS new file mode 100644 index 0000000000..72e8ffc0db --- /dev/null +++ b/plugins/apps/poc-tokens-plugin/src/assets/CORS @@ -0,0 +1 @@ +* diff --git a/plugins/apps/poc-tokens-plugin/src/assets/_headers b/plugins/apps/poc-tokens-plugin/src/assets/_headers new file mode 100644 index 0000000000..c776a4792b --- /dev/null +++ b/plugins/apps/poc-tokens-plugin/src/assets/_headers @@ -0,0 +1,2 @@ +/* + Access-Control-Allow-Origin: * diff --git a/plugins/apps/poc-tokens-plugin/src/assets/favicon.ico b/plugins/apps/poc-tokens-plugin/src/assets/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..fc5e208af4ff02eaad5a27b0a74c269b8ea09c1b GIT binary patch literal 15406 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 literal 0 HcmV?d00001 diff --git a/plugins/apps/poc-tokens-plugin/src/assets/icon.png b/plugins/apps/poc-tokens-plugin/src/assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..cf045fb5e6a44121b2b4eccf90c6e4499790abac GIT binary patch literal 35196 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 + + + + Angular example plugin + + + + + + + + diff --git a/plugins/apps/poc-tokens-plugin/src/main.ts b/plugins/apps/poc-tokens-plugin/src/main.ts new file mode 100644 index 0000000000..8882c4517f --- /dev/null +++ b/plugins/apps/poc-tokens-plugin/src/main.ts @@ -0,0 +1,7 @@ +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/plugins/apps/poc-tokens-plugin/src/model.ts b/plugins/apps/poc-tokens-plugin/src/model.ts new file mode 100644 index 0000000000..8f76935892 --- /dev/null +++ b/plugins/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/plugins/apps/poc-tokens-plugin/src/plugin.ts b/plugins/apps/poc-tokens-plugin/src/plugin.ts new file mode 100644 index 0000000000..cccf6f678e --- /dev/null +++ b/plugins/apps/poc-tokens-plugin/src/plugin.ts @@ -0,0 +1,246 @@ +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/plugins/apps/poc-tokens-plugin/src/styles.css b/plugins/apps/poc-tokens-plugin/src/styles.css new file mode 100644 index 0000000000..007341e2f7 --- /dev/null +++ b/plugins/apps/poc-tokens-plugin/src/styles.css @@ -0,0 +1,23 @@ +/* @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/plugins/apps/poc-tokens-plugin/tsconfig.app.json b/plugins/apps/poc-tokens-plugin/tsconfig.app.json new file mode 100644 index 0000000000..936913d9af --- /dev/null +++ b/plugins/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/plugins/apps/poc-tokens-plugin/tsconfig.editor.json b/plugins/apps/poc-tokens-plugin/tsconfig.editor.json new file mode 100644 index 0000000000..b927bb69fc --- /dev/null +++ b/plugins/apps/poc-tokens-plugin/tsconfig.editor.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*.ts"], + "compilerOptions": { + "types": ["node"] + } +} diff --git a/plugins/apps/poc-tokens-plugin/tsconfig.json b/plugins/apps/poc-tokens-plugin/tsconfig.json new file mode 100644 index 0000000000..4c48587cfe --- /dev/null +++ b/plugins/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/plugins/apps/poc-tokens-plugin/tsconfig.plugin.json b/plugins/apps/poc-tokens-plugin/tsconfig.plugin.json new file mode 100644 index 0000000000..961987f7a1 --- /dev/null +++ b/plugins/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/plugins/libs/plugin-types/index.d.ts b/plugins/libs/plugin-types/index.d.ts index ff56cb2490..57b29ff591 100644 --- a/plugins/libs/plugin-types/index.d.ts +++ b/plugins/libs/plugin-types/index.d.ts @@ -2,10 +2,8 @@ * These are methods and properties available on the `penpot` global object. * */ -export interface Penpot extends Omit< - Context, - 'addListener' | 'removeListener' -> { +export interface Penpot + extends Omit { ui: { /** * Opens the plugin UI. It is possible to develop a plugin without interface (see Palette color example) but if you need, the way to open this UI is using `penpot.ui.open`. @@ -2533,6 +2531,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. @@ -2800,9 +2805,9 @@ export interface LibraryTypography extends LibraryElement { fontId: string; /** - * The font family of the typography element. + * The font families of the typography element. */ - fontFamily: string; + fontFamilies: string; /** * The unique identifier of the font variant used in the typography element. @@ -3728,6 +3733,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 */ @@ -3892,6 +3908,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. @@ -4279,6 +4308,1033 @@ 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: 'borderRadius'; + + /** + * 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, or undefined if no value has been found in active sets. + */ + readonly resolvedValue: number | undefined; +} + +/* + * The value of a TokenShadow in its composite form. + */ +export interface TokenShadowValue { + /** + * The color as a string (e.g. "#FF5733"). + */ + color: string; + + /** + * If the shadow is inset or drop. + */ + inset: boolean; + + /** + * The horizontal offset of the shadow in pixels. + */ + offsetX: number; + + /** + * The vertical offset of the shadow in pixels. + */ + offsetY: number; + + /** + * The spread distance of the shadow in pixels. + */ + spread: number; + + /** + * The amount of blur to apply to the shadow. + */ + blur: number; +} + +/* + * The value of a TokenShadow in its composite of strings form. + */ +export interface TokenShadowValueString { + /** + * The color as a string (e.g. "#FF5733"), or a reference + * to a color token. + */ + color: string; + + /** + * If the shadow is inset or drop, or a reference of a + * boolean token. + */ + inset: string; + + /** + * The horizontal offset of the shadow in pixels, or a reference + * to a number token. + */ + offsetX: string; + + /** + * The vertical offset of the shadow in pixels, or a reference + * to a number token. + */ + offsetY: string; + + /** + * The spread distance of the shadow in pixels, or a reference + * to a number token. + */ + spread: string; + + /** + * The amount of blur to apply to the shadow, or a reference + * to a number token. + */ + blur: string; +} + +/** + * Represents a token of type Shadow. + * This interface extends `TokenBase` and specifies the data type of the value. + */ +export interface TokenShadow extends TokenBase { + /** + * The type of the token. + */ + readonly type: 'shadow'; + + /** + * The value as defined in the token itself. + * It may be a string with a reference to other token, or else + * an array of TokenShadowValueString. + */ + value: string | TokenShadowValueString[]; + + /** + * The value calculated by finding all tokens with the same name in active sets + * and resolving the references. + * + * It's an array of TokenShadowValue, or undefined if no value has been found + * in active sets. + */ + readonly resolvedValue: TokenShadowValue[] | 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, or undefined if no value has been found in active sets. + */ + readonly resolvedValue: number | undefined; +} + +/** + * Represents a token of type FontFamilies. + * This interface extends `TokenBase` and specifies the data type of the value. + */ +export interface TokenFontFamilies extends TokenBase { + /** + * The type of the token. + */ + readonly type: 'fontFamilies'; + + /** + * The value as defined in the token itself. + * It may be a string with a reference to other token, or else + * an array of strings with one or more font families (each family + * is an item in the array). + */ + value: string | string[]; + + /** + * The value calculated by finding all tokens with the same name in active sets + * and resolving the references. + * + * It's an array of strings with one or more font families, + * or undefined if no value has been found in active sets. + */ + readonly resolvedValue: string[] | undefined; +} + +/** + * Represents a token of type FontSizes. + * This interface extends `TokenBase` and specifies the data type of the value. + */ +export interface TokenFontSizes extends TokenBase { + /** + * The type of the token. + */ + readonly type: 'fontSizes'; + + /** + * 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, or undefined if no value has been found in active sets. + */ + readonly resolvedValue: number | undefined; +} + +/** + * Represents a token of type FontWeights. + * This interface extends `TokenBase` and specifies the data type of the value. + */ +export interface TokenFontWeights extends TokenBase { + /** + * The type of the token. + */ + readonly type: 'fontWeights'; + + /** + * 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 ("bold", "strong", etc.), or undefined if no value has + * been found in active sets. + */ + readonly resolvedValue: string | undefined; +} + +/** + * Represents a token of type LetterSpacing. + * This interface extends `TokenBase` and specifies the data type of the value. + */ +export interface TokenLetterSpacing extends TokenBase { + /** + * The type of the token. + */ + readonly type: 'letterSpacing'; + + /** + * 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, or undefined if no value has been found in active sets. + */ + readonly resolvedValue: number | 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: '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, or undefined if no value has been found in active sets. + */ + 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, or undefined if no value has been found + * in active sets. + */ + 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, or undefined if no value has been found + * in active sets. + */ + 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, or undefined if no value has been found in active sets. + */ + 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, or undefined if no value has been found in active sets. + */ + readonly resolvedValue: number | undefined; +} + +/** + * Represents a token of type BorderWidth. + * This interface extends `TokenBase` and specifies the data type of the value. + */ +export interface TokenBorderWidth extends TokenBase { + /** + * The type of the token. + */ + readonly type: 'borderWidth'; + + /** + * 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, or undefined if no value has been found in active sets. + */ + 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: 'textCase'; + + /** + * 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 ("none", "uppercase", "lowercase", "capitalize"), or + * undefined if no value has been found in active sets. + */ + 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: 'textDecoration'; + + /** + * 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, or undefined if no value has been found + * in active sets. + */ + readonly resolvedValue: string | undefined; +} + +/* + * The value of a TokenTypography in its composite form. + */ +export interface TokenTypographyValue { + /** + * The letter spacing, as a number. + */ + letterSpacing: number; + + /** + * The list of font families. + */ + fontFamilies: string[]; + + /** + * The font size, as a positive number. + */ + fontSizes: number; + + /** + * The font weight, as a weight string ("bold", "strong", etc.). + */ + fontWeights: string; + + /** + * The line height, as a number. + */ + lineHeight: number; + + /** + * The text case as a string ("none", "uppercase", "lowercase" "capitalize"). + */ + textCase: string; + + /** + * The text decoration as a string ("none", "underline", "strike-through"). + */ + textDecoration: string; +} + +/* + * The value of a TokenTypography in its composite of strings form. + */ +export interface TokenTypographyValueString { + /** + * The letter spacing, as a number, or a reference to a TokenLetterSpacing. + */ + letterSpacing: string; + + /** + * The list of font families, or a reference to a TokenFontFamilies. + */ + fontFamilies: string | string[]; + + /** + * The font size, as a positive number, or a reference to a TokenFontSizes. + */ + fontSizes: string; + + /** + * The font weight, as a weight string ("bold", "strong", etc.), or a + * reference to a TokenFontWeights. + */ + fontWeight: string; + + /** + * The line height, as a number. Note that there not exists an individual + * token type line height, only part of a Typography token. If you need to + * put here a reference, use a NumberToken. + */ + lineHeight: string; + + /** + * The text case as a string ("none", "uppercase", "lowercase" "capitalize"), + * or a reference to a TokenTextCase. + */ + textCase: string; + + /** + * The text decoration as a string ("none", "underline", "strike-through"), + * or a reference to a TokenTextDecoration. + */ + textDecoration: string; +} + +/** + * Represents a token of type Typography. + * This interface extends `TokenBase` and specifies the data type of the value. + */ +export interface TokenTypography extends TokenBase { + /** + * The type of the token. + */ + readonly type: 'typography'; + + /** + * The value as defined in the token itself. + * It may be a string with a reference to other token, or a + * TokenTypographyValueString. + */ + value: string | TokenTypographyValueString; + + /** + * The value calculated by finding all tokens with the same name in active sets + * and resolving the references. + * + * It's a TokenTypographyValue, or undefined if no value has been found + * in active sets. + */ + readonly resolvedValue: TokenTypographyValue[] | undefined; +} + +/** + * The supported Design Tokens in Penpot. + */ +export type Token = + | TokenBorderRadius + | TokenShadow + | TokenColor + | TokenDimension + | TokenFontFamilies + | TokenFontSizes + | TokenFontWeights + | TokenLetterSpacing + | TokenNumber + | TokenOpacity + | TokenRotation + | TokenSizing + | TokenSpacing + | TokenBorderWidth + | TokenTextCase + | TokenTextDecoration + | TokenTypography; + +/** + * 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 Shadow token can be applied to. + */ +type TokenShadowProps = 'shadow'; + +/** + * 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 FontFamilies token can be applied to. + */ +type TokenFontFamiliesProps = 'font-families'; + +/** + * The properties that a FontSizes token can be applied to. + */ +type TokenFontSizesProps = '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 TokenLetterSpacingProps = '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 BorderWidth token can be applied to. + */ +type TokenBorderWidthProps = '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'; + +/** + * The properties that a Typography token can be applied to. + */ +type TokenTypographyProps = 'typography'; + +/** + * 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 + | TokenShadowProps + | TokenColorProps + | TokenDimensionProps + | TokenFontFamiliesProps + | TokenFontSizesProps + | TokenFontWeightProps + | TokenLetterSpacingProps + | TokenNumberProps + | TokenOpacityProps + | TokenSizingProps + | TokenSpacingProps + | TokenBorderWidthProps + | TokenTextCaseProps + | TokenTextDecorationProps + | TokenTypographyProps; + +/** + * The supported types of Design Tokens in Penpot. + */ +export type TokenType = + | 'borderRadius' + | 'shadow' + | 'color' + | 'dimension' + | 'fontFamilies' + | 'fontSizes' + | 'fontWeights' + | 'letterSpacing' + | 'number' + | 'opacity' + | 'rotation' + | 'sizing' + | 'spacing' + | 'borderWidth' + | 'textCase' + | 'textDecoration' + | 'typography'; + /** * Represents a user in Penpot. */ diff --git a/plugins/package.json b/plugins/package.json index 722feae2a5..50a9e29f7b 100644 --- a/plugins/package.json +++ b/plugins/package.json @@ -16,6 +16,7 @@ "start:plugin:table": "nx run table-plugin:init", "start:plugin:renamelayers": "nx run rename-layers-plugin:init", "start:plugin:colors-to-tokens": "nx run colors-to-tokens-plugin:init", + "start:plugin:poc-tokens": "nx run poc-tokens-plugin:init", "build": "nx build plugins-runtime --emptyOutDir=true", "build:plugins": "nx run-many -t build --parallel -p tag:type:plugin --exclude=poc-state-plugin", "build:styles-example": "nx run example-styles:build",