mirror of
https://github.com/penpot/penpot-plugins.git
synced 2025-01-02 04:40:11 -05:00
feat: refactor contrast plugin
This commit is contained in:
parent
14c4983bf2
commit
1d76bfe985
26 changed files with 549 additions and 462 deletions
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"presets": ["@nx/js/babel"]
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"jsc": {
|
||||
"parser": {
|
||||
"syntax": "typescript"
|
||||
},
|
||||
"target": "es2016"
|
||||
}
|
||||
}
|
|
@ -1,25 +1,43 @@
|
|||
import baseConfig from '../../eslint.config.js';
|
||||
import typescriptEslintParser from '@typescript-eslint/parser';
|
||||
import { compat } from '../../eslint.base.config.js';
|
||||
|
||||
export default [
|
||||
...baseConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
parser: typescriptEslintParser,
|
||||
parserOptions: { project: './apps/contrast-plugin/tsconfig.app.json' },
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
||||
rules: {},
|
||||
},
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
rules: {},
|
||||
},
|
||||
{
|
||||
files: ['**/*.js', '**/*.jsx'],
|
||||
rules: {},
|
||||
},
|
||||
{ ignores: ['vite.config.ts'] },
|
||||
...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'] },
|
||||
];
|
||||
|
|
|
@ -2,7 +2,92 @@
|
|||
"name": "contrast-plugin",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"projectType": "application",
|
||||
"prefix": "app",
|
||||
"sourceRoot": "apps/contrast-plugin/src",
|
||||
"tags": ["type:plugin"],
|
||||
"targets": {}
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@angular-devkit/build-angular:application",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"outputPath": "dist/apps/contrast-plugin",
|
||||
"index": "apps/contrast-plugin/src/index.html",
|
||||
"browser": "apps/contrast-plugin/src/main.ts",
|
||||
"polyfills": ["zone.js"],
|
||||
"tsConfig": "apps/contrast-plugin/tsconfig.app.json",
|
||||
"assets": [
|
||||
"apps/contrast-plugin/src/favicon.ico",
|
||||
"apps/contrast-plugin/src/assets"
|
||||
],
|
||||
"styles": [
|
||||
"libs/plugins-styles/src/lib/styles.css",
|
||||
"apps/contrast-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"
|
||||
},
|
||||
"serve": {
|
||||
"executor": "@angular-devkit/build-angular:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "contrast-plugin:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "contrast-plugin:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"executor": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"buildTarget": "contrast-plugin:build"
|
||||
}
|
||||
},
|
||||
"buildPlugin": {
|
||||
"executor": "@nx/esbuild:esbuild",
|
||||
"outputs": [
|
||||
"{options.outputPath}"
|
||||
],
|
||||
"options": {
|
||||
"minify": true,
|
||||
"outputPath": "apps/contrast-plugin/src/assets/",
|
||||
"main": "apps/contrast-plugin/src/plugin.ts",
|
||||
"tsConfig": "apps/contrast-plugin/tsconfig.plugin.json",
|
||||
"generatePackageJson": false,
|
||||
"format": [
|
||||
"esm"
|
||||
],
|
||||
"deleteOutputPath": false
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
{
|
||||
"name": "Contrast plugin",
|
||||
"host": "http://localhost:4302",
|
||||
"code": "/plugin.js",
|
||||
"icon": "/icon.png",
|
||||
"permissions": [
|
||||
"page:read",
|
||||
"file:read",
|
||||
"selection:read"
|
||||
]
|
||||
}
|
77
apps/contrast-plugin/src/app/app.component.css
Normal file
77
apps/contrast-plugin/src/app/app.component.css
Normal file
|
@ -0,0 +1,77 @@
|
|||
.wrapper {
|
||||
padding-block-start: var(--spacing-24);
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.contrast-preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
padding-block-end: var(--spacing-20);
|
||||
border-block-end: 2px solid var(--background-quaternary);
|
||||
}
|
||||
|
||||
.color-box {
|
||||
block-size: 66px;
|
||||
border-radius: var(--spacing-8);
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
var(--color1) 0%,
|
||||
var(--color1) 50%,
|
||||
var(--color2) 50%,
|
||||
var(--color2) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.select-colors {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.contrast-ratio {
|
||||
padding-block: var(--spacing-24);
|
||||
|
||||
span {
|
||||
color: var(--foreground-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.contrast-results {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-16);
|
||||
}
|
||||
|
||||
.contrast-result {
|
||||
.title {
|
||||
margin-block-end: var(--spacing-4);
|
||||
}
|
||||
.list {
|
||||
display: flex;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
inline-size: 42px;
|
||||
block-size: 32px;
|
||||
color: var(--app-white);
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--spacing-8);
|
||||
|
||||
&.good {
|
||||
background-color: var(--success-950);
|
||||
border-color: var(--success-500);
|
||||
}
|
||||
|
||||
&.fail {
|
||||
background-color: var(--error-950);
|
||||
border-color: var(--error-700);
|
||||
}
|
||||
}
|
231
apps/contrast-plugin/src/app/app.component.ts
Normal file
231
apps/contrast-plugin/src/app/app.component.ts
Normal file
|
@ -0,0 +1,231 @@
|
|||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import type {
|
||||
PluginMessageEvent,
|
||||
PluginUIEvent,
|
||||
ThemePluginEvent,
|
||||
} from '../model';
|
||||
import { filter, fromEvent, map, merge, take } from 'rxjs';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { PenpotShape } from '@penpot/plugin-types';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
selector: 'app-root',
|
||||
template: `
|
||||
<div class="wrapper body-s">
|
||||
@if (selection().length === 0) {
|
||||
<p class="empty-preview">
|
||||
Select two filled shapes to calculate the color contrast between them.
|
||||
</p>
|
||||
} @else if (selection().length === 1) {
|
||||
<p class="empty-preview">
|
||||
Select <span class="bold">one more</span> filled shape to calculate the
|
||||
color contrast between the selected colors.
|
||||
</p>
|
||||
} @else if (selection().length >= 2) {
|
||||
<div class="contrast-preview">
|
||||
<p>Selected colors:</p>
|
||||
<div class="color-box"></div>
|
||||
<ul class="select-colors">
|
||||
<li>
|
||||
{{ color1() }}
|
||||
</li>
|
||||
<li>{{ color2() }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p class="contrast-ratio">
|
||||
Contrast ratio: <span>{{ result() }} : 1</span>
|
||||
</p>
|
||||
<div class="contrast-results">
|
||||
<div class="contrast-result">
|
||||
<p class="title">Normal text:</p>
|
||||
<ul class="list">
|
||||
<li
|
||||
class="tag"
|
||||
[ngClass]="
|
||||
result() >= contrastStandards.AA.normal ? 'good' : 'fail'
|
||||
"
|
||||
>
|
||||
AA
|
||||
</li>
|
||||
<li
|
||||
class="tag"
|
||||
[ngClass]="
|
||||
result() >= contrastStandards.AAA.normal ? 'good' : 'fail'
|
||||
"
|
||||
>
|
||||
AAA
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="contrast-result">
|
||||
<p class="title">
|
||||
Large text
|
||||
<span class="body-xs">(starting from 19px bold or 24px):</span>
|
||||
</p>
|
||||
<ul class="list">
|
||||
<li
|
||||
class="tag"
|
||||
[ngClass]="
|
||||
result() >= contrastStandards.AA.large ? 'good' : 'fail'
|
||||
"
|
||||
>
|
||||
AA
|
||||
</li>
|
||||
<li
|
||||
class="tag"
|
||||
[ngClass]="
|
||||
result() >= contrastStandards.AAA.large ? 'good' : 'fail'
|
||||
"
|
||||
>
|
||||
AAA
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="contrast-result">
|
||||
<p class="title">
|
||||
Graphics <span class="body-xs">(such as form input borders):</span>
|
||||
</p>
|
||||
<ul class="list">
|
||||
<li
|
||||
class="tag"
|
||||
[ngClass]="
|
||||
result() >= contrastStandards.graphics ? 'good' : 'fail'
|
||||
"
|
||||
>
|
||||
AA
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './app.component.css',
|
||||
host: {
|
||||
'[attr.data-theme]': 'theme()',
|
||||
'[style.--color1]': 'color1()',
|
||||
'[style.--color2]': 'color2()',
|
||||
},
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
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)
|
||||
);
|
||||
|
||||
selection = toSignal(
|
||||
this.#messages$.pipe(
|
||||
filter(
|
||||
(event) => event.data.type === 'init' || event.data.type === 'selection'
|
||||
),
|
||||
map((event) => {
|
||||
if (event.data.type === 'init') {
|
||||
return event.data.content.selection;
|
||||
} else if (event.data.type === 'selection') {
|
||||
return event.data.content;
|
||||
}
|
||||
|
||||
return [];
|
||||
}),
|
||||
map((shapes) => {
|
||||
return shapes
|
||||
.map((shape) => this.#getShapeColor(shape))
|
||||
.filter((color): color is string => !!color);
|
||||
})
|
||||
),
|
||||
{
|
||||
initialValue: [],
|
||||
}
|
||||
);
|
||||
|
||||
theme = toSignal(
|
||||
merge(
|
||||
this.#initialTheme$,
|
||||
this.#messages$.pipe(
|
||||
map((event) => event.data),
|
||||
filter((data): data is ThemePluginEvent => data.type === 'theme'),
|
||||
map((data) => {
|
||||
return data.content;
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
color1 = computed(() => {
|
||||
return this.selection().at(-2);
|
||||
});
|
||||
|
||||
color2 = computed(() => {
|
||||
return this.selection().at(-1);
|
||||
});
|
||||
|
||||
result = computed<number>(() => {
|
||||
const color1 = this.color1();
|
||||
const color2 = this.color2();
|
||||
|
||||
if (!color1 || !color2) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const lum1 = this.#getLuminosity(color1) + 0.05;
|
||||
const lum2 = this.#getLuminosity(color2) + 0.05;
|
||||
|
||||
const result = lum1 > lum2 ? lum1 / lum2 : lum2 / lum1;
|
||||
|
||||
return Number(result.toFixed(2));
|
||||
});
|
||||
|
||||
contrastStandards = {
|
||||
AA: {
|
||||
normal: 4.5,
|
||||
large: 3,
|
||||
},
|
||||
AAA: {
|
||||
normal: 7,
|
||||
large: 4.5,
|
||||
},
|
||||
graphics: 3,
|
||||
} as const;
|
||||
|
||||
constructor() {
|
||||
this.#sendMessage({ type: 'ready' });
|
||||
}
|
||||
|
||||
#getLuminosity(color: string) {
|
||||
const rgb = this.#hexToRgb(color);
|
||||
const a = rgb.map((v) => {
|
||||
v /= 255;
|
||||
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
||||
});
|
||||
return 0.2126 * a[0] + 0.7152 * a[1] + 0.0722 * a[2];
|
||||
}
|
||||
|
||||
#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];
|
||||
}
|
||||
|
||||
#getShapeColor(shape?: PenpotShape): string | undefined {
|
||||
return shape?.fills?.[0]?.fillColor ?? shape?.strokes?.[0]?.strokeColor;
|
||||
}
|
||||
|
||||
#sendMessage(message: PluginUIEvent) {
|
||||
parent.postMessage(message, '*');
|
||||
}
|
||||
}
|
6
apps/contrast-plugin/src/app/app.config.ts
Normal file
6
apps/contrast-plugin/src/app/app.config.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { ApplicationConfig } from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [provideRouter([])],
|
||||
};
|
|
@ -1,101 +0,0 @@
|
|||
.wrapper {
|
||||
&[data-theme='dark'] {
|
||||
color: var(--app-white);
|
||||
}
|
||||
|
||||
&[data-theme='light'] {
|
||||
color: var(--app-black);
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
|
@ -1,212 +0,0 @@
|
|||
/* eslint-disable */
|
||||
import 'plugins-styles/lib/styles.css';
|
||||
import './app.element.css';
|
||||
|
||||
export class AppElement extends HTMLElement {
|
||||
public static observedAttributes = [];
|
||||
public shapes: any;
|
||||
|
||||
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);
|
||||
const a = rgb.map((v) => {
|
||||
v /= 255;
|
||||
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
||||
});
|
||||
return 0.2126 * a[0] + 0.7152 * a[1] + 0.0722 * a[2];
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
initCalculate(shapes: any) {
|
||||
const obj0 = shapes[0]?.fills?.[0]?.fillColor;
|
||||
const obj1 = shapes[1]?.fills?.[0]?.fillColor;
|
||||
|
||||
if (obj0 && obj1) {
|
||||
this.calculateContrast(obj0, obj1);
|
||||
}
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.data.type === 'init') {
|
||||
this.setAttribute('data-theme', event.data.content.theme);
|
||||
if (event.data.content.selection.length >= 2) {
|
||||
this.initCalculate(event.data.content.shapes);
|
||||
}
|
||||
} else if (event.data.type === 'selection') {
|
||||
if (event.data.content.shapes.length >= 2) {
|
||||
this.initCalculate(event.data.content.shapes);
|
||||
} else {
|
||||
this.setColors(null, null);
|
||||
this.setResult('0');
|
||||
this.setA11yTags(0);
|
||||
}
|
||||
} else if (event.data.type === 'theme') {
|
||||
this.setAttribute('data-theme', event.data.content.theme);
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
7
apps/contrast-plugin/src/assets/manifest.json
Normal file
7
apps/contrast-plugin/src/assets/manifest.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "Contrast",
|
||||
"host": "http://localhost:4302",
|
||||
"description": "Measure contrast plugin",
|
||||
"code": "/assets/plugin.js",
|
||||
"permissions": ["page:read", "file:read", "selection:read"]
|
||||
}
|
|
@ -2,15 +2,11 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>ContrastPlugin</title>
|
||||
<title>contrast-plugin</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>
|
|
@ -1 +1,7 @@
|
|||
import './app/app.element';
|
||||
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)
|
||||
);
|
||||
|
|
29
apps/contrast-plugin/src/model.ts
Normal file
29
apps/contrast-plugin/src/model.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { PenpotShape } from '@penpot/plugin-types';
|
||||
|
||||
export interface InitPluginUIEvent {
|
||||
type: 'ready';
|
||||
}
|
||||
|
||||
export type PluginUIEvent = InitPluginUIEvent;
|
||||
|
||||
export interface InitPluginEvent {
|
||||
type: 'init';
|
||||
content: {
|
||||
theme: string;
|
||||
selection: PenpotShape[];
|
||||
};
|
||||
}
|
||||
export interface SelectionPluginEvent {
|
||||
type: 'selection';
|
||||
content: PenpotShape[];
|
||||
}
|
||||
|
||||
export interface ThemePluginEvent {
|
||||
type: 'theme';
|
||||
content: string;
|
||||
}
|
||||
|
||||
export type PluginMessageEvent =
|
||||
| InitPluginEvent
|
||||
| SelectionPluginEvent
|
||||
| ThemePluginEvent;
|
|
@ -1,15 +1,17 @@
|
|||
penpot.ui.open('Contrast plugin', '', {
|
||||
width: 450,
|
||||
height: 625,
|
||||
import type { PluginMessageEvent, PluginUIEvent } from './model.js';
|
||||
|
||||
penpot.ui.open('CONTRAST PLUGIN', `?theme=${penpot.getTheme()}`, {
|
||||
width: 235,
|
||||
height: 445,
|
||||
});
|
||||
|
||||
penpot.ui.onMessage<{ content: string }>((message) => {
|
||||
if (message.content === 'ready') {
|
||||
penpot.ui.sendMessage({
|
||||
penpot.ui.onMessage<PluginUIEvent>((message) => {
|
||||
if (message.type === 'ready') {
|
||||
sendMessage({
|
||||
type: 'init',
|
||||
content: {
|
||||
theme: penpot.getTheme(),
|
||||
shapes: penpot.getSelectedShapes(),
|
||||
selection: penpot.getSelectedShapes(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -17,10 +19,14 @@ penpot.ui.onMessage<{ content: string }>((message) => {
|
|||
|
||||
penpot.on('selectionchange', () => {
|
||||
const shapes = penpot.getSelectedShapes();
|
||||
penpot.ui.sendMessage({ type: 'selection', content: { shapes } });
|
||||
sendMessage({ type: 'selection', content: shapes });
|
||||
});
|
||||
|
||||
penpot.on('themechange', () => {
|
||||
const theme = penpot.getTheme();
|
||||
penpot.ui.sendMessage({ type: 'theme', content: { theme } });
|
||||
sendMessage({ type: 'theme', content: theme });
|
||||
});
|
||||
|
||||
function sendMessage(message: PluginMessageEvent) {
|
||||
penpot.ui.sendMessage(message);
|
||||
}
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
/* You can add global styles to this file, and also import other style files */
|
|
@ -2,8 +2,9 @@
|
|||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"types": ["node"]
|
||||
"types": []
|
||||
},
|
||||
"exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"],
|
||||
"include": ["src/**/*.ts", "../../libs/plugin-types/index.d.ts"]
|
||||
"files": ["src/main.ts"],
|
||||
"include": ["src/**/*.d.ts"],
|
||||
"exclude": ["src/**/*.test.ts", "src/**/*.spec.ts"]
|
||||
}
|
||||
|
|
7
apps/contrast-plugin/tsconfig.editor.json
Normal file
7
apps/contrast-plugin/tsconfig.editor.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": ["src/**/*.ts"],
|
||||
"compilerOptions": {
|
||||
"types": []
|
||||
}
|
||||
}
|
|
@ -1,30 +1,33 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"files": [],
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"target": "es2022",
|
||||
"useDefineForClassFields": false,
|
||||
"esModuleInterop": true,
|
||||
"noEmit": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"skipLibCheck": true,
|
||||
"types": ["vite/client"]
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
"path": "./tsconfig.editor.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.plugin.json"
|
||||
}
|
||||
]
|
||||
],
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
}
|
||||
}
|
||||
|
|
8
apps/contrast-plugin/tsconfig.plugin.json
Normal file
8
apps/contrast-plugin/tsconfig.plugin.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"types": []
|
||||
},
|
||||
"files": ["src/plugin.ts"],
|
||||
"include": ["../../libs/plugin-types/index.d.ts"]
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
]
|
||||
}
|
|
@ -1,57 +1,19 @@
|
|||
/// <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: 4302,
|
||||
host: '0.0.0.0',
|
||||
},
|
||||
|
||||
preview: {
|
||||
port: 4302,
|
||||
host: '0.0.0.0',
|
||||
},
|
||||
|
||||
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',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
cacheDir: '../node_modules/.vite/contrast-plugin',
|
||||
test: {
|
||||
globals: true,
|
||||
cache: {
|
||||
dir: '../../node_modules/.vitest',
|
||||
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',
|
||||
reportsDirectory: '../coverage/contrast-plugin',
|
||||
provider: 'v8',
|
||||
},
|
||||
},
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
--font-line-height-s: 1.2;
|
||||
--font-line-height-m: 1.4;
|
||||
--font-line-height-l: 1.5;
|
||||
--font-size-xs: 10px;
|
||||
--font-size-s: 12px;
|
||||
--font-size-m: 14px;
|
||||
--font-size-l: 16px;
|
||||
|
@ -68,6 +69,12 @@ code {
|
|||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.body-xs {
|
||||
font-weight: var(--font-weight-regular);
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: var(--font-line-height-s);
|
||||
}
|
||||
|
||||
.body-s {
|
||||
font-weight: var(--font-weight-regular);
|
||||
font-size: var(--font-size-s);
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
"start:plugin:all": "concurrently --kill-others \"npm:start:plugin:*(!all)\"",
|
||||
"start:plugin:poc-state": "npx nx run-many --targets=buildPlugin,serve --projects=poc-state-plugin --watch --host 0.0.0.0 --port 4301",
|
||||
"start:plugin:contrast": "npx nx run contrast-plugin:build --watch & npx nx run contrast-plugin:preview",
|
||||
"start:contrast-plugin": "npx nx run-many --targets=buildPlugin,serve --projects=contrast-plugin --watch --host 0.0.0.0 --port 4302",
|
||||
"start:plugin:icons": "npx nx run-many --targets=buildPlugin,serve --projects=icons-plugin --watch --host 0.0.0.0 --port 4303",
|
||||
"start:plugin:loremipsum": "npx nx run-many --targets=buildPlugin,serve --projects=lorem-ipsum-plugin --watch --port 4304",
|
||||
"start:plugin:palette": "npx nx run create-palette-plugin:build --watch & npx nx run create-palette-plugin:preview",
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
"importHelpers": true,
|
||||
"target": "es2015",
|
||||
"module": "esnext",
|
||||
"lib": ["es2020", "dom"],
|
||||
"lib": ["es2022", "dom"],
|
||||
"skipLibCheck": true,
|
||||
"skipDefaultLibCheck": true,
|
||||
"baseUrl": ".",
|
||||
|
|
Loading…
Reference in a new issue