From 4e03cd04f9b189906a94498407e521bddadded77 Mon Sep 17 00:00:00 2001 From: makhnatkin Date: Wed, 17 Jun 2026 12:47:53 +0200 Subject: [PATCH 01/21] feat: add grid block templates experiment --- demo/src/defaults/grid-block-templates.ts | 55 +++ .../GridBlockTemplates.stories.tsx | 11 + .../GridBlockTemplates.tsx | 75 ++++ .../GridBlockTemplates.test.ts | 46 ++ .../BlockInsertPopup.tsx | 129 ++++++ .../GridBlockTemplates.scss | 128 ++++++ .../GridBlockTemplatesView.tsx | 413 ++++++++++++++++++ .../GridBlockTemplatesNodeView/NodeView.tsx | 113 +++++ .../TemplatesPopup.scss | 27 ++ .../TemplatesPopup.tsx | 155 +++++++ .../GridBlockTemplatesNodeView/const.ts | 4 + .../GridBlockTemplatesNodeView/index.ts | 1 + .../GridBlockTemplatesSpecs/const.ts | 24 + .../GridBlockTemplatesSpecs/index.ts | 93 ++++ .../additional/GridBlockTemplates/actions.ts | 27 ++ .../additional/GridBlockTemplates/const.ts | 1 + .../additional/GridBlockTemplates/index.ts | 29 ++ .../GridBlockTemplates/templates/index.ts | 17 + .../templates/parse.test.ts | 100 +++++ .../GridBlockTemplates/templates/parse.ts | 95 ++++ .../templates/storage.test.ts | 95 ++++ .../GridBlockTemplates/templates/storage.ts | 95 ++++ .../additional/GridBlockTemplates/types.ts | 41 ++ .../src/i18n/grid-block-templates/en.json | 21 + .../src/i18n/grid-block-templates/index.ts | 8 + .../src/i18n/grid-block-templates/ru.json | 21 + 26 files changed, 1824 insertions(+) create mode 100644 demo/src/defaults/grid-block-templates.ts create mode 100644 demo/src/stories/examples/grid-block-templates/GridBlockTemplates.stories.tsx create mode 100644 demo/src/stories/examples/grid-block-templates/GridBlockTemplates.tsx create mode 100644 packages/editor/src/extensions/additional/GridBlockTemplates/GridBlockTemplates.test.ts create mode 100644 packages/editor/src/extensions/additional/GridBlockTemplates/GridBlockTemplatesNodeView/BlockInsertPopup.tsx create mode 100644 packages/editor/src/extensions/additional/GridBlockTemplates/GridBlockTemplatesNodeView/GridBlockTemplates.scss create mode 100644 packages/editor/src/extensions/additional/GridBlockTemplates/GridBlockTemplatesNodeView/GridBlockTemplatesView.tsx create mode 100644 packages/editor/src/extensions/additional/GridBlockTemplates/GridBlockTemplatesNodeView/NodeView.tsx create mode 100644 packages/editor/src/extensions/additional/GridBlockTemplates/GridBlockTemplatesNodeView/TemplatesPopup.scss create mode 100644 packages/editor/src/extensions/additional/GridBlockTemplates/GridBlockTemplatesNodeView/TemplatesPopup.tsx create mode 100644 packages/editor/src/extensions/additional/GridBlockTemplates/GridBlockTemplatesNodeView/const.ts create mode 100644 packages/editor/src/extensions/additional/GridBlockTemplates/GridBlockTemplatesNodeView/index.ts create mode 100644 packages/editor/src/extensions/additional/GridBlockTemplates/GridBlockTemplatesSpecs/const.ts create mode 100644 packages/editor/src/extensions/additional/GridBlockTemplates/GridBlockTemplatesSpecs/index.ts create mode 100644 packages/editor/src/extensions/additional/GridBlockTemplates/actions.ts create mode 100644 packages/editor/src/extensions/additional/GridBlockTemplates/const.ts create mode 100644 packages/editor/src/extensions/additional/GridBlockTemplates/index.ts create mode 100644 packages/editor/src/extensions/additional/GridBlockTemplates/templates/index.ts create mode 100644 packages/editor/src/extensions/additional/GridBlockTemplates/templates/parse.test.ts create mode 100644 packages/editor/src/extensions/additional/GridBlockTemplates/templates/parse.ts create mode 100644 packages/editor/src/extensions/additional/GridBlockTemplates/templates/storage.test.ts create mode 100644 packages/editor/src/extensions/additional/GridBlockTemplates/templates/storage.ts create mode 100644 packages/editor/src/extensions/additional/GridBlockTemplates/types.ts create mode 100644 packages/editor/src/i18n/grid-block-templates/en.json create mode 100644 packages/editor/src/i18n/grid-block-templates/index.ts create mode 100644 packages/editor/src/i18n/grid-block-templates/ru.json diff --git a/demo/src/defaults/grid-block-templates.ts b/demo/src/defaults/grid-block-templates.ts new file mode 100644 index 00000000..d5d34e77 --- /dev/null +++ b/demo/src/defaults/grid-block-templates.ts @@ -0,0 +1,55 @@ +import type {GridBlockTemplate} from '@gravity-ui/markdown-editor/extensions/additional/GridBlockTemplates/templates/index.js'; + +export const gridBlockTemplates: GridBlockTemplate[] = [ + { + id: 'feature-grid', + title: 'Feature grid', + type: 'container', + content: `
+
Fast setup

Start from a reusable layout.

+
Editable blocks

Change content inline.

+
Static output

Serialize to HTML.

+
`, + containerCss: + 'display:grid;grid-template-columns:repeat(3,1fr);gap:16px;padding:16px;background:#f8fafc;border-radius:12px', + blocks: [ + { + css: 'padding:18px;background:#fff;border:1px solid #e5e7eb;border-radius:8px', + content: 'Fast setup

Start from a reusable layout.

', + }, + { + css: 'padding:18px;background:#fff;border:1px solid #e5e7eb;border-radius:8px', + content: 'Editable blocks

Change content inline.

', + }, + { + css: 'padding:18px;background:#fff;border:1px solid #e5e7eb;border-radius:8px', + content: 'Static output

Serialize to HTML.

', + }, + ], + }, + { + id: 'header-two-columns', + title: 'Header and two columns', + type: 'container', + content: `
+

Section title

+
Left column
+
Right column
+
`, + containerCss: 'display:grid;grid-template-columns:1fr 1fr;gap:14px;padding:14px', + blocks: [ + { + css: 'grid-column:1 / -1;padding:24px;background:#2563eb;color:#fff;border-radius:10px', + content: '

Section title

', + }, + { + css: 'padding:18px;background:#eff6ff;border-radius:8px', + content: 'Left column', + }, + { + css: 'padding:18px;background:#f0fdf4;border-radius:8px', + content: 'Right column', + }, + ], + }, +]; diff --git a/demo/src/stories/examples/grid-block-templates/GridBlockTemplates.stories.tsx b/demo/src/stories/examples/grid-block-templates/GridBlockTemplates.stories.tsx new file mode 100644 index 00000000..79709059 --- /dev/null +++ b/demo/src/stories/examples/grid-block-templates/GridBlockTemplates.stories.tsx @@ -0,0 +1,11 @@ +import type {StoryObj} from '@storybook/react'; + +import {GridBlockTemplatesDemo as component} from './GridBlockTemplates'; + +export const Story: StoryObj = {}; +Story.storyName = 'Grid block templates'; + +export default { + title: 'Examples / Grid block templates', + component, +}; diff --git a/demo/src/stories/examples/grid-block-templates/GridBlockTemplates.tsx b/demo/src/stories/examples/grid-block-templates/GridBlockTemplates.tsx new file mode 100644 index 00000000..e6ea16f3 --- /dev/null +++ b/demo/src/stories/examples/grid-block-templates/GridBlockTemplates.tsx @@ -0,0 +1,75 @@ +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 {gridBlockTemplates} from '../../../defaults/grid-block-templates'; +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: { + items: gridBlockTemplates, + showButton: true, + allowAdd: true, + }, + }), + }, + }, + [], + ); + + return ( + ( + + )} + /> + ); +}); diff --git a/packages/editor/src/extensions/additional/GridBlockTemplates/GridBlockTemplates.test.ts b/packages/editor/src/extensions/additional/GridBlockTemplates/GridBlockTemplates.test.ts new file mode 100644 index 00000000..136425c6 --- /dev/null +++ b/packages/editor/src/extensions/additional/GridBlockTemplates/GridBlockTemplates.test.ts @@ -0,0 +1,46 @@ +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 to yfm html block', () => { + expect( + serializer.serialize( + doc( + gridBlockTemplates({ + [GridBlockTemplatesAttrs.blocks]: [ + { + id: 'block-1', + css: 'padding: 12px;', + content: 'First', + }, + ], + [GridBlockTemplatesAttrs.containerCss]: 'display: grid;', + [GridBlockTemplatesAttrs.EntityId]: 'grid_block_templates-1', + }), + ), + ), + ).toBe( + [ + '::: html', + '
', + '
First
', + '
', + ':::', + ].join('\n'), + ); + }); +}); diff --git a/packages/editor/src/extensions/additional/GridBlockTemplates/GridBlockTemplatesNodeView/BlockInsertPopup.tsx b/packages/editor/src/extensions/additional/GridBlockTemplates/GridBlockTemplatesNodeView/BlockInsertPopup.tsx new file mode 100644 index 00000000..6b7c36db --- /dev/null +++ b/packages/editor/src/extensions/additional/GridBlockTemplates/GridBlockTemplatesNodeView/BlockInsertPopup.tsx @@ -0,0 +1,129 @@ +import {useMemo, useState} from 'react'; + +import {Code, Plus} 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 {parseTemplateBlock} from '../templates'; +import type {GridBlockBlockTemplate, GridBlockTemplateBlock} from '../types'; + +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 = ({ + 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 filtered = useMemo(() => { + const query = filter.trim().toLowerCase(); + if (!query) return templates; + return templates.filter((template) => template.title.toLowerCase().includes(query)); + }, [templates, filter]); + + const close = () => { + setAddingCustomHtml(false); + setInput(''); + setFilter(''); + onClose(); + }; + + const handleApplyHtml = () => { + onApplyHtml(parseTemplateBlock(input)); + close(); + }; + + return ( + +
+ {showCustomHtmlEditor ? ( +
+