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:
parent
06db3af490
commit
b535718c0e
15 changed files with 204 additions and 74 deletions
|
@ -7,4 +7,3 @@
|
|||
"selection:read"
|
||||
]
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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 });
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 });
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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));
|
||||
|
|
6
libs/plugins-runtime/src/lib/index.d.ts
vendored
6
libs/plugins-runtime/src/lib/index.d.ts
vendored
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue