diff --git a/apps/contrast-plugin/.babelrc b/apps/contrast-plugin/.babelrc new file mode 100644 index 0000000..f2f3806 --- /dev/null +++ b/apps/contrast-plugin/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["@nx/js/babel"] +} diff --git a/apps/contrast-plugin/.eslintrc.json b/apps/contrast-plugin/.eslintrc.json new file mode 100644 index 0000000..9d9c0db --- /dev/null +++ b/apps/contrast-plugin/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/apps/contrast-plugin/.swcrc b/apps/contrast-plugin/.swcrc new file mode 100644 index 0000000..a2d5b04 --- /dev/null +++ b/apps/contrast-plugin/.swcrc @@ -0,0 +1,8 @@ +{ + "jsc": { + "parser": { + "syntax": "typescript" + }, + "target": "es2016" + } +} diff --git a/apps/contrast-plugin/index.html b/apps/contrast-plugin/index.html new file mode 100644 index 0000000..88aef45 --- /dev/null +++ b/apps/contrast-plugin/index.html @@ -0,0 +1,16 @@ + + + + + ContrastPlugin + + + + + + + + + + + diff --git a/apps/contrast-plugin/project.json b/apps/contrast-plugin/project.json new file mode 100644 index 0000000..497af03 --- /dev/null +++ b/apps/contrast-plugin/project.json @@ -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": {} +} diff --git a/apps/contrast-plugin/public/favicon.ico b/apps/contrast-plugin/public/favicon.ico new file mode 100644 index 0000000..317ebcb Binary files /dev/null and b/apps/contrast-plugin/public/favicon.ico differ diff --git a/apps/contrast-plugin/public/manifest.json b/apps/contrast-plugin/public/manifest.json new file mode 100644 index 0000000..f5d75e8 --- /dev/null +++ b/apps/contrast-plugin/public/manifest.json @@ -0,0 +1,10 @@ +{ + "name": "Contrast plugin", + "code": "http://localhost:4201/plugin.js", + "permissions": [ + "page:read", + "file:read", + "selection:read" + ] + } + \ No newline at end of file diff --git a/apps/contrast-plugin/src/app/app.element.css b/apps/contrast-plugin/src/app/app.element.css new file mode 100644 index 0000000..0b90bd7 --- /dev/null +++ b/apps/contrast-plugin/src/app/app.element.css @@ -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; +} \ No newline at end of file diff --git a/apps/contrast-plugin/src/app/app.element.spec.ts b/apps/contrast-plugin/src/app/app.element.spec.ts new file mode 100644 index 0000000..ce93b60 --- /dev/null +++ b/apps/contrast-plugin/src/app/app.element.spec.ts @@ -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' + ); + }); +}); diff --git a/apps/contrast-plugin/src/app/app.element.ts b/apps/contrast-plugin/src/app/app.element.ts new file mode 100644 index 0000000..7ca5dff --- /dev/null +++ b/apps/contrast-plugin/src/app/app.element.ts @@ -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 = ` +
+
+

Select two colors to calculate contrast

+

SMALL sample text

+

LARGE sample text

+ +
+

Selected colors:

+ +

Contrast ratio: 0 : 1

+

Normal text:

+ +

Large text (24px or 19px + bold):

+ +

Graphics (such as form input borders):

+ +
+ `; + + parent.postMessage({ content: 'ready' }, '*'); + } +} +customElements.define('app-root', AppElement); diff --git a/apps/contrast-plugin/src/assets/.gitkeep b/apps/contrast-plugin/src/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apps/contrast-plugin/src/main.ts b/apps/contrast-plugin/src/main.ts new file mode 100644 index 0000000..fdb879d --- /dev/null +++ b/apps/contrast-plugin/src/main.ts @@ -0,0 +1 @@ +import './app/app.element'; diff --git a/apps/contrast-plugin/src/plugin.ts b/apps/contrast-plugin/src/plugin.ts new file mode 100644 index 0000000..acd637c --- /dev/null +++ b/apps/contrast-plugin/src/plugin.ts @@ -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 }); +}); \ No newline at end of file diff --git a/apps/contrast-plugin/src/styles.css b/apps/contrast-plugin/src/styles.css new file mode 100644 index 0000000..90d4ee0 --- /dev/null +++ b/apps/contrast-plugin/src/styles.css @@ -0,0 +1 @@ +/* You can add global styles to this file, and also import other style files */ diff --git a/apps/contrast-plugin/tsconfig.app.json b/apps/contrast-plugin/tsconfig.app.json new file mode 100644 index 0000000..14d797a --- /dev/null +++ b/apps/contrast-plugin/tsconfig.app.json @@ -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"] +} diff --git a/apps/contrast-plugin/tsconfig.json b/apps/contrast-plugin/tsconfig.json new file mode 100644 index 0000000..cee4bba --- /dev/null +++ b/apps/contrast-plugin/tsconfig.json @@ -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" + } + ] +} diff --git a/apps/contrast-plugin/tsconfig.spec.json b/apps/contrast-plugin/tsconfig.spec.json new file mode 100644 index 0000000..3c002c2 --- /dev/null +++ b/apps/contrast-plugin/tsconfig.spec.json @@ -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" + ] +} diff --git a/apps/contrast-plugin/vite.config.ts b/apps/contrast-plugin/vite.config.ts new file mode 100644 index 0000000..4cc4784 --- /dev/null +++ b/apps/contrast-plugin/vite.config.ts @@ -0,0 +1,58 @@ +/// +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', + }, + }, +}); diff --git a/apps/example-plugin/src/plugin.ts b/apps/example-plugin/src/plugin.ts index 36869d2..c3b98bd 100644 --- a/apps/example-plugin/src/plugin.ts +++ b/apps/example-plugin/src/plugin.ts @@ -1,6 +1,7 @@ penpot.log('Hello from plugin'); penpot.ui.open('Plugin name', 'http://localhost:4201', { + theme: 'light', width: 500, height: 600, }); diff --git a/docs/plugin-usage.md b/docs/plugin-usage.md index dc9711c..b728a50 100644 --- a/docs/plugin-usage.md +++ b/docs/plugin-usage.md @@ -2,6 +2,7 @@ Open UI: ```ts penpot.ui.open('Plugin name', 'http://localhost:4201', { + theme: 'light', width: 500, height: 600, }); diff --git a/libs/plugins-runtime/package.json b/libs/plugins-runtime/package.json index a63950a..a8900d2 100644 --- a/libs/plugins-runtime/package.json +++ b/libs/plugins-runtime/package.json @@ -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" diff --git a/libs/plugins-runtime/src/index.ts b/libs/plugins-runtime/src/index.ts index 68929e1..7d882e5 100644 --- a/libs/plugins-runtime/src/index.ts +++ b/libs/plugins-runtime/src/index.ts @@ -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); }); } diff --git a/libs/plugins-runtime/src/lib/api/index.ts b/libs/plugins-runtime/src/lib/api/index.ts index fc4b2df..d5dcf96 100644 --- a/libs/plugins-runtime/src/lib/api/index.ts +++ b/libs/plugins-runtime/src/lib/api/index.ts @@ -11,9 +11,12 @@ export let uiMessagesCallbacks: Callback[] = []; 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[]> = 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; } diff --git a/libs/plugins-runtime/src/lib/create-modal.ts b/libs/plugins-runtime/src/lib/create-modal.ts index 6cf4300..a6468ad 100644 --- a/libs/plugins-runtime/src/lib/create-modal.ts +++ b/libs/plugins-runtime/src/lib/create-modal.ts @@ -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)); diff --git a/libs/plugins-runtime/src/lib/index.d.ts b/libs/plugins-runtime/src/lib/index.d.ts index 6f88630..da7de9f 100644 --- a/libs/plugins-runtime/src/lib/index.d.ts +++ b/libs/plugins-runtime/src/lib/index.d.ts @@ -14,7 +14,7 @@ interface File { interface EventsMap { pagechange: Page; filechange: File; - selectionchange: string; + selectionchange: string[]; } interface Penpot { diff --git a/libs/plugins-runtime/src/lib/models/open-ui-options.schema.ts b/libs/plugins-runtime/src/lib/models/open-ui-options.schema.ts index f008033..2cf9ba1 100644 --- a/libs/plugins-runtime/src/lib/models/open-ui-options.schema.ts +++ b/libs/plugins-runtime/src/lib/models/open-ui-options.schema.ts @@ -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(), }); diff --git a/libs/plugins-runtime/src/lib/plugin-modal.ts b/libs/plugins-runtime/src/lib/plugin-modal.ts index bd8f9b3..2d8b048 100644 --- a/libs/plugins-runtime/src/lib/plugin-modal.ts +++ b/libs/plugins-runtime/src/lib/plugin-modal.ts @@ -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; diff --git a/libs/plugins-styles/src/lib/core/fonts.css b/libs/plugins-styles/src/lib/core/fonts.css index 7bae92f..0d12885 100644 --- a/libs/plugins-styles/src/lib/core/fonts.css +++ b/libs/plugins-styles/src/lib/core/fonts.css @@ -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);