fix(icons): migrate Marketplace Builder to Lucide

Replace every PrimeIcons pi-* usage across the builder: overview page
(back link, next-step arrow, section cards, readiness checklist,
quick links), sidebar nav (group/section status dots), main layout
(back/home/menu/help icons), brand + homepage overview panels
(checklist ok/pending dots, contrast warning), footer/homepage/widgets
section editors (drag handles, move up/down, duplicate, remove), and
the HTML editor toolbar (list/link/image/table/divider/code/embed).

Notable correctness fix: PrimeIcons reused pi-bars for both the
hamburger menu toggle AND every drag handle - two different meanings
sharing one icon (exactly the kind of icon collision the audit calls
out). Added a dedicated 'grip' icon (GripVertical) for drag handles so
menu and drag-to-reorder are visually distinct.

Added a global .spin utility (icon-registry has no built-in spinner
animation) for the one loading-spinner icon in the builder overview
checklist.

All icon-bearing fields (BuilderGroup.icon, BlockCatalogEntry.icon,
WIDGET_ICONS, STATUS_ICON, HtmlEditorToolbarCommand.icon, etc.) are
now typed AppIconName instead of string.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
sdarbinyan
2026-07-20 02:31:06 +04:00
parent 330f24c8d7
commit 043192accf
21 changed files with 125 additions and 95 deletions

View File

@@ -1,4 +1,5 @@
import { ProjectEditorSectionId } from '../models/project-editor.model'; import { ProjectEditorSectionId } from '../models/project-editor.model';
import { AppIconName } from '../../../shared/ui/icon/icon-registry';
export type BuilderGroupId = export type BuilderGroupId =
| 'marketplace' | 'marketplace'
@@ -12,7 +13,7 @@ export type BuilderGroupId =
export interface BuilderGroup { export interface BuilderGroup {
id: BuilderGroupId; id: BuilderGroupId;
icon: string; icon: AppIconName;
labelKey: string; labelKey: string;
descriptionKey: string; descriptionKey: string;
purposeKey: string; purposeKey: string;
@@ -31,7 +32,7 @@ export interface BuilderGroup {
export const BUILDER_GROUPS: BuilderGroup[] = [ export const BUILDER_GROUPS: BuilderGroup[] = [
{ {
id: 'marketplace', id: 'marketplace',
icon: 'pi-shop', icon: 'store',
labelKey: 'builder.groupMarketplaceLabel', labelKey: 'builder.groupMarketplaceLabel',
descriptionKey: 'builder.groupMarketplaceDescription', descriptionKey: 'builder.groupMarketplaceDescription',
purposeKey: 'builder.groupMarketplacePurpose', purposeKey: 'builder.groupMarketplacePurpose',
@@ -41,7 +42,7 @@ export const BUILDER_GROUPS: BuilderGroup[] = [
}, },
{ {
id: 'branding', id: 'branding',
icon: 'pi-palette', icon: 'palette',
labelKey: 'builder.groupBrandingLabel', labelKey: 'builder.groupBrandingLabel',
descriptionKey: 'builder.groupBrandingDescription', descriptionKey: 'builder.groupBrandingDescription',
purposeKey: 'builder.groupBrandingPurpose', purposeKey: 'builder.groupBrandingPurpose',
@@ -53,7 +54,7 @@ export const BUILDER_GROUPS: BuilderGroup[] = [
}, },
{ {
id: 'homepage', id: 'homepage',
icon: 'pi-home', icon: 'home',
labelKey: 'builder.groupHomepageLabel', labelKey: 'builder.groupHomepageLabel',
descriptionKey: 'builder.groupHomepageDescription', descriptionKey: 'builder.groupHomepageDescription',
purposeKey: 'builder.groupHomepagePurpose', purposeKey: 'builder.groupHomepagePurpose',
@@ -63,7 +64,7 @@ export const BUILDER_GROUPS: BuilderGroup[] = [
}, },
{ {
id: 'content', id: 'content',
icon: 'pi-file-edit', icon: 'edit',
labelKey: 'builder.groupContentLabel', labelKey: 'builder.groupContentLabel',
descriptionKey: 'builder.groupContentDescription', descriptionKey: 'builder.groupContentDescription',
purposeKey: 'builder.groupContentPurpose', purposeKey: 'builder.groupContentPurpose',
@@ -73,7 +74,7 @@ export const BUILDER_GROUPS: BuilderGroup[] = [
}, },
{ {
id: 'languages', id: 'languages',
icon: 'pi-globe', icon: 'globe',
labelKey: 'builder.groupLanguagesLabel', labelKey: 'builder.groupLanguagesLabel',
descriptionKey: 'builder.groupLanguagesDescription', descriptionKey: 'builder.groupLanguagesDescription',
purposeKey: 'builder.groupLanguagesPurpose', purposeKey: 'builder.groupLanguagesPurpose',
@@ -83,7 +84,7 @@ export const BUILDER_GROUPS: BuilderGroup[] = [
}, },
{ {
id: 'features', id: 'features',
icon: 'pi-sliders-h', icon: 'slidersHorizontal',
labelKey: 'builder.groupFeaturesLabel', labelKey: 'builder.groupFeaturesLabel',
descriptionKey: 'builder.groupFeaturesDescription', descriptionKey: 'builder.groupFeaturesDescription',
purposeKey: 'builder.groupFeaturesPurpose', purposeKey: 'builder.groupFeaturesPurpose',
@@ -93,7 +94,7 @@ export const BUILDER_GROUPS: BuilderGroup[] = [
}, },
{ {
id: 'navigation', id: 'navigation',
icon: 'pi-compass', icon: 'compass',
labelKey: 'builder.groupNavigationLabel', labelKey: 'builder.groupNavigationLabel',
descriptionKey: 'builder.groupNavigationDescription', descriptionKey: 'builder.groupNavigationDescription',
purposeKey: 'builder.groupNavigationPurpose', purposeKey: 'builder.groupNavigationPurpose',
@@ -103,7 +104,7 @@ export const BUILDER_GROUPS: BuilderGroup[] = [
}, },
{ {
id: 'preview', id: 'preview',
icon: 'pi-desktop', icon: 'monitor',
labelKey: 'builder.groupPreviewLabel', labelKey: 'builder.groupPreviewLabel',
descriptionKey: 'builder.groupPreviewDescription', descriptionKey: 'builder.groupPreviewDescription',
purposeKey: 'builder.groupPreviewPurpose', purposeKey: 'builder.groupPreviewPurpose',

View File

@@ -14,7 +14,7 @@
(click)="runCommand(item)" (click)="runCommand(item)"
> >
@if (item.icon) { @if (item.icon) {
<span class="pi {{ item.icon }}" aria-hidden="true"></span> <app-icon [name]="item.icon" [size]="16" />
} @else { } @else {
{{ item.label }} {{ item.label }}
} }

View File

@@ -3,14 +3,16 @@ import { TranslateService } from '../../../../i18n/translate.service';
import { TranslatePipe } from '../../../../i18n/translate.pipe'; import { TranslatePipe } from '../../../../i18n/translate.pipe';
import { validateHtml } from '../../schema/validators/primitives'; import { validateHtml } from '../../schema/validators/primitives';
import { CodeEditorComponent } from '../../../../shared/ui/code-editor/code-editor.component'; import { CodeEditorComponent } from '../../../../shared/ui/code-editor/code-editor.component';
import { IconComponent } from '../../../../shared/ui/icon/icon.component';
import { AppIconName } from '../../../../shared/ui/icon/icon-registry';
export interface HtmlEditorToolbarCommand { export interface HtmlEditorToolbarCommand {
id: string; id: string;
label: string; label: string;
command: string; command: string;
value?: string; value?: string;
/** PrimeIcons class (e.g. 'pi-list'); when set the icon renders instead of the label. */ /** Lucide icon name (see icon-registry.ts); when set the icon renders instead of the label. */
icon?: string; icon?: AppIconName;
/** i18n key for the tooltip/aria-label. */ /** i18n key for the tooltip/aria-label. */
titleKey?: string; titleKey?: string;
} }
@@ -21,14 +23,14 @@ export const HTML_EDITOR_TOOLBAR: HtmlEditorToolbarCommand[] = [
{ id: 'underline', label: 'U', command: 'underline', titleKey: 'builder.htmlToolUnderline' }, { id: 'underline', label: 'U', command: 'underline', titleKey: 'builder.htmlToolUnderline' },
{ id: 'h2', label: 'H2', command: 'formatBlock', value: 'H2', titleKey: 'builder.htmlToolHeading' }, { id: 'h2', label: 'H2', command: 'formatBlock', value: 'H2', titleKey: 'builder.htmlToolHeading' },
{ id: 'h3', label: 'H3', command: 'formatBlock', value: 'H3', titleKey: 'builder.htmlToolSubheading' }, { id: 'h3', label: 'H3', command: 'formatBlock', value: 'H3', titleKey: 'builder.htmlToolSubheading' },
{ id: 'ul', label: '', icon: 'pi-list', command: 'insertUnorderedList', titleKey: 'builder.htmlToolBulletList' }, { id: 'ul', label: '', icon: 'list', command: 'insertUnorderedList', titleKey: 'builder.htmlToolBulletList' },
{ id: 'ol', label: '1.', command: 'insertOrderedList', titleKey: 'builder.htmlToolNumberedList' }, { id: 'ol', label: '1.', command: 'insertOrderedList', titleKey: 'builder.htmlToolNumberedList' },
{ id: 'link', label: '', icon: 'pi-link', command: 'createLink', titleKey: 'builder.htmlToolLink' }, { id: 'link', label: '', icon: 'link', command: 'createLink', titleKey: 'builder.htmlToolLink' },
{ id: 'image', label: '', icon: 'pi-image', command: 'insertImage', titleKey: 'builder.htmlToolImage' }, { id: 'image', label: '', icon: 'image', command: 'insertImage', titleKey: 'builder.htmlToolImage' },
{ id: 'table', label: '', icon: 'pi-table', command: 'insertHTML', value: '<table><tr><td>&nbsp;</td><td>&nbsp;</td></tr></table>', titleKey: 'builder.htmlToolTable' }, { id: 'table', label: '', icon: 'table', command: 'insertHTML', value: '<table><tr><td>&nbsp;</td><td>&nbsp;</td></tr></table>', titleKey: 'builder.htmlToolTable' },
{ id: 'hr', label: '', icon: 'pi-minus', command: 'insertHorizontalRule', titleKey: 'builder.htmlToolDivider' }, { id: 'hr', label: '', icon: 'minus', command: 'insertHorizontalRule', titleKey: 'builder.htmlToolDivider' },
{ id: 'codeblock', label: '', icon: 'pi-code', command: 'formatBlock', value: 'PRE', titleKey: 'builder.htmlToolCodeBlock' }, { id: 'codeblock', label: '', icon: 'code', command: 'formatBlock', value: 'PRE', titleKey: 'builder.htmlToolCodeBlock' },
{ id: 'embed', label: '', icon: 'pi-video', command: 'insertEmbed', titleKey: 'builder.htmlToolEmbed' }, { id: 'embed', label: '', icon: 'video', command: 'insertEmbed', titleKey: 'builder.htmlToolEmbed' },
]; ];
/** Visual grouping for the toolbar: text style | headings | lists | insert | advanced. */ /** Visual grouping for the toolbar: text style | headings | lists | insert | advanced. */
@@ -43,7 +45,7 @@ export const HTML_EDITOR_TOOLBAR_GROUPS: string[][] = [
@Component({ @Component({
selector: 'app-marketplace-html-editor', selector: 'app-marketplace-html-editor',
standalone: true, standalone: true,
imports: [TranslatePipe, CodeEditorComponent], imports: [TranslatePipe, CodeEditorComponent, IconComponent],
templateUrl: './marketplace-html-editor.component.html', templateUrl: './marketplace-html-editor.component.html',
styleUrls: ['./marketplace-html-editor.component.scss'], styleUrls: ['./marketplace-html-editor.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush

View File

@@ -2,9 +2,9 @@
@for (item of groups(); track item.group.id) { @for (item of groups(); track item.group.id) {
<div class="editor-nav-group"> <div class="editor-nav-group">
<div class="editor-nav-group__header"> <div class="editor-nav-group__header">
<span class="pi {{ item.group.icon }} editor-nav-group__icon" aria-hidden="true"></span> <app-icon [name]="item.group.icon" [size]="18" class="editor-nav-group__icon" />
<span class="editor-nav-group__label">{{ item.group.labelKey | translate }}</span> <span class="editor-nav-group__label">{{ item.group.labelKey | translate }}</span>
<span class="pi {{ item.statusIcon }} editor-nav-group__status editor-nav-group__status--{{ item.status }}" [attr.aria-label]="item.statusLabelKey | translate"></span> <app-icon [name]="item.statusIcon" [size]="14" [ariaLabel]="item.statusLabelKey | translate" class="editor-nav-group__status editor-nav-group__status--{{ item.status }}" />
</div> </div>
<ul class="editor-nav-group__list"> <ul class="editor-nav-group__list">
@for (section of item.sections; track section.id) { @for (section of item.sections; track section.id) {
@@ -16,7 +16,7 @@
class="editor-nav-link" class="editor-nav-link"
(click)="onLinkClick()" (click)="onLinkClick()"
> >
<span class="pi {{ section.statusIcon }} editor-nav-link__status editor-nav-link__status--{{ section.status }}" [attr.aria-label]="section.statusLabelKey | translate"></span> <app-icon [name]="section.statusIcon" [size]="14" [ariaLabel]="section.statusLabelKey | translate" class="editor-nav-link__status editor-nav-link__status--{{ section.status }}" />
<span class="editor-nav-link__label">{{ section.labelKey | translate }}</span> <span class="editor-nav-link__label">{{ section.labelKey | translate }}</span>
@if (section.issueCount > 0) { @if (section.issueCount > 0) {
<span class="editor-nav-badge" [attr.aria-label]="section.issueCount + ' ' + ('builder.title' | translate)">{{ section.issueCount }}</span> <span class="editor-nav-badge" [attr.aria-label]="section.issueCount + ' ' + ('builder.title' | translate)">{{ section.issueCount }}</span>

View File

@@ -5,12 +5,14 @@ import { LangRoutePipe } from '../../../pipes/lang-route.pipe';
import { ProjectEditorSectionId, BuilderSectionStatus } from '../models/project-editor.model'; import { ProjectEditorSectionId, BuilderSectionStatus } from '../models/project-editor.model';
import { ProjectEditorFacade } from '../facade/project-editor.facade'; import { ProjectEditorFacade } from '../facade/project-editor.facade';
import { BUILDER_GROUPS, BuilderGroup } from '../builder/builder-groups.model'; import { BUILDER_GROUPS, BuilderGroup } from '../builder/builder-groups.model';
import { IconComponent } from '../../../shared/ui/icon/icon.component';
import { AppIconName } from '../../../shared/ui/icon/icon-registry';
const STATUS_ICON: Record<BuilderSectionStatus, string> = { const STATUS_ICON: Record<BuilderSectionStatus, AppIconName> = {
complete: 'pi-check-circle', complete: 'checkCircle',
'in-progress': 'pi-circle-fill', 'in-progress': 'circle',
'not-started': 'pi-circle', 'not-started': 'circle',
unknown: 'pi-question-circle', unknown: 'help',
}; };
const STATUS_LABEL_KEY: Record<BuilderSectionStatus, string> = { const STATUS_LABEL_KEY: Record<BuilderSectionStatus, string> = {
@@ -24,7 +26,7 @@ interface NavSectionItem {
id: ProjectEditorSectionId; id: ProjectEditorSectionId;
labelKey: string; labelKey: string;
status: BuilderSectionStatus; status: BuilderSectionStatus;
statusIcon: string; statusIcon: AppIconName;
statusLabelKey: string; statusLabelKey: string;
modified: boolean; modified: boolean;
issueCount: number; issueCount: number;
@@ -33,7 +35,7 @@ interface NavSectionItem {
interface NavGroupItem { interface NavGroupItem {
group: BuilderGroup; group: BuilderGroup;
status: BuilderSectionStatus; status: BuilderSectionStatus;
statusIcon: string; statusIcon: AppIconName;
statusLabelKey: string; statusLabelKey: string;
sections: NavSectionItem[]; sections: NavSectionItem[];
} }
@@ -56,7 +58,7 @@ const SECTION_LABEL_KEYS: Record<ProjectEditorSectionId, string> = {
@Component({ @Component({
selector: 'app-project-editor-nav', selector: 'app-project-editor-nav',
standalone: true, standalone: true,
imports: [TranslatePipe, RouterLink, RouterLinkActive, LangRoutePipe], imports: [TranslatePipe, RouterLink, RouterLinkActive, LangRoutePipe, IconComponent],
templateUrl: './project-editor-nav.component.html', templateUrl: './project-editor-nav.component.html',
styleUrls: ['./project-editor-nav.component.scss'], styleUrls: ['./project-editor-nav.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,

View File

@@ -1,6 +1,6 @@
<main class="builder-overview"> <main class="builder-overview">
<a class="builder-overview__back" [routerLink]="['/', 'backoffice', 'dashboard'] | langRoute"> <a class="builder-overview__back" [routerLink]="['/', 'backoffice', 'dashboard'] | langRoute">
<span class="pi pi-arrow-left" aria-hidden="true"></span>{{ 'adminShell.pages.dashboard.title' | translate }} <app-icon name="arrowLeft" [size]="16" />{{ 'adminShell.pages.dashboard.title' | translate }}
</a> </a>
<header class="builder-overview__hero"> <header class="builder-overview__hero">
<p class="builder-overview__eyebrow">{{ 'builder.appName' | translate }}</p> <p class="builder-overview__eyebrow">{{ 'builder.appName' | translate }}</p>
@@ -26,7 +26,7 @@
</div> </div>
<div class="builder-overview__next-step"> <div class="builder-overview__next-step">
<span class="pi pi-arrow-right-circle" aria-hidden="true"></span> <app-icon name="arrowRight" [size]="20" />
<div> <div>
<p class="builder-overview__next-step-label">{{ 'builder.overviewNextStepLabel' | translate }}</p> <p class="builder-overview__next-step-label">{{ 'builder.overviewNextStepLabel' | translate }}</p>
@if (nextStepLabelKey(); as labelKey) { @if (nextStepLabelKey(); as labelKey) {
@@ -46,11 +46,13 @@
@for (group of groups; track group.id) { @for (group of groups; track group.id) {
<a class="builder-overview__card" [routerLink]="groupSectionRoute(group.sections[0])"> <a class="builder-overview__card" [routerLink]="groupSectionRoute(group.sections[0])">
<div class="builder-overview__card-header"> <div class="builder-overview__card-header">
<span class="pi {{ group.icon }} builder-overview__card-icon" aria-hidden="true"></span> <app-icon [name]="group.icon" [size]="20" class="builder-overview__card-icon" />
<span <app-icon
class="pi {{ statusIcon[groupStatus().get(group.id) ?? 'unknown'] }} builder-overview__card-status builder-overview__card-status--{{ groupStatus().get(group.id) ?? 'unknown' }}" [name]="statusIcon[groupStatus().get(group.id) ?? 'unknown']"
[attr.aria-label]="statusLabelKey[groupStatus().get(group.id) ?? 'unknown'] | translate" [size]="16"
></span> [ariaLabel]="statusLabelKey[groupStatus().get(group.id) ?? 'unknown'] | translate"
class="builder-overview__card-status builder-overview__card-status--{{ groupStatus().get(group.id) ?? 'unknown' }}"
/>
</div> </div>
<h3 class="builder-overview__card-title">{{ group.labelKey | translate }}</h3> <h3 class="builder-overview__card-title">{{ group.labelKey | translate }}</h3>
<p class="builder-overview__card-description">{{ group.descriptionKey | translate }}</p> <p class="builder-overview__card-description">{{ group.descriptionKey | translate }}</p>
@@ -66,10 +68,10 @@
@for (check of readinessChecklist(); track check.labelKey) { @for (check of readinessChecklist(); track check.labelKey) {
<li class="builder-overview__check builder-overview__check--{{ check.state }}"> <li class="builder-overview__check builder-overview__check--{{ check.state }}">
@switch (check.state) { @switch (check.state) {
@case ('true') { <span class="pi pi-check-circle" aria-hidden="true"></span> } @case ('true') { <app-icon name="checkCircle" [size]="16" /> }
@case ('false') { <span class="pi pi-circle" aria-hidden="true"></span> } @case ('false') { <app-icon name="circle" [size]="16" /> }
@case ('loading') { <span class="pi pi-spin pi-spinner" aria-hidden="true"></span> } @case ('loading') { <app-icon name="spinner" [size]="16" class="spin" /> }
@default { <span class="pi pi-question-circle" aria-hidden="true"></span> } @default { <app-icon name="help" [size]="16" /> }
} }
<span>{{ check.labelKey | translate }}</span> <span>{{ check.labelKey | translate }}</span>
</li> </li>
@@ -81,10 +83,10 @@
<h2 id="builder-overview-links-title">{{ 'builder.overviewQuickLinksTitle' | translate }}</h2> <h2 id="builder-overview-links-title">{{ 'builder.overviewQuickLinksTitle' | translate }}</h2>
<div class="builder-overview__quick-links"> <div class="builder-overview__quick-links">
<a class="builder-overview__quick-link" [routerLink]="groupSectionRoute('preview')"> <a class="builder-overview__quick-link" [routerLink]="groupSectionRoute('preview')">
<span class="pi pi-desktop" aria-hidden="true"></span>{{ 'builder.preview' | translate }} <app-icon name="monitor" [size]="18" />{{ 'builder.preview' | translate }}
</a> </a>
<a class="builder-overview__quick-link" [routerLink]="groupSectionRoute('general')"> <a class="builder-overview__quick-link" [routerLink]="groupSectionRoute('general')">
<span class="pi pi-shop" aria-hidden="true"></span>{{ 'builder.general' | translate }} <app-icon name="store" [size]="18" />{{ 'builder.general' | translate }}
</a> </a>
</div> </div>
</section> </section>

View File

@@ -9,12 +9,14 @@ import { ProjectEditorFacade } from '../facade/project-editor.facade';
import { BackofficeDataService } from '../../../core/backoffice/backoffice-data.service'; import { BackofficeDataService } from '../../../core/backoffice/backoffice-data.service';
import { BUILDER_GROUPS } from '../builder/builder-groups.model'; import { BUILDER_GROUPS } from '../builder/builder-groups.model';
import { BuilderSectionStatus } from '../models/project-editor.model'; import { BuilderSectionStatus } from '../models/project-editor.model';
import { IconComponent } from '../../../shared/ui/icon/icon.component';
import { AppIconName } from '../../../shared/ui/icon/icon-registry';
const STATUS_ICON: Record<BuilderSectionStatus, string> = { const STATUS_ICON: Record<BuilderSectionStatus, AppIconName> = {
complete: 'pi-check-circle', complete: 'checkCircle',
'in-progress': 'pi-circle-fill', 'in-progress': 'circle',
'not-started': 'pi-circle', 'not-started': 'circle',
unknown: 'pi-question-circle', unknown: 'help',
}; };
const STATUS_LABEL_KEY: Record<BuilderSectionStatus, string> = { const STATUS_LABEL_KEY: Record<BuilderSectionStatus, string> = {
@@ -49,7 +51,7 @@ const SECTION_LABEL_KEYS: Record<string, string> = {
@Component({ @Component({
selector: 'app-builder-overview-page', selector: 'app-builder-overview-page',
standalone: true, standalone: true,
imports: [RouterLink, TranslatePipe, LangRoutePipe], imports: [RouterLink, TranslatePipe, LangRoutePipe, IconComponent],
templateUrl: './builder-overview-page.component.html', templateUrl: './builder-overview-page.component.html',
styleUrl: './builder-overview-page.component.scss', styleUrl: './builder-overview-page.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,

View File

@@ -5,11 +5,11 @@
<aside class="project-editor-layout__sidebar"> <aside class="project-editor-layout__sidebar">
<a class="project-editor-layout__brand" [routerLink]="overviewRoute()"> <a class="project-editor-layout__brand" [routerLink]="overviewRoute()">
<span class="pi pi-arrow-left" aria-hidden="true"></span> <app-icon name="arrowLeft" [size]="16" />
{{ 'builder.appName' | translate }} {{ 'builder.appName' | translate }}
</a> </a>
<a class="project-editor-layout__dashboard-link" [routerLink]="dashboardRoute()"> <a class="project-editor-layout__dashboard-link" [routerLink]="dashboardRoute()">
<span class="pi pi-home" aria-hidden="true"></span> <app-icon name="home" [size]="16" />
{{ 'builder.backToDashboard' | translate }} {{ 'builder.backToDashboard' | translate }}
</a> </a>
<app-project-editor-nav (linkClicked)="closeMobileDrawer()" /> <app-project-editor-nav (linkClicked)="closeMobileDrawer()" />
@@ -24,7 +24,7 @@
[attr.aria-label]="(mobileDrawerOpen() ? 'builder.closeSidebar' : 'builder.openSidebar') | translate" [attr.aria-label]="(mobileDrawerOpen() ? 'builder.closeSidebar' : 'builder.openSidebar') | translate"
(click)="toggleMobileDrawer()" (click)="toggleMobileDrawer()"
> >
<span class="pi pi-bars" aria-hidden="true"></span> <app-icon name="menu" [size]="20" />
</button> </button>
<div class="project-editor-layout__heading"> <div class="project-editor-layout__heading">
@@ -54,7 +54,7 @@
</div> </div>
<button type="button" class="project-editor-layout__help-toggle" [attr.aria-expanded]="helpOpen()" (click)="toggleHelp()"> <button type="button" class="project-editor-layout__help-toggle" [attr.aria-expanded]="helpOpen()" (click)="toggleHelp()">
<span class="pi pi-question-circle" aria-hidden="true"></span> <app-icon name="help" [size]="16" />
{{ (helpOpen() ? 'builder.helpToggleHide' : 'builder.helpToggleShow') | translate }} {{ (helpOpen() ? 'builder.helpToggleHide' : 'builder.helpToggleShow') | translate }}
</button> </button>
</header> </header>

View File

@@ -18,6 +18,7 @@ import { ProjectEditorPreviewSectionComponent } from '../sections/preview-sectio
import { TranslatePipe } from '../../../i18n/translate.pipe'; import { TranslatePipe } from '../../../i18n/translate.pipe';
import { TranslateService } from '../../../i18n/translate.service'; import { TranslateService } from '../../../i18n/translate.service';
import { LanguageService } from '../../../services/language.service'; import { LanguageService } from '../../../services/language.service';
import { IconComponent } from '../../../shared/ui/icon/icon.component';
import { StaticPagesEditorComponent } from '../../content-management/components/static-pages-editor.component'; import { StaticPagesEditorComponent } from '../../content-management/components/static-pages-editor.component';
import { ProjectEditorSaveBarComponent } from '../components/save-bar/project-editor-save-bar.component'; import { ProjectEditorSaveBarComponent } from '../components/save-bar/project-editor-save-bar.component';
import { EDITOR_SECTION_BOOTSTRAP_KEYS, ProjectEditorSectionId } from '../models/project-editor.model'; import { EDITOR_SECTION_BOOTSTRAP_KEYS, ProjectEditorSectionId } from '../models/project-editor.model';
@@ -62,6 +63,7 @@ const SECTION_LABEL_KEYS: Record<ProjectEditorSectionId, string> = {
ProjectEditorNavigationSectionComponent, ProjectEditorNavigationSectionComponent,
ProjectEditorPreviewSectionComponent, ProjectEditorPreviewSectionComponent,
ProjectEditorSaveBarComponent, ProjectEditorSaveBarComponent,
IconComponent,
], ],
templateUrl: './project-editor-page.component.html', templateUrl: './project-editor-page.component.html',
styleUrls: ['./project-editor-page.component.scss'], styleUrls: ['./project-editor-page.component.scss'],

View File

@@ -9,12 +9,12 @@
<p>{{ 'builder.brandOverviewSubtitle' | translate }}</p> <p>{{ 'builder.brandOverviewSubtitle' | translate }}</p>
@if (nextStepKey(); as key) { @if (nextStepKey(); as key) {
<p class="brand-overview__next-step"> <p class="brand-overview__next-step">
<span class="pi pi-arrow-right-circle" aria-hidden="true"></span> <app-icon name="arrowRight" [size]="16" />
{{ 'builder.overviewNextStepLabel' | translate }}: {{ key | translate }} {{ 'builder.overviewNextStepLabel' | translate }}: {{ key | translate }}
</p> </p>
} @else { } @else {
<p class="brand-overview__next-step brand-overview__next-step--done"> <p class="brand-overview__next-step brand-overview__next-step--done">
<span class="pi pi-check-circle" aria-hidden="true"></span> <app-icon name="checkCircle" [size]="16" />
{{ 'builder.brandOverviewComplete' | translate }} {{ 'builder.brandOverviewComplete' | translate }}
</p> </p>
} }
@@ -27,7 +27,7 @@
<ul class="brand-overview__checklist"> <ul class="brand-overview__checklist">
@for (item of healthItems(); track item.labelKey) { @for (item of healthItems(); track item.labelKey) {
<li [class.brand-overview__check--ok]="item.ok"> <li [class.brand-overview__check--ok]="item.ok">
<span class="pi" [class.pi-check-circle]="item.ok" [class.pi-circle]="!item.ok" aria-hidden="true"></span> <app-icon [name]="item.ok ? 'checkCircle' : 'circle'" [size]="16" />
{{ item.labelKey | translate }} {{ item.labelKey | translate }}
</li> </li>
} }
@@ -46,7 +46,7 @@
</div> </div>
@if (!textContrastPasses()) { @if (!textContrastPasses()) {
<p class="brand-overview__contrast-warning" role="status"> <p class="brand-overview__contrast-warning" role="status">
<span class="pi pi-exclamation-triangle" aria-hidden="true"></span> <app-icon name="warning" [size]="16" />
{{ 'builder.brandContrastWarning' | translate }} {{ 'builder.brandContrastWarning' | translate }}
</p> </p>
} }

View File

@@ -3,6 +3,7 @@ import { TranslatePipe } from '../../../../i18n/translate.pipe';
import { LanguageService } from '../../../../services/language.service'; import { LanguageService } from '../../../../services/language.service';
import { ProjectEditorFacade } from '../../facade/project-editor.facade'; import { ProjectEditorFacade } from '../../facade/project-editor.facade';
import { contrastRatio } from './contrast.util'; import { contrastRatio } from './contrast.util';
import { IconComponent } from '../../../../shared/ui/icon/icon.component';
interface BrandHealthItem { interface BrandHealthItem {
labelKey: string; labelKey: string;
@@ -12,7 +13,7 @@ interface BrandHealthItem {
@Component({ @Component({
selector: 'app-brand-overview', selector: 'app-brand-overview',
standalone: true, standalone: true,
imports: [TranslatePipe], imports: [TranslatePipe, IconComponent],
templateUrl: './brand-overview.component.html', templateUrl: './brand-overview.component.html',
styleUrl: './brand-overview.component.scss', styleUrl: './brand-overview.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,

View File

@@ -89,7 +89,7 @@
@for (column of columns(); track column.id) { @for (column of columns(); track column.id) {
<div class="footer-column" cdkDrag> <div class="footer-column" cdkDrag>
<div class="footer-column__head" cdkDragHandle> <div class="footer-column__head" cdkDragHandle>
<span class="pi pi-bars footer-column__grip" aria-hidden="true"></span> <app-icon name="grip" [size]="16" class="footer-column__grip" />
<app-input [ngModel]="column.title" (ngModelChange)="updateColumnTitle(column.id, $event)" [placeholder]="'builder.footerColumnTitlePlaceholder' | translate" /> <app-input [ngModel]="column.title" (ngModelChange)="updateColumnTitle(column.id, $event)" [placeholder]="'builder.footerColumnTitlePlaceholder' | translate" />
<button type="button" class="footer-column__remove" (click)="removeColumn(column.id)" [attr.aria-label]="'builder.removeColumn' | translate">&times;</button> <button type="button" class="footer-column__remove" (click)="removeColumn(column.id)" [attr.aria-label]="'builder.removeColumn' | translate">&times;</button>
</div> </div>
@@ -97,7 +97,7 @@
<div class="footer-column__links" cdkDropList [cdkDropListData]="column.links" (cdkDropListDropped)="dropLink(column.id, $event)"> <div class="footer-column__links" cdkDropList [cdkDropListData]="column.links" (cdkDropListDropped)="dropLink(column.id, $event)">
@for (link of column.links; track link.id) { @for (link of column.links; track link.id) {
<div class="footer-link" cdkDrag> <div class="footer-link" cdkDrag>
<span class="pi pi-bars footer-link__grip" cdkDragHandle aria-hidden="true"></span> <app-icon name="grip" [size]="14" cdkDragHandle class="footer-link__grip" />
<app-input [ngModel]="link.label" (ngModelChange)="updateLink(column.id, link.id, { label: $event })" [placeholder]="'builder.footerLinkLabelPlaceholder' | translate" /> <app-input [ngModel]="link.label" (ngModelChange)="updateLink(column.id, link.id, { label: $event })" [placeholder]="'builder.footerLinkLabelPlaceholder' | translate" />
<select class="footer-link__source" [ngModel]="linkSourceMode(link)" (ngModelChange)="setLinkSourceMode(column.id, link.id, $event)"> <select class="footer-link__source" [ngModel]="linkSourceMode(link)" (ngModelChange)="setLinkSourceMode(column.id, link.id, $event)">

View File

@@ -10,6 +10,7 @@ import { FormFieldComponent } from '../../../shared/ui/form-field/form-field.com
import { SectionCardComponent } from '../../../shared/ui/section-card/section-card.component'; import { SectionCardComponent } from '../../../shared/ui/section-card/section-card.component';
import { KeyValueEditorComponent } from '../../../shared/ui/key-value-editor/key-value-editor.component'; import { KeyValueEditorComponent } from '../../../shared/ui/key-value-editor/key-value-editor.component';
import { ImageFieldComponent } from '../../../shared/ui/image-field/image-field.component'; import { ImageFieldComponent } from '../../../shared/ui/image-field/image-field.component';
import { IconComponent } from '../../../shared/ui/icon/icon.component';
import { FooterColumnConfig, FooterLinkConfig, FooterPaymentIconConfig, FooterSocialLinkConfig } from '../../../shared/models/config'; import { FooterColumnConfig, FooterLinkConfig, FooterPaymentIconConfig, FooterSocialLinkConfig } from '../../../shared/models/config';
import { isValidHttpUrl } from '../schema/validators/primitives'; import { isValidHttpUrl } from '../schema/validators/primitives';
import { ContentPageService } from '../../content-management/services/content-page.service'; import { ContentPageService } from '../../content-management/services/content-page.service';
@@ -26,7 +27,8 @@ import { ContentPageService } from '../../content-management/services/content-pa
FormFieldComponent, FormFieldComponent,
SectionCardComponent, SectionCardComponent,
KeyValueEditorComponent, KeyValueEditorComponent,
ImageFieldComponent ImageFieldComponent,
IconComponent
], ],
templateUrl: './footer-section.component.html', templateUrl: './footer-section.component.html',
styleUrls: ['./section.shared.scss', './footer-section.component.scss'], styleUrls: ['./section.shared.scss', './footer-section.component.scss'],

View File

@@ -10,11 +10,11 @@
@for (section of sections(); track section.id) { @for (section of sections(); track section.id) {
<div class="block-card" cdkDrag [class.block-card--hidden]="section.visible === false"> <div class="block-card" cdkDrag [class.block-card--hidden]="section.visible === false">
<div class="block-card__handle" cdkDragHandle [attr.aria-label]="'builder.dragToReorder' | translate"> <div class="block-card__handle" cdkDragHandle [attr.aria-label]="'builder.dragToReorder' | translate">
<span class="pi pi-bars" aria-hidden="true"></span> <app-icon name="grip" [size]="16" />
</div> </div>
<div class="block-card__preview"> <div class="block-card__preview">
<span class="pi {{ blockIcon(section.type) }}" aria-hidden="true"></span> <app-icon [name]="blockIcon(section.type)" [size]="20" />
</div> </div>
<div class="block-card__body"> <div class="block-card__body">
@@ -50,10 +50,10 @@
<span>{{ 'builder.visible' | translate }}</span> <span>{{ 'builder.visible' | translate }}</span>
</label> </label>
<button type="button" [attr.aria-label]="'builder.widgetDuplicate' | translate" (click)="duplicateBlock(section.id)"> <button type="button" [attr.aria-label]="'builder.widgetDuplicate' | translate" (click)="duplicateBlock(section.id)">
<span class="pi pi-copy" aria-hidden="true"></span> <app-icon name="copy" [size]="16" />
</button> </button>
<button type="button" class="block-card__remove" [attr.aria-label]="'builder.widgetRemove' | translate" (click)="removeBlock(section.id)"> <button type="button" class="block-card__remove" [attr.aria-label]="'builder.widgetRemove' | translate" (click)="removeBlock(section.id)">
<span class="pi pi-trash" aria-hidden="true"></span> <app-icon name="trash" [size]="16" />
</button> </button>
</div> </div>
</div> </div>
@@ -65,7 +65,7 @@
<div class="block-catalog__grid"> <div class="block-catalog__grid">
@for (entry of blockCatalog; track entry.type) { @for (entry of blockCatalog; track entry.type) {
<button type="button" class="block-catalog__item" (click)="addBlock(entry.type)"> <button type="button" class="block-catalog__item" (click)="addBlock(entry.type)">
<span class="pi {{ entry.icon }}" aria-hidden="true"></span> <app-icon [name]="entry.icon" [size]="18" />
<span>{{ entry.labelKey | translate }}</span> <span>{{ entry.labelKey | translate }}</span>
</button> </button>
} }

View File

@@ -9,6 +9,8 @@ import { SectionCardComponent } from '../../../shared/ui/section-card/section-ca
import { ToggleComponent } from '../../../shared/ui/toggle/toggle.component'; import { ToggleComponent } from '../../../shared/ui/toggle/toggle.component';
import { HomepageOverviewComponent } from './homepage/homepage-overview.component'; import { HomepageOverviewComponent } from './homepage/homepage-overview.component';
import { SectionConfig } from '../../../shared/models/config'; import { SectionConfig } from '../../../shared/models/config';
import { IconComponent } from '../../../shared/ui/icon/icon.component';
import { AppIconName } from '../../../shared/ui/icon/icon-registry';
export interface LayoutStrategyOption { export interface LayoutStrategyOption {
value: 'stack' | 'grid' | 'hero' | 'carousel' | 'split'; value: 'stack' | 'grid' | 'hero' | 'carousel' | 'split';
@@ -19,21 +21,21 @@ export interface LayoutStrategyOption {
/** Merchant-facing catalog of the block types an admin can add to the homepage. Not the widget manifest (that carries technical settingsSchema) - this is presentation-only, matching the pattern already used in widgets-section.component.ts. */ /** Merchant-facing catalog of the block types an admin can add to the homepage. Not the widget manifest (that carries technical settingsSchema) - this is presentation-only, matching the pattern already used in widgets-section.component.ts. */
interface BlockCatalogEntry { interface BlockCatalogEntry {
type: string; type: string;
icon: string; icon: AppIconName;
labelKey: string; labelKey: string;
descKey: string; descKey: string;
defaultLayout: LayoutStrategyOption['value']; defaultLayout: LayoutStrategyOption['value'];
} }
const BLOCK_CATALOG: BlockCatalogEntry[] = [ const BLOCK_CATALOG: BlockCatalogEntry[] = [
{ type: 'hero', icon: 'pi-image', labelKey: 'builder.blockHero', descKey: 'builder.blockHeroDesc', defaultLayout: 'hero' }, { type: 'hero', icon: 'image', labelKey: 'builder.blockHero', descKey: 'builder.blockHeroDesc', defaultLayout: 'hero' },
{ type: 'categories', icon: 'pi-th-large', labelKey: 'builder.blockCategories', descKey: 'builder.blockCategoriesDesc', defaultLayout: 'grid' }, { type: 'categories', icon: 'layoutGrid', labelKey: 'builder.blockCategories', descKey: 'builder.blockCategoriesDesc', defaultLayout: 'grid' },
{ type: 'product-collection', icon: 'pi-box', labelKey: 'builder.blockFeaturedProducts', descKey: 'builder.blockFeaturedProductsDesc', defaultLayout: 'grid' }, { type: 'product-collection', icon: 'package', labelKey: 'builder.blockFeaturedProducts', descKey: 'builder.blockFeaturedProductsDesc', defaultLayout: 'grid' },
{ type: 'product-carousel', icon: 'pi-images', labelKey: 'builder.blockProductCarousel', descKey: 'builder.blockProductCarouselDesc', defaultLayout: 'carousel' }, { type: 'product-carousel', icon: 'images', labelKey: 'builder.blockProductCarousel', descKey: 'builder.blockProductCarouselDesc', defaultLayout: 'carousel' },
{ type: 'recently-viewed', icon: 'pi-history', labelKey: 'builder.blockRecentlyViewed', descKey: 'builder.blockRecentlyViewedDesc', defaultLayout: 'grid' }, { type: 'recently-viewed', icon: 'history', labelKey: 'builder.blockRecentlyViewed', descKey: 'builder.blockRecentlyViewedDesc', defaultLayout: 'grid' },
{ type: 'banner', icon: 'pi-megaphone', labelKey: 'builder.blockBanner', descKey: 'builder.blockBannerDesc', defaultLayout: 'split' }, { type: 'banner', icon: 'megaphone', labelKey: 'builder.blockBanner', descKey: 'builder.blockBannerDesc', defaultLayout: 'split' },
{ type: 'partners', icon: 'pi-verified', labelKey: 'builder.blockPartners', descKey: 'builder.blockPartnersDesc', defaultLayout: 'stack' }, { type: 'partners', icon: 'verified', labelKey: 'builder.blockPartners', descKey: 'builder.blockPartnersDesc', defaultLayout: 'stack' },
{ type: 'html', icon: 'pi-code', labelKey: 'builder.blockCustomHtml', descKey: 'builder.blockCustomHtmlDesc', defaultLayout: 'stack' }, { type: 'html', icon: 'code', labelKey: 'builder.blockCustomHtml', descKey: 'builder.blockCustomHtmlDesc', defaultLayout: 'stack' },
]; ];
const BLOCK_BY_TYPE = new Map(BLOCK_CATALOG.map(entry => [entry.type, entry])); const BLOCK_BY_TYPE = new Map(BLOCK_CATALOG.map(entry => [entry.type, entry]));
@@ -41,7 +43,7 @@ const BLOCK_BY_TYPE = new Map(BLOCK_CATALOG.map(entry => [entry.type, entry]));
@Component({ @Component({
selector: 'app-project-editor-homepage-section', selector: 'app-project-editor-homepage-section',
standalone: true, standalone: true,
imports: [DragDropModule, FormsModule, TranslatePipe, InputComponent, SectionCardComponent, ToggleComponent, HomepageOverviewComponent], imports: [DragDropModule, FormsModule, TranslatePipe, InputComponent, SectionCardComponent, ToggleComponent, HomepageOverviewComponent, IconComponent],
templateUrl: './homepage-section.component.html', templateUrl: './homepage-section.component.html',
styleUrls: ['./section.shared.scss', './homepage-section.component.scss'], styleUrls: ['./section.shared.scss', './homepage-section.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
@@ -80,8 +82,8 @@ export class ProjectEditorHomepageSectionComponent {
return entry ? this.translate.t(entry.descKey) : ''; return entry ? this.translate.t(entry.descKey) : '';
} }
blockIcon(type: string): string { blockIcon(type: string): AppIconName {
return BLOCK_BY_TYPE.get(type)?.icon ?? 'pi-stop'; return BLOCK_BY_TYPE.get(type)?.icon ?? 'stop';
} }
drop(event: CdkDragDrop<unknown[]>): void { drop(event: CdkDragDrop<unknown[]>): void {

View File

@@ -14,12 +14,12 @@
</p> </p>
@if (nextStepKey(); as key) { @if (nextStepKey(); as key) {
<p class="homepage-overview__next-step"> <p class="homepage-overview__next-step">
<span class="pi pi-arrow-right-circle" aria-hidden="true"></span> <app-icon name="arrowRight" [size]="16" />
{{ 'builder.overviewNextStepLabel' | translate }}: {{ key | translate }} {{ 'builder.overviewNextStepLabel' | translate }}: {{ key | translate }}
</p> </p>
} @else { } @else {
<p class="homepage-overview__next-step homepage-overview__next-step--done"> <p class="homepage-overview__next-step homepage-overview__next-step--done">
<span class="pi pi-check-circle" aria-hidden="true"></span> <app-icon name="checkCircle" [size]="16" />
{{ 'builder.homepageOverviewComplete' | translate }} {{ 'builder.homepageOverviewComplete' | translate }}
</p> </p>
} }
@@ -32,7 +32,7 @@
<ul class="homepage-overview__checklist"> <ul class="homepage-overview__checklist">
@for (item of healthItems(); track item.labelKey) { @for (item of healthItems(); track item.labelKey) {
<li [class.homepage-overview__check--ok]="item.ok"> <li [class.homepage-overview__check--ok]="item.ok">
<span class="pi" [class.pi-check-circle]="item.ok" [class.pi-circle]="!item.ok" aria-hidden="true"></span> <app-icon [name]="item.ok ? 'checkCircle' : 'circle'" [size]="16" />
{{ item.labelKey | translate }} {{ item.labelKey | translate }}
</li> </li>
} }

View File

@@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/c
import { TranslatePipe } from '../../../../i18n/translate.pipe'; import { TranslatePipe } from '../../../../i18n/translate.pipe';
import { LanguageService } from '../../../../services/language.service'; import { LanguageService } from '../../../../services/language.service';
import { ProjectEditorFacade } from '../../facade/project-editor.facade'; import { ProjectEditorFacade } from '../../facade/project-editor.facade';
import { IconComponent } from '../../../../shared/ui/icon/icon.component';
interface HomepageHealthItem { interface HomepageHealthItem {
labelKey: string; labelKey: string;
@@ -11,7 +12,7 @@ interface HomepageHealthItem {
@Component({ @Component({
selector: 'app-homepage-overview', selector: 'app-homepage-overview',
standalone: true, standalone: true,
imports: [TranslatePipe], imports: [TranslatePipe, IconComponent],
templateUrl: './homepage-overview.component.html', templateUrl: './homepage-overview.component.html',
styleUrl: './homepage-overview.component.scss', styleUrl: './homepage-overview.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,

View File

@@ -7,27 +7,27 @@
@for (entry of widgets(); track entry.widget.id; let first = $first; let last = $last) { @for (entry of widgets(); track entry.widget.id; let first = $first; let last = $last) {
<article class="sub-card widget-card" [class.widget-card--hidden]="!isVisible(entry.widget)"> <article class="sub-card widget-card" [class.widget-card--hidden]="!isVisible(entry.widget)">
<header class="widget-card__header"> <header class="widget-card__header">
<span class="pi {{ widgetIcon(entry.widget.type) }} widget-card__icon" aria-hidden="true"></span> <app-icon [name]="widgetIcon(entry.widget.type)" [size]="18" class="widget-card__icon" />
<h3 class="widget-card__title">{{ widgetLabel(entry.widget) }}</h3> <h3 class="widget-card__title">{{ widgetLabel(entry.widget) }}</h3>
@if (!isVisible(entry.widget)) { @if (!isVisible(entry.widget)) {
<span class="widget-card__hidden-badge">{{ 'builder.widgetHidden' | translate }}</span> <span class="widget-card__hidden-badge">{{ 'builder.widgetHidden' | translate }}</span>
} }
<div class="widget-card__actions"> <div class="widget-card__actions">
<button type="button" [attr.aria-label]="'builder.widgetMoveUp' | translate" [disabled]="first" (click)="moveWidget(entry.widget.id, -1)"> <button type="button" [attr.aria-label]="'builder.widgetMoveUp' | translate" [disabled]="first" (click)="moveWidget(entry.widget.id, -1)">
<span class="pi pi-arrow-up" aria-hidden="true"></span> <app-icon name="arrowUp" [size]="16" />
</button> </button>
<button type="button" [attr.aria-label]="'builder.widgetMoveDown' | translate" [disabled]="last" (click)="moveWidget(entry.widget.id, 1)"> <button type="button" [attr.aria-label]="'builder.widgetMoveDown' | translate" [disabled]="last" (click)="moveWidget(entry.widget.id, 1)">
<span class="pi pi-arrow-down" aria-hidden="true"></span> <app-icon name="arrowDown" [size]="16" />
</button> </button>
<label class="widget-card__visible-toggle"> <label class="widget-card__visible-toggle">
<app-toggle [ngModel]="isVisible(entry.widget)" (ngModelChange)="toggleVisible(entry.widget.id)" [ariaLabel]="'builder.widgetVisible' | translate" /> <app-toggle [ngModel]="isVisible(entry.widget)" (ngModelChange)="toggleVisible(entry.widget.id)" [ariaLabel]="'builder.widgetVisible' | translate" />
<span>{{ 'builder.widgetVisible' | translate }}</span> <span>{{ 'builder.widgetVisible' | translate }}</span>
</label> </label>
<button type="button" [attr.aria-label]="'builder.widgetDuplicate' | translate" (click)="duplicateWidget(entry.widget.id)"> <button type="button" [attr.aria-label]="'builder.widgetDuplicate' | translate" (click)="duplicateWidget(entry.widget.id)">
<span class="pi pi-copy" aria-hidden="true"></span> <app-icon name="copy" [size]="16" />
</button> </button>
<button type="button" class="widget-card__remove" [attr.aria-label]="'builder.widgetRemove' | translate" (click)="removeWidget(entry.widget.id)"> <button type="button" class="widget-card__remove" [attr.aria-label]="'builder.widgetRemove' | translate" (click)="removeWidget(entry.widget.id)">
<span class="pi pi-trash" aria-hidden="true"></span> <app-icon name="trash" [size]="16" />
</button> </button>
</div> </div>
</header> </header>

View File

@@ -8,16 +8,18 @@ import { ButtonComponent } from '../../../shared/ui/button/button.component';
import { SectionCardComponent } from '../../../shared/ui/section-card/section-card.component'; import { SectionCardComponent } from '../../../shared/ui/section-card/section-card.component';
import { ToggleComponent } from '../../../shared/ui/toggle/toggle.component'; import { ToggleComponent } from '../../../shared/ui/toggle/toggle.component';
import { WidgetConfig } from '../../../shared/models/config'; import { WidgetConfig } from '../../../shared/models/config';
import { IconComponent } from '../../../shared/ui/icon/icon.component';
import { AppIconName } from '../../../shared/ui/icon/icon-registry';
interface HeroSlideDraft { interface HeroSlideDraft {
title: string; title: string;
subtitle: string; subtitle: string;
} }
const WIDGET_ICONS: Record<string, string> = { const WIDGET_ICONS: Record<string, AppIconName> = {
hero: 'pi-image', hero: 'image',
categories: 'pi-th-large', categories: 'layoutGrid',
'product-collection': 'pi-box', 'product-collection': 'package',
}; };
const WIDGET_LABEL_KEYS: Record<string, string> = { const WIDGET_LABEL_KEYS: Record<string, string> = {
@@ -29,7 +31,7 @@ const WIDGET_LABEL_KEYS: Record<string, string> = {
@Component({ @Component({
selector: 'app-project-editor-widgets-section', selector: 'app-project-editor-widgets-section',
standalone: true, standalone: true,
imports: [FormsModule, TranslatePipe, InputComponent, ButtonComponent, SectionCardComponent, ToggleComponent], imports: [FormsModule, TranslatePipe, InputComponent, ButtonComponent, SectionCardComponent, ToggleComponent, IconComponent],
templateUrl: './widgets-section.component.html', templateUrl: './widgets-section.component.html',
styleUrls: ['./section.shared.scss', './widgets-section.component.scss'], styleUrls: ['./section.shared.scss', './widgets-section.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
@@ -43,8 +45,8 @@ export class ProjectEditorWidgetsSectionComponent {
return messageKey ? this.translate.t(messageKey) : null; return messageKey ? this.translate.t(messageKey) : null;
}; };
widgetIcon(type: string): string { widgetIcon(type: string): AppIconName {
return WIDGET_ICONS[type] ?? 'pi-stop'; return WIDGET_ICONS[type] ?? 'stop';
} }
widgetLabel(widget: WidgetConfig): string { widgetLabel(widget: WidgetConfig): string {

View File

@@ -29,6 +29,7 @@ import {
LucideFlag as Flag, LucideFlag as Flag,
LucideFolder as Folder, LucideFolder as Folder,
LucideGlobe as Globe, LucideGlobe as Globe,
LucideGripVertical as GripVertical,
LucideHeart as Heart, LucideHeart as Heart,
LucideHelpCircle as HelpCircle, LucideHelpCircle as HelpCircle,
LucideHistory as History, LucideHistory as History,
@@ -144,6 +145,7 @@ export const APP_ICONS = {
creditCard: CreditCard, creditCard: CreditCard,
flag: Flag, flag: Flag,
globe: Globe, globe: Globe,
grip: GripVertical,
history: History, history: History,
link: Link, link: Link,
list: List, list: List,

View File

@@ -140,6 +140,15 @@ a, button, input, textarea, select {
outline-offset: 2px; outline-offset: 2px;
} }
/* Spinner rotation for app-icon name="spinner" used as a loading indicator */
@keyframes app-icon-spin {
to { transform: rotate(360deg); }
}
.spin {
animation: app-icon-spin 1s linear infinite;
}
/* Respect OS-level reduced-motion preference globally: neutralizes hover /* Respect OS-level reduced-motion preference globally: neutralizes hover
transforms, card-entrance/skeleton-shimmer animations, and smooth-scroll transforms, card-entrance/skeleton-shimmer animations, and smooth-scroll
everywhere in one place rather than requiring every component to opt in everywhere in one place rather than requiring every component to opt in