0
Fork 0
mirror of https://github.com/penpot/penpot-plugins.git synced 2025-01-21 06:02:34 -05:00

feat(icons-plugin): add styles and theme support

This commit is contained in:
María Valderrama 2024-05-21 14:01:54 +02:00
parent ec729e2e59
commit 66ebaffb06
11 changed files with 180 additions and 44 deletions

View file

@ -0,0 +1,29 @@
.icons-plugin {
display: flex;
flex-direction: column;
height: 100vh;
gap: 5px;
overflow: hidden;
.icons-search,
.icons-list {
width: 100%;
}
.icons-search {
padding-top: var(--spacing-20);
}
.icons-list {
flex-grow: 1;
overflow: auto;
display: grid;
padding: 3px 3px 0;
background-color: var(--app-white);
border-radius: var(--spacing-8);
justify-content: start;
grid-template-columns: repeat(6, 1fr);
grid-auto-rows: max-content;
justify-items: center;
}
}

View file

@ -1,9 +1,12 @@
import { Component, signal } from '@angular/core';
import { RouterModule } from '@angular/router';
import { Component, inject, signal } from '@angular/core';
import { ActivatedRoute, RouterModule } from '@angular/router';
import { FeatherIconNames, icons } from 'feather-icons';
import { SafeHtmlPipe } from './pipes/safe-html.pipe';
import { IconButtonComponent } from './components/icon-button/icon-button.component';
import { IconSearchComponent } from './components/icon-search/icon-search.component';
import { toSignal } from '@angular/core/rxjs-interop';
import { filter, fromEvent, map, merge, take } from 'rxjs';
import { PluginMessageEvent } from '../model';
@Component({
selector: 'app-root',
@ -15,29 +18,70 @@ import { IconSearchComponent } from './components/icon-search/icon-search.compon
IconSearchComponent,
],
styleUrl: './app.component.css',
template: `<div>
<div>
template: `<div class="icons-plugin">
<div class="icons-search">
<app-icon-search
(searchIcons)="this.searchIcons($event)"
></app-icon-search>
</div>
@for (key of iconKeys(); track key) {
<app-icon-button
[icon]="icons()[key]"
(insertIcon)="this.insertIcon(key)"
></app-icon-button>
@if (iconKeys().length === 0) {
<div class="no-icons-found">No icons found</div>
} @else {
<div class="icons-list">
@for (key of iconKeys(); track key) {
<app-icon-button
[icon]="icons()[key]"
(insertIcon)="this.insertIcon(key)"
></app-icon-button>
}
</div>
}
</div>`,
host: {
'[attr.data-theme]': 'theme()',
},
})
export class AppComponent {
public route = inject(ActivatedRoute);
public icons = signal(icons);
public iconKeys = signal(Object.keys(icons) as FeatherIconNames[]);
public messages$ = fromEvent<MessageEvent<PluginMessageEvent>>(
window,
'message'
);
public initialTheme$ = this.route.queryParamMap.pipe(
map((params) => params.get('theme')),
filter((theme) => !!theme),
take(1)
);
public theme = toSignal(
merge(
this.initialTheme$,
this.messages$.pipe(
filter((event) => event.data.type === 'theme'),
map((event) => {
return event.data.content;
})
)
)
);
public insertIcon(key: FeatherIconNames): void {
if (key && this.icons()[key] && this.icons()[key].toSvg()) {
if (
key &&
this.icons()[key] &&
this.icons()[key].toSvg({
'stroke-width': '3',
})
) {
this.sendMessage({
content: this.icons()[key].toSvg(),
name: this.icons()[key].name || key,
type: 'insert-icon',
content: {
svg: this.icons()[key].toSvg(),
name: this.icons()[key].name || key,
},
});
}
}

View file

@ -0,0 +1,6 @@
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
export const appConfig: ApplicationConfig = {
providers: [provideRouter([])],
};

View file

@ -1,16 +1,17 @@
.icon-button {
padding: 0.25rem;
color: #000;
padding: var(--spacing-4);
background: transparent;
border-radius: 0.5rem;
cursor: pointer;
border-radius: var(--spacing-8);
color: var(--app-black);
&:hover {
background: rgba(0, 0, 0, 0.1);
&:hover,
&:focus,
&:active {
background-color: var(--lb-tertiary);
}
&:focus,
&:active {
box-shadow: inset 0 0 0 2px rgba(0, 0, 0, 0.1);
box-shadow: inset 0 0 0 2px var(--da-primary);
}
}

View file

@ -21,7 +21,7 @@ import { FeatherIcon } from 'feather-icons';
view-box="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
[innerHtml]="icon().contents | safeHtml"

View file

@ -0,0 +1,16 @@
.search-icon {
width: 100%;
border-radius: var(--spacing-8);
padding: var(--spacing-8);
padding-left: var(--spacing-28);
background: var(--background-tertiary)
url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" class="main-grid-item-icon" fill="none" stroke="%238f9da3" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"><circle cx="11" cy="11" r="8" /><line x1="21" x2="16.65" y1="21" y2="16.65" /></svg>')
var(--spacing-8) no-repeat;
color: var(--foreground-secondary);
font-size: var(--font-size-s);
font-weight: var(--font-weight-regular);
&:focus {
outline: none;
}
}

View file

@ -6,11 +6,14 @@ import { icons } from 'feather-icons';
standalone: true,
imports: [],
styleUrl: './icon-search.component.css',
template: `<input
type="search"
[placeholder]="'Search ' + iconsCount() + ' icons'"
(input)="onSearchIcons($event)"
/>`,
template: `
<input
class="search-icon"
type="search"
placeholder="Search an icon"
(input)="onSearchIcons($event)"
/>
`,
})
export class IconSearchComponent {
public searchIcons = output<string>();

View file

@ -2,9 +2,5 @@
"name": "Icons plugin",
"host": "http://localhost:4303",
"code": "/assets/plugin.js",
"permissions": [
"page:read",
"file:read",
"selection:read"
]
"permissions": ["page:read", "file:read", "selection:read"]
}

View file

@ -1,4 +1,7 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent).catch((err) => console.error(err));
bootstrapApplication(AppComponent, appConfig).catch((err) =>
console.error(err)
);

View file

@ -0,0 +1,27 @@
export interface InitPluginEvent {
type: 'init';
content: {
theme: string;
};
}
export interface InsertIconEvent {
type: 'insert-icon';
content: {
svg: string;
name: string;
};
}
export interface InitPluginUIEvent {
type: 'ready';
}
export type PluginUIEvent = InitPluginUIEvent | InsertIconEvent;
export interface ThemePluginEvent {
type: 'theme';
content: string;
}
export type PluginMessageEvent = InitPluginEvent | ThemePluginEvent;

View file

@ -1,17 +1,28 @@
penpot.ui.open('Icons plugin', '', {
width: 500,
height: 600,
import type { PluginMessageEvent, PluginUIEvent } from './model.js';
penpot.ui.open('FEATHER ICONS PLUGIN', `?theme=${penpot.getTheme()}`, {
width: 292,
height: 540,
});
penpot.ui.onMessage<{ content: string; name: string }>((message) => {
if (!message.content || !message.name) {
return;
penpot.ui.onMessage<PluginUIEvent>((message) => {
if (message.type === 'insert-icon') {
const { name, svg } = message.content;
if (!svg || !name) {
return;
}
const icon = penpot.createShapeFromSvg(svg);
icon.name = name;
icon.x = penpot.viewport.center.x;
icon.y = penpot.viewport.center.y;
}
const svgIcon = message.content;
const iconName = message.name;
const icon = penpot.createShapeFromSvg(svgIcon);
icon.name = iconName;
icon.x = penpot.viewport.center.x;
icon.y = penpot.viewport.center.y;
});
penpot.on('themechange', (theme) => {
sendMessage({ type: 'theme', content: theme });
});
function sendMessage(message: PluginMessageEvent) {
penpot.ui.sendMessage(message);
}