diff --git a/README.md b/README.md index b05451b..0f5cee4 100644 --- a/README.md +++ b/README.md @@ -64,11 +64,12 @@ Open in your browser: `http://localhost:4210/` ## Sample plugins -| Plugin | Description | PORT | Start command | Manifest URL | -| ---------------- | ----------------------------------------------------------- | ---- | ----------------------------- | ------------------------------------------ | -| poc-state-plugin | Sandbox plugin to test new plugins api functionality | 4301 | npm run start:pc-plugin | http://localhost:4301/assets/manifest.json | -| contrast-plugin | Sample plugin that gives you color contrast information | 4302 | npm run start:contrast-plugin | http://localhost:4302/manifest.json | -| icons-plugin | Tool to add icons from [Feather](https://feathericons.com/) | 4303 | npm run start:icons-plugin | http://localhost:4303/assets/manifest.json | +| Plugin | Description | PORT | Start command | Manifest URL | +| ------------------ | ----------------------------------------------------------- | ---- | ------------------------------- | ------------------------------------------ | +| poc-state-plugin | Sandbox plugin to test new plugins api functionality | 4301 | npm run start:pc-plugin | http://localhost:4301/assets/manifest.json | +| contrast-plugin | Sample plugin that gives you color contrast information | 4302 | npm run start:contrast-plugin | http://localhost:4302/manifest.json | +| icons-plugin | Tool to add icons from [Feather](https://feathericons.com/) | 4303 | npm run start:icons-plugin | http://localhost:4303/assets/manifest.json | +| lorem-ipsum-plugin | Generate Lorem ipsum text | 4304 | npm run start:loremipsum-plugin | http://localhost:4304/assets/manifest.json | ## Web Apps diff --git a/apps/lorem-ipsum-plugin/eslint.config.js b/apps/lorem-ipsum-plugin/eslint.config.js new file mode 100644 index 0000000..29c2176 --- /dev/null +++ b/apps/lorem-ipsum-plugin/eslint.config.js @@ -0,0 +1,43 @@ +import baseConfig from '../../eslint.config.js'; +import { compat } from '../../eslint.base.config.js'; + +export default [ + ...baseConfig, + ...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'] }, +]; diff --git a/apps/lorem-ipsum-plugin/project.json b/apps/lorem-ipsum-plugin/project.json new file mode 100644 index 0000000..8406278 --- /dev/null +++ b/apps/lorem-ipsum-plugin/project.json @@ -0,0 +1,93 @@ +{ + "name": "lorem-ipsum-plugin", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "projectType": "application", + "prefix": "app", + "sourceRoot": "apps/lorem-ipsum-plugin/src", + "tags": ["type:plugin"], + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:application", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/apps/lorem-ipsum-plugin", + "index": "apps/lorem-ipsum-plugin/src/index.html", + "browser": "apps/lorem-ipsum-plugin/src/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "apps/lorem-ipsum-plugin/tsconfig.app.json", + "assets": [ + "apps/lorem-ipsum-plugin/src/favicon.ico", + "apps/lorem-ipsum-plugin/src/assets" + ], + "styles": [ + "libs/plugins-styles/src/lib/styles.css", + "apps/lorem-ipsum-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": "lorem-ipsum-plugin:build:production" + }, + "development": { + "buildTarget": "lorem-ipsum-plugin:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "executor": "@angular-devkit/build-angular:extract-i18n", + "options": { + "buildTarget": "lorem-ipsum-plugin:build" + } + }, + "buildPlugin": { + "executor": "@nx/esbuild:esbuild", + "outputs": [ + "{options.outputPath}" + ], + "options": { + "minify": true, + "outputPath": "apps/lorem-ipsum-plugin/src/assets/", + "main": "apps/lorem-ipsum-plugin/src/plugin.ts", + "tsConfig": "apps/lorem-ipsum-plugin/tsconfig.plugin.json", + "generatePackageJson": false, + "format": [ + "esm" + ], + "deleteOutputPath": false + } + }, + } +} diff --git a/apps/lorem-ipsum-plugin/src/app/app.component.css b/apps/lorem-ipsum-plugin/src/app/app.component.css new file mode 100644 index 0000000..fd38612 --- /dev/null +++ b/apps/lorem-ipsum-plugin/src/app/app.component.css @@ -0,0 +1,47 @@ +p { + color: var(--df-secondary); + margin-block-end: var(--spacing-16); +} + +.generation-options { + display: flex; + gap: var(--spacing-8); +} + +.generation-size { + inline-size: 60px; +} + +.generation-type { + inline-size: 100%; +} + +.sections-wrapper { + display: flex; + flex-direction: column; + height: 100vh; +} + +section { + padding-block-start: var(--spacing-24); + + button { + inline-size: 100%; + } +} + +.regular-generate { + padding-block-end: var(--spacing-24); + border-block-end: 2px solid var(--background-quaternary); + + button { + margin-block-start: var(--spacing-12); + } +} + +.extra-options { + margin-block-start: auto; + display: flex; + flex-direction: column; + gap: var(--spacing-20); +} diff --git a/apps/lorem-ipsum-plugin/src/app/app.component.ts b/apps/lorem-ipsum-plugin/src/app/app.component.ts new file mode 100644 index 0000000..09802d2 --- /dev/null +++ b/apps/lorem-ipsum-plugin/src/app/app.component.ts @@ -0,0 +1,118 @@ +import { Component, inject } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import type { + GenerationTypes, + PluginMessageEvent, + PluginUIEvent, +} from '../model'; +import { filter, fromEvent, map, merge, take } from 'rxjs'; + +@Component({ + standalone: true, + imports: [ReactiveFormsModule], + selector: 'app-root', + template: ` +
+
+

+ Select a text field to replace it with a placeholder text. +

+ +
+ + + +
+ +
+
+
+ + +
+ +
+ + +
+
+
+ `, + styleUrl: './app.component.css', + host: { + '[attr.data-theme]': 'theme()', + }, +}) +export class AppComponent { + route = inject(ActivatedRoute); + messages$ = fromEvent>(window, 'message'); + + initialTheme$ = this.route.queryParamMap.pipe( + map((params) => params.get('theme')), + filter((theme) => !!theme), + take(1) + ); + + theme = toSignal( + merge( + this.initialTheme$, + this.messages$.pipe( + filter((event) => event.data.type === 'theme'), + map((event) => { + return event.data.content; + }) + ) + ) + ); + + form = new FormGroup({ + num: new FormControl(1, { nonNullable: true }), + type: new FormControl('paragraphs', { nonNullable: true }), + startWith: new FormControl(true, { nonNullable: true }), + autoClose: new FormControl(true, { nonNullable: true }), + }); + + constructor() { + this.#sendMessage({ type: 'ready' }); + } + + generate() { + const formValue = this.form.getRawValue(); + + this.#sendMessage({ + type: 'text', + generationType: formValue.type, + startWithLorem: formValue.startWith, + size: formValue.num, + autoClose: formValue.autoClose, + }); + } + + #sendMessage(message: PluginUIEvent) { + parent.postMessage(message, '*'); + } +} diff --git a/apps/lorem-ipsum-plugin/src/app/app.config.ts b/apps/lorem-ipsum-plugin/src/app/app.config.ts new file mode 100644 index 0000000..1b3e4af --- /dev/null +++ b/apps/lorem-ipsum-plugin/src/app/app.config.ts @@ -0,0 +1,6 @@ +import { ApplicationConfig } from '@angular/core'; +import { provideRouter } from '@angular/router'; + +export const appConfig: ApplicationConfig = { + providers: [provideRouter([])], +}; diff --git a/apps/lorem-ipsum-plugin/src/assets/manifest.json b/apps/lorem-ipsum-plugin/src/assets/manifest.json new file mode 100644 index 0000000..e90b11d --- /dev/null +++ b/apps/lorem-ipsum-plugin/src/assets/manifest.json @@ -0,0 +1,7 @@ +{ + "name": "Lorem ipsum", + "host": "http://localhost:4304", + "description": "Lorem ipsum text generator plugin", + "code": "/assets/plugin.js", + "permissions": ["page:read", "file:read", "selection:read"] +} diff --git a/apps/lorem-ipsum-plugin/src/generator.spec.ts b/apps/lorem-ipsum-plugin/src/generator.spec.ts new file mode 100644 index 0000000..03fc303 --- /dev/null +++ b/apps/lorem-ipsum-plugin/src/generator.spec.ts @@ -0,0 +1,69 @@ +import { describe, it, expect } from 'vitest'; +import { + generateCharacters, + generateWords, + generateSentences, + generateParagraphs, +} from './generator'; + +describe('generateCharacters', () => { + it('should generate the correct number of characters starting with "Lorem ipsum"', () => { + const result = generateCharacters(20); + expect(result.length).toBe(20); + expect(result.startsWith('Lorem ipsum')).toBe(true); + }); + + it('should generate the correct number of characters without starting with "Lorem ipsum"', () => { + const result = generateCharacters(40, false); + expect(result.length).toBe(40); + expect(result.startsWith('Lorem ipsum')).toBe(false); + }); +}); + +describe('generateWords', () => { + it('should generate the correct number of words starting with "Lorem ipsum"', () => { + const result = generateWords(5); + const words = result.split(' '); + expect(words.length).toBe(5); + expect(result.startsWith('Lorem ipsum')).toBe(true); + }); + + it('should generate the correct number of words without starting with "Lorem ipsum"', () => { + const result = generateWords(10, false); + const words = result.split(' '); + expect(words.length).toBe(10); + expect(result.startsWith('Lorem ipsum')).toBe(false); + }); +}); + +describe('generateSentences', () => { + it('should generate the correct number of sentences starting with "Lorem ipsum"', () => { + const result = generateSentences(3); + const sentences = result.split('. '); + expect(sentences.length).toBe(3); + expect(result.startsWith('Lorem ipsum')).toBe(true); + }); + + it('should generate the correct number of sentences without starting with "Lorem ipsum"', () => { + const result = generateSentences(6, false); + const sentences = result.split('. '); + expect(sentences.length).toBe(6); + expect(result.startsWith('Lorem ipsum')).toBe(false); + }); +}); + +describe('generateParagraphs', () => { + it('should generate the correct number of paragraphs starting with "Lorem ipsum"', () => { + const result = generateParagraphs(2); + const paragraphs = result.split('\n\n'); + expect(paragraphs.length).toBe(2); + expect(result.startsWith('Lorem ipsum')).toBe(true); + }); + + it('should generate the correct number of paragraphs without starting with "Lorem ipsum"', () => { + const result = generateParagraphs(4, false); + const paragraphs = result.split('\n\n'); + expect(paragraphs.length).toBe(4); + expect(result.startsWith('Lorem ipsum')).toBe(false); + }); +}); diff --git a/apps/lorem-ipsum-plugin/src/generator.ts b/apps/lorem-ipsum-plugin/src/generator.ts new file mode 100644 index 0000000..6477dbe --- /dev/null +++ b/apps/lorem-ipsum-plugin/src/generator.ts @@ -0,0 +1,142 @@ +const wordList = [ + 'dolor', + 'sit', + 'amet', + 'consectetur', + 'adipiscing', + 'elit', + 'sed', + 'do', + 'eiusmod', + 'tempor', + 'incididunt', + 'labore', + 'et', + 'dolore', + 'magna', + 'aliqua', + 'enim', + 'ad', + 'minim', + 'veniam', + 'quis', + 'nostrud', + 'exercitation', + 'ullamco', + 'laboris', + 'nisi', + 'ut', + 'aliquip', + 'ex', + 'ea', + 'commodo', + 'consequat', + 'duis', + 'aute', + 'irure', + 'in', + 'reprehenderit', + 'voluptate', + 'velit', + 'esse', + 'cillum', + 'eu', + 'fugiat', + 'nulla', + 'pariatur', + 'excepteur', + 'sint', + 'occaecat', + 'cupidatat', + 'non', + 'proident', + 'sunt', + 'culpa', + 'qui', + 'officia', + 'deserunt', + 'mollit', + 'anim', + 'id', + 'est', + 'laborum', +]; + +const lorem = 'Lorem ipsum' as const; + +function* randomWordGenerator() { + let copyWordList: string[] = []; + + while (true) { + if (!copyWordList.length) { + copyWordList = [...wordList]; + } + + const newWordIndex = Math.floor(Math.random() * copyWordList.length); + + yield copyWordList[newWordIndex]; + + copyWordList.splice(newWordIndex, 1); + } +} + +const getRandomWordGenerator = randomWordGenerator(); + +function getRandomWord() { + return getRandomWordGenerator.next().value; +} + +export function generateCharacters(count: number, startWithLorem = true) { + let text = ''; + + if (startWithLorem) { + text = lorem + ' '; + } + + while (text.length < count) { + text += getRandomWord() + ' '; + } + + return text.slice(0, count); +} + +export function generateWords(count: number, startWithLorem = true) { + let words = []; + + if (startWithLorem) { + words.push(...lorem.split(' ').slice(0, count)); + } + + for (let i = words.length; i < count; i++) { + words.push(getRandomWord()); + } + + return words.join(' '); +} + +export function generateSentences(count: number, startWithLorem = true) { + let sentences = []; + for (let i = 0; i < count; i++) { + let sentenceLength = Math.floor(Math.random() * 10) + 3; // between 3 and 12 words per sentence + let sentence = generateWords(sentenceLength, false); + + if (startWithLorem && i === 0) { + sentence = + lorem + ' ' + sentence.charAt(0).toLowerCase() + sentence.slice(1); + } + + sentences.push(sentence.charAt(0).toUpperCase() + sentence.slice(1) + '.'); + } + return sentences.join(' '); +} + +export function generateParagraphs(count: number, startWithLorem = true) { + let paragraphs = []; + for (let i = 0; i < count; i++) { + let paragraphLength = Math.floor(Math.random() * 5) + 3; // between 3 and 7 sentences per paragraph + paragraphs.push( + generateSentences(paragraphLength, startWithLorem && i === 0) + ); + } + return paragraphs.join('\n\n'); +} diff --git a/apps/lorem-ipsum-plugin/src/index.html b/apps/lorem-ipsum-plugin/src/index.html new file mode 100644 index 0000000..9163482 --- /dev/null +++ b/apps/lorem-ipsum-plugin/src/index.html @@ -0,0 +1,12 @@ + + + + + lorem-ipsum-plugin + + + + + + + diff --git a/apps/lorem-ipsum-plugin/src/main.ts b/apps/lorem-ipsum-plugin/src/main.ts new file mode 100644 index 0000000..514c89a --- /dev/null +++ b/apps/lorem-ipsum-plugin/src/main.ts @@ -0,0 +1,7 @@ +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) +); diff --git a/apps/lorem-ipsum-plugin/src/model.ts b/apps/lorem-ipsum-plugin/src/model.ts new file mode 100644 index 0000000..dce13e4 --- /dev/null +++ b/apps/lorem-ipsum-plugin/src/model.ts @@ -0,0 +1,40 @@ +export type GenerationTypes = + | 'paragraphs' + | 'sentences' + | 'words' + | 'characters'; + +export interface InitPluginUIEvent { + type: 'ready'; +} + +export interface TextPluginUIEvent { + type: 'text'; + generationType: GenerationTypes; + startWithLorem: boolean; + size: number; + autoClose: boolean; +} +export type PluginUIEvent = InitPluginUIEvent | TextPluginUIEvent; + +export interface InitPluginEvent { + type: 'init'; + content: { + theme: string; + selection: number; + }; +} +export interface SelectionPluginEvent { + type: 'selection'; + content: number; +} + +export interface ThemePluginEvent { + type: 'theme'; + content: string; +} + +export type PluginMessageEvent = + | InitPluginEvent + | SelectionPluginEvent + | ThemePluginEvent; diff --git a/apps/lorem-ipsum-plugin/src/plugin.ts b/apps/lorem-ipsum-plugin/src/plugin.ts new file mode 100644 index 0000000..71163ff --- /dev/null +++ b/apps/lorem-ipsum-plugin/src/plugin.ts @@ -0,0 +1,70 @@ +import { PenpotText } from '@penpot/plugin-types'; +import type { + PluginMessageEvent, + PluginUIEvent, + TextPluginUIEvent, +} from './model.js'; +import { + generateParagraphs, + generateSentences, + generateWords, + generateCharacters, +} from './generator.js'; + +penpot.ui.open('LOREM IPSUM PLUGIN', `?theme=${penpot.getTheme()}`); + +penpot.on('themechange', (theme) => { + sendMessage({ type: 'theme', content: theme }); +}); + +function getSelectedShapes(): PenpotText[] { + return penpot.selection.filter((it): it is PenpotText => { + return penpot.utils.types.isText(it); + }); +} + +penpot.on('selectionchange', () => { + sendMessage({ type: 'selection', content: getSelectedShapes().length }); +}); + +penpot.ui.onMessage((message) => { + if (message.type === 'text') { + generateText(message); + + if (message.autoClose) { + penpot.closePlugin(); + } + } +}); + +function sendMessage(message: PluginMessageEvent) { + penpot.ui.sendMessage(message); +} + +function generateText(event: TextPluginUIEvent) { + const selection = getSelectedShapes(); + + if (!selection.length) { + const text = penpot.createText(''); + text.x = penpot.viewport.center.x; + text.y = penpot.viewport.center.y; + selection.push(text); + } + + selection.forEach((it) => { + switch (event.generationType) { + case 'paragraphs': + it.characters = generateParagraphs(event.size, event.startWithLorem); + break; + case 'sentences': + it.characters = generateSentences(event.size, event.startWithLorem); + break; + case 'words': + it.characters = generateWords(event.size, event.startWithLorem); + break; + case 'characters': + it.characters = generateCharacters(event.size, event.startWithLorem); + break; + } + }); +} diff --git a/apps/lorem-ipsum-plugin/src/styles.css b/apps/lorem-ipsum-plugin/src/styles.css new file mode 100644 index 0000000..e69de29 diff --git a/apps/lorem-ipsum-plugin/tsconfig.app.json b/apps/lorem-ipsum-plugin/tsconfig.app.json new file mode 100644 index 0000000..fff4a41 --- /dev/null +++ b/apps/lorem-ipsum-plugin/tsconfig.app.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"], + "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"] +} diff --git a/apps/lorem-ipsum-plugin/tsconfig.editor.json b/apps/lorem-ipsum-plugin/tsconfig.editor.json new file mode 100644 index 0000000..4ee6393 --- /dev/null +++ b/apps/lorem-ipsum-plugin/tsconfig.editor.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*.ts"], + "compilerOptions": { + "types": [] + } +} diff --git a/apps/lorem-ipsum-plugin/tsconfig.json b/apps/lorem-ipsum-plugin/tsconfig.json new file mode 100644 index 0000000..4c48587 --- /dev/null +++ b/apps/lorem-ipsum-plugin/tsconfig.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + "target": "es2022", + "useDefineForClassFields": false, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.editor.json" + }, + { + "path": "./tsconfig.plugin.json" + } + ], + "extends": "../../tsconfig.base.json", + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/apps/lorem-ipsum-plugin/tsconfig.plugin.json b/apps/lorem-ipsum-plugin/tsconfig.plugin.json new file mode 100644 index 0000000..961987f --- /dev/null +++ b/apps/lorem-ipsum-plugin/tsconfig.plugin.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "types": [] + }, + "files": ["src/plugin.ts"], + "include": ["../../libs/plugin-types/index.d.ts"] +} diff --git a/apps/lorem-ipsum-plugin/vite.config.ts b/apps/lorem-ipsum-plugin/vite.config.ts new file mode 100644 index 0000000..1b3b2d1 --- /dev/null +++ b/apps/lorem-ipsum-plugin/vite.config.ts @@ -0,0 +1,20 @@ +/// +import { defineConfig } from 'vite'; + +export default defineConfig({ + root: __dirname, + cacheDir: '../node_modules/.vite/lorem-ipsum-plugin', + 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/lorem-ipsum-plugin', + provider: 'v8', + }, + }, +}); diff --git a/docs/create-angular-plugin.md b/docs/create-angular-plugin.md index dfa7b78..9b89493 100644 --- a/docs/create-angular-plugin.md +++ b/docs/create-angular-plugin.md @@ -7,7 +7,7 @@ This guide walks you through the steps to create an Angular plugin. First, you need to create the scaffolding for your plugin. Use the following command, replacing `example-plugin` with the name of your plugin: ```sh -npx nx g @nx/angular:app example-plugin --directory=apps/example-plugin +npx nx g @nx/angular:app example-plugin --directory=apps/example-plugin --bundler=esbuild ``` ### Step 2: Configure the Manifest @@ -17,7 +17,7 @@ Next, create a `manifest.json` file inside the `/src/assets` directory. This fil ```json { "name": "Example plugin", - "code": "http://localhost:4202/assets/plugin.js", + "code": "http://localhost:4200/assets/plugin.js", "permissions": ["page:read", "file:read", "selection:read"] } ``` @@ -27,6 +27,7 @@ Next, create a `manifest.json` file inside the `/src/assets` directory. This fil Now, add the following configuration to your `project.json` to compile the `plugin.ts` file: ```typescript +"tags": ["type:plugin"], "targets": { "buildPlugin": { "executor": "@nx/esbuild:esbuild", @@ -87,12 +88,20 @@ Add the reference to the main tsconfig.json: ], ``` -### Step 5: Run the plugin +### Strep 5: Hello world plugin code + +Create the file `apps/example-plugin/src/plugin.ts` with the following code: + +```ts +console.log('Hello Plugin'); +``` + +### Step 6: Run the plugin Run this command: ```sh -npx nx run-many --targets=buildPlugin,serve --projects=poc-state-plugin --watch +npx nx run-many --targets=buildPlugin,serve --projects=example-plugin --watch ``` This will run two tasks: `serve`, the usual Angular server, and `buildPlugin`, which will compile the `plugin.ts` file. diff --git a/libs/plugin-types/index.d.ts b/libs/plugin-types/index.d.ts index 0de06f9..8dd89c2 100644 --- a/libs/plugin-types/index.d.ts +++ b/libs/plugin-types/index.d.ts @@ -460,7 +460,7 @@ export interface Penpot open: ( name: string, url: string, - options: { width: number; height: number } + options?: { width: number; height: number } ) => void; /** * Description of sendMessage diff --git a/libs/plugins-runtime/src/lib/api/index.ts b/libs/plugins-runtime/src/lib/api/index.ts index bd268d5..6685664 100644 --- a/libs/plugins-runtime/src/lib/api/index.ts +++ b/libs/plugins-runtime/src/lib/api/index.ts @@ -71,7 +71,7 @@ export function createApi(context: PenpotContext, manifest: Manifest): Penpot { const penpot: Penpot = { ui: { - open: (name: string, url: string, options: OpenUIOptions) => { + open: (name: string, url: string, options?: OpenUIOptions) => { const theme = context.getTheme() as 'light' | 'dark'; modal = openUIApi( diff --git a/libs/plugins-runtime/src/lib/api/openUI.api.ts b/libs/plugins-runtime/src/lib/api/openUI.api.ts index 675add3..46cb7ca 100644 --- a/libs/plugins-runtime/src/lib/api/openUI.api.ts +++ b/libs/plugins-runtime/src/lib/api/openUI.api.ts @@ -4,7 +4,12 @@ import { createModal } from '../create-modal.js'; export default z .function() - .args(z.string(), z.string(), z.enum(['dark', 'light']), openUISchema) + .args( + z.string(), + z.string(), + z.enum(['dark', 'light']), + openUISchema.optional() + ) .implement((title, url, theme, options) => { return createModal(title, url, theme, options); }); diff --git a/libs/plugins-runtime/src/lib/create-modal.ts b/libs/plugins-runtime/src/lib/create-modal.ts index 2f8d01a..8204f89 100644 --- a/libs/plugins-runtime/src/lib/create-modal.ts +++ b/libs/plugins-runtime/src/lib/create-modal.ts @@ -6,7 +6,7 @@ export function createModal( name: string, url: string, theme: PenpotTheme, - options: OpenUIOptions + options?: OpenUIOptions ) { const modal = document.createElement('plugin-modal') as PluginModalElement; @@ -14,8 +14,8 @@ export function createModal( modal.setAttribute('title', name); modal.setAttribute('iframe-src', url); - modal.setAttribute('width', String(options.width || 285)); - modal.setAttribute('height', String(options.height || 540)); + modal.setAttribute('width', String(options?.width || 285)); + modal.setAttribute('height', String(options?.height || 540)); document.body.appendChild(modal); diff --git a/libs/plugins-runtime/src/lib/load-plugin.ts b/libs/plugins-runtime/src/lib/load-plugin.ts index 29ed87d..116429d 100644 --- a/libs/plugins-runtime/src/lib/load-plugin.ts +++ b/libs/plugins-runtime/src/lib/load-plugin.ts @@ -33,6 +33,7 @@ export const ɵloadPlugin = async function (manifest: Manifest) { penpot: harden(lastApi), fetch: window.fetch.bind(window), console: harden(window.console), + Math: harden(Math), }); c.evaluate(code); diff --git a/libs/plugins-runtime/src/lib/modal/plugin.modal.css b/libs/plugins-runtime/src/lib/modal/plugin.modal.css index af1f419..569eb0b 100644 --- a/libs/plugins-runtime/src/lib/modal/plugin.modal.css +++ b/libs/plugins-runtime/src/lib/modal/plugin.modal.css @@ -43,7 +43,6 @@ justify-content: space-between; border-block-end: 2px solid var(--color-background-quaternary); padding-block-end: var(--spacing-4); - margin-block-end: var(--spacing-20); } button { diff --git a/libs/plugins-styles/src/lib/core/fonts.css b/libs/plugins-styles/src/lib/core/fonts.css index 0d12885..cbb5a88 100644 --- a/libs/plugins-styles/src/lib/core/fonts.css +++ b/libs/plugins-styles/src/lib/core/fonts.css @@ -1,98 +1,100 @@ -@import url('https://fonts.googleapis.com/css2?family=Work+Sans:wght@400+500&display=swap'); +@import url('https://fonts.googleapis.com/css?family=Work+Sans:wght@400+500&display=swap'); :root { - /* Font weight */ - --font-weight-regular: 400; - --font-weight-bold: 500; - --font-line-height-s: 1.2; - --font-line-height-m: 1.4; - --font-line-height-l: 1.5; - --font-size-s: 12px; - --font-size-m: 14px; - --font-size-l: 16px; + /* Font weight */ + --font-weight-regular: 400; + --font-weight-bold: 500; + --font-line-height-s: 1.2; + --font-line-height-m: 1.4; + --font-line-height-l: 1.5; + --font-size-s: 12px; + --font-size-m: 14px; + --font-size-l: 16px; } -html, body { - font-family: 'Work Sans', sans-serif; - font-optical-sizing: auto; - font-style: normal; +html, +body { + font-family: 'Work Sans', sans-serif; + font-optical-sizing: auto; + font-style: normal; } code { - font-family: 'Work Sans', sans-serif; + font-family: 'Work Sans', sans-serif; } .display { - font-weight: var(--font-weight-regular); - font-size: 36px; - line-height: var(--font-line-height-s); + font-weight: var(--font-weight-regular); + font-size: 36px; + line-height: var(--font-line-height-s); } .title-s { - font-weight: var(--font-weight-regular); - font-size: var(--font-size-m); - line-height: var(--font-line-height-s); + font-weight: var(--font-weight-regular); + font-size: var(--font-size-m); + line-height: var(--font-line-height-s); } .title-m { - font-weight: var(--font-weight-regular); - font-size: 20px; - line-height: var(--font-line-height-s); + font-weight: var(--font-weight-regular); + font-size: 20px; + line-height: var(--font-line-height-s); } .title-l { - font-weight: var(--font-weight-regular); - font-size: 24px; - line-height: 1.1; + font-weight: var(--font-weight-regular); + font-size: 24px; + line-height: 1.1; } .headline-s { - font-weight: var(--font-weight-bold); - font-size: var(--font-size-s); - line-height: var(--font-line-height-s); - text-transform: uppercase; + font-weight: var(--font-weight-bold); + font-size: var(--font-size-s); + line-height: var(--font-line-height-s); + text-transform: uppercase; } .headline-m { - font-weight: var(--font-weight-regular); - font-size: var(--font-size-l); - line-height: var(--font-line-height-m); - text-transform: uppercase; + font-weight: var(--font-weight-regular); + font-size: var(--font-size-l); + line-height: var(--font-line-height-m); + text-transform: uppercase; } .headline-l { - font-weight: var(--font-weight-regular); - font-size: 18px; - line-height: var(--font-line-height-s); - text-transform: uppercase; + font-weight: var(--font-weight-regular); + font-size: 18px; + line-height: var(--font-line-height-s); + text-transform: uppercase; } .body-s { - font-weight: var(--font-weight-regular); - font-size: var(--font-size-s); - line-height: var(--font-line-height-m); + font-weight: var(--font-weight-regular); + font-size: var(--font-size-s); + line-height: var(--font-line-height-m); } .body-m { - font-weight: var(--font-weight-regular); - font-size: var(--font-size-m); - line-height: var(--font-line-height-l); + font-weight: var(--font-weight-regular); + font-size: var(--font-size-m); + line-height: var(--font-line-height-l); } .body-l { - font-weight: var(--font-weight-regular); - font-size: var(--font-size-l); - line-height: var(--font-line-height-l); + font-weight: var(--font-weight-regular); + font-size: var(--font-size-l); + line-height: var(--font-line-height-l); } .caption { - font-weight: var(--font-weight-regular); - font-size: var(--font-size-s); - line-height: var(--font-line-height-s); + font-weight: var(--font-weight-regular); + font-size: var(--font-size-s); + line-height: var(--font-line-height-s); } -code, .code-font { - font-weight: var(--font-weight-regular); - font-size: var(--font-size-s); - line-height: var(--font-line-height-l); -} \ No newline at end of file +code, +.code-font { + font-weight: var(--font-weight-regular); + font-size: var(--font-size-s); + line-height: var(--font-line-height-l); +} diff --git a/package.json b/package.json index f559706..07585b3 100644 --- a/package.json +++ b/package.json @@ -10,10 +10,11 @@ "start:rpc-api": "npx nx serve rpc-api", "start:styles-example": "npx nx run example-styles:serve --host 0.0.0.0 --port 4201", "start:icons-plugin": "npx nx run-many --targets=buildPlugin,serve --projects=icons-plugin --watch --host 0.0.0.0 --port 4303", + "start:loremipsum-plugin": "npx nx run-many --targets=buildPlugin,serve --projects=lorem-ipsum-plugin --watch --port 4304", "build": "npx nx build plugins-runtime --emptyOutDir=true", "lint": "nx run-many --all --target=lint --parallel", "lint:affected": "npx nx affected --target=lint", - "test": "npx nx test plugins-runtime", + "test": "nx run-many -t test -p plugins-runtime lorem-ipsum-plugin", "publish": "nx run-many -t publish -p plugins-styles plugin-types --parallel=false --", "registry": "nx local-registry", "prepare": "husky",