0
Fork 0
mirror of https://github.com/penpot/penpot-plugins.git synced 2025-01-06 14:50:21 -05:00

feat: add theme support

This commit is contained in:
Juanfran 2024-03-08 12:27:58 +01:00
parent 06db3af490
commit b535718c0e
15 changed files with 204 additions and 74 deletions

View file

@ -7,4 +7,3 @@
"selection:read"
]
}

View file

@ -1,5 +1,11 @@
.wrapper {
color: var(--app-white);
&[data-theme='dark'] {
color: var(--app-white);
}
&[data-theme='light'] {
color: var(--app-black);
}
}
.color {
@ -92,4 +98,4 @@
border-left: var(--spacing-12) solid transparent;
border-right: var(--spacing-12) solid transparent;
border-bottom: var(--spacing-24) solid transparent;
}
}

View file

@ -8,7 +8,8 @@ export class AppElement extends HTMLElement {
const luminosityFirstColor = this.getLuminosity(firstColor);
const luminositySecondColor = this.getLuminosity(secondColor);
const result = (luminosityFirstColor + 0.05) / (luminositySecondColor + 0.05);
const result =
(luminosityFirstColor + 0.05) / (luminositySecondColor + 0.05);
this.setColors(firstColor, secondColor);
this.setResult(result.toFixed(2).toString());
this.setA11yTags(result);
@ -16,14 +17,18 @@ export class AppElement extends HTMLElement {
getLuminosity(color: string) {
const rgb = this.hexToRgb(color);
return 0.2126 * (rgb[0]/255) + 0.7152 * (rgb[1]/255) + 0.0722 * (rgb[2]/255);
return (
0.2126 * (rgb[0] / 255) +
0.7152 * (rgb[1] / 255) +
0.0722 * (rgb[2] / 255)
);
}
hexToRgb(hex: string) {
const r = parseInt(hex.slice(1, 3), 16)
const g = parseInt(hex.slice(3, 5), 16)
const b = parseInt(hex.slice(5, 7), 16)
return [ r, g, b ];
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return [r, g, b];
}
setResult(text: string) {
@ -56,13 +61,24 @@ export class AppElement extends HTMLElement {
code2.innerText = secondColor ? secondColor : '';
}
if (contrastPreview && smallText && largeText && circle && square && triangle) {
contrastPreview.style.background = secondColor ? secondColor : 'transparent';
if (
contrastPreview &&
smallText &&
largeText &&
circle &&
square &&
triangle
) {
contrastPreview.style.background = secondColor
? secondColor
: 'transparent';
smallText.style.color = firstColor ? firstColor : 'transparent';
largeText.style.color = firstColor ? firstColor : 'transparent';
circle.style.background = firstColor ? firstColor : 'transparent';
square.style.background = firstColor ? firstColor : 'transparent';
triangle.style.borderBottom = firstColor ? `var(--spacing-24) solid ${firstColor}` : 'var(--spacing-24) solid transparent';
triangle.style.borderBottom = firstColor
? `var(--spacing-24) solid ${firstColor}`
: 'var(--spacing-24) solid transparent';
}
const emptyPreview = document.getElementById('empty-preview');
@ -75,45 +91,45 @@ export class AppElement extends HTMLElement {
setA11yTags(result: number) {
const selectors = {
aa: document.getElementById('aa'),
aaa: document.getElementById('aaa'),
aaLg: document.getElementById('aa-lg'),
aaaLg: document.getElementById('aaa-lg'),
graphics: document.getElementById('graphics')
aa: document.getElementById('aa'),
aaa: document.getElementById('aaa'),
aaLg: document.getElementById('aa-lg'),
aaaLg: document.getElementById('aaa-lg'),
graphics: document.getElementById('graphics'),
};
const fail = 'tag fail';
const good = 'tag good';
function setClass(selector: HTMLElement | null, className: string) {
if (selector) {
selector.className = className;
}
if (selector) {
selector.className = className;
}
}
if (result > 7) {
setClass(selectors.aa, good);
setClass(selectors.aaa, good);
setClass(selectors.aaLg, good);
setClass(selectors.aaaLg, good);
setClass(selectors.graphics, good);
setClass(selectors.aa, good);
setClass(selectors.aaa, good);
setClass(selectors.aaLg, good);
setClass(selectors.aaaLg, good);
setClass(selectors.graphics, good);
} else if (result > 4.5) {
setClass(selectors.aa, good);
setClass(selectors.aaa, fail);
setClass(selectors.aaLg, good);
setClass(selectors.aaaLg, good);
setClass(selectors.graphics, good);
setClass(selectors.aa, good);
setClass(selectors.aaa, fail);
setClass(selectors.aaLg, good);
setClass(selectors.aaaLg, good);
setClass(selectors.graphics, good);
} else if (result > 3) {
setClass(selectors.aa, fail);
setClass(selectors.aaa, fail);
setClass(selectors.aaLg, good);
setClass(selectors.aaaLg, fail);
setClass(selectors.graphics, good);
setClass(selectors.aa, fail);
setClass(selectors.aaa, fail);
setClass(selectors.aaLg, good);
setClass(selectors.aaaLg, fail);
setClass(selectors.graphics, good);
} else {
setClass(selectors.aa, fail);
setClass(selectors.aaa, fail);
setClass(selectors.aaLg, fail);
setClass(selectors.aaaLg, fail);
setClass(selectors.graphics, fail);
setClass(selectors.aa, fail);
setClass(selectors.aaa, fail);
setClass(selectors.aaLg, fail);
setClass(selectors.aaaLg, fail);
setClass(selectors.graphics, fail);
}
}
@ -122,7 +138,7 @@ export class AppElement extends HTMLElement {
if (event.data.type === 'selection') {
if (event.data.content.length === 2) {
this.calculateContrast('#d5d1d1', '#000410');
} else {
} else {
this.setColors(null, null);
this.setResult('0');
this.setA11yTags(0);
@ -130,10 +146,14 @@ export class AppElement extends HTMLElement {
} else if (event.data.type === 'page') {
console.log('refrespage', event.data);
} else if (event.data.type === 'init') {
this.setAttribute('data-theme', event.data.content.theme);
if (event.data.content.selection.length === 2) {
//TODO get real colors from selection
this.calculateContrast('#d5d1d1', '#000410');
}
} else if (event.data.type === 'theme') {
this.setAttribute('data-theme', event.data.content);
}
});

View file

@ -1,5 +1,4 @@
penpot.ui.open('Contrast plugin', 'http://localhost:4201', {
theme: 'dark',
width: 450,
height: 625,
});
@ -16,6 +15,7 @@ penpot.ui.onMessage<{ content: string }>((message) => {
pageId: pageState.id,
fileId: fileState.id,
revn: fileState.revn,
theme: penpot.getTheme(),
selection: penpot.getSelection(),
},
});
@ -24,4 +24,8 @@ penpot.ui.onMessage<{ content: string }>((message) => {
penpot.on('selectionchange', (id) => {
penpot.ui.sendMessage({ type: 'selection', content: id });
});
});
penpot.on('themechange', (theme) => {
penpot.ui.sendMessage({ type: 'theme', content: theme });
});

View file

@ -45,11 +45,14 @@ export class AppElement extends HTMLElement {
this.#revn = event.data.content.revn;
this.refreshPage(event.data.content.pageId, event.data.content.name);
this.refreshSelectionId(event.data.content.selection);
this.setAttribute('data-theme', event.data.content.theme);
} else if (event.data.type === 'theme') {
this.setAttribute('data-theme', event.data.content);
}
});
this.innerHTML = `
<div class="wrapper" data-theme="light">
<div class="wrapper">
<h1>Test area</h1>
<p>Current project name: <span id="project-name">Unknown</span></p>

View file

@ -1,7 +1,6 @@
penpot.log('Hello from plugin');
penpot.ui.open('Plugin name', 'http://localhost:4201', {
theme: 'light',
width: 500,
height: 600,
});
@ -27,6 +26,7 @@ penpot.ui.onMessage<{ content: string }>((message) => {
pageId: pageState.id,
fileId: fileState.id,
revn: fileState.revn,
theme: penpot.getTheme(),
selection: penpot.getSelection(),
},
});
@ -57,3 +57,7 @@ penpot.on('filechange', (file) => {
penpot.on('selectionchange', (id) => {
penpot.ui.sendMessage({ type: 'selection', content: id });
});
penpot.on('themechange', (theme) => {
penpot.ui.sendMessage({ type: 'theme', content: theme });
});

View file

@ -1,5 +1,5 @@
html, body {
html,
body {
background: var(--app-black);
color: var(--app-white);
margin: 0 var(--spacing-20);
@ -23,12 +23,12 @@ section {
margin-block-end: var(--spacing-20);
padding: var(--spacing-32);
&[data-theme="dark"] {
border: 1px solid var(--db-quaternary);
&[data-theme='dark'] {
border: 1px solid var(--db-quaternary);
}
&[data-theme="light"] {
border: 1px solid var(--lb-quaternary);
&[data-theme='light'] {
border: 1px solid var(--lb-quaternary);
}
}
@ -58,7 +58,7 @@ section {
.color-preview {
block-size: var(--spacing-36);
border: 1px solid #8F9DA3;
border: 1px solid #8f9da3;
border-radius: var(--spacing-4);
display: block;
inline-size: var(--spacing-36);
@ -87,7 +87,7 @@ section {
gap: var(--spacing-20);
&:not(:last-child) {
margin-block-end: var(--spacing-20);
margin-block-end: var(--spacing-20);
}
}
@ -95,4 +95,4 @@ section {
.icons-section {
display: flex;
gap: var(--spacing-8);
}
}

View file

@ -19,6 +19,9 @@ penpot.ui.getPageState();
// selection id
penpot.ui.getSelection();
// current theme (dark/light)
penpot.ui.getTheme();
```
### Messages
@ -53,7 +56,7 @@ window.addEventListener('message', function (event) {
### Events
Current events `pagechange`, `filechange` and `selectionchange`.
Current events `pagechange`, `filechange`,`selectionchange` and `themechange`.
```ts
const event = (page) => {

View file

@ -2,7 +2,7 @@ import 'ses';
import './lib/plugin-modal';
import { ɵloadPlugin } from './lib/load-plugin';
import { setFileState, setPageState, setSelection } from './lib/api';
import { setFileState, setPageState, setSelection, setTheme } from './lib/api';
import { getSelectedUuids } from 'plugins-parser';
repairIntrinsics({
@ -38,4 +38,12 @@ export function initialize(api: any) {
setSelection(selectionData);
});
api.addListener('plugin-theme', 'theme', (theme: 'light' | 'default') => {
console.log('Theme change:', theme);
const newTheme: Theme = theme === 'default' ? 'dark' : theme;
setTheme(newTheme);
});
}

View file

@ -1,3 +1,4 @@
import { setModalTheme } from '../create-modal';
import { Manifest, Permissions } from '../models/manifest.model';
import { OpenUIOptions } from '../models/open-ui-options.model';
import openUIApi from './openUI.api';
@ -5,18 +6,21 @@ import z from 'zod';
type Callback<T> = (message: T) => void;
const validEvents = ['pagechange', 'filechange', 'selectionchange'] as const;
const validEvents = [
'pagechange',
'filechange',
'selectionchange',
'themechange',
] as const;
export let uiMessagesCallbacks: Callback<unknown>[] = [];
let modal: HTMLElement | null = null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let pageState = {} as any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let fileState = {} as any;
let selection: null | string[] = null;
let pageState: Page | null = null;
let fileState: File | null = null;
let selection: string[] = [];
let themeState: Theme = 'dark';
const eventListeners: Map<string, Callback<unknown>[]> = new Map();
@ -56,6 +60,20 @@ export function setSelection(selectionId: string[]) {
triggerEvent('selectionchange', selectionId);
}
export function setTheme(theme: Theme) {
if (themeState === theme) {
return;
}
themeState = theme;
if (modal) {
setModalTheme(modal, themeState);
}
triggerEvent('themechange', theme);
}
export function createApi(manifest: Manifest) {
const closePlugin = () => {
modal?.removeEventListener('close', closePlugin);
@ -77,7 +95,7 @@ export function createApi(manifest: Manifest) {
const penpot: Penpot = {
ui: {
open: (name: string, url: string, options: OpenUIOptions) => {
modal = openUIApi(name, url, options);
modal = openUIApi(name, url, themeState, options);
modal.addEventListener('close', closePlugin, {
once: true,
@ -154,8 +172,11 @@ export function createApi(manifest: Manifest) {
return selection;
},
getTheme: () => {
return themeState;
},
fetch,
} as const;
};
return penpot;
}

View file

@ -4,7 +4,7 @@ import { createModal } from '../create-modal';
export default z
.function()
.args(z.string(), z.string(), openUISchema)
.implement((title, url, options) => {
return createModal(title, url, options);
.args(z.string(), z.string(), z.enum(['dark', 'light']), openUISchema)
.implement((title, url, theme, options) => {
return createModal(title, url, theme, options);
});

View file

@ -4,6 +4,7 @@ import {
setFileState,
setPageState,
setSelection,
setTheme,
triggerEvent,
uiMessagesCallbacks,
} from './index.js';
@ -16,6 +17,7 @@ vi.mock('./openUI.api', () => {
dispatchEvent: vi.fn(),
removeEventListener: vi.fn(),
remove: vi.fn(),
setAttribute: vi.fn(),
})),
};
});
@ -50,7 +52,7 @@ describe('Plugin api', () => {
const modalMock = openUIApiMock.mock.results[0].value;
expect(openUIApiMock).toHaveBeenCalledWith(name, url, options);
expect(openUIApiMock).toHaveBeenCalledWith(name, url, 'dark', options);
expect(modalMock.addEventListener).toHaveBeenCalledWith(
'close',
expect.any(Function),
@ -133,6 +135,36 @@ describe('Plugin api', () => {
});
});
it('selectionchange', () => {
const callback = vi.fn();
api.on('selectionchange', callback);
triggerEvent('selectionchange', 'test');
api.off('selectionchange', callback);
triggerEvent('selectionchange', 'test');
expect(callback).toHaveBeenCalledWith('test');
expect(callback).toHaveBeenCalledTimes(1);
});
it('themechange', () => {
const callback = vi.fn();
api.on('themechange', callback);
triggerEvent('themechange', 'light');
api.off('themechange', callback);
triggerEvent('themechange', 'light');
expect(callback).toHaveBeenCalledWith('light');
expect(callback).toHaveBeenCalledTimes(1);
});
describe.concurrent('permissions', () => {
const api = createApi({
name: 'test',
@ -199,7 +231,7 @@ describe('Plugin api', () => {
});
it('get selection', () => {
const selection = '123';
const selection = ['123'];
setSelection(selection);
@ -208,6 +240,23 @@ describe('Plugin api', () => {
expect(currentSelection).toEqual(selection);
});
it('set theme refresh modal theme', () => {
const name = 'test';
const url = 'http://fake.com';
const options = { width: 100, height: 100 };
const openUIApiMock = vi.mocked(openUIApi);
api.ui.open(name, url, options);
setTheme('light');
const modalMock = openUIApiMock.mock.results[0].value;
expect(modalMock.setAttribute).toHaveBeenCalledWith('data-theme', 'light');
expect(modalMock.setAttribute).toHaveBeenCalledTimes(1);
expect(api.getTheme()).toBe('light');
});
it('close puglin', () => {
const name = 'test';
const url = 'http://fake.com';

View file

@ -1,9 +1,19 @@
import { OpenUIOptions } from './models/open-ui-options.model';
export function createModal(name: string, url: string, options: OpenUIOptions) {
export function setModalTheme(modal: HTMLElement, theme: Theme) {
modal.setAttribute('data-theme', theme);
}
export function createModal(
name: string,
url: string,
theme: Theme,
options: OpenUIOptions
) {
const modal = document.createElement('plugin-modal');
modal.setAttribute('data-theme', options.theme);
setModalTheme(modal, theme);
modal.setAttribute('title', name);
modal.setAttribute('iframe-src', url);
modal.setAttribute('width', String(options.width || 300));

View file

@ -15,8 +15,11 @@ interface EventsMap {
pagechange: Page;
filechange: File;
selectionchange: string[];
themechange: Theme;
}
type Theme = 'light' | 'dark';
interface Penpot {
ui: {
open: (
@ -40,7 +43,8 @@ interface Penpot {
) => void;
getFileState: () => File | null;
getPageState: () => Page | null;
getSelection: () => string | null;
getSelection: () => string[];
getTheme: () => Theme;
fetch: typeof fetch;
}

View file

@ -1,7 +1,6 @@
import { z } from 'zod';
export const openUISchema = z.object({
theme: z.literal('dark') || z.literal('light'),
width: z.number().positive(),
height: z.number().positive(),
});