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:
@@ -1,9 +1,30 @@
|
||||
<div class="html-editor">
|
||||
<div class="html-editor-toolbar">
|
||||
@for (item of toolbar; track item.id) {
|
||||
<button type="button" (click)="runCommand(item)">{{ item.label }}</button>
|
||||
<div class="html-editor-toolbar" role="toolbar">
|
||||
@for (group of toolbarGroups; track $index) {
|
||||
<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>
|
||||
|
||||
@if (showCode()) {
|
||||
|
||||
@@ -7,9 +7,54 @@
|
||||
.html-editor-toolbar {
|
||||
display: flex;
|
||||
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 {
|
||||
min-height: 160px;
|
||||
border: 1px solid var(--border, #ccc);
|
||||
|
||||
@@ -9,22 +9,35 @@ export interface HtmlEditorToolbarCommand {
|
||||
label: string;
|
||||
command: 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[] = [
|
||||
{ id: 'bold', label: 'B', command: 'bold' },
|
||||
{ id: 'italic', label: 'I', command: 'italic' },
|
||||
{ id: 'underline', label: 'U', command: 'underline' },
|
||||
{ id: 'h2', label: 'H2', command: 'formatBlock', value: 'H2' },
|
||||
{ id: 'h3', label: 'H3', command: 'formatBlock', value: 'H3' },
|
||||
{ id: 'ul', label: 'List', command: 'insertUnorderedList' },
|
||||
{ id: 'ol', label: '1,2,3', command: 'insertOrderedList' },
|
||||
{ id: 'link', label: 'Link', command: 'createLink' },
|
||||
{ id: 'image', label: 'Image', command: 'insertImage' },
|
||||
{ id: 'table', label: 'Table', command: 'insertHTML', value: '<table><tr><td> </td><td> </td></tr></table>' },
|
||||
{ id: 'hr', label: 'HR', command: 'insertHorizontalRule' },
|
||||
{ id: 'codeblock', label: 'Code', command: 'formatBlock', value: 'PRE' },
|
||||
{ id: 'embed', label: 'Embed', command: 'insertEmbed' },
|
||||
{ id: 'bold', label: 'B', command: 'bold', titleKey: 'builder.htmlToolBold' },
|
||||
{ id: 'italic', label: 'I', command: 'italic', titleKey: 'builder.htmlToolItalic' },
|
||||
{ id: 'underline', label: 'U', command: 'underline', titleKey: 'builder.htmlToolUnderline' },
|
||||
{ id: 'h2', label: 'H2', command: 'formatBlock', value: 'H2', titleKey: 'builder.htmlToolHeading' },
|
||||
{ id: 'h3', label: 'H3', command: 'formatBlock', value: 'H3', titleKey: 'builder.htmlToolSubheading' },
|
||||
{ id: 'ul', label: '', icon: 'pi-list', command: 'insertUnorderedList', titleKey: 'builder.htmlToolBulletList' },
|
||||
{ id: 'ol', label: '1.', command: 'insertOrderedList', titleKey: 'builder.htmlToolNumberedList' },
|
||||
{ id: 'link', label: '', icon: 'pi-link', command: 'createLink', titleKey: 'builder.htmlToolLink' },
|
||||
{ id: 'image', label: '', icon: 'pi-image', command: 'insertImage', titleKey: 'builder.htmlToolImage' },
|
||||
{ id: 'table', label: '', icon: 'pi-table', command: 'insertHTML', value: '<table><tr><td> </td><td> </td></tr></table>', titleKey: 'builder.htmlToolTable' },
|
||||
{ id: 'hr', label: '', icon: 'pi-minus', command: 'insertHorizontalRule', titleKey: 'builder.htmlToolDivider' },
|
||||
{ id: 'codeblock', label: '', icon: 'pi-code', command: 'formatBlock', value: 'PRE', titleKey: 'builder.htmlToolCodeBlock' },
|
||||
{ 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({
|
||||
@@ -43,6 +56,9 @@ export class MarketplaceHtmlEditorComponent implements OnChanges, AfterViewInit
|
||||
@ViewChild('surface', { static: true }) surface!: ElementRef<HTMLDivElement>;
|
||||
|
||||
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 codeValue = signal('');
|
||||
readonly codeError = signal<string | null>(null);
|
||||
|
||||
@@ -687,6 +687,19 @@ export const en: Translations = {
|
||||
linkVisibleDesc: 'Whether this navigation link is shown to shoppers.',
|
||||
appName: 'Marketplace Builder',
|
||||
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',
|
||||
statusComplete: 'Complete',
|
||||
statusInProgress: 'In progress',
|
||||
|
||||
@@ -687,6 +687,19 @@ export const hy: Translations = {
|
||||
linkVisibleDesc: 'Արդյո՞ք այս նավիգացիոն հղումը ցուցադրվում է գնորդներին։',
|
||||
appName: 'Մարկետփլեյսի կոնստրուկտոր',
|
||||
backToDashboard: 'Վերադառնալ կառավարման վահանակ',
|
||||
htmlToolBold: 'Թավ',
|
||||
htmlToolItalic: 'Շեղ',
|
||||
htmlToolUnderline: 'Ընդգծված',
|
||||
htmlToolHeading: 'Վերնագիր',
|
||||
htmlToolSubheading: 'Ենթավերնագիր',
|
||||
htmlToolBulletList: 'Կետային ցուցակ',
|
||||
htmlToolNumberedList: 'Համարակալված ցուցակ',
|
||||
htmlToolLink: 'Տեղադրել հղում',
|
||||
htmlToolImage: 'Տեղադրել նկար',
|
||||
htmlToolTable: 'Տեղադրել աղյուսակ',
|
||||
htmlToolDivider: 'Բաժանարար գիծ',
|
||||
htmlToolCodeBlock: 'Կոդի բլոկ',
|
||||
htmlToolEmbed: 'Ներդնել տեսանյութ',
|
||||
breadcrumbRoot: 'Մարկետփլեյսի կոնստրուկտոր',
|
||||
statusComplete: 'Ավարտված',
|
||||
statusInProgress: 'Ընթացքի մեջ',
|
||||
|
||||
@@ -687,6 +687,19 @@ export const ru: Translations = {
|
||||
linkVisibleDesc: 'Показывать ли эту ссылку навигации покупателям.',
|
||||
appName: 'Конструктор маркетплейса',
|
||||
backToDashboard: 'Назад в панель управления',
|
||||
htmlToolBold: 'Жирный',
|
||||
htmlToolItalic: 'Курсив',
|
||||
htmlToolUnderline: 'Подчёркнутый',
|
||||
htmlToolHeading: 'Заголовок',
|
||||
htmlToolSubheading: 'Подзаголовок',
|
||||
htmlToolBulletList: 'Маркированный список',
|
||||
htmlToolNumberedList: 'Нумерованный список',
|
||||
htmlToolLink: 'Вставить ссылку',
|
||||
htmlToolImage: 'Вставить изображение',
|
||||
htmlToolTable: 'Вставить таблицу',
|
||||
htmlToolDivider: 'Разделительная линия',
|
||||
htmlToolCodeBlock: 'Блок кода',
|
||||
htmlToolEmbed: 'Встроить видео',
|
||||
breadcrumbRoot: 'Конструктор маркетплейса',
|
||||
statusComplete: 'Готово',
|
||||
statusInProgress: 'В процессе',
|
||||
|
||||
@@ -685,6 +685,20 @@ export interface Translations {
|
||||
linkVisibleDesc: string;
|
||||
appName: 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;
|
||||
statusComplete: string;
|
||||
statusInProgress: string;
|
||||
|
||||
Reference in New Issue
Block a user