('.plugins-list')?.prepend(plugin);
+ }
+
+ loadPluginList() {
+ const plugins = this.getPlugins();
+
+ for (const plugin of plugins) {
+ this.createPlugin(plugin.name, plugin.url);
+ }
+ }
+
+ getPlugins() {
+ const pluginsStorage = localStorage.getItem('plugins');
+
+ if (!pluginsStorage) {
+ return [];
+ }
+
+ return JSON.parse(pluginsStorage) as {
+ name: string;
+ url: string;
+ }[];
+ }
+
+ savePlugins(plugins: { name: string; url: string }[]) {
+ localStorage.setItem('plugins', JSON.stringify(plugins));
+ }
+
+ submitNewPlugin(event: Event) {
+ event.preventDefault();
+
+ const form = event.target as HTMLFormElement;
+ const input = form.querySelector('input') as HTMLInputElement;
+
+ if (!input) {
+ return;
+ }
+
+ const url = input.value;
+
+ input.value = '';
+
+ void loadManifest(url)
+ .then((manifest) => {
+ this.createPlugin(manifest.name, url);
+
+ const pluginsStorage = localStorage.getItem('plugins');
+
+ if (!pluginsStorage) {
+ localStorage.setItem(
+ 'plugins',
+ JSON.stringify([{ name: manifest.name, url }])
+ );
+ } else {
+ const plugins = this.getPlugins();
+ plugins.push({ name: manifest.name, url });
+
+ this.savePlugins(plugins);
+ }
+
+ this.error(false);
+ })
+ .catch((error) => {
+ console.error(error);
+ this.error(true);
+ });
+ }
+
+ error(show: boolean) {
+ this.dialog?.querySelector('.error')?.classList.toggle('show', show);
+ }
+
+ connectedCallback() {
+ if (!this.shadowRoot) {
+ throw new Error('Error creating shadow root');
+ }
+
+ this.dialog = document.createElement('dialog');
+
+ this.dialog.innerHTML = `
+
+
+
+ Error instaling plugin
+
+
+
+ `;
+
+ this.dialog.querySelector('.close')?.addEventListener('click', () => {
+ this.closeModal();
+ });
+
+ this.shadowRoot.appendChild(this.dialog);
+
+ this.dialog.addEventListener('submit', (event: Event) => {
+ this.submitNewPlugin(event);
+ });
+
+ this.loadPluginList();
+
+ const style = document.createElement('style');
+ style.textContent = `
+ * {
+ font-family worksans, sans-serif
+ }
+
+ ::backdrop {
+ background-color: rgba(0, 0, 0, 0.8);
+ }
+
+ dialog {
+ border: 0;
+ width: 700px;
+ height: 500px;
+ padding: 20px;
+ background-color: white;
+ border-radius: 10px;
+ flex-direction: column;
+ display: none;
+ }
+
+ dialog[open] {
+ display: flex;
+ }
+
+ .header {
+ display: flex;
+ justify-content: space-between;
+ }
+
+ h1 {
+ margin: 0;
+ margin-block-end: 10px;
+ }
+
+ ul {
+ padding: 0;
+ }
+
+ li {
+ list-style: none;
+ }
+
+ .input {
+ display: flex;
+ border: 1px solid;
+ border-radius: calc( 0.25rem * 2);
+ font-size: 12px;
+ font-weight: 400;
+ line-height: 1.4;
+ outline: none;
+ padding-block: calc( 0.25rem * 2);
+ padding-inline: calc( 0.25rem * 2);
+ background-color: #f3f4f6;
+ border-color: #f3f4f6;
+ color: #000;
+
+ &:hover {
+ background-color: #eef0f2;
+ border-color: #eef0f2;
+ }
+
+ &:focus {
+ background-color: #ffffff
+ border-color: ##6911d4;
+ }
+ }
+
+ button {
+ background: transparent;
+ border: 0;
+ cursor: pointer;
+ }
+
+ .button {
+ border: 1px solid transparent;
+ font-weight: 500;
+ font-size: 12px;
+ border-radius: 8px;
+ line-height: 1.2;
+ padding: 8px 24px 8px 24px;
+ text-transform: uppercase;
+ background-color: #7EFFF5;
+ border: 1px solid 7EFFF5;
+ outline: 2px solid transparent;
+
+ &:hover:not(:disabled) {
+ cursor: pointer;
+ }
+
+ &:focus-visible {
+ outline: none;
+ }
+ }
+
+ .remove {
+ background-color: #ff3277;
+ border: 1px solid #ff3277;
+ outline: 2px solid transparent;
+ }
+
+ form {
+ display: flex;
+ gap: 10px;
+ margin-block-end: 20px;
+ }
+
+ .url-input {
+ inline-size: 400px;
+ }
+
+ .plugins-list {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ }
+
+ .plugin {
+ display: flex;
+ justify-content: space-between;
+ }
+
+ .actions {
+ display: flex;
+ gap: 10px;
+ }
+
+ .error {
+ display: none;
+ color: red;
+
+ &.show {
+ display: block;
+ }
+ }
+ `;
+
+ this.shadowRoot.appendChild(style);
+ }
+
+ closeModal() {
+ this.shadowRoot?.querySelector('dialog')?.close();
+
+ window.removeEventListener('paste', pasteEventHandler, true);
+ }
+
+ openModal() {
+ this.shadowRoot?.querySelector('dialog')?.showModal();
+
+ // prevent paste event in penpot workspace
+ window.addEventListener('paste', pasteEventHandler, true);
+ }
+}
+
+export function initInstaller() {
+ customElements.define('installer-modal', InstallerElement);
+
+ const modal = document.createElement('installer-modal');
+
+ document.body.appendChild(modal);
+
+ document.addEventListener('keydown', (event: KeyboardEvent) => {
+ if (event.key.toUpperCase() === 'I' && event.ctrlKey) {
+ document.querySelector('installer-modal')?.openModal();
+ }
+ });
+}
diff --git a/libs/plugins-runtime/src/lib/parse-manifest.ts b/libs/plugins-runtime/src/lib/parse-manifest.ts
index bfd77e7b..db34dc9a 100644
--- a/libs/plugins-runtime/src/lib/parse-manifest.ts
+++ b/libs/plugins-runtime/src/lib/parse-manifest.ts
@@ -2,7 +2,7 @@ import { Manifest } from './models/manifest.model';
import { manifestSchema } from './models/manifest.schema';
import { PluginConfig } from './models/plugin-config.model';
-function loadManifest(url: string): Promise {
+export function loadManifest(url: string): Promise {
return fetch(url)
.then((response) => response.json())
.then((manifest: Manifest): Manifest => {