@olddevs/ui 4.5.1 → 4.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/editor/RichTextEditor.d.ts +2 -2
  3. package/dist/editor/RichTextEditor.d.ts.map +1 -1
  4. package/dist/editor/RichTextEditor.js +3 -0
  5. package/dist/editor/RichTextEditor.js.map +1 -1
  6. package/dist/editor/core/blockTypes.d.ts +2 -8
  7. package/dist/editor/core/blockTypes.d.ts.map +1 -1
  8. package/dist/editor/core/blockTypes.js +11 -54
  9. package/dist/editor/core/blockTypes.js.map +1 -1
  10. package/dist/editor/core/containers.d.ts.map +1 -1
  11. package/dist/editor/core/containers.js +1 -0
  12. package/dist/editor/core/containers.js.map +1 -1
  13. package/dist/editor/core/customMarkdown.js +1 -1
  14. package/dist/editor/core/customMarkdown.js.map +1 -1
  15. package/dist/editor/core/customTool.d.ts +6 -6
  16. package/dist/editor/core/customTool.d.ts.map +1 -1
  17. package/dist/editor/core/customToolEngine.d.ts +2 -2
  18. package/dist/editor/core/extensions.d.ts +7 -2
  19. package/dist/editor/core/extensions.d.ts.map +1 -1
  20. package/dist/editor/core/extensions.js +96 -64
  21. package/dist/editor/core/extensions.js.map +1 -1
  22. package/dist/editor/core/fieldNotice.d.ts +1 -1
  23. package/dist/editor/core/insertItems.d.ts +5 -28
  24. package/dist/editor/core/insertItems.d.ts.map +1 -1
  25. package/dist/editor/core/insertItems.js +3 -94
  26. package/dist/editor/core/insertItems.js.map +1 -1
  27. package/dist/editor/core/insertMenuTool.d.ts +13 -0
  28. package/dist/editor/core/insertMenuTool.d.ts.map +1 -0
  29. package/dist/editor/core/insertMenuTool.js +20 -0
  30. package/dist/editor/core/insertMenuTool.js.map +1 -0
  31. package/dist/editor/core/schema.d.ts +20 -5
  32. package/dist/editor/core/schema.d.ts.map +1 -1
  33. package/dist/editor/core/schema.js +18 -3
  34. package/dist/editor/core/schema.js.map +1 -1
  35. package/dist/editor/core/shortcuts.d.ts +56 -5
  36. package/dist/editor/core/shortcuts.d.ts.map +1 -1
  37. package/dist/editor/core/shortcuts.js +91 -156
  38. package/dist/editor/core/shortcuts.js.map +1 -1
  39. package/dist/editor/core/skeleton.d.ts +1 -1
  40. package/dist/editor/core/skeleton.d.ts.map +1 -1
  41. package/dist/editor/core/skeleton.js +1 -0
  42. package/dist/editor/core/skeleton.js.map +1 -1
  43. package/dist/editor/core/toolButtons.d.ts +3 -3
  44. package/dist/editor/core/toolDefinition.d.ts +3 -3
  45. package/dist/editor/core/toolDescriptor.d.ts +49 -30
  46. package/dist/editor/core/toolDescriptor.d.ts.map +1 -1
  47. package/dist/editor/core/toolDescriptor.js +2 -5
  48. package/dist/editor/core/toolDescriptor.js.map +1 -1
  49. package/dist/editor/core/toolLoader.d.ts +1 -1
  50. package/dist/editor/core/toolLoader.d.ts.map +1 -1
  51. package/dist/editor/core/toolOptions.d.ts +19 -2
  52. package/dist/editor/core/toolOptions.d.ts.map +1 -1
  53. package/dist/editor/core/toolOptions.js.map +1 -1
  54. package/dist/editor/core/tools.d.ts +10 -2
  55. package/dist/editor/core/tools.d.ts.map +1 -1
  56. package/dist/editor/core/tools.js +1 -0
  57. package/dist/editor/core/tools.js.map +1 -1
  58. package/dist/editor/features/builtins.d.ts +6 -8
  59. package/dist/editor/features/builtins.d.ts.map +1 -1
  60. package/dist/editor/features/builtins.js +48 -21
  61. package/dist/editor/features/builtins.js.map +1 -1
  62. package/dist/editor/features/code/codeBlockTool.d.ts +5 -6
  63. package/dist/editor/features/code/codeBlockTool.d.ts.map +1 -1
  64. package/dist/editor/features/code/codeBlockTool.js +10 -1
  65. package/dist/editor/features/code/codeBlockTool.js.map +1 -1
  66. package/dist/editor/features/color/colorTools.d.ts +14 -0
  67. package/dist/editor/features/color/colorTools.d.ts.map +1 -0
  68. package/dist/editor/features/color/colorTools.js +39 -0
  69. package/dist/editor/features/color/colorTools.js.map +1 -0
  70. package/dist/editor/features/details/details.d.ts +68 -0
  71. package/dist/editor/features/details/details.d.ts.map +1 -0
  72. package/dist/editor/features/details/details.js +237 -0
  73. package/dist/editor/features/details/details.js.map +1 -0
  74. package/dist/editor/features/details/detailsMarkdown.d.ts +17 -0
  75. package/dist/editor/features/details/detailsMarkdown.d.ts.map +1 -0
  76. package/dist/editor/features/details/detailsMarkdown.js +84 -0
  77. package/dist/editor/features/details/detailsMarkdown.js.map +1 -0
  78. package/dist/editor/features/details/detailsTool.d.ts +16 -0
  79. package/dist/editor/features/details/detailsTool.d.ts.map +1 -0
  80. package/dist/editor/features/details/detailsTool.js +39 -0
  81. package/dist/editor/features/details/detailsTool.js.map +1 -0
  82. package/dist/editor/features/emoji/emojiTool.d.ts +1 -1
  83. package/dist/editor/features/heading/headingTool.d.ts +18 -0
  84. package/dist/editor/features/heading/headingTool.d.ts.map +1 -0
  85. package/dist/editor/features/heading/headingTool.js +59 -0
  86. package/dist/editor/features/heading/headingTool.js.map +1 -0
  87. package/dist/editor/features/history/historyTool.d.ts +15 -0
  88. package/dist/editor/features/history/historyTool.d.ts.map +1 -0
  89. package/dist/editor/features/history/historyTool.js +28 -0
  90. package/dist/editor/features/history/historyTool.js.map +1 -0
  91. package/dist/editor/features/image/imageTool.d.ts +19 -0
  92. package/dist/editor/features/image/imageTool.d.ts.map +1 -0
  93. package/dist/editor/features/image/imageTool.js +28 -0
  94. package/dist/editor/features/image/imageTool.js.map +1 -0
  95. package/dist/editor/features/layout/layoutTool.d.ts +11 -0
  96. package/dist/editor/features/layout/layoutTool.d.ts.map +1 -0
  97. package/dist/editor/features/layout/layoutTool.js +20 -0
  98. package/dist/editor/features/layout/layoutTool.js.map +1 -0
  99. package/dist/editor/features/lists/listTools.d.ts +15 -0
  100. package/dist/editor/features/lists/listTools.d.ts.map +1 -0
  101. package/dist/editor/features/lists/listTools.js +78 -0
  102. package/dist/editor/features/lists/listTools.js.map +1 -0
  103. package/dist/editor/features/marks/markTools.d.ts +16 -0
  104. package/dist/editor/features/marks/markTools.d.ts.map +1 -0
  105. package/dist/editor/features/marks/markTools.js +43 -0
  106. package/dist/editor/features/marks/markTools.js.map +1 -0
  107. package/dist/editor/features/search/searchTool.d.ts +1 -1
  108. package/dist/editor/features/table/tableTool.d.ts +3 -3
  109. package/dist/editor/features/wrapper/wrapperTool.d.ts +11 -0
  110. package/dist/editor/features/wrapper/wrapperTool.d.ts.map +1 -0
  111. package/dist/editor/features/wrapper/wrapperTool.js +32 -0
  112. package/dist/editor/features/wrapper/wrapperTool.js.map +1 -0
  113. package/dist/editor/index.d.ts +1 -1
  114. package/dist/editor/index.d.ts.map +1 -1
  115. package/dist/editor/index.js.map +1 -1
  116. package/dist/editor/schema-entry.d.ts +1 -1
  117. package/dist/editor/styles/prose.d.ts.map +1 -1
  118. package/dist/editor/styles/prose.js +26 -0
  119. package/dist/editor/styles/prose.js.map +1 -1
  120. package/dist/editor/ui/EditorToolbar.d.ts.map +1 -1
  121. package/dist/editor/ui/EditorToolbar.js +5 -38
  122. package/dist/editor/ui/EditorToolbar.js.map +1 -1
  123. package/dist/editor/ui/SelectionBubble.d.ts.map +1 -1
  124. package/dist/editor/ui/SelectionBubble.js +1 -1
  125. package/dist/editor/ui/SelectionBubble.js.map +1 -1
  126. package/dist/editor/ui/ToolButtonView.d.ts.map +1 -1
  127. package/dist/editor/ui/ToolButtonView.js +1 -1
  128. package/dist/editor/ui/ToolButtonView.js.map +1 -1
  129. package/dist/i18n/defaults.d.ts.map +1 -1
  130. package/dist/i18n/defaults.js +3 -0
  131. package/dist/i18n/defaults.js.map +1 -1
  132. package/dist/i18n/messages.d.ts +16 -1
  133. package/dist/i18n/messages.d.ts.map +1 -1
  134. package/dist/icons.d.ts +8 -1
  135. package/dist/icons.d.ts.map +1 -1
  136. package/dist/icons.js +1 -1
  137. package/dist/icons.js.map +1 -1
  138. package/dist/locales/ru.d.ts.map +1 -1
  139. package/dist/locales/ru.js +3 -0
  140. package/dist/locales/ru.js.map +1 -1
  141. package/docs/api/editor-extension.en.md +56 -56
  142. package/docs/api/editor-extension.md +55 -55
  143. package/docs/api/messages.en.md +4 -1
  144. package/docs/api/messages.md +3 -0
  145. package/docs/api/migration-notes.en.md +3 -3
  146. package/docs/api/migration-notes.md +2 -2
  147. package/docs/components/content/rich-text-view.en.md +18 -12
  148. package/docs/components/content/rich-text-view.md +17 -11
  149. package/docs/components/inputs/rich-text-editor.en.md +140 -87
  150. package/docs/components/inputs/rich-text-editor.md +139 -86
  151. package/docs/conventions.en.md +2 -2
  152. package/docs/conventions.md +1 -1
  153. package/docs/getting-started.en.md +9 -9
  154. package/docs/getting-started.md +8 -8
  155. package/docs/glossary.en.md +80 -52
  156. package/docs/glossary.md +79 -51
  157. package/docs/icons.en.md +4 -1
  158. package/docs/icons.md +3 -0
  159. package/package.json +2 -1
  160. package/src/editor/RichTextEditor.tsx +9 -6
  161. package/src/editor/core/atomText.ts +2 -2
  162. package/src/editor/core/blockTypes.ts +19 -83
  163. package/src/editor/core/containers.ts +4 -2
  164. package/src/editor/core/customMarkdown.ts +2 -2
  165. package/src/editor/core/customTool.ts +6 -6
  166. package/src/editor/core/customToolEngine.ts +9 -9
  167. package/src/editor/core/extensions.ts +165 -110
  168. package/src/editor/core/fieldNotice.ts +1 -1
  169. package/src/editor/core/handle.ts +2 -2
  170. package/src/editor/core/insertItems.ts +12 -158
  171. package/src/editor/core/insertMenuTool.ts +32 -0
  172. package/src/editor/core/jsonDocument.ts +1 -1
  173. package/src/editor/core/schema.ts +42 -7
  174. package/src/editor/core/shortcuts.ts +116 -177
  175. package/src/editor/core/skeleton.ts +4 -3
  176. package/src/editor/core/toolButtons.ts +3 -3
  177. package/src/editor/core/toolDefinition.ts +4 -4
  178. package/src/editor/core/toolDescriptor.ts +65 -52
  179. package/src/editor/core/toolLoader.ts +3 -3
  180. package/src/editor/core/toolOptions.ts +21 -2
  181. package/src/editor/core/tools.ts +10 -1
  182. package/src/editor/core/viewIsland.ts +1 -1
  183. package/src/editor/features/builtins.ts +76 -41
  184. package/src/editor/features/code/codeBlockTool.ts +16 -8
  185. package/src/editor/features/color/colorTools.ts +68 -0
  186. package/src/editor/features/details/details.ts +389 -0
  187. package/src/editor/features/details/detailsMarkdown.ts +158 -0
  188. package/src/editor/features/details/detailsTool.ts +69 -0
  189. package/src/editor/features/emoji/emojiTool.tsx +1 -1
  190. package/src/editor/features/heading/headingTool.ts +96 -0
  191. package/src/editor/features/history/historyTool.ts +42 -0
  192. package/src/editor/features/image/imageTool.ts +49 -0
  193. package/src/editor/features/layout/layoutTool.ts +40 -0
  194. package/src/editor/features/lists/listTools.ts +112 -0
  195. package/src/editor/features/marks/markTools.ts +78 -0
  196. package/src/editor/features/search/searchTool.ts +1 -1
  197. package/src/editor/features/table/tableTool.ts +3 -3
  198. package/src/editor/features/toc/tocTool.ts +1 -1
  199. package/src/editor/features/wrapper/wrapperTool.ts +52 -0
  200. package/src/editor/index.ts +1 -0
  201. package/src/editor/schema-entry.ts +1 -1
  202. package/src/editor/styles/prose.ts +49 -0
  203. package/src/editor/ui/EditorToolbar.tsx +7 -50
  204. package/src/editor/ui/SelectionBubble.tsx +2 -5
  205. package/src/editor/ui/ToolButtonView.tsx +2 -1
  206. package/src/i18n/defaults.ts +3 -0
  207. package/src/i18n/messages.ts +14 -1
  208. package/src/icons.ts +7 -0
  209. package/src/locales/ru.ts +3 -0
@@ -1,20 +1,8 @@
1
1
  "use client";
2
2
 
3
- import type { ChainedCommands } from "@tiptap/core";
4
3
  import type { Node as PmNode } from "@tiptap/pm/model";
5
4
  import type { EditorState } from "@tiptap/pm/state";
6
- import type { ComponentType } from "react";
7
- import {
8
- Heading1Icon,
9
- Heading2Icon,
10
- Heading3Icon,
11
- Heading4Icon,
12
- ListIcon,
13
- OrderedListIcon,
14
- TaskListIcon,
15
- TextIcon,
16
- type IconProps,
17
- } from "../../icons.js";
5
+ import { TextIcon } from "../../icons.js";
18
6
  import type { PrefixedName } from "./customTool.js";
19
7
  import type { InsertItem } from "./insertItems.js";
20
8
  import { shortcutKeys } from "./shortcuts.js";
@@ -30,8 +18,8 @@ import type { Tool } from "./tools.js";
30
18
  * заново — одной транзакцией, на одну отмену. Своего пути смены типа у меню
31
19
  * нет: два пути разошлись бы на первом же списке внутри цитаты.
32
20
  *
33
- * Отдельный реестр, а не выборка из `INSERT_ITEMS`, потому что у меню «¶»
34
- * другие слова и иконки: «Текст» с иконкой `T`, а не
21
+ * Пункты «¶» — поле описания `blockTypes`, а не выборка из `insertItems`,
22
+ * потому что у меню «¶» могут быть другие слова и иконки: «Текст» с иконкой `T`, а не
35
23
  * «Абзац» с пилкроу, и уровень заголовка на иконке. Блока кода в нём нет — у
36
24
  * него своя кнопка Панели.
37
25
  */
@@ -61,70 +49,24 @@ export function turnIntoTypes(
61
49
  .map((item) => ({ ...item, section: "turnInto", tool, keys: item.keys ?? [] }));
62
50
  }
63
51
 
64
- const heading = (level: 1 | 2 | 3 | 4, icon: ComponentType<IconProps>): BlockTypeItem => ({
65
- id: `heading${level}`,
66
- section: "turnInto",
67
- tool: "heading",
68
- alsoTool: level === 1 ? "heading1" : undefined,
69
- label: (m) => m.heading({ level }),
70
- icon,
71
- keys: shortcutKeys(`h${level}`),
72
- run: (chain: ChainedCommands) => chain.setHeading({ level }),
73
- });
74
-
75
52
  /**
76
- * Пункты «¶» по порядку показа. Сочетания — подписи из общей таблицы
77
- * (`shortcuts.ts`), по тем же id, что у Меню вставки и справочника:
78
- * вешают их расширения движка, и подпись обязана совпадать с ними.
53
+ * «Текст» — пункт кита, а не Инструмента: абзац есть в любом наборе. Сочетание —
54
+ * подпись из общей таблицы (`shortcuts.ts`): вешает его расширение движка.
79
55
  */
80
- export const BLOCK_TYPE_ITEMS: readonly BlockTypeItem[] = [
81
- {
82
- id: "text",
83
- section: "turnInto",
84
- tool: null,
85
- label: (m) => m.textBlock,
86
- icon: TextIcon,
87
- keys: shortcutKeys("paragraph"),
88
- run: (chain) => chain.setParagraph(),
89
- },
90
- heading(1, Heading1Icon),
91
- heading(2, Heading2Icon),
92
- heading(3, Heading3Icon),
93
- heading(4, Heading4Icon),
94
- {
95
- id: "bulletList",
96
- section: "turnInto",
97
- tool: "bulletList",
98
- label: (m) => m.bulletList,
99
- icon: ListIcon,
100
- keys: shortcutKeys("bulletList"),
101
- run: (chain) => chain.toggleBulletList(),
102
- },
103
- {
104
- id: "orderedList",
105
- section: "turnInto",
106
- tool: "orderedList",
107
- label: (m) => m.orderedList,
108
- icon: OrderedListIcon,
109
- keys: shortcutKeys("orderedList"),
110
- run: (chain) => chain.toggleOrderedList(),
111
- },
112
- {
113
- id: "taskList",
114
- section: "turnInto",
115
- tool: "taskList",
116
- label: (m) => m.taskList,
117
- icon: TaskListIcon,
118
- keys: shortcutKeys("taskList"),
119
- run: (chain) => chain.toggleTaskList(),
120
- },
121
- ];
56
+ const TEXT_ITEM: BlockTypeItem = {
57
+ id: "text",
58
+ section: "turnInto",
59
+ tool: null,
60
+ label: (m) => m.textBlock,
61
+ icon: TextIcon,
62
+ keys: shortcutKeys("paragraph"),
63
+ run: (chain) => chain.setParagraph(),
64
+ };
122
65
 
123
66
  /**
124
67
  * Место встроенных Инструментов в «¶». «Текст» стоит первым всегда: абзац —
125
- * не Инструмент. Держится отдельно от `BLOCK_TYPE_ITEMS`: Инструмент,
126
- * переехавший на описание (`toolDescriptor.ts`), уходит из реестра, а место
127
- * остаётся здесь.
68
+ * не Инструмент. Пункты живут в описаниях, а место на поверхности — привилегия
69
+ * кита.
128
70
  */
129
71
  const BLOCK_TYPE_ORDER: readonly Tool[] = [
130
72
  "heading",
@@ -135,18 +77,12 @@ const BLOCK_TYPE_ORDER: readonly Tool[] = [
135
77
  ];
136
78
 
137
79
  /**
138
- * Пункты «¶» набора — проекция списка описаний: у переехавшего Инструмента —
139
- * из описания (`blockTypes`), у остальных — из `BLOCK_TYPE_ITEMS`.
80
+ * Пункты «¶» набора: «Текст» и проекция списка описаний (`blockTypes`).
140
81
  */
141
82
  function blockTypeItemsOf(tools: ToolList): readonly BlockTypeItem[] {
142
83
  return [
143
- ...BLOCK_TYPE_ITEMS.filter((item) => item.tool === null),
144
- ...projectRecords(
145
- tools,
146
- BLOCK_TYPE_ORDER,
147
- (tool) => BLOCK_TYPE_ITEMS.filter((item) => item.tool === tool),
148
- (descriptor) => descriptor.blockTypes
149
- ),
84
+ TEXT_ITEM,
85
+ ...projectRecords(tools, BLOCK_TYPE_ORDER, (descriptor) => descriptor.blockTypes),
150
86
  ].filter((item) => item.alsoTool === undefined || tools.has(item.alsoTool));
151
87
  }
152
88
 
@@ -24,8 +24,9 @@ export { WRAPPER_NODE };
24
24
 
25
25
  /**
26
26
  * Узлы, внутри которых контейнерного Блока нет ни одного: Обёртка, цитата,
27
- * пункты списков и ячейки таблицы. Колонки здесь нет — Обёртку она принимает;
28
- * Раскладка добавляет её своим аргументом.
27
+ * пункты списков, ячейки таблицы и содержимое сворачиваемого Блока
28
+ * (`details.ts`). Колонки здесь нет — Обёртку она принимает; Раскладка
29
+ * добавляет её своим аргументом.
29
30
  */
30
31
  const CONTAINER_FREE_NODES = [
31
32
  WRAPPER_NODE,
@@ -34,6 +35,7 @@ const CONTAINER_FREE_NODES = [
34
35
  "taskItem",
35
36
  "tableCell",
36
37
  "tableHeader",
38
+ "detailsContent",
37
39
  ];
38
40
 
39
41
  /**
@@ -12,7 +12,7 @@ import { devWarn } from "../../lib/dev-warn.js";
12
12
  /*
13
13
  Markdown своего Инструмента по умолчанию (ADR-0058, п. 3.1).
14
14
 
15
- Свои правила Markdown — в Части для движка: `renderMarkdown` и токенайзер
15
+ Свои правила Markdown — в Extensions: `renderMarkdown` и токенайзер
16
16
  Tiptap, отдельного поля в описании нет. Без них `@tiptap/markdown` молча
17
17
  выгрузил бы узел пустой строкой — вместе со всем текстом внутри. Поэтому
18
18
  Node и Mark без `renderMarkdown` кит дописывает сам: Блок выгружается своим
@@ -31,7 +31,7 @@ export const MARKDOWN_FALLBACK = "kitMarkdownFallback";
31
31
  /** Предупреждение о Markdown по умолчанию — одно на Инструмент (`devWarn` помнит текст). */
32
32
  export function warnNoMarkdown(tool: string): void {
33
33
  devWarn(
34
- `custom tool "${tool}" has no Markdown rules (renderMarkdown in its engine part): ` +
34
+ `custom tool "${tool}" has no Markdown rules (renderMarkdown in its Extensions): ` +
35
35
  "getMarkdown() writes its blocks as their content and its atoms as their text"
36
36
  );
37
37
  }
@@ -67,7 +67,7 @@ export interface CustomBlock {
67
67
  /**
68
68
  * Остров Блока в `RichTextView` — клиентский компонент из модуля с
69
69
  * директивой `"use client"` (ADR-0058, п. 3.3). Без него Блок в Просмотре —
70
- * очищенный HTML. Редактор поле не читает: его вид — Часть для движка.
70
+ * очищенный HTML. Редактор поле не читает: его вид задают Extensions.
71
71
  *
72
72
  * Только компонент из модуля с директивой: серверный компонент и функция-
73
73
  * загрузчик через границу RSC не проходят — сервер получает ссылку на
@@ -103,7 +103,7 @@ export interface CustomMark {
103
103
  * встроенных (`ToolDescriptor`, ADR-0058, п. 10). Передаётся в `tools`
104
104
  * вперемешку с именами Инструментов кита.
105
105
  *
106
- * Обязательное — имя, Часть для движка и правила Схемы для каждого Блока и
106
+ * Обязательное — имя, Extensions и правила Схемы для каждого Блока и
107
107
  * Метки, которые она определяет. Остальное — по умолчанию (ADR-0058, п. 1.6):
108
108
  * обычный Блок без Признаков, без UI-записей.
109
109
  */
@@ -111,8 +111,8 @@ export interface CustomTool {
111
111
  /** Имя с Префиксом потребителя, уникальное в `tools`. */
112
112
  name: PrefixedName;
113
113
  /**
114
- * Часть для движка — расширения Tiptap: Node, Mark, Extension, плагины
115
- * ProseMirror, React-вид узла, `renderText` атома. Её контракт следует
114
+ * Extensions — расширения Tiptap: Node, Mark, Extension, плагины
115
+ * ProseMirror, React-вид узла, `renderText` атома. Их контракт следует
116
116
  * мажору Tiptap. Node и Mark стоят на верхнем уровне массива: группу,
117
117
  * разбор HTML и свои атрибуты им ставит кит по правилам.
118
118
  *
@@ -123,9 +123,9 @@ export interface CustomTool {
123
123
  * того поле показывает Скелет; промис кэшируется на функции загрузчика.
124
124
  */
125
125
  engine: Extensions | (() => Promise<Extensions>);
126
- /** Правила Схемы для каждого Node Части для движка — по имени узла. */
126
+ /** Правила Схемы для каждого Node из Extensions — по имени узла. */
127
127
  blocks?: Readonly<Record<PrefixedName, CustomBlock>>;
128
- /** Правила Схемы для каждой Mark Части для движка — по имени Метки. */
128
+ /** Правила Схемы для каждой Mark из Extensions — по имени Метки. */
129
129
  marks?: Readonly<Record<PrefixedName, CustomMark>>;
130
130
 
131
131
  /* UI-записи (ADR-0058, п. 5 и 6): кнопки, пункты вставки и Действия над Блоком. */
@@ -43,13 +43,13 @@ import { loaded } from "./toolLoader.js";
43
43
  Свой Инструмент в движке (ADR-0058, разделы 1 и 2): из
44
44
  публичного описания — внутреннее, и проверка набора при сборке.
45
45
 
46
- ПРАВИЛА СХЕМЫ — ЕДИНСТВЕННЫЙ ИСТОЧНИК HTML. Node и Mark Части для движка кит
46
+ ПРАВИЛА СХЕМЫ — ЕДИНСТВЕННЫЙ ИСТОЧНИК HTML. Node и Mark из Extensions кит
47
47
  дописывает сам: группу и строчность — по виду Блока, разбор HTML — только
48
48
  `div|span[data-block="<имя>"]` и `span[data-mark="<имя>"]`, свои атрибуты —
49
49
  `data-<имя>` со значением по виду из правил, Признаки Блока — в spec узла.
50
50
  Своё правило разбора чужого Блока поэтому не займёт тег кита, а значение вне
51
51
  правил не войдёт в Документ ни из `value`, ни из вставки: проверяет код кита,
52
- и ослабить проверку Частью для движка нельзя. Поведение — содержимое,
52
+ и ослабить проверку через Extensions нельзя. Поведение — содержимое,
53
53
  `atom`, React-вид, `renderText`, команды — остаётся за потребителем.
54
54
  */
55
55
 
@@ -67,7 +67,7 @@ function allowed(rule: CustomAttribute, value: unknown): string | null {
67
67
  return isAllowedAttributeValue(rule, text) ? text : null;
68
68
  }
69
69
 
70
- /** Node или Mark Части для движка, дописанный по своим правилам Схемы. */
70
+ /** Node или Mark из Extensions, дописанный по своим правилам Схемы. */
71
71
  function bindRules(extension: AnyExtension, tool: CustomTool): AnyExtension {
72
72
  const { name } = extension;
73
73
  const block = extension.type === "node" ? blocksOf(tool)[name] : undefined;
@@ -80,7 +80,7 @@ function bindRules(extension: AnyExtension, tool: CustomTool): AnyExtension {
80
80
  const config = {
81
81
  ...markdownFallback(extension, tool.name),
82
82
  parseHTML: () => [{ tag: selector }],
83
- // Свой `renderHTML` остаётся у Части для движка (текст внутри атома);
83
+ // Свой `renderHTML` остаётся у Extensions (текст внутри атома);
84
84
  // соглашение с ним сверяет `customToolsError`.
85
85
  ...(getExtensionField(extension, "renderHTML") === undefined && {
86
86
  renderHTML: ({ node, HTMLAttributes }: { node?: PmNode; HTMLAttributes: object }) =>
@@ -211,7 +211,7 @@ function uiRecords(tool: CustomTool) {
211
211
  return { insertItems, blockTypes, blockActions, textCards };
212
212
  }
213
213
 
214
- /** Часть для движка: массив сразу или то, что отдал загрузчик; не массив — пустая. */
214
+ /** Extensions: массив сразу или то, что отдал загрузчик; не массив — пустой массив. */
215
215
  function engineOf(tool: CustomTool): Extensions {
216
216
  const engine = typeof tool.engine === "function" ? loaded(tool.engine) : tool.engine;
217
217
  return Array.isArray(engine) ? engine : [];
@@ -222,7 +222,7 @@ export function describeCustomTool(tool: CustomTool): ToolDescriptor {
222
222
  return {
223
223
  name: tool.name,
224
224
  custom: tool,
225
- // Загрузчик: правила Схемы ложатся на Часть, когда она пришла (`engineOf`).
225
+ // Загрузчик: правила Схемы ложатся на Extensions, когда они пришли (`engineOf`).
226
226
  load: typeof tool.engine === "function" ? tool.engine : undefined,
227
227
  engine: () => engineOf(tool).map((extension) => bindRules(extension, tool)),
228
228
  ...uiRecords(tool),
@@ -330,8 +330,8 @@ export interface CustomToolsError {
330
330
  /**
331
331
  * Ошибка сборки набора Инструментов или `null` (ADR-0058, п. 1.5, 2.4).
332
332
  *
333
- * Имя без Префикса потребителя, совпадение имён, Node или Mark Части для
334
- * движка без правил Схемы (и правило без них), атрибут, которого нет в
333
+ * Имя без Префикса потребителя, совпадение имён, Node или Mark из
334
+ * Extensions без правил Схемы (и правило без них), атрибут, которого нет в
335
335
  * правилах, HTML мимо соглашения, Схема, которую движок не собрал, — всё это
336
336
  * потеря данных на сервере или в самом Редакторе, и автор должен увидеть её
337
337
  * до первого сохранения. `extensions` — полный набор движка, свои уже в нём.
@@ -442,7 +442,7 @@ export function customToolsError(tools: ToolList, extensions: Extensions): Custo
442
442
  if (extra) return fail(`has attribute "${extra}" with no rule`);
443
443
  /*
444
444
  HTML — по соглашению: `toDOM` экземпляра по умолчанию даёт тег и
445
- метку с именем. Свой `renderHTML` Части для движка мог уйти от них, и
445
+ метку с именем. Свой `renderHTML` в Extensions мог уйти от них, и
446
446
  тогда Блок не прочитался бы обратно из собственного `getHTML()`.
447
447
  */
448
448
  const { tag, marker } = customElement(
@@ -7,35 +7,25 @@
7
7
 
8
8
  import { Extension, type Extensions } from "@tiptap/core";
9
9
  import { Plugin, PluginKey } from "@tiptap/pm/state";
10
- import { TaskItem, TaskList } from "@tiptap/extension-list";
11
- import { Subscript } from "@tiptap/extension-subscript";
12
- import { Superscript } from "@tiptap/extension-superscript";
13
10
  import { Placeholder } from "@tiptap/extensions";
14
- import { StarterKit } from "@tiptap/starter-kit";
11
+ import { StarterKit, type StarterKitOptions } from "@tiptap/starter-kit";
15
12
  import { AtomText } from "./atomText.js";
16
13
  import { BlockCommands, BlockMenuKey } from "./blockCommands.js";
17
14
  import { FieldNoticeExtension, type FieldNoticeOptions } from "./fieldNotice.js";
18
- import { RichImage, type RichImageOptions } from "../features/image/image.js";
15
+ import type { RichImageOptions } from "../features/image/image.js";
19
16
  import { createDocumentNode } from "./document.js";
20
- import { LAYOUT_NODE, layoutExtensions } from "../features/layout/layout.js";
21
17
  import { createMarkdown, MarkdownPaste } from "./markdown.js";
22
18
  import { MaxLength } from "./maxLength.js";
19
+ import { TriggerCommand, type SlashBridge, type TriggerBinding } from "./slashCommand.js";
23
20
  import {
24
- HighlightExtension,
25
- TEXT_COLOR_EXTENSIONS,
26
- TextAlignExtension,
27
- } from "../features/color/styleExtensions.js";
28
- import {
29
- SlashCommand,
30
- TriggerCommand,
31
- type SlashBridge,
32
- type TriggerBinding,
33
- } from "./slashCommand.js";
34
- import { describedTools, type ToolList } from "./toolDescriptor.js";
21
+ type BlockTraits,
22
+ type ToolDescriptor,
23
+ type ToolEngineContext,
24
+ type ToolList,
25
+ } from "./toolDescriptor.js";
35
26
  import { ToolButtons, type ToolPopoverEntry, type ToolPopoverRequest } from "./toolButtons.js";
36
27
  import { ToolEntryShortcuts } from "./toolEntryShortcuts.js";
37
28
  import type { Tool } from "./tools.js";
38
- import { WRAPPER_EXTENSIONS, WRAPPER_NODE } from "../features/wrapper/wrapper.js";
39
29
 
40
30
  export interface EditorExtensionOptions {
41
31
  /** Включённые инструменты: выключенный не попадает в Схему вовсе. */
@@ -46,6 +36,11 @@ export interface EditorExtensionOptions {
46
36
  limit: () => number | undefined;
47
37
  /** Имя флажка пункта чек-листа — из каталога, читается при каждой перерисовке пункта. */
48
38
  taskCheckbox: (text: string) => string;
39
+ /**
40
+ * Имя кнопки раскрытия сворачиваемого Блока по тексту его заголовка — из
41
+ * каталога, читается при каждой перерисовке кнопки.
42
+ */
43
+ detailsToggle: (text: string) => string;
49
44
  /**
50
45
  * Мост Меню вставки по `/`. Нет — меню не открывается: инструмент
51
46
  * `insertMenu` выключен или пунктов для него нет.
@@ -123,6 +118,115 @@ const HandledTabStaysInside = Extension.create({
123
118
  ],
124
119
  });
125
120
 
121
+ /**
122
+ * Признаки Блоков описаний (`blocks`) — в spec узлов Схемы: `blockContainer`,
123
+ * `turnInto` и `plainAtom`, которые читают ручка и Меню выделения. Расширение
124
+ * без разметки: `extendNodeSchema` у Tiptap зовётся для каждого узла набора, и
125
+ * признак находит свой узел по имени. Нет признаков — расширения нет.
126
+ */
127
+ function blockTraitsExtension(traits: Readonly<Record<string, BlockTraits>>) {
128
+ return Extension.create({
129
+ name: "blockTraits",
130
+ extendNodeSchema: (extension) => {
131
+ const own = Object.hasOwn(traits, extension.name) ? traits[extension.name] : undefined;
132
+ return own
133
+ ? {
134
+ blockContainer: own.container === true,
135
+ turnInto: own.turnInto === true,
136
+ plainAtom: own.plainAtom === true,
137
+ }
138
+ : {};
139
+ },
140
+ });
141
+ }
142
+
143
+ /**
144
+ * Ключи StarterKit, которые принадлежат Инструментам. Все выключены, пока их не
145
+ * включит доля описания (`starterKit`): Метка, которой нет в наборе, не
146
+ * попадает в Схему. Цитата, разделитель, блок кода и
147
+ * ссылка — свои расширения кита, а `trailingNode` выключен всегда (см. выше).
148
+ * Документ, абзац, текст, перенос, пункт списка, его клавиши и курсоры
149
+ * StarterKit остаются его умолчаниями: они есть в любом наборе.
150
+ */
151
+ const STARTER_KIT_OFF: Partial<StarterKitOptions> = {
152
+ heading: false,
153
+ bold: false,
154
+ italic: false,
155
+ underline: false,
156
+ strike: false,
157
+ code: false,
158
+ bulletList: false,
159
+ orderedList: false,
160
+ undoRedo: false,
161
+ blockquote: false,
162
+ codeBlock: false,
163
+ horizontalRule: false,
164
+ link: false,
165
+ trailingNode: false,
166
+ };
167
+
168
+ /** Место символов-триггеров в `ENGINE_ORDER`: они не Инструмент, но порядок держат. */
169
+ const TRIGGERS = "triggers";
170
+
171
+ /**
172
+ * ПОРЯДОК РЕГИСТРАЦИИ Extensions встроенных Инструментов — привилегия кита, как
173
+ * порядок поверхностей, и описание его не задаёт. От него зависят:
174
+ *
175
+ * - **ранг Меток** — а с ним вложенность Меток в сохранённом HTML;
176
+ * - **вставка и drop** (ADR-0058, п. 9.5): плагины ProseMirror у Tiptap идут в
177
+ * обратном порядке регистрации при равном `priority`, и зарегистрированное
178
+ * последним опрашивается первым. Свои Инструменты стоят после встроенных
179
+ * (`expandTools`), поэтому `transformPastedHTML`, `transformPasted`,
180
+ * `handlePaste` и `handleDrop` своего Инструмента видят событие раньше и могут
181
+ * забрать его. Давать встроенному плагину `priority` выше 100 нельзя без правки
182
+ * этого правила;
183
+ * - **клавиши**: `Enter`, `Backspace` и стрелки сворачиваемого Блока (`details`)
184
+ * срабатывают только в его заголовке и в пустом конце содержимого, поэтому
185
+ * с клавишами других Блоков не спорят. Он стоит сразу за контейнерами
186
+ * (Раскладка, Обёртка): позже ядра и StarterKit, значит, опрашивается раньше
187
+ * них — `Enter` в заголовке уходит в содержимое, а не режет заголовок надвое;
188
+ * - **клавиши**: `Alt+F10` блока кода (`codeBlockTool.ts`) зарегистрирован после
189
+ * `ToolbarFocus` и опрашивается раньше; Tab в таблице — раньше Раскладки.
190
+ *
191
+ * Список сохраняет порядок, который был до переезда встроенных на описания, —
192
+ * эталонные тесты (`rich-text-editor-golden.test.tsx`) держат сохранённый HTML и клавиши. Метки и
193
+ * Блоки из StarterKit регистрирует он сам, в своём порядке.
194
+ */
195
+ const ENGINE_ORDER: readonly (Tool | typeof TRIGGERS)[] = [
196
+ "superscript",
197
+ "subscript",
198
+ "color",
199
+ "highlight",
200
+ "align",
201
+ "taskList",
202
+ "image",
203
+ "layout",
204
+ "wrapper",
205
+ "details",
206
+ "insertMenu",
207
+ TRIGGERS,
208
+ "history",
209
+ "heading",
210
+ "heading1",
211
+ "bold",
212
+ "italic",
213
+ "underline",
214
+ "strike",
215
+ "code",
216
+ "clearFormatting",
217
+ "link",
218
+ "bulletList",
219
+ "orderedList",
220
+ "blockquote",
221
+ "codeBlock",
222
+ "horizontalRule",
223
+ "table",
224
+ "emoji",
225
+ "source",
226
+ "search",
227
+ "toc",
228
+ ];
229
+
126
230
  /**
127
231
  * Набор расширений — это и есть Схема в коде: что не зарегистрировано здесь,
128
232
  * ProseMirror не разберёт ни из `value`, ни из буфера. Белый список ДАННЫМИ,
@@ -148,56 +252,29 @@ const HandledTabStaysInside = Extension.create({
148
252
  * `codeBlock` из StarterKit: язык из `codeLanguages.ts` и подсветка
149
253
  * декорациями lowlight (ADR-0051). В Документ по-прежнему уходит только
150
254
  * `class="language-*"` на `<code>`. Оно, как и таблица, приходит
151
- * ленивой Частью для движка из описания (`codeBlockTool.ts`, `tableTool.ts`).
255
+ * из описания ленивыми Extensions (`codeBlockTool.ts`, `tableTool.ts`).
152
256
  */
153
257
  export function createEditorExtensions(options: EditorExtensionOptions): Extensions {
154
258
  /*
155
- Движок — проекция списка описаний (`toolDescriptor.ts`): Часть для движка
156
- переехавшего Инструмента берётся из описания, остальных — отсюда, как
157
- раньше. `on` — «Инструмент включён и его расширение ещё здесь»; связи
158
- между Инструментами (уровни заголовка, Обёртка в колонке) спрашивают сам
159
- набор.
259
+ Движок — проекция списка описаний (`toolDescriptor.ts`): Extensions
260
+ Инструмента берутся из его описания. Связи между Инструментами (уровни
261
+ заголовка, Обёртка в колонке) спрашивают `has` контекста.
160
262
  */
161
- const described = describedTools(options.tools);
162
- const on = (tool: Tool) =>
163
- options.tools.has(tool) && !described.some((descriptor) => descriptor.name === tool);
164
- // `false` выключает расширение внутри StarterKit, `undefined` оставляет его
165
- // настройки по умолчанию.
166
- const toggle = (tool: Tool) => (on(tool) ? undefined : false);
263
+ const described = options.tools.descriptors;
264
+ // Без самого набора: связи между Инструментами — только через `has`.
265
+ const { tools, ...bridges } = options;
266
+ const context: ToolEngineContext = { ...bridges, has: (tool) => tools.has(tool) };
167
267
  // Контейнеры Блоков верхнего уровня: их принимает только свой Документ.
168
- const containers = [
169
- ...(on("layout") ? [LAYOUT_NODE] : []),
170
- ...(on("wrapper") ? [WRAPPER_NODE] : []),
171
- ...described.flatMap((descriptor) => descriptor.documentContent ?? []),
172
- ];
268
+ const containers = described.flatMap((descriptor) => descriptor.documentContent ?? []);
173
269
 
174
270
  const extensions: Extensions = [
175
271
  StarterKit.configure({
272
+ ...STARTER_KIT_OFF,
273
+ // Доли описаний — одним StarterKit: порядок его Меток не меняется.
274
+ ...Object.assign({}, ...described.map((descriptor) => descriptor.starterKit?.(context))),
176
275
  // С Раскладкой или Обёрткой Документ — свой (`document.ts`): они не в
177
276
  // группе `block`, и принять их может только тот, кто назвал их поимённо.
178
277
  document: containers.length > 0 ? false : undefined,
179
- // H1 — только с `heading1`; без него `#` и `<h1>` остаются
180
- // прежними: правило ввода и разбор HTML строятся по этим уровням.
181
- heading: on("heading")
182
- ? { levels: options.tools.has("heading1") ? [1, 2, 3, 4] : [2, 3, 4] }
183
- : false,
184
- bold: toggle("bold"),
185
- italic: toggle("italic"),
186
- underline: toggle("underline"),
187
- strike: toggle("strike"),
188
- code: toggle("code"),
189
- bulletList: toggle("bulletList"),
190
- orderedList: toggle("orderedList"),
191
- // Цитата и разделитель переехали на описание (`features/builtins.ts`): их Часть для движка — оттуда.
192
- blockquote: false,
193
- // Блок кода — `RichCodeBlock` из описания (`codeBlockTool.ts`): язык и подсветка (ADR-0051).
194
- codeBlock: false,
195
- horizontalRule: false,
196
- undoRedo: toggle("history"),
197
- // Ссылка — `RichLink` из описания Инструмента (`linkTool.tsx`): StarterKit
198
- // отдаёт только опции, а разбор `<a>` нужно сузить до Схемы.
199
- link: false,
200
- trailingNode: false,
201
278
  }),
202
279
  ...(containers.length > 0 ? [createDocumentNode(containers)] : []),
203
280
  Placeholder.configure({ placeholder: () => options.placeholder() }),
@@ -212,64 +289,36 @@ export function createEditorExtensions(options: EditorExtensionOptions): Extensi
212
289
  HandledTabStaysInside,
213
290
  // Команды над Блоками — своей разметки нет, включены всегда (`blockCommands.ts`).
214
291
  // После ядра Tiptap: его `Mod+A` (весь Документ) опрашивается позже нашего.
215
- BlockCommands.configure({ clearFormatting: on("clearFormatting") }),
292
+ BlockCommands.configure({ clearFormatting: options.tools.has("clearFormatting") }),
216
293
  // `⌘/` — меню ручки Блока по месту клавиши, раньше карт Меток (`blockCommands.ts`).
217
294
  BlockMenuKey.configure({ openBlockMenu: options.openBlockMenu }),
218
295
  // `showNotice` — в каждом Редакторе: сообщить может любой Инструмент (`fieldNotice.ts`).
219
296
  FieldNoticeExtension.configure({ onNotice: options.onNotice }),
220
297
  ];
221
- if (on("superscript")) extensions.push(Superscript);
222
- if (on("subscript")) extensions.push(Subscript);
223
- // Цвет, маркер и выравнивание пишут `data-color` и `data-align`, а не `style`
224
- // (`styleExtensions.ts`; единственное исключение — цвет автора, `customColor.ts`):
225
- // выключенный инструмент не отдаёт ни атрибута, ни правила разбора, и такой
226
- // атрибут из вставки отбрасывается.
227
- if (on("color")) extensions.push(...TEXT_COLOR_EXTENSIONS);
228
- if (on("highlight")) extensions.push(HighlightExtension);
229
- if (on("align")) extensions.push(TextAlignExtension);
230
- if (on("taskList")) {
231
- extensions.push(
232
- TaskList,
233
- TaskItem.configure({
234
- // Вложенные чек-листы — как вложенные обычные списки: Tab и Shift+Tab.
235
- nested: true,
236
- a11y: { checkboxLabel: (node) => options.taskCheckbox(node.textContent) },
237
- })
238
- );
239
- }
240
- // Картинка — `figure` с подписью (`image.ts`). Без инструмента `<img>` из
241
- // вставки и `value` отбрасывается, а файлы из буфера не перехватываются.
242
- if (on("image") && options.image) extensions.push(RichImage.configure(options.image));
243
- // Раскладка — узлы колонок и их клавиатура (`layout.ts`). Без инструмента
244
- // `div[data-block]` из вставки и `value` не разбирается, и Блоки колонок
245
- // встают в Документ подряд.
246
- if (on("layout")) extensions.push(...layoutExtensions({ wrapper: options.tools.has("wrapper") }));
247
- // Обёртка — узел и выход из неё по Enter (`wrapper.ts`). Без инструмента
248
- // `div[data-block="wrapper"]` из вставки и `value` не разбирается, и её
249
- // Блоки встают на её место подряд.
250
- if (on("wrapper")) extensions.push(...WRAPPER_EXTENSIONS);
251
- if (on("insertMenu") && options.slash) {
252
- extensions.push(SlashCommand.configure({ bridge: options.slash }));
298
+ // Признаки Блоков описаний (`blocks`) — одним расширением на весь набор.
299
+ const traits: Record<string, BlockTraits> = Object.assign(
300
+ {},
301
+ ...described.map((descriptor) => descriptor.blocks)
302
+ );
303
+ if (Object.keys(traits).length > 0) extensions.push(blockTraitsExtension(traits));
304
+ // Extensions встроенных Инструментов — в порядке кита, свои — следом в порядке списка.
305
+ const engineOf = (descriptor: ToolDescriptor) =>
306
+ descriptor.engine?.(context, descriptor.options) ?? [];
307
+ for (const slot of ENGINE_ORDER) {
308
+ if (slot === TRIGGERS) {
309
+ if (options.triggers?.length) {
310
+ extensions.push(TriggerCommand.configure({ triggers: options.triggers }));
311
+ }
312
+ continue;
313
+ }
314
+ const descriptor = described.find((candidate) => candidate.name === slot);
315
+ if (descriptor) extensions.push(...engineOf(descriptor));
253
316
  }
254
- if (options.triggers?.length) {
255
- extensions.push(TriggerCommand.configure({ triggers: options.triggers }));
317
+ for (const descriptor of described) {
318
+ if (!(ENGINE_ORDER as readonly string[]).includes(descriptor.name)) {
319
+ extensions.push(...engineOf(descriptor));
320
+ }
256
321
  }
257
- /*
258
- Части для движка переехавших Инструментов — в порядке списка. ЗДЕСЬ ЖЕ
259
- ЗАДАЁТСЯ ПОРЯДОК ВСТАВКИ И DROP (ADR-0058, п. 9.5): плагины ProseMirror у
260
- Tiptap идут в обратном порядке регистрации при равном `priority`, и
261
- зарегистрированное последним опрашивается первым. Свои Инструменты стоят в
262
- конце списка (`expandTools`: за встроенными), а встроенные вставка Markdown,
263
- загрузка картинок и Раскладка зарегистрированы выше — поэтому
264
- `transformPastedHTML`, `transformPasted`, `handlePaste` и `handleDrop` своего
265
- Инструмента видят событие раньше и могут забрать его. Менять порядок
266
- регистрации или давать встроенному плагину `priority` выше 100 нельзя без
267
- правки этого правила. На том же порядке держится `Alt+F10` блока кода
268
- (`codeBlockTool.ts`): его расширение здесь, после `ToolbarFocus`, и
269
- опрашивается раньше.
270
- */
271
- for (const descriptor of described)
272
- extensions.push(...(descriptor.engine?.(options, descriptor.options) ?? []));
273
322
  // Сочетания кнопок описаний вешает кит: поле записи — их единственный источник.
274
323
  const buttons = described.flatMap((descriptor) => descriptor.buttons ?? []);
275
324
  // Поповер пункта вставки и Действия открывает та же команда, что у кнопки.
@@ -281,8 +330,14 @@ export function createEditorExtensions(options: EditorExtensionOptions): Extensi
281
330
  .flatMap(({ id, label, popover }): ToolPopoverEntry[] =>
282
331
  popover ? [{ id, label: ({ messages, locale }) => label(messages, locale), popover }] : []
283
332
  );
284
- if (buttons.length + entries.length > 0) {
285
- extensions.push(ToolButtons.configure({ buttons, entries, openPopover: options.openPopover }));
333
+ // Кнопке без сочетания и поповера (отмена, картинка) расширение не нужно: её
334
+ // `run` зовёт Панель. Так набор расширений не меняется от того, что кнопка
335
+ // переехала в описание.
336
+ const keyed = buttons.filter((button) => button.shortcut || button.popover);
337
+ if (keyed.length + entries.length > 0) {
338
+ extensions.push(
339
+ ToolButtons.configure({ buttons: keyed, entries, openPopover: options.openPopover })
340
+ );
286
341
  }
287
342
  // То же — у пунктов вставки и Действий над Блоком.
288
343
  const keyedItems = described.flatMap((d) => d.insertItems ?? []).filter((i) => i.shortcut);