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

Merge pull request #9 from penpot/contrast-plugin

feat: contrast plugin
This commit is contained in:
Marina López 2024-03-08 11:10:47 +01:00 committed by GitHub
commit 06db3af490
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 552 additions and 13 deletions

View file

@ -0,0 +1,3 @@
{
"presets": ["@nx/js/babel"]
}

View file

@ -0,0 +1,18 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}

View file

@ -0,0 +1,8 @@
{
"jsc": {
"parser": {
"syntax": "typescript"
},
"target": "es2016"
}
}

View file

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>ContrastPlugin</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="stylesheet" href="/src/styles.css" />
</head>
<body>
<app-root></app-root>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View file

@ -0,0 +1,8 @@
{
"name": "contrast-plugin",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
"sourceRoot": "apps/contrast-plugin/src",
"tags": ["type:plugin"],
"targets": {}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,10 @@
{
"name": "Contrast plugin",
"code": "http://localhost:4201/plugin.js",
"permissions": [
"page:read",
"file:read",
"selection:read"
]
}

View file

@ -0,0 +1,95 @@
.wrapper {
color: var(--app-white);
}
.color {
align-items: center;
display: flex;
margin-inline-end: var(--spacing-16);
}
.color-preview {
block-size: var(--spacing-36);
border: 1px solid var(--df-secondary);
border-radius: var(--spacing-4);
display: block;
inline-size: var(--spacing-36);
margin-inline-end: var(--spacing-16);
}
.fail {
background-color: var(--error-500);
}
.good {
background-color: var(--success-500);
}
.title {
margin-block-end: var(--spacing-8);
}
.list {
display: flex;
margin-block-end: var(--spacing-16);
}
.tag {
border-radius: var(--spacing-4);
color: var(--db-primary);
margin-inline-end: var(--spacing-16);
padding: var(--spacing-4) var(--spacing-8);
text-transform: uppercase;
}
.contrast-preview {
align-items: center;
border: 1px solid var(--df-secondary);
border-radius: var(--spacing-4);
box-sizing: content-box;
block-size: calc(2 * var(--spacing-40));
display: flex;
flex-direction: column;
justify-content: center;
inline-size: calc(100% - var(--spacing-16));
margin-block-end: var(--spacing-16);
padding-block: var(--spacing-24);
}
.empty-preview {
position: absolute;
}
.text {
color: transparent;
margin-block-end: var(--spacing-8);
&.small {
font-size: 18px;
}
&.large {
font-size: 24px;
}
}
.icons-list {
display: flex;
gap: var(--spacing-8);
margin-block-start: var(--spacing-8);
}
.shape {
block-size: var(--spacing-24);
inline-size: var(--spacing-24);
}
.circle {
border-radius: 50%;
}
.triangle {
border-left: var(--spacing-12) solid transparent;
border-right: var(--spacing-12) solid transparent;
border-bottom: var(--spacing-24) solid transparent;
}

View file

@ -0,0 +1,21 @@
import { AppElement } from './app.element';
describe('AppElement', () => {
let app: AppElement;
beforeEach(() => {
app = new AppElement();
});
it('should create successfully', () => {
expect(app).toBeTruthy();
});
it('should have a greeting', () => {
app.connectedCallback();
expect(app.querySelector('h1').innerHTML).toContain(
'Welcome contrast-plugin'
);
});
});

View file

@ -0,0 +1,184 @@
import 'plugins-styles/lib/styles.css';
import './app.element.css';
export class AppElement extends HTMLElement {
public static observedAttributes = [];
calculateContrast(firstColor: string, secondColor: string) {
const luminosityFirstColor = this.getLuminosity(firstColor);
const luminositySecondColor = this.getLuminosity(secondColor);
const result = (luminosityFirstColor + 0.05) / (luminositySecondColor + 0.05);
this.setColors(firstColor, secondColor);
this.setResult(result.toFixed(2).toString());
this.setA11yTags(result);
}
getLuminosity(color: string) {
const rgb = this.hexToRgb(color);
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 ];
}
setResult(text: string) {
const selector = document.getElementById('result');
if (selector) {
selector.innerText = `${text} : 1`;
}
}
setColors(firstColor: string | null, secondColor: string | null) {
const color1 = document.getElementById('first-color');
const color2 = document.getElementById('second-color');
const code1 = document.getElementById('first-color-code');
const code2 = document.getElementById('second-color-code');
const contrastPreview = document.getElementById('contrast-preview');
const smallText = document.getElementById('small-text');
const largeText = document.getElementById('large-text');
const circle = document.getElementById('circle');
const square = document.getElementById('square');
const triangle = document.getElementById('triangle');
if (color1 && code1) {
color1.style.background = firstColor ? firstColor : 'transparent';
code1.innerText = firstColor ? firstColor : '';
}
if (color2 && code2) {
color2.style.background = secondColor ? secondColor : 'transparent';
code2.innerText = secondColor ? secondColor : '';
}
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';
}
const emptyPreview = document.getElementById('empty-preview');
if (!firstColor && !secondColor && emptyPreview) {
emptyPreview.style.display = 'block';
} else if (emptyPreview) {
emptyPreview.style.display = 'none';
}
}
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')
};
const fail = 'tag fail';
const good = 'tag good';
function setClass(selector: HTMLElement | null, className: string) {
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);
} 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);
} else if (result > 3) {
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);
}
}
connectedCallback() {
window.addEventListener('message', (event) => {
if (event.data.type === 'selection') {
if (event.data.content.length === 2) {
this.calculateContrast('#d5d1d1', '#000410');
} else {
this.setColors(null, null);
this.setResult('0');
this.setA11yTags(0);
}
} else if (event.data.type === 'page') {
console.log('refrespage', event.data);
} else if (event.data.type === 'init') {
if (event.data.content.selection.length === 2) {
//TODO get real colors from selection
this.calculateContrast('#d5d1d1', '#000410');
}
}
});
this.innerHTML = `
<div class="wrapper">
<div id="contrast-preview" class="contrast-preview">
<p id="empty-preview" class="empty-preview">Select two colors to calculate contrast</p>
<p id="small-text" data-color="text" data-second class="text small">SMALL sample text</p>
<p id="large-text" data-color="text" data-second class="text large">LARGE sample text</p>
<ul class="icons-list">
<span id="circle" class="shape circle"></span>
<span id="square" class="shape square"></span>
<span id="triangle" class="triangle"></span>
</ul>
</div>
<p class="title body-l">Selected colors:</p>
<ul class="list">
<li class="color">
<span id="first-color" data-first class="color-preview"></span>
<code id="first-color-code"></code>
</li>
<li class="color">
<span id="second-color" data-second class="color-preview"></span>
<code id="second-color-code"></code>
</li>
</ul>
<p class="title body-l">Contrast ratio: <span id="result">0 : 1</span></p>
<p class="title body-l">Normal text:</p>
<ul class="list">
<li id="aa" class="tag fail">AA</li>
<li id="aaa" class="tag fail">AAA</li>
</ul>
<p class="title body-l">Large text (24px or 19px + bold):</p>
<ul class="list">
<li id="aa-lg" class="tag fail">AA</li>
<li id="aaa-lg" class="tag fail">AAA</li>
</ul>
<p class="title body-l">Graphics (such as form input borders):</p>
<ul class="list">
<li id="graphics" class="tag fail">AA</li>
</ul>
</div>
`;
parent.postMessage({ content: 'ready' }, '*');
}
}
customElements.define('app-root', AppElement);

View file

View file

@ -0,0 +1 @@
import './app/app.element';

View file

@ -0,0 +1,27 @@
penpot.ui.open('Contrast plugin', 'http://localhost:4201', {
theme: 'dark',
width: 450,
height: 625,
});
penpot.ui.onMessage<{ content: string }>((message) => {
if (message.content === 'ready') {
const pageState = penpot.getPageState();
const fileState = penpot.getFileState();
penpot.ui.sendMessage({
type: 'init',
content: {
name: pageState.name,
pageId: pageState.id,
fileId: fileState.id,
revn: fileState.revn,
selection: penpot.getSelection(),
},
});
}
});
penpot.on('selectionchange', (id) => {
penpot.ui.sendMessage({ type: 'selection', content: id });
});

View file

@ -0,0 +1 @@
/* You can add global styles to this file, and also import other style files */

View file

@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": ["node"]
},
"exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"],
"include": ["src/**/*.ts", "../../libs/plugins-runtime/src/lib/index.d.ts"]
}

View file

@ -0,0 +1,30 @@
{
"extends": "../../tsconfig.base.json",
"files": [],
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ESNext", "DOM"],
"moduleResolution": "Node",
"strict": true,
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"noEmit": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"skipLibCheck": true,
"types": ["vite/client"]
},
"include": ["src"],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}

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,58 @@
/// <reference types='vitest' />
import { defineConfig } from 'vite';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({
root: __dirname,
cacheDir: '../../node_modules/.vite/apps/contrast-plugin',
server: {
port: 4200,
host: 'localhost',
},
preview: {
port: 4300,
host: 'localhost',
},
plugins: [nxViteTsPaths()],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
build: {
outDir: '../../dist/apps/contrast-plugin',
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
rollupOptions: {
input: {
plugin: 'src/plugin.ts',
index: './index.html',
},
output: {
entryFileNames: '[name].js',
},
},
},
test: {
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/apps/contrast-plugin',
provider: 'v8',
},
},
});

View file

@ -1,6 +1,7 @@
penpot.log('Hello from plugin');
penpot.ui.open('Plugin name', 'http://localhost:4201', {
theme: 'light',
width: 500,
height: 600,
});

View file

@ -2,6 +2,7 @@ Open UI:
```ts
penpot.ui.open('Plugin name', 'http://localhost:4201', {
theme: 'light',
width: 500,
height: 600,
});

View file

@ -5,6 +5,7 @@
"happy-dom": "^13.6.2"
},
"dependencies": {
"plugins-data-parser": "^0.0.1",
"vitest": "1.2.2",
"ses": "^1.1.0",
"zod": "^3.22.4"

View file

@ -3,6 +3,7 @@ import './lib/plugin-modal';
import { ɵloadPlugin } from './lib/load-plugin';
import { setFileState, setPageState, setSelection } from './lib/api';
import { getSelectedUuids } from 'plugins-parser';
repairIntrinsics({
evalTaming: 'unsafeEval',
@ -32,8 +33,9 @@ export function initialize(api: any) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
api.addListener('plugin-selection', 'selection', (selection: any) => {
console.log('Selection Changed:', selection);
const selectionData = getSelectedUuids(selection);
console.log('Selection Changed:', selectionData);
setSelection(selection?.linked_map?.head?.uuid);
setSelection(selectionData);
});
}

View file

@ -11,9 +11,12 @@ export let uiMessagesCallbacks: Callback<unknown>[] = [];
let modal: HTMLElement | null = null;
let pageState: Page | null = null;
let fileState: File | null = null;
let selection: null | string = 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;
const eventListeners: Map<string, Callback<unknown>[]> = new Map();
@ -43,8 +46,8 @@ export function setFileState(file: File) {
triggerEvent('filechange', file);
}
export function setSelection(selectionId: string) {
if (selectionId === selection) {
export function setSelection(selectionId: string[]) {
if (JSON.stringify(selectionId) === JSON.stringify(selection)) {
return;
}

View file

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

View file

@ -14,7 +14,7 @@ interface File {
interface EventsMap {
pagechange: Page;
filechange: File;
selectionchange: string;
selectionchange: string[];
}
interface Penpot {

View file

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

View file

@ -76,17 +76,28 @@ export class PluginModalElement extends HTMLElement {
display: flex;
flex-direction: column;
position: fixed;
inset-block-end: 10px;
inset-block-start: 10px;
inset-inline-start: 10px;
z-index: 1000;
background: white;
padding: 20px;
border-radius: 5px;
border-radius: 20px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
inline-size: ${width}px;
block-size: ${height}px;
}
:host([data-theme="dark"]) {
background: #2e3434;
border: 1px solid #2e3434;
color: #ffffff;
}
:host([data-theme="light"]) {
background: #ffffff;
border: 1px solid #eef0f2;
color: #18181a;
}
.header {
display: flex;
justify-content: space-between;
@ -99,7 +110,6 @@ export class PluginModalElement extends HTMLElement {
}
h1 {
color: #000;
font-family: Arial, sans-serif;
margin: 0;
margin-block-end: 10px;

View file

@ -18,6 +18,10 @@ html, body {
font-style: normal;
}
code {
font-family: 'Work Sans', sans-serif;
}
.display {
font-weight: var(--font-weight-regular);
font-size: 36px;
@ -87,7 +91,7 @@ html, body {
line-height: var(--font-line-height-s);
}
.code-font {
code, .code-font {
font-weight: var(--font-weight-regular);
font-size: var(--font-size-s);
line-height: var(--font-line-height-l);