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-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()) {

View File

@@ -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);

View File

@@ -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>&nbsp;</td><td>&nbsp;</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>&nbsp;</td><td>&nbsp;</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);