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

@@ -9,12 +9,12 @@
<p>{{ 'builder.brandOverviewSubtitle' | translate }}</p>
@if (nextStepKey(); as key) {
<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 }}
</p>
} @else {
<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 }}
</p>
}
@@ -27,7 +27,7 @@
<ul class="brand-overview__checklist">
@for (item of healthItems(); track item.labelKey) {
<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 }}
</li>
}
@@ -46,7 +46,7 @@
</div>
@if (!textContrastPasses()) {
<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 }}
</p>
}

View File

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

View File

@@ -89,7 +89,7 @@
@for (column of columns(); track column.id) {
<div class="footer-column" cdkDrag>
<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" />
<button type="button" class="footer-column__remove" (click)="removeColumn(column.id)" [attr.aria-label]="'builder.removeColumn' | translate">&times;</button>
</div>
@@ -97,7 +97,7 @@
<div class="footer-column__links" cdkDropList [cdkDropListData]="column.links" (cdkDropListDropped)="dropLink(column.id, $event)">
@for (link of column.links; track link.id) {
<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" />
<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 { KeyValueEditorComponent } from '../../../shared/ui/key-value-editor/key-value-editor.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 { isValidHttpUrl } from '../schema/validators/primitives';
import { ContentPageService } from '../../content-management/services/content-page.service';
@@ -26,7 +27,8 @@ import { ContentPageService } from '../../content-management/services/content-pa
FormFieldComponent,
SectionCardComponent,
KeyValueEditorComponent,
ImageFieldComponent
ImageFieldComponent,
IconComponent
],
templateUrl: './footer-section.component.html',
styleUrls: ['./section.shared.scss', './footer-section.component.scss'],

View File

@@ -10,11 +10,11 @@
@for (section of sections(); track section.id) {
<div class="block-card" cdkDrag [class.block-card--hidden]="section.visible === false">
<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 class="block-card__preview">
<span class="pi {{ blockIcon(section.type) }}" aria-hidden="true"></span>
<app-icon [name]="blockIcon(section.type)" [size]="20" />
</div>
<div class="block-card__body">
@@ -50,10 +50,10 @@
<span>{{ 'builder.visible' | translate }}</span>
</label>
<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 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>
</div>
</div>
@@ -65,7 +65,7 @@
<div class="block-catalog__grid">
@for (entry of blockCatalog; track 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>
</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 { HomepageOverviewComponent } from './homepage/homepage-overview.component';
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 {
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. */
interface BlockCatalogEntry {
type: string;
icon: string;
icon: AppIconName;
labelKey: string;
descKey: string;
defaultLayout: LayoutStrategyOption['value'];
}
const BLOCK_CATALOG: BlockCatalogEntry[] = [
{ type: 'hero', icon: 'pi-image', labelKey: 'builder.blockHero', descKey: 'builder.blockHeroDesc', defaultLayout: 'hero' },
{ type: 'categories', icon: 'pi-th-large', labelKey: 'builder.blockCategories', descKey: 'builder.blockCategoriesDesc', defaultLayout: 'grid' },
{ type: 'product-collection', icon: 'pi-box', labelKey: 'builder.blockFeaturedProducts', descKey: 'builder.blockFeaturedProductsDesc', defaultLayout: 'grid' },
{ type: 'product-carousel', icon: 'pi-images', labelKey: 'builder.blockProductCarousel', descKey: 'builder.blockProductCarouselDesc', defaultLayout: 'carousel' },
{ type: 'recently-viewed', icon: 'pi-history', labelKey: 'builder.blockRecentlyViewed', descKey: 'builder.blockRecentlyViewedDesc', defaultLayout: 'grid' },
{ type: 'banner', icon: 'pi-megaphone', labelKey: 'builder.blockBanner', descKey: 'builder.blockBannerDesc', defaultLayout: 'split' },
{ type: 'partners', icon: 'pi-verified', labelKey: 'builder.blockPartners', descKey: 'builder.blockPartnersDesc', defaultLayout: 'stack' },
{ type: 'html', icon: 'pi-code', labelKey: 'builder.blockCustomHtml', descKey: 'builder.blockCustomHtmlDesc', defaultLayout: 'stack' },
{ type: 'hero', icon: 'image', labelKey: 'builder.blockHero', descKey: 'builder.blockHeroDesc', defaultLayout: 'hero' },
{ type: 'categories', icon: 'layoutGrid', labelKey: 'builder.blockCategories', descKey: 'builder.blockCategoriesDesc', defaultLayout: 'grid' },
{ type: 'product-collection', icon: 'package', labelKey: 'builder.blockFeaturedProducts', descKey: 'builder.blockFeaturedProductsDesc', defaultLayout: 'grid' },
{ type: 'product-carousel', icon: 'images', labelKey: 'builder.blockProductCarousel', descKey: 'builder.blockProductCarouselDesc', defaultLayout: 'carousel' },
{ type: 'recently-viewed', icon: 'history', labelKey: 'builder.blockRecentlyViewed', descKey: 'builder.blockRecentlyViewedDesc', defaultLayout: 'grid' },
{ type: 'banner', icon: 'megaphone', labelKey: 'builder.blockBanner', descKey: 'builder.blockBannerDesc', defaultLayout: 'split' },
{ type: 'partners', icon: 'verified', labelKey: 'builder.blockPartners', descKey: 'builder.blockPartnersDesc', 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]));
@@ -41,7 +43,7 @@ const BLOCK_BY_TYPE = new Map(BLOCK_CATALOG.map(entry => [entry.type, entry]));
@Component({
selector: 'app-project-editor-homepage-section',
standalone: true,
imports: [DragDropModule, FormsModule, TranslatePipe, InputComponent, SectionCardComponent, ToggleComponent, HomepageOverviewComponent],
imports: [DragDropModule, FormsModule, TranslatePipe, InputComponent, SectionCardComponent, ToggleComponent, HomepageOverviewComponent, IconComponent],
templateUrl: './homepage-section.component.html',
styleUrls: ['./section.shared.scss', './homepage-section.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
@@ -80,8 +82,8 @@ export class ProjectEditorHomepageSectionComponent {
return entry ? this.translate.t(entry.descKey) : '';
}
blockIcon(type: string): string {
return BLOCK_BY_TYPE.get(type)?.icon ?? 'pi-stop';
blockIcon(type: string): AppIconName {
return BLOCK_BY_TYPE.get(type)?.icon ?? 'stop';
}
drop(event: CdkDragDrop<unknown[]>): void {

View File

@@ -14,12 +14,12 @@
</p>
@if (nextStepKey(); as key) {
<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 }}
</p>
} @else {
<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 }}
</p>
}
@@ -32,7 +32,7 @@
<ul class="homepage-overview__checklist">
@for (item of healthItems(); track item.labelKey) {
<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 }}
</li>
}

View File

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

View File

@@ -7,27 +7,27 @@
@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)">
<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>
@if (!isVisible(entry.widget)) {
<span class="widget-card__hidden-badge">{{ 'builder.widgetHidden' | translate }}</span>
}
<div class="widget-card__actions">
<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 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>
<label class="widget-card__visible-toggle">
<app-toggle [ngModel]="isVisible(entry.widget)" (ngModelChange)="toggleVisible(entry.widget.id)" [ariaLabel]="'builder.widgetVisible' | translate" />
<span>{{ 'builder.widgetVisible' | translate }}</span>
</label>
<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 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>
</div>
</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 { ToggleComponent } from '../../../shared/ui/toggle/toggle.component';
import { WidgetConfig } from '../../../shared/models/config';
import { IconComponent } from '../../../shared/ui/icon/icon.component';
import { AppIconName } from '../../../shared/ui/icon/icon-registry';
interface HeroSlideDraft {
title: string;
subtitle: string;
}
const WIDGET_ICONS: Record<string, string> = {
hero: 'pi-image',
categories: 'pi-th-large',
'product-collection': 'pi-box',
const WIDGET_ICONS: Record<string, AppIconName> = {
hero: 'image',
categories: 'layoutGrid',
'product-collection': 'package',
};
const WIDGET_LABEL_KEYS: Record<string, string> = {
@@ -29,7 +31,7 @@ const WIDGET_LABEL_KEYS: Record<string, string> = {
@Component({
selector: 'app-project-editor-widgets-section',
standalone: true,
imports: [FormsModule, TranslatePipe, InputComponent, ButtonComponent, SectionCardComponent, ToggleComponent],
imports: [FormsModule, TranslatePipe, InputComponent, ButtonComponent, SectionCardComponent, ToggleComponent, IconComponent],
templateUrl: './widgets-section.component.html',
styleUrls: ['./section.shared.scss', './widgets-section.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
@@ -43,8 +45,8 @@ export class ProjectEditorWidgetsSectionComponent {
return messageKey ? this.translate.t(messageKey) : null;
};
widgetIcon(type: string): string {
return WIDGET_ICONS[type] ?? 'pi-stop';
widgetIcon(type: string): AppIconName {
return WIDGET_ICONS[type] ?? 'stop';
}
widgetLabel(widget: WidgetConfig): string {