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: `
+
+ `,
+ 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",