0
Fork 0
mirror of https://github.com/penpot/penpot-plugins.git synced 2025-04-15 16:31:36 -05:00

feat: colors to tokens export plugin

This commit is contained in:
Juanfran 2025-01-23 14:56:53 +01:00
parent 815181d20a
commit 7f8a011037
25 changed files with 1886 additions and 9 deletions

View file

@ -47,15 +47,16 @@ A table listing the available plugins and their corresponding startup commands i
## Sample plugins
| 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 |
| 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 |
## Web Apps

View file

@ -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,
},
},
},
];

View file

@ -0,0 +1,79 @@
{
"name": "colors-to-tokens-plugin",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
"prefix": "app",
"sourceRoot": "apps/colors-to-tokens-plugin/src",
"tags": ["type:plugin"],
"targets": {
"build": {
"executor": "@angular-devkit/build-angular:application",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/apps/colors-to-tokens-plugin",
"index": "apps/colors-to-tokens-plugin/src/index.html",
"browser": "apps/colors-to-tokens-plugin/src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "apps/colors-to-tokens-plugin/tsconfig.app.json",
"assets": [
"apps/colors-to-tokens-plugin/src/favicon.ico",
"apps/colors-to-tokens-plugin/src/assets"
],
"styles": [
"libs/plugins-styles/src/lib/styles.css",
"apps/colors-to-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": "colors-to-tokens-plugin:build:production"
},
"development": {
"buildTarget": "colors-to-tokens-plugin:build:development",
"host": "0.0.0.0",
"port": 4308
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"executor": "@angular-devkit/build-angular:extract-i18n",
"options": {
"buildTarget": "colors-to-tokens-plugin:build"
}
}
}
}

View file

@ -0,0 +1,87 @@
:host {
display: flex;
flex-direction: column;
gap: var(--spacing-24);
padding-top: var(--spacing-36);
}
.title {
color: var(--foreground-primary);
}
.description {
padding-bottom: var(--spacing-4);
a {
color: var(--accent-primary);
text-decoration: none;
}
}
.title,
.description {
text-wrap: pretty;
text-align: center;
}
.actions {
display: flex;
gap: var(--spacing-8);
justify-content: center;
}
.download-btn {
display: flex;
gap: var(--spacing-4);
align-items: center;
app-svg {
--svg-stroke-color: var(--background-primary);
}
}
.restart-btn {
display: flex;
gap: var(--spacing-4);
align-items: center;
app-svg {
--svg-stroke-color: var(--foreground-secondary);
}
&:hover {
app-svg {
--svg-stroke-color: var(--accent-primary);
}
}
}
/* Override default button appearance */
.download-btn[data-appearance='primary']:is(button):disabled {
color: var(--background-secondary);
background-color: var(--accent-primary-muted);
border: 2px solid var(--accent-primary-muted);
app-svg {
--svg-stroke-color: var(--background-primary);
}
}
.success {
display: flex;
background-color: var(--success-950);
border-radius: var(--spacing-8);
border: 1px solid var(--success-500);
color: var(--app-white);
gap: var(--spacing-8);
padding: var(--spacing-8);
app-svg {
--svg-stroke-color: var(--success-500);
}
}
.download-note {
padding: 0 var(--spacing-8);
text-align: center;
}

View file

@ -0,0 +1,168 @@
import { Component, effect, inject, linkedSignal } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { ActivatedRoute } from '@angular/router';
import type {
PluginMessageEvent,
PluginUIEvent,
ThemePluginEvent,
SetColorsPluginEvent,
} from '../model';
import { filter, fromEvent, map, merge, take } from 'rxjs';
import { transformToToken } from './utils/transform-to-token';
import { SvgComponent } from './components/svg.component';
@Component({
selector: 'app-root',
imports: [SvgComponent],
template: `
<h1 class="title title-m">Convert your colors assets to Design Tokens</h1>
<p class="description body-m">
A Penpot plugin to generate a JSON file with your color styles in a
<a target="_blank" href="https://tr.designtokens.org/format/"
>Design Token Standard format</a
>.
</p>
@if (result()) {
<div class="success body-s">
<app-svg name="tick" />
Colors convertered to tokens successfully!
</div>
}
<div class="actions">
@if (result()) {
<button
type="button"
data-appearance="secondary"
class="restart-btn"
(click)="restart()"
>
<app-svg name="reload" />
Restart
</button>
} @else {
<button type="button" (click)="convert()" data-appearance="primary">
Convert colors
</button>
}
<button
(click)="handleDownload()"
class="download-btn"
type="button"
data-appearance="primary"
[attr.disabled]="result() ? null : true"
>
<app-svg name="download" />
Download
</button>
</div>
<!-- @if (result()) {
<p class="body-m download-note">
Now you can modify and import it (link to help center)
</p>
} -->
`,
styleUrl: './app.component.css',
host: {
'[attr.data-theme]': 'theme()',
},
})
export class AppComponent {
route = inject(ActivatedRoute);
messages$ = fromEvent<MessageEvent<PluginMessageEvent>>(window, 'message');
initialTheme$ = this.route.queryParamMap.pipe(
map((params) => params.get('theme')),
filter((theme) => !!theme),
take(1),
);
theme = toSignal(
merge(
this.initialTheme$,
this.messages$.pipe(
filter(
(event): event is MessageEvent<ThemePluginEvent> =>
event.data.type === 'theme',
),
map((event) => {
return event.data.content;
}),
),
),
);
#result = toSignal(
this.messages$.pipe(
filter(
(event): event is MessageEvent<SetColorsPluginEvent> =>
event.data.type === 'set-colors',
),
map((event) => {
if (event.data.colors) {
try {
const tokens = transformToToken(event.data.colors);
return {
tokens,
name: event.data.fileName,
};
} catch (error) {
console.error(error);
}
}
return null;
}),
),
{
initialValue: null,
},
);
result = linkedSignal(() => this.#result());
constructor() {
effect(() => {
if (this.result()) {
this.#sendMessage({
type: 'resize',
width: 410,
height: 340,
});
} else {
this.#sendMessage({ type: 'reset' });
}
});
}
#sendMessage(message: PluginUIEvent): void {
parent.postMessage(message, '*');
}
convert(): void {
this.#sendMessage({ type: 'get-colors' });
}
restart(): void {
this.result.set(null);
}
handleDownload() {
const fileTokens = this.#result();
if (!fileTokens) return;
const blob = new Blob([JSON.stringify(fileTokens.tokens)], {
type: 'text/json',
});
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = fileTokens.name + '-tokens.json';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
}

View file

@ -0,0 +1,6 @@
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
export const appConfig: ApplicationConfig = {
providers: [provideRouter([])],
};

View file

@ -0,0 +1,14 @@
:host {
display: block;
--svg-stroke-color: transparent;
--svg-fill-color: transparent;
inline-size: var(--spacing-16);
block-size: var(--spacing-16);
}
svg {
stroke: var(--svg-stroke-color);
fill: var(--svg-fill-color);
}

View file

@ -0,0 +1,48 @@
import { Component, input } from '@angular/core';
@Component({
selector: 'app-svg',
template: `
@switch (name()) {
@case ('tick') {
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="1154.667 712.01 14.666 11.333"
>
<path d="m1167.333 714.01-7.333 7.333-3.333-3.333" />
<path
stroke-linecap="round"
d="m1167.333 714.01-7.333 7.333-3.333-3.333"
/>
</svg>
}
@case ('download') {
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="859 710.01 16 16"
>
<path
stroke-linecap="round"
d="M873 720.01v2.667a1.335 1.335 0 0 1-1.333 1.333h-9.334a1.335 1.335 0 0 1-1.333-1.333v-2.667m2.667-3.333L867 720.01m0 0 3.333-3.333M867 720.01v-8"
/>
</svg>
}
@case ('reload') {
<svg
viewBox="0 0 16 16"
stroke-linecap="round"
stroke-linejoin="round"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M2.4 8a6 6 0 1 1 1.758 4.242M2.4 8l2.1-2zm0 0L1 5.5z"></path>
</svg>
}
}
`,
styleUrl: './svg.component.css',
})
export class SvgComponent {
name = input.required<'tick' | 'download' | 'reload'>();
}

View file

@ -0,0 +1,498 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`transform colors to tokens 1`] = `
{
"colors": {
"blue": {
"050": {
"$type": "color",
"$value": "#ebf8ff",
},
"100": {
"$type": "color",
"$value": "#bee3f8",
},
"650": {
"$type": "color",
"$value": "#2a4365",
},
"700": {
"$type": "color",
"$value": "#2c5282",
},
"900": {
"$type": "color",
"$value": "#1a365d",
},
},
"gray": {
"100": {
"$type": "color",
"$value": "#edf2f7",
},
"200": {
"$type": "color",
"$value": "#e2e8f0",
},
"400": {
"$type": "color",
"$value": "#a0aec0",
},
"600": {
"$type": "color",
"$value": "#4a5568",
},
"700": {
"$type": "color",
"$value": "#2d3748",
},
"800": {
"$type": "color",
"$value": "#1a202c",
},
},
"green": {
"050": {
"$type": "color",
"$value": "#f0fff4",
},
"100": {
"$type": "color",
"$value": "#c6f6d5",
},
"300": {
"$type": "color",
"$value": "#68d391",
},
"500": {
"$type": "color",
"$value": "#38a169",
},
"600": {
"$type": "color",
"$value": "#2f855a",
},
"700": {
"$type": "color",
"$value": "#276749",
},
"800": {
"$type": "color",
"$value": "#22543d",
},
},
"pink": {
"100": {
"$type": "color",
"$value": "#fed7e2",
},
},
"purple": {
"100": {
"$type": "color",
"$value": "#e9d8fd",
},
"300": {
"$type": "color",
"$value": "#b794f4",
},
"500": {
"$type": "color",
"$value": "#805ad5",
},
},
"red": {
"100": {
"$type": "color",
"$value": "#FED7D7",
},
"300": {
"$type": "color",
"$value": "#FC8181",
},
"500": {
"$type": "color",
"$value": "#e53e3e",
},
"600": {
"$type": "color",
"$value": "#c53030",
},
"800": {
"$type": "color",
"$value": "#822727",
},
},
"shadow": {
"dark": {
"$type": "color",
"$value": "#00000069",
},
"light": {
"$type": "color",
"$value": "#00000016",
},
"mid": {
"$type": "color",
"$value": "#00000060",
},
},
"white": {
"$type": "color",
"$value": "#ffffff",
},
"yellow": {
"050": {
"$type": "color",
"$value": "#fffff0",
},
"100": {
"$type": "color",
"$value": "#fefcbf",
},
"200": {
"$type": "color",
"$value": "#faf089",
},
"700": {
"$type": "color",
"$value": "#975a16",
},
"800": {
"$type": "color",
"$value": "#744210",
},
},
},
"ui/darkmode": {
"dm": {
"background": {
"blue": {
"$type": "color",
"$value": "#1a365d",
},
"green": {
"$type": "color",
"$value": "#1c4532",
},
"yellow": {
"$type": "color",
"$value": "#5f370e",
},
},
"button": {
"blue": {
"$type": "color",
"$value": "#2b6cb0",
},
"default": {
"$type": "color",
"$value": "#4a5568",
},
"green": {
"$type": "color",
"$value": "#2f855a",
},
"yellow": {
"$type": "color",
"$value": "#975a16",
},
"yellow[DONTUSE]": {
"$type": "color",
"$value": "#fefcbf",
},
},
"card": {
"background": {
"$type": "color",
"$value": "#2d3748",
},
},
"chart": {
"accent": {
"$type": "color",
"$value": "#9f7aea",
"alt": {
"$type": "color",
"$value": "#ecc94b",
},
},
"background": {
"$type": "color",
"$value": "#4a5568",
},
"green": {
"$type": "color",
"$value": "#38a169",
},
"red": {
"$type": "color",
"$value": "#e53e3e",
},
"yellow": {
"$type": "color",
"$value": "#ecc94b",
},
},
"dashboard": {
"background": {
"$type": "color",
"$value": "#1a202c",
},
},
"footer": {
"$type": "color",
"$value": "#1a202c",
},
"icon": {
"default": {
"$type": "color",
"$value": "#e2e8f0",
},
"secondary": {
"$type": "color",
"$value": "#718096",
},
},
"input": {
"$type": "color",
"$value": "#4a5568",
},
"label": {
"$type": "color",
"$value": "#a0aec0",
},
"sidebar": {
"$type": "color",
"$value": "#171923",
},
"text": {
"blue": {
"$type": "color",
"$value": "#63b3ed",
},
"default": {
"$type": "color",
"$value": "#a0aec0",
},
"emphasis": {
"$type": "color",
"$value": "#edf2f7",
},
"green": {
"default": {
"$type": "color",
"$value": "#68d391",
},
"emphasis": {
"$type": "color",
"$value": "#9ae6b4",
},
},
"red": {
"default": {
"$type": "color",
"$value": "#f56565",
},
"emphasis": {
"$type": "color",
"$value": "#FC8181",
},
},
"yellow": {
"$type": "color",
"$value": "#faf089",
},
},
},
},
"ui/lightmode": {
"lm": {
"axis": {
"line": {
"$type": "color",
"$value": "#a0aec0",
},
},
"background": {
"blue": {
"$type": "color",
"$value": "#ebf8ff",
},
"green": {
"$type": "color",
"$value": "#f0fff4",
},
"yellow": {
"$type": "color",
"$value": "#fffff0",
},
},
"bar": {
"line": {
"$type": "color",
"$value": "#a0aec040",
},
},
"button": {
"$type": "color",
"$value": "#E2E8F0",
"active": {
"$type": "color",
"$value": "#a0aec0",
},
"blue": {
"$type": "color",
"$value": "#bee3f8",
},
"green": {
"$type": "color",
"$value": "#c6f6d5",
},
"yellow": {
"$type": "color",
"$value": "#fefcbf",
},
},
"card": {
"background": {
"$type": "color",
"$value": "#ffffff",
},
},
"chart": {
"accent": {
"$type": "color",
"$value": "#b794f4",
"alt": {
"$type": "color",
"$value": "#faf089",
},
},
"background": {
"$type": "color",
"$value": "#E2E8F0",
},
"green": {
"$type": "color",
"$value": "#68d391",
},
"red": {
"$type": "color",
"$value": "#FC8181",
},
"yellow": {
"$type": "color",
"$value": "#faf089",
},
},
"dashboard": {
"background": {
"$type": "color",
"$value": "#EDF2F7",
},
},
"footer": {
"$type": "color",
"$value": "#e2e8f0",
},
"icon": {
"blue": {
"$type": "color",
"$value": "#2a4365",
},
"blue1": {
"$type": "color",
"$value": "#bee3f8",
},
"blue2": {
"$type": "color",
"$value": "#0000ff",
},
"default": {
"$type": "color",
"$value": "#A0AEC0",
},
"green": {
"$type": "color",
"$value": "#276749",
},
"red": {
"$type": "color",
"$value": "#c53030",
},
"secondary": {
"$type": "color",
"$value": "#e2e8f0",
},
"yellow": {
"$type": "color",
"$value": "#744210",
},
},
"input": {
"$type": "color",
"$value": "#EDF2F7",
},
"label": {
"$type": "color",
"$value": "#4a5568",
},
"placeholder": {
"$type": "color",
"$value": "#718096",
},
"shadow": {
"$type": "color",
"$value": "#00000020",
},
"sidebar": {
"$type": "color",
"$value": "#1a202c",
},
"text": {
"blue": {
"$type": "color",
"$value": "#2a4365",
},
"default": {
"$type": "color",
"$value": "#2d3748",
},
"emphasis": {
"$type": "color",
"$value": "#000000",
},
"green": {
"default": {
"$type": "color",
"$value": "#22543d",
},
"emphasis": {
"$type": "color",
"$value": "#38a169",
},
},
"red": {
"default": {
"$type": "color",
"$value": "#822727",
},
"emphasis": {
"$type": "color",
"$value": "#e53e3e",
},
},
"secondary": {
"$type": "color",
"$value": "#718096",
},
"yellow": {
"$type": "color",
"$value": "#744210",
},
},
},
},
}
`;

View file

@ -0,0 +1,654 @@
import { expect, test } from 'vitest';
import { transformToToken } from './transform-to-token';
const initColors = [
{
name: 'dm chart yellow',
color: '#ecc94b',
opacity: 1,
path: 'ui / dark mode',
},
{
name: 'dm text blue',
color: '#63b3ed',
opacity: 1,
path: 'ui / dark mode',
},
{
name: 'green 700',
color: '#276749',
opacity: 1,
path: 'colors',
},
{
name: 'dm text green emphasis',
color: '#9ae6b4',
opacity: 1,
path: 'ui / dark mode',
},
{
name: 'lm icon blue',
color: '#2a4365',
opacity: 1,
path: 'ui / light mode',
},
{
name: 'lm card background',
color: '#ffffff',
opacity: 1,
path: 'ui / light mode',
},
{
name: 'gray 600',
color: '#4a5568',
opacity: 1,
path: 'colors',
},
{
name: 'yellow 200',
color: '#faf089',
opacity: 1,
path: 'colors',
},
{
name: 'lm button',
color: '#E2E8F0',
opacity: 1,
path: 'ui / light mode',
},
{
name: 'green 300',
color: '#68d391',
opacity: 1,
path: 'colors',
},
{
name: 'dm chart red',
color: '#e53e3e',
opacity: 1,
path: 'ui / dark mode',
},
{
name: 'gray 400',
color: '#a0aec0',
opacity: 1,
path: 'colors',
},
{
name: 'lm dashboard background',
color: '#EDF2F7',
opacity: 1,
path: 'ui / light mode',
},
{
name: 'lm text yellow',
color: '#744210',
opacity: 1,
path: 'ui / light mode',
},
{
name: 'pink 100',
color: '#fed7e2',
opacity: 1,
path: 'colors',
},
{
name: 'lm text secondary',
color: '#718096',
opacity: 1,
path: 'ui / light mode',
},
{
name: 'blue 900',
color: '#1a365d',
opacity: 1,
path: 'colors',
},
{
name: 'green 800',
color: '#22543d',
opacity: 1,
path: 'colors',
},
{
name: 'red 300',
color: '#FC8181',
opacity: 1,
path: 'colors',
},
{
name: 'lm button yellow',
color: '#fefcbf',
opacity: 1,
path: 'ui / light mode',
},
{
name: 'lm text red emphasis',
color: '#e53e3e',
opacity: 1,
path: 'ui / light mode',
},
{
name: 'dm chart green',
color: '#38a169',
opacity: 1,
path: 'ui / dark mode',
},
{
name: 'lm placeholder',
color: '#718096',
opacity: 1,
path: 'ui / light mode',
},
{
name: 'purple 500',
color: '#805ad5',
opacity: 1,
path: 'colors',
},
{
name: 'lm chart yellow',
color: '#faf089',
opacity: 1,
path: 'ui / light mode',
},
{
name: 'shadow light',
color: '#000000',
opacity: 0.16078432,
path: 'colors',
},
{
name: 'dm footer',
color: '#1a202c',
opacity: 1,
path: 'ui / dark mode',
},
{
name: 'red 800',
color: '#822727',
opacity: 1,
path: 'colors',
},
{
name: 'lm text blue',
color: '#2a4365',
opacity: 1,
path: 'ui / light mode',
},
{
name: 'purple 300',
color: '#b794f4',
opacity: 1,
path: 'colors',
},
{
name: 'purple 100',
color: '#e9d8fd',
opacity: 1,
path: 'colors',
},
{
name: 'dm background blue',
color: '#1a365d',
opacity: 1,
path: 'ui / dark mode',
},
{
name: 'dm text red default',
color: '#f56565',
opacity: 1,
path: 'ui / dark mode',
},
{
name: 'gray 700',
color: '#2d3748',
opacity: 1,
path: 'colors',
},
{
name: 'lm button blue',
color: '#bee3f8',
opacity: 1,
path: 'ui / light mode',
},
{
name: 'dm icon default',
color: '#e2e8f0',
opacity: 1,
path: 'ui / dark mode',
},
{
name: 'green 600',
color: '#2f855a',
opacity: 1,
path: 'colors',
},
{
name: 'yellow 100',
color: '#fefcbf',
opacity: 1,
path: 'colors',
},
{
name: 'blue 100',
color: '#bee3f8',
opacity: 1,
path: 'colors',
},
{
name: 'dm background yellow',
color: '#5f370e',
opacity: 1,
path: 'ui / dark mode',
},
{
name: 'blue 650',
color: '#2a4365',
opacity: 1,
path: 'colors',
},
{
name: 'dm text green default',
color: '#68d391',
opacity: 1,
path: 'ui / dark mode',
},
{
name: 'lm bar line',
color: '#a0aec0',
opacity: 0.4,
path: 'ui / light mode',
},
{
name: 'dm background green',
color: '#1c4532',
opacity: 1,
path: 'ui / dark mode',
},
{
name: 'dm button blue',
color: '#2b6cb0',
opacity: 1,
path: 'ui / dark mode',
},
{
name: 'gray 100',
color: '#edf2f7',
opacity: 1,
path: 'colors',
},
{
name: 'dm button yellow',
color: '#975a16',
opacity: 1,
path: 'ui / dark mode',
},
/* 3 different blue colors in the same path */
{
name: 'lm icon blue',
color: '#bee3f8',
opacity: 1,
path: 'ui / light mode',
},
{
name: 'lm icon blue',
color: '#0000ff',
opacity: 1,
path: 'ui / light mode',
},
{
name: 'lm text default',
color: '#2d3748',
opacity: 1,
path: 'ui / light mode',
},
{
name: 'lm icon red',
color: '#c53030',
opacity: 1,
path: 'ui / light mode',
},
{
name: 'lm text green default',
color: '#22543d',
opacity: 1,
path: 'ui / light mode',
},
{
name: 'lm icon green',
color: '#276749',
opacity: 1,
path: 'ui / light mode',
},
{
name: 'shadow dark',
color: '#000000',
opacity: 0.69803923,
path: 'colors',
},
{
name: 'lm chart background',
color: '#E2E8F0',
opacity: 1,
path: 'ui / light mode',
},
{
name: 'yellow 050',
color: '#fffff0',
opacity: 1,
path: 'colors',
},
{
name: 'green 100',
color: '#c6f6d5',
opacity: 1,
path: 'colors',
},
{
name: 'lm sidebar',
color: '#1a202c',
opacity: 1,
path: 'ui / light mode',
},
{
name: 'dm label',
color: '#a0aec0',
opacity: 1,
path: 'ui / dark mode',
},
{
name: 'green 050',
color: '#f0fff4',
opacity: 1,
path: 'colors',
},
{
name: 'dm button green',
color: '#2f855a',
opacity: 1,
path: 'ui / dark mode',
},
{
name: 'lm label',
color: '#4a5568',
opacity: 1,
path: 'ui / light mode',
},
{
name: 'lm button active',
color: '#a0aec0',
opacity: 1,
path: 'ui / light mode',
},
{
name: 'dm icon secondary',
color: '#718096',
opacity: 1,
path: 'ui / dark mode',
},
{
name: 'dm chart background',
color: '#4a5568',
opacity: 1,
path: 'ui / dark mode',
},
{
name: 'lm axis line',
color: '#a0aec0',
opacity: 1,
path: 'ui / light mode',
},
{
name: 'dm button yellow[DONTUSE]',
color: '#fefcbf',
opacity: 1,
path: 'ui / dark mode',
},
{
name: 'dm sidebar',
color: '#171923',
opacity: 1,
path: 'ui / dark mode',
},
{
name: 'dm text emphasis',
color: '#edf2f7',
opacity: 1,
path: 'ui / dark mode',
},
{
name: 'lm chart green',
color: '#68d391',
opacity: 1,
path: 'ui / light mode',
},
{
name: 'lm background blue',
color: '#ebf8ff',
opacity: 1,
path: 'ui / light mode',
},
{
name: 'red 100',
color: '#FED7D7',
opacity: 1,
path: 'colors',
},
{
name: 'dm text default',
color: '#a0aec0',
opacity: 1,
path: 'ui / dark mode',
},
{
name: 'red 500',
color: '#e53e3e',
opacity: 1,
path: 'colors',
},
{
name: 'yellow 800',
color: '#744210',
opacity: 1,
path: 'colors',
},
{
name: 'blue 050',
color: '#ebf8ff',
opacity: 1,
path: 'colors',
},
{
name: 'gray 800',
color: '#1a202c',
opacity: 1,
path: 'colors',
},
{
name: 'lm background yellow',
color: '#fffff0',
opacity: 1,
path: 'ui / light mode',
},
{
name: 'dm chart accent',
color: '#9f7aea',
opacity: 1,
path: 'ui / dark mode',
},
{
name: 'lm shadow',
color: '#000000',
opacity: 0.2,
path: 'ui / light mode',
},
{
name: 'dm text red emphasis',
color: '#FC8181',
opacity: 1,
path: 'ui / dark mode',
},
{
name: 'dm input',
color: '#4a5568',
opacity: 1,
path: 'ui / dark mode',
},
{
name: 'dm chart accent alt',
color: '#ecc94b',
opacity: 1,
path: 'ui / dark mode',
},
{
name: 'lm button green',
color: '#c6f6d5',
opacity: 1,
path: 'ui / light mode',
},
{
name: 'red 600',
color: '#c53030',
opacity: 1,
path: 'colors',
},
{
name: 'lm icon secondary',
color: '#e2e8f0',
opacity: 1,
path: 'ui / light mode',
},
{
name: 'dm text yellow',
color: '#faf089',
opacity: 1,
path: 'ui / dark mode',
},
{
name: 'green 500',
color: '#38a169',
opacity: 1,
path: 'colors',
},
{
name: 'shadow mid',
color: '#000000',
opacity: 0.6,
path: 'colors',
},
{
name: 'lm text green emphasis',
color: '#38a169',
opacity: 1,
path: 'ui / light mode',
},
{
name: 'blue 700',
color: '#2c5282',
opacity: 1,
path: 'colors',
},
{
name: 'lm text red default',
color: '#822727',
opacity: 1,
path: 'ui / light mode',
},
{
name: 'lm footer',
color: '#e2e8f0',
opacity: 1,
path: 'ui / light mode',
},
{
name: 'lm text emphasis',
color: '#000000',
opacity: 1,
path: 'ui / light mode',
},
{
name: 'dm card background',
color: '#2d3748',
opacity: 1,
path: 'ui / dark mode',
},
{
name: 'lm chart accent',
color: '#b794f4',
opacity: 1,
path: 'ui / light mode',
},
{
name: 'white',
color: '#ffffff',
opacity: 1,
path: 'colors',
},
{
name: 'dm button default',
color: '#4a5568',
opacity: 1,
path: 'ui / dark mode',
},
{
name: 'lm input',
color: '#EDF2F7',
opacity: 1,
path: 'ui / light mode',
},
{
name: 'lm chart accent alt',
color: '#faf089',
opacity: 1,
path: 'ui / light mode',
},
{
name: 'gray 200',
color: '#e2e8f0',
opacity: 1,
path: 'colors',
},
{
name: 'lm icon yellow',
color: '#744210',
opacity: 1,
path: 'ui / light mode',
},
{
name: 'dm dashboard background',
color: '#1a202c',
opacity: 1,
path: 'ui / dark mode',
},
{
name: 'lm icon default',
color: '#A0AEC0',
opacity: 1,
path: 'ui / light mode',
},
{
name: 'yellow 700',
color: '#975a16',
opacity: 1,
path: 'colors',
},
{
name: 'lm background green',
color: '#f0fff4',
opacity: 1,
path: 'ui / light mode',
},
{
name: 'lm chart red',
color: '#FC8181',
opacity: 1,
path: 'ui / light mode',
},
];
test('transform colors to tokens', () => {
const result = transformToToken(initColors);
expect(result).toMatchSnapshot();
});

View file

@ -0,0 +1,47 @@
import { LibraryColor } from '@penpot/plugin-types';
import { TokenStructure } from '../../model';
export function transformToToken(colors: LibraryColor[]) {
const result: TokenStructure = {};
colors.forEach((data) => {
const currentOpacity = data.opacity ?? 1;
const opacity = currentOpacity < 1 ? Math.floor(currentOpacity * 100) : '';
const value = `${data.color}${opacity}`;
const names: string[] = data.name.split(' ');
const key: string = data.path.replace(' \\/ ', '/').replace(/ /g, '');
if (!result[key]) {
result[key] = {};
}
const props = [key, ...names];
let acc = result;
props.forEach((prop, index) => {
if (!acc[prop]) {
acc[prop] = {};
}
if (index === props.length - 1) {
let propIndex = 1;
const initialProp = prop;
while (acc[prop]?.$value) {
prop = `${initialProp}${propIndex}`;
propIndex++;
}
acc[prop] = {
$value: value,
$type: 'color',
};
}
acc = acc[prop] as TokenStructure;
});
});
return result;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View file

@ -0,0 +1,7 @@
{
"name": "Colors to Tokens",
"description": "Generate a design tokens file from a list of colors",
"code": "/assets/plugin.js",
"icon": "/assets/icon.png",
"permissions": ["content:read", "library:read", "allow:downloads"]
}

View file

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>colors-to-tokens-plugin</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<app-root></app-root>
</body>
</html>

View file

@ -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)
);

View file

@ -0,0 +1,42 @@
import { LibraryColor } from '@penpot/plugin-types';
export interface Token {
$value: string;
$type: string;
}
export type TokenStructure = {
[key: string]: Token | TokenStructure;
};
export interface GETColorsPluginUIEvent {
type: 'get-colors';
}
export interface ResetPluginUIEvent {
type: 'reset';
}
export interface ResizePluginUIEvent {
type: 'resize';
height: number;
width: number;
}
export type PluginUIEvent =
| GETColorsPluginUIEvent
| ResizePluginUIEvent
| ResetPluginUIEvent;
export interface ThemePluginEvent {
type: 'theme';
content: string;
}
export interface SetColorsPluginEvent {
type: 'set-colors';
colors: LibraryColor[] | null;
fileName: string;
}
export type PluginMessageEvent = ThemePluginEvent | SetColorsPluginEvent;

View file

@ -0,0 +1,50 @@
import type { PluginMessageEvent, PluginUIEvent } from './model.js';
const defaultSize = {
width: 410,
height: 280,
};
penpot.ui.open('COLORS TO TOKENS', `?theme=${penpot.theme}`, {
width: defaultSize.width,
height: defaultSize.height,
});
penpot.on('themechange', (theme) => {
sendMessage({ type: 'theme', content: theme });
});
penpot.ui.onMessage<PluginUIEvent>((message) => {
if (message.type === 'get-colors') {
const colors = penpot.library.local.colors.filter(
(color) => !color.gradient,
);
const fileName = penpot.currentFile?.name ?? 'Untitled';
sendMessage({
type: 'set-colors',
colors,
fileName,
});
} else if (message.type === 'resize') {
if (
penpot.ui.size?.width === defaultSize.width &&
penpot.ui.size?.height === defaultSize.height
) {
resize(message.width, message.height);
}
} else if (message.type === 'reset') {
resize(defaultSize.width, defaultSize.height);
}
});
function resize(width: number, height: number) {
if ('resize' in penpot.ui) {
(penpot as any).ui.resize(width, height);
}
}
function sendMessage(message: PluginMessageEvent) {
penpot.ui.sendMessage(message);
}

View file

@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": []
},
"files": ["src/main.ts"],
"include": ["src/**/*.d.ts"],
"exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"]
}

View file

@ -0,0 +1,7 @@
{
"extends": "./tsconfig.json",
"include": ["src/**/*.ts"],
"compilerOptions": {
"types": []
}
}

View file

@ -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
}
}

View file

@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": []
},
"files": ["src/plugin.ts"],
"include": ["../../libs/plugin-types/index.d.ts"]
}

View file

@ -0,0 +1,26 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": [
"vitest/globals",
"vitest/importMeta",
"vite/client",
"node",
"vitest"
]
},
"include": [
"vite.config.ts",
"vitest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
]
}

View file

@ -0,0 +1,21 @@
/// <reference types='vitest' />
import { defineConfig } from 'vite';
export default defineConfig({
root: __dirname,
cacheDir: '../node_modules/.vite/colors-to-tokens-plugin',
test: {
watch: false,
globals: true,
cache: {
dir: '../node_modules/.vitest',
},
environment: 'jsdom',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
reporters: ['default'],
coverage: {
reportsDirectory: '../coverage/colors-to-tokens-plugin',
provider: 'v8',
},
},
});

View file

@ -15,6 +15,7 @@
"start:plugin:palette": "npx nx run create-palette-plugin:build --watch & npx nx run create-palette-plugin:preview",
"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",
"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",