feat(builder): modern grouped rich-text toolbar with icons and tooltips

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

P0 user feedback: unstyled B/I/U/H2/List/1,2,3 buttons were not understandable for non-technical users.

- Toolbar buttons grouped by intent (text style | headings | lists | insert | advanced) with visual separators, hover states, consistent 30px hit targets
- Icons (PrimeIcons) for list/link/image/table/divider/code/embed; B/I/U keep their conventional letters but rendered in their own style (bold/italic/underline) as every mainstream editor does; every button gets a translated tooltip and aria-label (en/ru/hy)
- Code view toggle moved to the right edge, visually de-emphasized - it is the expert path, not a primary action
- Toolbar visually attaches to the editing surface (shared border, joined radius) so it reads as one control
This commit is contained in:
sdarbinyan
2026-07-19 08:48:22 +04:00
parent afaf79d327
commit 9eacd00136
7 changed files with 153 additions and 18 deletions

View File

@@ -1,9 +1,30 @@
<div class="html-editor"> <div class="html-editor">
<div class="html-editor-toolbar"> <div class="html-editor-toolbar" role="toolbar">
@for (item of toolbar; track item.id) { @for (group of toolbarGroups; track $index) {
<button type="button" (click)="runCommand(item)">{{ item.label }}</button> <div class="html-editor-toolbar__group">
@for (item of group; track item.id) {
<button
type="button"
class="html-editor-toolbar__btn"
[class.html-editor-toolbar__btn--bold]="item.id === 'bold'"
[class.html-editor-toolbar__btn--italic]="item.id === 'italic'"
[class.html-editor-toolbar__btn--underline]="item.id === 'underline'"
[title]="item.titleKey ? (item.titleKey | translate) : item.label"
[attr.aria-label]="item.titleKey ? (item.titleKey | translate) : item.label"
(click)="runCommand(item)"
>
@if (item.icon) {
<span class="pi {{ item.icon }}" aria-hidden="true"></span>
} @else {
{{ item.label }}
}
</button>
}
</div>
} }
<button type="button" (click)="toggleCode()">{{ (showCode() ? 'builder.htmlEditorPreview' : 'builder.htmlEditorCode') | translate }}</button> <div class="html-editor-toolbar__group html-editor-toolbar__group--end">
<button type="button" class="html-editor-toolbar__btn html-editor-toolbar__btn--wide" (click)="toggleCode()">{{ (showCode() ? 'builder.htmlEditorPreview' : 'builder.htmlEditorCode') | translate }}</button>
</div>
</div> </div>
@if (showCode()) { @if (showCode()) {

View File

@@ -7,9 +7,54 @@
.html-editor-toolbar { .html-editor-toolbar {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 0.25rem; align-items: center;
gap: 8px;
padding: 6px;
border: 1px solid var(--border-color, #d3dad9);
border-bottom: none;
border-radius: var(--radius-sm) var(--radius-sm) 0 0;
background: var(--bg-secondary, #f7f8f8);
} }
.html-editor-toolbar__group {
display: flex;
gap: 2px;
& + .html-editor-toolbar__group:not(.html-editor-toolbar__group--end) {
padding-left: 8px;
border-left: 1px solid var(--border-color, #d3dad9);
}
}
.html-editor-toolbar__group--end {
margin-left: auto;
}
.html-editor-toolbar__btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 30px;
height: 30px;
padding: 0 6px;
border: none;
border-radius: var(--radius-sm);
background: transparent;
color: var(--text-primary, #1e3c38);
font-size: 0.85rem;
cursor: pointer;
.pi { font-size: 0.85rem; }
&:hover { background: var(--bg-tertiary, #e8ecea); }
&:focus-visible { outline: 2px solid var(--color-primary, #2f8f5b); outline-offset: 1px; }
}
.html-editor-toolbar__btn--bold { font-weight: 700; }
.html-editor-toolbar__btn--italic { font-style: italic; font-family: serif; }
.html-editor-toolbar__btn--underline { text-decoration: underline; }
.html-editor-toolbar__btn--wide { padding: 0 10px; font-size: 0.8rem; color: var(--text-secondary, #6b7280); }
.html-editor-surface { .html-editor-surface {
min-height: 160px; min-height: 160px;
border: 1px solid var(--border, #ccc); border: 1px solid var(--border, #ccc);

View File

@@ -9,22 +9,35 @@ export interface HtmlEditorToolbarCommand {
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. */
icon?: string;
/** i18n key for the tooltip/aria-label. */
titleKey?: string;
} }
export const HTML_EDITOR_TOOLBAR: HtmlEditorToolbarCommand[] = [ export const HTML_EDITOR_TOOLBAR: HtmlEditorToolbarCommand[] = [
{ id: 'bold', label: 'B', command: 'bold' }, { id: 'bold', label: 'B', command: 'bold', titleKey: 'builder.htmlToolBold' },
{ id: 'italic', label: 'I', command: 'italic' }, { id: 'italic', label: 'I', command: 'italic', titleKey: 'builder.htmlToolItalic' },
{ id: 'underline', label: 'U', command: 'underline' }, { id: 'underline', label: 'U', command: 'underline', titleKey: 'builder.htmlToolUnderline' },
{ id: 'h2', label: 'H2', command: 'formatBlock', value: 'H2' }, { id: 'h2', label: 'H2', command: 'formatBlock', value: 'H2', titleKey: 'builder.htmlToolHeading' },
{ id: 'h3', label: 'H3', command: 'formatBlock', value: 'H3' }, { id: 'h3', label: 'H3', command: 'formatBlock', value: 'H3', titleKey: 'builder.htmlToolSubheading' },
{ id: 'ul', label: 'List', command: 'insertUnorderedList' }, { id: 'ul', label: '', icon: 'pi-list', command: 'insertUnorderedList', titleKey: 'builder.htmlToolBulletList' },
{ id: 'ol', label: '1,2,3', command: 'insertOrderedList' }, { id: 'ol', label: '1.', command: 'insertOrderedList', titleKey: 'builder.htmlToolNumberedList' },
{ id: 'link', label: 'Link', command: 'createLink' }, { id: 'link', label: '', icon: 'pi-link', command: 'createLink', titleKey: 'builder.htmlToolLink' },
{ id: 'image', label: 'Image', command: 'insertImage' }, { id: 'image', label: '', icon: 'pi-image', command: 'insertImage', titleKey: 'builder.htmlToolImage' },
{ id: 'table', label: 'Table', command: 'insertHTML', value: '<table><tr><td>&nbsp;</td><td>&nbsp;</td></tr></table>' }, { id: 'table', label: '', icon: 'pi-table', command: 'insertHTML', value: '<table><tr><td>&nbsp;</td><td>&nbsp;</td></tr></table>', titleKey: 'builder.htmlToolTable' },
{ id: 'hr', label: 'HR', command: 'insertHorizontalRule' }, { id: 'hr', label: '', icon: 'pi-minus', command: 'insertHorizontalRule', titleKey: 'builder.htmlToolDivider' },
{ id: 'codeblock', label: 'Code', command: 'formatBlock', value: 'PRE' }, { id: 'codeblock', label: '', icon: 'pi-code', command: 'formatBlock', value: 'PRE', titleKey: 'builder.htmlToolCodeBlock' },
{ id: 'embed', label: 'Embed', command: 'insertEmbed' }, { id: 'embed', label: '', icon: 'pi-video', command: 'insertEmbed', titleKey: 'builder.htmlToolEmbed' },
];
/** Visual grouping for the toolbar: text style | headings | lists | insert | advanced. */
export const HTML_EDITOR_TOOLBAR_GROUPS: string[][] = [
['bold', 'italic', 'underline'],
['h2', 'h3'],
['ul', 'ol'],
['link', 'image', 'table', 'hr'],
['codeblock', 'embed'],
]; ];
@Component({ @Component({
@@ -43,6 +56,9 @@ export class MarketplaceHtmlEditorComponent implements OnChanges, AfterViewInit
@ViewChild('surface', { static: true }) surface!: ElementRef<HTMLDivElement>; @ViewChild('surface', { static: true }) surface!: ElementRef<HTMLDivElement>;
readonly toolbar = HTML_EDITOR_TOOLBAR; readonly toolbar = HTML_EDITOR_TOOLBAR;
readonly toolbarGroups = HTML_EDITOR_TOOLBAR_GROUPS.map(group =>
group.map(id => HTML_EDITOR_TOOLBAR.find(item => item.id === id)!).filter(Boolean)
);
readonly showCode = signal(false); readonly showCode = signal(false);
readonly codeValue = signal(''); readonly codeValue = signal('');
readonly codeError = signal<string | null>(null); readonly codeError = signal<string | null>(null);

View File

@@ -687,6 +687,19 @@ export const en: Translations = {
linkVisibleDesc: 'Whether this navigation link is shown to shoppers.', linkVisibleDesc: 'Whether this navigation link is shown to shoppers.',
appName: 'Marketplace Builder', appName: 'Marketplace Builder',
backToDashboard: 'Back to dashboard', backToDashboard: 'Back to dashboard',
htmlToolBold: 'Bold',
htmlToolItalic: 'Italic',
htmlToolUnderline: 'Underline',
htmlToolHeading: 'Heading',
htmlToolSubheading: 'Subheading',
htmlToolBulletList: 'Bulleted list',
htmlToolNumberedList: 'Numbered list',
htmlToolLink: 'Insert link',
htmlToolImage: 'Insert image',
htmlToolTable: 'Insert table',
htmlToolDivider: 'Divider line',
htmlToolCodeBlock: 'Code block',
htmlToolEmbed: 'Embed video',
breadcrumbRoot: 'Marketplace Builder', breadcrumbRoot: 'Marketplace Builder',
statusComplete: 'Complete', statusComplete: 'Complete',
statusInProgress: 'In progress', statusInProgress: 'In progress',

View File

@@ -687,6 +687,19 @@ export const hy: Translations = {
linkVisibleDesc: 'Արդյո՞ք այս նավիգացիոն հղումը ցուցադրվում է գնորդներին։', linkVisibleDesc: 'Արդյո՞ք այս նավիգացիոն հղումը ցուցադրվում է գնորդներին։',
appName: 'Մարկետփլեյսի կոնստրուկտոր', appName: 'Մարկետփլեյսի կոնստրուկտոր',
backToDashboard: 'Վերադառնալ կառավարման վահանակ', backToDashboard: 'Վերադառնալ կառավարման վահանակ',
htmlToolBold: 'Թավ',
htmlToolItalic: 'Շեղ',
htmlToolUnderline: 'Ընդգծված',
htmlToolHeading: 'Վերնագիր',
htmlToolSubheading: 'Ենթավերնագիր',
htmlToolBulletList: 'Կետային ցուցակ',
htmlToolNumberedList: 'Համարակալված ցուցակ',
htmlToolLink: 'Տեղադրել հղում',
htmlToolImage: 'Տեղադրել նկար',
htmlToolTable: 'Տեղադրել աղյուսակ',
htmlToolDivider: 'Բաժանարար գիծ',
htmlToolCodeBlock: 'Կոդի բլոկ',
htmlToolEmbed: 'Ներդնել տեսանյութ',
breadcrumbRoot: 'Մարկետփլեյսի կոնստրուկտոր', breadcrumbRoot: 'Մարկետփլեյսի կոնստրուկտոր',
statusComplete: 'Ավարտված', statusComplete: 'Ավարտված',
statusInProgress: 'Ընթացքի մեջ', statusInProgress: 'Ընթացքի մեջ',

View File

@@ -687,6 +687,19 @@ export const ru: Translations = {
linkVisibleDesc: 'Показывать ли эту ссылку навигации покупателям.', linkVisibleDesc: 'Показывать ли эту ссылку навигации покупателям.',
appName: 'Конструктор маркетплейса', appName: 'Конструктор маркетплейса',
backToDashboard: 'Назад в панель управления', backToDashboard: 'Назад в панель управления',
htmlToolBold: 'Жирный',
htmlToolItalic: 'Курсив',
htmlToolUnderline: 'Подчёркнутый',
htmlToolHeading: 'Заголовок',
htmlToolSubheading: 'Подзаголовок',
htmlToolBulletList: 'Маркированный список',
htmlToolNumberedList: 'Нумерованный список',
htmlToolLink: 'Вставить ссылку',
htmlToolImage: 'Вставить изображение',
htmlToolTable: 'Вставить таблицу',
htmlToolDivider: 'Разделительная линия',
htmlToolCodeBlock: 'Блок кода',
htmlToolEmbed: 'Встроить видео',
breadcrumbRoot: 'Конструктор маркетплейса', breadcrumbRoot: 'Конструктор маркетплейса',
statusComplete: 'Готово', statusComplete: 'Готово',
statusInProgress: 'В процессе', statusInProgress: 'В процессе',

View File

@@ -685,6 +685,20 @@ export interface Translations {
linkVisibleDesc: string; linkVisibleDesc: string;
appName: string; appName: string;
backToDashboard: string; backToDashboard: string;
htmlToolBold: string;
htmlToolItalic: string;
htmlToolUnderline: string;
htmlToolHeading: string;
htmlToolSubheading: string;
htmlToolBulletList: string;
htmlToolNumberedList: string;
htmlToolLink: string;
htmlToolImage: string;
htmlToolTable: string;
htmlToolDivider: string;
htmlToolCodeBlock: string;
htmlToolEmbed: string;
breadcrumbRoot: string; breadcrumbRoot: string;
statusComplete: string; statusComplete: string;
statusInProgress: string; statusInProgress: string;