Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
4e03cd0
feat: add grid block templates experiment
makhnatkin Jun 17, 2026
ba852c9
feat: refine grid block templates interactions
makhnatkin Jun 17, 2026
f3df99e
feat(GridBlockTemplates): pointer drag handle and verbatim raw HTML b…
makhnatkin Jun 17, 2026
92c1dca
feat(GridBlockTemplates): add scoped custom CSS field for the container
makhnatkin Jun 17, 2026
fb62e81
feat(GridBlockTemplates): make the templates list scrollable
makhnatkin Jun 17, 2026
01b4c96
feat(GridBlockTemplates): switch to selector-based CSS, drop inline s…
makhnatkin Jun 17, 2026
206dab9
refactor: tidy grid block templates node view
makhnatkin Jun 17, 2026
fd217ca
fix: address grid templates lint
makhnatkin Jun 17, 2026
6bb30aa
fix: order grid templates demo imports
makhnatkin Jun 17, 2026
868e5e1
feat: improve grid block templates ui
makhnatkin Jun 17, 2026
ac65a91
feat: support grouped grid templates
makhnatkin Jun 17, 2026
e3c474b
feat: simplify grouped template menus
makhnatkin Jun 17, 2026
338eb43
refactor: simplify grid template parser
makhnatkin Jun 17, 2026
625bdbe
feat: improve grid block templates
makhnatkin Jun 17, 2026
cbbe2c8
fix: stabilize grid block hover editing
makhnatkin Jun 17, 2026
413d653
fix: polish grid block inline editing
makhnatkin Jun 17, 2026
6afaa29
fix: add grid block actions menu
makhnatkin Jun 17, 2026
d0364d0
fix: update grid block settings live
makhnatkin Jun 17, 2026
2a9032e
fix: remove hardcoded grid block templates
makhnatkin Jun 17, 2026
75d2f75
fix: support css-only grid templates
makhnatkin Jun 17, 2026
21a2a31
fix: add import template icon
makhnatkin Jun 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type {StoryObj} from '@storybook/react';

import {GridBlockTemplatesDemo as component} from './GridBlockTemplates';

export const Story: StoryObj<typeof component> = {};
Story.storyName = 'Grid block templates';

export default {
title: 'Examples / Grid block templates',
component,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {memo} from 'react';

import {LayoutCells} from '@gravity-ui/icons';
import {
MarkdownEditorView,
type ToolbarsPreset,
useMarkdownEditor,
} from '@gravity-ui/markdown-editor';
import {ToolbarName as Toolbar} from '@gravity-ui/markdown-editor/_/modules/toolbars/constants.js';
import {defaultPreset} from '@gravity-ui/markdown-editor/_/modules/toolbars/presets.js';
import {GridBlockTemplates as GridBlockTemplatesExtension} from '@gravity-ui/markdown-editor/extensions/additional/GridBlockTemplates/index.js';

import {PlaygroundLayout} from '../../../components/PlaygroundLayout';

const gridBlockTemplatesItemId = 'gridBlockTemplates';

const toolbarsPreset: ToolbarsPreset = {
items: {
...defaultPreset.items,
[gridBlockTemplatesItemId]: {
view: {
icon: {data: LayoutCells},
title: 'Grid block templates',
},
wysiwyg: {
exec: (e) => e.actions.createGridBlockTemplates.run(),
isActive: (e) => e.actions.createGridBlockTemplates.isActive(),
isEnable: (e) => e.actions.createGridBlockTemplates.isEnable(),
},
},
},
orders: {
...defaultPreset.orders,
[Toolbar.wysiwygMain]: [
[gridBlockTemplatesItemId],
...defaultPreset.orders[Toolbar.wysiwygMain],
],
},
};

export const GridBlockTemplatesDemo = memo(function GridBlockTemplatesDemo() {
const editor = useMarkdownEditor(
{
initial: {mode: 'wysiwyg', markup: ''},
wysiwygConfig: {
extensions: (builder) =>
builder.use(GridBlockTemplatesExtension, {
templates: {
showButton: true,
allowAdd: true,
},
}),
},
},
[],
);

return (
<PlaygroundLayout
editor={editor}
view={({className}) => (
<MarkdownEditorView
autofocus
stickyToolbar
settingsVisible
editor={editor}
className={className}
toolbarsPreset={toolbarsPreset}
/>
)}
/>
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import {builders} from 'prosemirror-test-builder';

import {ExtensionsManager} from '../../../core';
import {BaseNode, BaseSchemaSpecs} from '../../specs';

import {GridBlockTemplatesSpecs} from './GridBlockTemplatesSpecs';
import {GridBlockTemplatesAttrs, gridBlockTemplatesNodeName} from './GridBlockTemplatesSpecs/const';

const {schema, serializer} = new ExtensionsManager({
extensions: (builder) => builder.use(BaseSchemaSpecs, {}).use(GridBlockTemplatesSpecs, {}),
}).buildDeps();

const {doc, gridBlockTemplates} = builders<'doc' | 'gridBlockTemplates'>(schema, {
doc: {nodeType: BaseNode.Doc},
gridBlockTemplates: {nodeType: gridBlockTemplatesNodeName},
});

describe('GridBlockTemplates extension', () => {
it('should serialize blocks without inline styles', () => {
expect(
serializer.serialize(
doc(
gridBlockTemplates({
[GridBlockTemplatesAttrs.blocks]: [
{id: 'block-1', css: '', content: '<strong>First</strong>'},
],
[GridBlockTemplatesAttrs.EntityId]: 'grid_block_templates-1',
}),
),
),
).toBe(
[
'::: html',
'<div class="grid-templates-scope-grid_block_templates-1">',
' <div class="grid">',
' <div class="block-1"><strong>First</strong></div>',
' </div>',
'</div>',
':::',
].join('\n'),
);
});

it('should emit scoped container and per-block css into a style tag', () => {
expect(
serializer.serialize(
doc(
gridBlockTemplates({
[GridBlockTemplatesAttrs.blocks]: [
{
id: 'block-1',
css: '& { padding: 12px; }\nh3 { margin: 0; }',
content: 'First',
},
],
[GridBlockTemplatesAttrs.customCss]: '.grid { align-items: center; }',
[GridBlockTemplatesAttrs.EntityId]: 'grid_block_templates-1',
}),
),
),
).toBe(
[
'::: html',
'<div class="grid-templates-scope-grid_block_templates-1">',
' <style>',
' .grid-templates-scope-grid_block_templates-1 .grid { align-items: center; }',
' .grid-templates-scope-grid_block_templates-1 .block-1 { padding: 12px; }',
' .grid-templates-scope-grid_block_templates-1 .block-1 h3 { margin: 0; }',
' </style>',
' <div class="grid">',
' <div class="block-1">First</div>',
' </div>',
'</div>',
':::',
].join('\n'),
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import {useState} from 'react';

import {Code} from '@gravity-ui/icons';
import {Button, Icon, Menu, Popup, TextInput} from '@gravity-ui/uikit';

import {TextAreaFixed as TextArea} from 'src/forms/TextInput';
import {i18n} from 'src/i18n/grid-block-templates';

import {parseRawBlock} from '../templates';
import type {GridBlockBlockTemplate, GridBlockTemplateBlock} from '../types';

import {GroupedTemplatesMenuItems} from './GroupedTemplatesMenuItems';
import {STOP_EVENT_CLASSNAME, cnGridBlockTemplates} from './const';

const b = cnGridBlockTemplates;
const stop = STOP_EVENT_CLASSNAME;

interface BlockInsertPopupProps {
anchor: HTMLElement | null;
open: boolean;
templates: GridBlockBlockTemplate[];
onClose: () => void;
onApplyTemplate: (template: GridBlockBlockTemplate) => void;
onApplyHtml: (block: GridBlockTemplateBlock) => void;
}

export const BlockInsertPopup: React.FC<BlockInsertPopupProps> = ({
anchor,
open,
templates,
onClose,
onApplyTemplate,
onApplyHtml,
}) => {
const [addingCustomHtml, setAddingCustomHtml] = useState(false);
const [input, setInput] = useState('');
const [filter, setFilter] = useState('');
const showCustomHtmlEditor = addingCustomHtml || templates.length === 0;

const close = () => {
setAddingCustomHtml(false);
setInput('');
setFilter('');
onClose();
};

const handleApplyHtml = () => {
onApplyHtml(parseRawBlock(input));
close();
};

return (
<Popup anchorElement={anchor} open={open} onOpenChange={close} placement="bottom-start">
<div className={b('templates', [stop])}>
{showCustomHtmlEditor ? (
<div className={b('templates-editor')}>
<TextArea
controlProps={{className: stop}}
value={input}
onUpdate={setInput}
placeholder={i18n('block_html_input_placeholder')}
minRows={8}
autoFocus
/>
<div className={b('templates-controls')}>
<Button view="flat" className={stop} onClick={close}>
<span className={stop}>{i18n('cancel')}</span>
</Button>
<Button
view="action"
className={stop}
disabled={!input.trim()}
onClick={handleApplyHtml}
>
<span className={stop}>{i18n('insert')}</span>
</Button>
</div>
</div>
) : (
<>
<div className={b('templates-search')}>
<TextInput
className={stop}
controlProps={{className: stop}}
size="s"
value={filter}
onUpdate={setFilter}
placeholder={i18n('search_templates')}
autoFocus
/>
</div>
<div className={b('templates-list', [stop])}>
<Menu className={stop}>
<Menu.Item
className={stop}
iconStart={<Icon data={Code} />}
onClick={() => setAddingCustomHtml(true)}
>
{i18n('custom_html')}
</Menu.Item>
<div
role="separator"
className={b('templates-separator', [stop])}
/>
<GroupedTemplatesMenuItems
templates={templates}
filter={filter}
emptyText={i18n('block_templates_empty')}
onApply={(template) => {
onApplyTemplate(template);
close();
}}
/>
</Menu>
</div>
</>
)}
</div>
</Popup>
);
};
Loading
Loading