@olddevs/ui 4.4.0 → 4.5.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 (159) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/editor/RichTextEditor.d.ts +11 -4
  3. package/dist/editor/RichTextEditor.d.ts.map +1 -1
  4. package/dist/editor/RichTextEditor.js +90 -60
  5. package/dist/editor/RichTextEditor.js.map +1 -1
  6. package/dist/editor/RichTextView.d.ts +2 -1
  7. package/dist/editor/RichTextView.d.ts.map +1 -1
  8. package/dist/editor/RichTextView.js.map +1 -1
  9. package/dist/editor/core/containers.d.ts +2 -6
  10. package/dist/editor/core/containers.d.ts.map +1 -1
  11. package/dist/editor/core/containers.js +2 -1
  12. package/dist/editor/core/containers.js.map +1 -1
  13. package/dist/editor/core/customTool.d.ts +8 -2
  14. package/dist/editor/core/customTool.d.ts.map +1 -1
  15. package/dist/editor/core/customTool.js.map +1 -1
  16. package/dist/editor/core/customToolEngine.d.ts +9 -1
  17. package/dist/editor/core/customToolEngine.d.ts.map +1 -1
  18. package/dist/editor/core/customToolEngine.js +12 -0
  19. package/dist/editor/core/customToolEngine.js.map +1 -1
  20. package/dist/editor/core/extensions.d.ts.map +1 -1
  21. package/dist/editor/core/extensions.js +1 -1
  22. package/dist/editor/core/extensions.js.map +1 -1
  23. package/dist/editor/core/handle.d.ts +18 -1
  24. package/dist/editor/core/handle.d.ts.map +1 -1
  25. package/dist/editor/core/handle.js +2 -1
  26. package/dist/editor/core/handle.js.map +1 -1
  27. package/dist/editor/core/handleWatchers.d.ts +30 -0
  28. package/dist/editor/core/handleWatchers.d.ts.map +1 -1
  29. package/dist/editor/core/handleWatchers.js +15 -0
  30. package/dist/editor/core/handleWatchers.js.map +1 -1
  31. package/dist/editor/core/nodeNames.d.ts +7 -0
  32. package/dist/editor/core/nodeNames.d.ts.map +1 -0
  33. package/dist/editor/core/nodeNames.js +2 -0
  34. package/dist/editor/core/nodeNames.js.map +1 -0
  35. package/dist/editor/core/schema.d.ts.map +1 -1
  36. package/dist/editor/core/schema.js +4 -2
  37. package/dist/editor/core/schema.js.map +1 -1
  38. package/dist/editor/core/toolCommands.d.ts +78 -0
  39. package/dist/editor/core/toolCommands.d.ts.map +1 -0
  40. package/dist/editor/core/toolCommands.js +2 -0
  41. package/dist/editor/core/toolCommands.js.map +1 -0
  42. package/dist/editor/core/toolDefinition.d.ts +139 -0
  43. package/dist/editor/core/toolDefinition.d.ts.map +1 -0
  44. package/dist/editor/core/toolDefinition.js +28 -0
  45. package/dist/editor/core/toolDefinition.js.map +1 -0
  46. package/dist/editor/core/toolDescriptor.d.ts +71 -2
  47. package/dist/editor/core/toolDescriptor.d.ts.map +1 -1
  48. package/dist/editor/core/toolDescriptor.js +6 -2
  49. package/dist/editor/core/toolDescriptor.js.map +1 -1
  50. package/dist/editor/core/toolOptions.d.ts +157 -0
  51. package/dist/editor/core/toolOptions.d.ts.map +1 -0
  52. package/dist/editor/core/toolOptions.js +21 -0
  53. package/dist/editor/core/toolOptions.js.map +1 -0
  54. package/dist/editor/core/toolState.d.ts +46 -0
  55. package/dist/editor/core/toolState.d.ts.map +1 -0
  56. package/dist/editor/core/toolState.js +2 -0
  57. package/dist/editor/core/toolState.js.map +1 -0
  58. package/dist/editor/core/tools.d.ts +3 -1
  59. package/dist/editor/core/tools.d.ts.map +1 -1
  60. package/dist/editor/core/tools.js.map +1 -1
  61. package/dist/editor/core/viewIsland.d.ts.map +1 -1
  62. package/dist/editor/core/viewIsland.js +2 -1
  63. package/dist/editor/core/viewIsland.js.map +1 -1
  64. package/dist/editor/features/builtins.d.ts +9 -4
  65. package/dist/editor/features/builtins.d.ts.map +1 -1
  66. package/dist/editor/features/builtins.js +36 -7
  67. package/dist/editor/features/builtins.js.map +1 -1
  68. package/dist/editor/features/search/SearchPanel.d.ts.map +1 -1
  69. package/dist/editor/features/search/SearchPanel.js +26 -12
  70. package/dist/editor/features/search/SearchPanel.js.map +1 -1
  71. package/dist/editor/features/search/revealMatch.d.ts +16 -0
  72. package/dist/editor/features/search/revealMatch.d.ts.map +1 -1
  73. package/dist/editor/features/search/revealMatch.js +22 -0
  74. package/dist/editor/features/search/revealMatch.js.map +1 -1
  75. package/dist/editor/features/search/searchPlugin.d.ts +8 -3
  76. package/dist/editor/features/search/searchPlugin.d.ts.map +1 -1
  77. package/dist/editor/features/search/searchPlugin.js +17 -6
  78. package/dist/editor/features/search/searchPlugin.js.map +1 -1
  79. package/dist/editor/features/search/searchSnapshot.d.ts +14 -0
  80. package/dist/editor/features/search/searchSnapshot.d.ts.map +1 -0
  81. package/dist/editor/features/search/searchSnapshot.js +17 -0
  82. package/dist/editor/features/search/searchSnapshot.js.map +1 -0
  83. package/dist/editor/features/search/searchTool.d.ts +3 -0
  84. package/dist/editor/features/search/searchTool.d.ts.map +1 -1
  85. package/dist/editor/features/search/searchTool.js +30 -1
  86. package/dist/editor/features/search/searchTool.js.map +1 -1
  87. package/dist/editor/features/toc/RichTextToc.d.ts +29 -1
  88. package/dist/editor/features/toc/RichTextToc.d.ts.map +1 -1
  89. package/dist/editor/features/toc/RichTextToc.js +55 -10
  90. package/dist/editor/features/toc/RichTextToc.js.map +1 -1
  91. package/dist/editor/features/toc/documentHeadings.d.ts +30 -0
  92. package/dist/editor/features/toc/documentHeadings.d.ts.map +1 -0
  93. package/dist/editor/features/toc/documentHeadings.js +20 -0
  94. package/dist/editor/features/toc/documentHeadings.js.map +1 -0
  95. package/dist/editor/features/toc/headings.d.ts +6 -2
  96. package/dist/editor/features/toc/headings.d.ts.map +1 -1
  97. package/dist/editor/features/toc/headings.js +4 -2
  98. package/dist/editor/features/toc/headings.js.map +1 -1
  99. package/dist/editor/features/toc/tocSnapshot.d.ts +10 -0
  100. package/dist/editor/features/toc/tocSnapshot.d.ts.map +1 -0
  101. package/dist/editor/features/toc/tocSnapshot.js +8 -0
  102. package/dist/editor/features/toc/tocSnapshot.js.map +1 -0
  103. package/dist/editor/features/toc/tocTool.d.ts +6 -0
  104. package/dist/editor/features/toc/tocTool.d.ts.map +1 -0
  105. package/dist/editor/features/toc/tocTool.js +21 -0
  106. package/dist/editor/features/toc/tocTool.js.map +1 -0
  107. package/dist/editor/hooks/useRichTextToolState.d.ts +43 -0
  108. package/dist/editor/hooks/useRichTextToolState.d.ts.map +1 -0
  109. package/dist/editor/hooks/useRichTextToolState.js +77 -0
  110. package/dist/editor/hooks/useRichTextToolState.js.map +1 -0
  111. package/dist/editor/index.d.ts +8 -0
  112. package/dist/editor/index.d.ts.map +1 -1
  113. package/dist/editor/index.js +3 -0
  114. package/dist/editor/index.js.map +1 -1
  115. package/docs/api/editor-extension.en.md +223 -23
  116. package/docs/api/editor-extension.md +224 -22
  117. package/docs/api/migration-notes.en.md +36 -1
  118. package/docs/api/migration-notes.md +35 -0
  119. package/docs/components/content/rich-text-toc.en.md +13 -11
  120. package/docs/components/content/rich-text-toc.md +12 -10
  121. package/docs/components/index.en.md +2 -2
  122. package/docs/components/index.md +1 -1
  123. package/docs/components/inputs/rich-text-editor.en.md +227 -11
  124. package/docs/components/inputs/rich-text-editor.md +228 -10
  125. package/docs/getting-started.en.md +7 -7
  126. package/docs/getting-started.md +6 -6
  127. package/docs/glossary.en.md +52 -48
  128. package/docs/glossary.md +51 -47
  129. package/package.json +1 -1
  130. package/src/editor/RichTextEditor.tsx +94 -39
  131. package/src/editor/RichTextView.tsx +2 -1
  132. package/src/editor/core/containers.ts +8 -6
  133. package/src/editor/core/customTool.ts +8 -2
  134. package/src/editor/core/customToolEngine.ts +20 -1
  135. package/src/editor/core/extensions.ts +2 -1
  136. package/src/editor/core/handle.ts +25 -1
  137. package/src/editor/core/handleWatchers.ts +45 -0
  138. package/src/editor/core/nodeNames.ts +7 -0
  139. package/src/editor/core/schema.ts +5 -2
  140. package/src/editor/core/toolCommands.ts +85 -0
  141. package/src/editor/core/toolDefinition.ts +208 -0
  142. package/src/editor/core/toolDescriptor.ts +94 -3
  143. package/src/editor/core/toolOptions.ts +211 -0
  144. package/src/editor/core/toolState.ts +56 -0
  145. package/src/editor/core/tools.ts +3 -1
  146. package/src/editor/core/viewIsland.ts +5 -4
  147. package/src/editor/features/builtins.ts +72 -13
  148. package/src/editor/features/search/SearchPanel.tsx +38 -14
  149. package/src/editor/features/search/revealMatch.ts +45 -0
  150. package/src/editor/features/search/searchPlugin.ts +40 -11
  151. package/src/editor/features/search/searchSnapshot.ts +29 -0
  152. package/src/editor/features/search/searchTool.ts +51 -1
  153. package/src/editor/features/toc/RichTextToc.tsx +108 -10
  154. package/src/editor/features/toc/documentHeadings.ts +59 -0
  155. package/src/editor/features/toc/headings.ts +10 -3
  156. package/src/editor/features/toc/tocSnapshot.ts +24 -0
  157. package/src/editor/features/toc/tocTool.ts +41 -0
  158. package/src/editor/hooks/useRichTextToolState.ts +153 -0
  159. package/src/editor/index.ts +19 -0
@@ -8,13 +8,16 @@ source: src/editor/index.ts
8
8
 
9
9
  ## Кратко для AI
10
10
 
11
- Карта того, чем продукт расширяет `RichTextEditor` из `@olddevs/ui/editor`, и граница стабильности этого API. Единица расширения — **свой Инструмент** (`CustomTool`): описание одной возможности Редактора, которое передаётся в `tools` вперемешку с именами кита. Он несёт Блоки и Метки с правилами Схемы (это данные), Часть для движка (расширения Tiptap, сразу или Загрузчиком) и UI-записи: кнопки, Пункты вставки, Действия над Блоком, Карточки у текста. Есть и свой Символ-триггер (`trigger`): `@` для упоминаний открывает тот же список, что `/`. Код экрана управляет полем через Хэндл (`ref`) и хук `useRichTextEditorState`, узнаёт о Готовности и Фокусе поля колбэками `onReady`, `onFocus` и `onBlur`, кладёт своё в Строку статуса (`status`) и сообщает об ошибке командой `showNotice`. Тот же массив `tools` понимают серверная очистка (`sanitizeRichText`, `richTextSchema`) и `RichTextView` с Островами. Всё перечисленное подчиняется semver кита с первого выпуска, HTML-соглашение заморожено, мажор Tiptap — мажор кита (раздел «Стабильность»). Подробности каждого пункта живут на странице [RichTextEditor](../components/inputs/rich-text-editor.md); здесь — карта, граница контракта и рецепты.
11
+ Карта того, чем продукт расширяет `RichTextEditor` из `@olddevs/ui/editor`, и граница стабильности этого API. Единица расширения — **свой Инструмент** (`CustomTool`): описание одной возможности Редактора, которое передаётся в `tools` вперемешку с именами кита. Он несёт Блоки и Метки с правилами Схемы (это данные), Часть для движка (расширения Tiptap, сразу или Загрузчиком) и UI-записи: кнопки, Пункты вставки, Действия над Блоком, Карточки у текста. Есть и свой Символ-триггер (`trigger`): `@` для упоминаний открывает тот же список, что `/`. Код экрана управляет полем через Хэндл (`ref`) и хук `useRichTextEditorState`, узнаёт о Готовности и Фокусе поля колбэками `onReady`, `onFocus` и `onBlur`, кладёт своё в Строку статуса (`status`) и сообщает об ошибке командой `showNotice`. У Инструмента три типизированные части, как `options`, `storage` и `commands` у расширения Tiptap: **Опции** (`tool(name, options)` у встроенных `search` и `toc`, `defineTool` у своего), **Состояние** (хук `useRichTextToolState`) и **Команды** (`handle.tool(name)`); их типы собраны в расширяемой карте `RichTextTools`, куда свой Инструмент дописывается через `declare module "@olddevs/ui/editor"`. Тот же массив `tools` понимают серверная очистка (`sanitizeRichText`, `richTextSchema`) и `RichTextView` с Островами. Всё перечисленное подчиняется semver кита с первого выпуска, HTML-соглашение заморожено, мажор Tiptap — мажор кита (раздел «Стабильность»). Подробности каждого пункта живут на странице [RichTextEditor](../components/inputs/rich-text-editor.md); здесь — карта, граница контракта и рецепты.
12
12
 
13
13
  ```tsx
14
14
  import {
15
15
  RICH_TEXT_PRESETS,
16
16
  RichTextEditor,
17
+ defineTool,
18
+ tool,
17
19
  useRichTextEditorState,
20
+ useRichTextToolState,
18
21
  type CustomTool,
19
22
  type RichTextEditorHandle,
20
23
  } from "@olddevs/ui/editor";
@@ -28,12 +31,16 @@ import { sanitizeRichText } from "@olddevs/ui/editor/schema";
28
31
  - Документу нужен свой Блок, Атом или своя Метка: Merge Tag, Embed, карточка товара, комментарий к фрагменту. Их Схема, HTML, Markdown и показ в `RichTextView` описываются одним Инструментом.
29
32
  - Своя команда в интерфейсе Редактора: кнопка на Панели, Пункт вставки в «/» и «+», Действие над Блоком в меню Ручки Блока, Карточка у текста.
30
33
  - Экран управляет полем: вставляет шаблон у каретки, рисует свою панель, сохраняет при потере фокуса, показывает «Сохранено» и число слов.
34
+ - Встроенный Инструмент нужно настроить (`toc` под шапку со своей переменной высоты, Поиск с включённым регистром) или узнать, что в нём происходит: счётчик совпадений в Строке статуса, внешняя навигация по заголовкам, кнопка «Найти» в шапке экрана.
35
+ - Свой Инструмент с Опциями, которому нужны Состояние и Команды для экрана: `defineTool` вместо ручной фабрики.
31
36
  - Сервер хранит Документ со своими Блоками и должен чистить его тем же набором правил, что Редактор.
32
37
 
33
38
  ## Когда не использовать
34
39
 
35
40
  - Нужное уже есть среди Инструментов кита ([«Инструменты и Пресеты»](../components/inputs/rich-text-editor.md#инструменты-и-пресеты)): передайте имя в `tools`, своё не пишите.
36
41
  - Чтобы спрятать встроенную кнопку или пункт, сузьте `tools`. Подменить встроенный Инструмент по имени нельзя: выключите его и поставьте свой под своим именем.
42
+ - Опции есть только у `search` и `toc`; `tool("bold", …)` не компилируется. Опции остальных встроенных добавляются по одному, когда о них попросят. Поменять поведение встроенного Инструмента подменой нельзя (`extend()` как у Tiptap нет): описания встроенных наружу не выходят.
43
+ - Сменить Опции у смонтированного поля нельзя: они читаются один раз, при создании движка, вместе с `tools`. Перемонтируйте поле ключом.
37
44
  - Произвольный React в строке Панели и в меню нельзя: кит рисует записи сам, React допускается только в содержимом поповера (кнопки, Пункта вставки, Действия над Блоком) и в Карточке у текста.
38
45
  - Во вставку и drop пропом вмешаться нельзя: это делает Часть для движка своего Инструмента ([«Порядок вставки и drop»](../components/inputs/rich-text-editor.md#порядок-вставки-и-drop)).
39
46
  - Чего нет, пока потребители об этом не попросили (ADR-0058, раздел 13): `insertImage` и HTML выделения в Хэндле, `onSelectionChange`, `onPaste`, `onDrop`, `onEmpty`, `onDestroy`, события истории, сигнал об удалённых картинках, запертые Блоки шаблона, вид записи «меню», Карточки и Строка статуса в `RichTextView`. До тех пор у экрана есть `ref.current.editor` и его события.
@@ -41,21 +48,26 @@ import { sanitizeRichText } from "@olddevs/ui/editor/schema";
41
48
 
42
49
  ## Карта API
43
50
 
44
- | Что | Где в API | Подробно |
45
- | -------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
46
- | Свой Инструмент | `CustomTool` в `tools`; Пресеты — `RICH_TEXT_PRESETS` | [«Свой Инструмент»](../components/inputs/rich-text-editor.md#свой-инструмент) |
47
- | Правила Схемы и HTML-соглашение | `blocks` (`CustomBlock`), `marks` (`CustomMark`), `CustomAttribute`, `PrefixedName` | [«Свой Инструмент»](../components/inputs/rich-text-editor.md#свой-инструмент) |
48
- | Кнопка | `buttons` (`CustomToolButton`, `CustomToolPopover`, `CustomToolLabel`) | [«Кнопка своего Инструмента»](../components/inputs/rich-text-editor.md#кнопка-своего-инструмента) |
49
- | Пункт вставки, Действие над Блоком | `insertItems` (`CustomInsertItem`), `blockActions` (`CustomBlockAction`) | [«Пункты вставки и Действия над Блоком»](../components/inputs/rich-text-editor.md#пункты-вставки-и-действия-над-блоком) |
50
- | Символ-триггер | `trigger` (`CustomTrigger`, `TriggerItem`, `TriggerContext`) | [«Свой символ-триггер»](../components/inputs/rich-text-editor.md#свой-символ-триггер) |
51
- | Карточка у текста | `textCards` (`CustomTextCard`), помощник `markRange` | [«Карточка у текста»](../components/inputs/rich-text-editor.md#карточка-у-текста) |
52
- | Хэндл и своя панель | `RichTextEditorHandle`, `useRichTextEditorState(ref, selector)` | [«Хэндл (`ref`) и своя панель вне поля»](../components/inputs/rich-text-editor.md#хэндл-ref-и-своя-панель-вне-поля) |
53
- | События | `onReady`, `onFocus`, `onBlur`; вставка и drop — Частью для движка | [«События»](../components/inputs/rich-text-editor.md#события) |
54
- | Строка статуса и Сообщение поля | проп `status`; команда движка `showNotice`, тип `FieldNotice` | [«Строка статуса и Сообщение поля»](../components/inputs/rich-text-editor.md#строка-статуса-и-сообщение-поля) |
55
- | Загрузчик, Готовность и Скелет | `engine: () => import(…)` | [«Загрузчик Части для движка»](../components/inputs/rich-text-editor.md#загрузчик-части-для-движка), [«Скелет»](../components/inputs/rich-text-editor.md#скелет) |
56
- | Текст атома: `getText`, `maxLength`, пустота | `renderText` Части для движка | [«Текст атома»](../components/inputs/rich-text-editor.md#текст-атома) |
57
- | Сервер, Markdown, Просмотр | `sanitizeRichText(html, DOMPurify, tools)`, `richTextSchema(tools)`, `RichTextView` | [«Вне Редактора»](../components/inputs/rich-text-editor.md#вне-редактора-сервер-markdown-и-просмотр), [RichTextView, «Свои Инструменты: третий аргумент»](../components/content/rich-text-view.md#свои-инструменты-третий-аргумент) |
58
- | Остров своего Блока | `CustomBlock.island`, `CustomIslandProps`, проп `tools` у `RichTextView` | [RichTextView, «Остров своего Блока»](../components/content/rich-text-view.md#остров-своего-блока) |
51
+ | Что | Где в API | Подробно |
52
+ | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
53
+ | Свой Инструмент | `CustomTool` в `tools`; Пресеты — `RICH_TEXT_PRESETS` | [«Свой Инструмент»](../components/inputs/rich-text-editor.md#свой-инструмент) |
54
+ | Правила Схемы и HTML-соглашение | `blocks` (`CustomBlock`), `marks` (`CustomMark`), `CustomAttribute`, `PrefixedName` | [«Свой Инструмент»](../components/inputs/rich-text-editor.md#свой-инструмент) |
55
+ | Кнопка | `buttons` (`CustomToolButton`, `CustomToolPopover`, `CustomToolLabel`) | [«Кнопка своего Инструмента»](../components/inputs/rich-text-editor.md#кнопка-своего-инструмента) |
56
+ | Пункт вставки, Действие над Блоком | `insertItems` (`CustomInsertItem`), `blockActions` (`CustomBlockAction`) | [«Пункты вставки и Действия над Блоком»](../components/inputs/rich-text-editor.md#пункты-вставки-и-действия-над-блоком) |
57
+ | Символ-триггер | `trigger` (`CustomTrigger`, `TriggerItem`, `TriggerContext`) | [«Свой символ-триггер»](../components/inputs/rich-text-editor.md#свой-символ-триггер) |
58
+ | Карточка у текста | `textCards` (`CustomTextCard`), помощник `markRange` | [«Карточка у текста»](../components/inputs/rich-text-editor.md#карточка-у-текста) |
59
+ | Хэндл и своя панель | `RichTextEditorHandle`, `useRichTextEditorState(ref, selector)` | [«Хэндл (`ref`) и своя панель вне поля»](../components/inputs/rich-text-editor.md#хэндл-ref-и-своя-панель-вне-поля) |
60
+ | Опции встроенного Инструмента | `tool(name, options)` → `ConfiguredTool` для `search` и `toc`; `SearchToolOptions`, `TocToolOptions`, `TocLevel` | [«Опции встроенного Инструмента: `tool()`»](../components/inputs/rich-text-editor.md#опции-встроенного-инструмента-tool) |
61
+ | Свой Инструмент с Опциями, Состоянием, Командами | `defineTool({ name, defaults, create, state, commands })` → `ToolDefinition`, `tool(definition, options)` | [«Опции, Состояние и Команды»](editor-extension.md#опции-состояние-и-команды) |
62
+ | Типы Опций, Состояния и Команд | расширяемый интерфейс `RichTextTools` | [«Карта `RichTextTools` и declaration merging»](editor-extension.md#карта-richtexttools-и-declaration-merging) |
63
+ | Состояние Инструмента | `useRichTextToolState(ref, name, selector?)`, `SearchToolState`, `TocToolState`, `TocToolHeading` | [«Состояние Инструмента: `useRichTextToolState`»](../components/inputs/rich-text-editor.md#состояние-инструмента-userichtexttoolstate) |
64
+ | Команды Инструмента | `handle.tool(name)`, `SearchToolCommands`, `TocToolCommands` | [«Команды Инструмента: `handle.tool()`»](../components/inputs/rich-text-editor.md#команды-инструмента-handletool) |
65
+ | События | `onReady`, `onFocus`, `onBlur`; вставка и drop — Частью для движка | [«События»](../components/inputs/rich-text-editor.md#события) |
66
+ | Строка статуса и Сообщение поля | проп `status`; команда движка `showNotice`, тип `FieldNotice` | [«Строка статуса и Сообщение поля»](../components/inputs/rich-text-editor.md#строка-статуса-и-сообщение-поля) |
67
+ | Загрузчик, Готовность и Скелет | `engine: () => import(…)` | [«Загрузчик Части для движка»](../components/inputs/rich-text-editor.md#загрузчик-части-для-движка), [«Скелет»](../components/inputs/rich-text-editor.md#скелет) |
68
+ | Текст атома: `getText`, `maxLength`, пустота | `renderText` Части для движка | [«Текст атома»](../components/inputs/rich-text-editor.md#текст-атома) |
69
+ | Сервер, Markdown, Просмотр | `sanitizeRichText(html, DOMPurify, tools)`, `richTextSchema(tools)`, `RichTextView` | [«Вне Редактора»](../components/inputs/rich-text-editor.md#вне-редактора-сервер-markdown-и-просмотр), [RichTextView, «Свои Инструменты: третий аргумент»](../components/content/rich-text-view.md#свои-инструменты-третий-аргумент) |
70
+ | Остров своего Блока | `CustomBlock.island`, `CustomIslandProps`, проп `tools` у `RichTextView` | [RichTextView, «Остров своего Блока»](../components/content/rich-text-view.md#остров-своего-блока) |
59
71
 
60
72
  ## Свой Инструмент и HTML-соглашение
61
73
 
@@ -95,12 +107,197 @@ import { sanitizeRichText } from "@olddevs/ui/editor/schema";
95
107
 
96
108
  ## Хэндл (`ref`), события, Строка статуса
97
109
 
98
- - **Хэндл** (`ref`) — вставка у каретки (`insertHTML`, `insertText` → `boolean`), чтение (`getText`, `getSelectedText`, `isEmpty`, `getHTML`, `getMarkdown`, `getJSON`), фокус (`focus("start" | "end" | "restore")`, `blur`), история (`undo`, `redo`, `canUndo`, `canRedo`), а ещё `clear`, `setMarkdown` и `setJSON` (JSON-конверт → `boolean`) и `openSearch({ replace? })` — открыть панель поиска и замены. Вставка — Правка автора: шаг истории, `onChange` после задержки, предел `maxLength`; за пределом вставка не выполняется. До Готовности методы не бросают ошибок и ничего не меняют: `insertHTML` и `insertText` возвращают `false`. `editor` — сырой Tiptap для команд Меток, Блоков и своих Инструментов; его API следует мажору Tiptap.
110
+ - **Хэндл** (`ref`) — вставка у каретки (`insertHTML`, `insertText` → `boolean`), чтение (`getText`, `getSelectedText`, `isEmpty`, `getHTML`, `getMarkdown`, `getJSON`), фокус (`focus("start" | "end" | "restore")`, `blur`), история (`undo`, `redo`, `canUndo`, `canRedo`), а ещё `clear`, `setMarkdown` и `setJSON` (JSON-конверт → `boolean`) и `tool(name)` — Команды Инструмента (раздел «Опции, Состояние и Команды» ниже). `openSearch({ replace? })` устарел: он работает как `tool("search")?.open({ replace })`, один раз предупреждает в разработке и удаляется в 5.0. Вставка — Правка автора: шаг истории, `onChange` после задержки, предел `maxLength`; за пределом вставка не выполняется. До Готовности методы не бросают ошибок и ничего не меняют: `insertHTML` и `insertText` возвращают `false`. `editor` — сырой Tiptap для команд Меток, Блоков и своих Инструментов; его API следует мажору Tiptap.
99
111
  - **`useRichTextEditorState(ref, selector)`** — состояние своей панели: перерисовка только при смене среза, `null` до движка и на сервере.
112
+ - **`useRichTextToolState(ref, name, selector?)`** — Состояние Инструмента: снимок данных, перерисовка только при его смене, `null` до монтажа, на сервере, после размонтирования Редактора и без Инструмента в наборе.
100
113
  - **`onReady()`** — один раз за монтаж, когда наступила Готовность; **`onFocus()`** и **`onBlur()`** — о Фокусе поля целиком (Документ, Панель, поповеры кита, поле HTML); перед `onBlur` отложенный `onChange` выталкивается.
101
114
  - **`status`** — React-узел экрана в строке под Документом, у начала; счётчик кита — в конце. Кит строку не озвучивает.
102
115
  - **`showNotice({ tone: "status" | "error", text } | null)`** — команда движка: одно Сообщение поля, новое заменяет прежнее; `status` только озвучивается, `error` видна строкой `role="alert"` до следующей Правки автора. Тем же каналом кит сообщает о картинках.
103
116
 
117
+ ## Опции, Состояние и Команды
118
+
119
+ У Инструмента три типизированные части. Это перенос `options`, `storage` и `commands` из Tiptap на контракт кита, а не движка (ADR-0069).
120
+
121
+ | Часть | Что это | Кто задаёт | Как пользуется экран |
122
+ | --------- | ----------------------------------- | ------------------------------------------------------------------------------------- | -------------------------------------------- |
123
+ | Опции | Что экран задаёт до создания движка | `tool(name, options)` у встроенных; `defaults` и `tool(definition, options)` у своего | — |
124
+ | Состояние | Снимок, который экран читает | Кит у встроенных; поле `state` у `defineTool` | `useRichTextToolState(ref, name, selector?)` |
125
+ | Команды | То, что экран вызывает | Кит у встроенных; поле `commands` у `defineTool` | `ref.current?.tool(name)?.команда()` |
126
+
127
+ Часть для движка по-прежнему следует мажору Tiptap (ADR-0058, п. 12.4). Три новые части от Tiptap не зависят: их типы и поведение подчиняются semver кита.
128
+
129
+ ### `tool()`: встроенный Инструмент с Опциями
130
+
131
+ `tool(name, options)` возвращает Настроенный Инструмент — непрозрачную запись для `tools` (её поля читает только кит). Опции есть у `search` (стартовые флажки) и `toc` (`offsetVar`, `levels`, `listWidth`); их значения и пример — на странице [RichTextEditor](../components/inputs/rich-text-editor.md#опции-встроенного-инструмента-tool). Запись включает Инструмент и без имени в `tools`, а имя из Пресета ей уступает. Две записи `tool()` с одним именем — ошибка набора.
132
+
133
+ `tool(definition, options)` настраивает тем же способом определение `defineTool`; Опции берутся из его `defaults`.
134
+
135
+ ### `defineTool()`: свой Инструмент с Опциями
136
+
137
+ `defineTool({ name, defaults, create, state?, commands? })` возвращает определение — элемент `tools`, который работает и без `tool()` (с умолчаниями из `defaults`), и с ним.
138
+
139
+ - **`defaults`** — полный набор Опций. Тип Опций выводится из него, второй раз его объявлять не нужно. Литерал расширяется до своего типа (`tone: "info"` станет `string`): нужен union-тип — укажите тип значения, `tone: "info" as "info" | "warning"`.
140
+ - **`create(options)`** отдаёт тело описания — ту же форму, что `CustomTool`, без имени (`CustomToolBody`): Часть для движка, правила Схемы, UI-записи. Зовётся один раз на запись `tools` (определение само или объект, который вернул `tool(definition, options)`): кит запоминает результат по паре «определение и объект Опций записи». Всё, что замкнуто в теле `create` (счётчик, кэш), поэтому общее у всех Редакторов с этой записью. Запись `tool(…)`, созданная в теле компонента, на каждом рендере новая, и Остров в `RichTextView` на каждом рендере перемонтируется: выносите набор `tools` из рендера, в константу модуля или в `useMemo`. Загрузчик Части для движка держите константой модуля: кэш загрузчика живёт на самой функции, и новая функция на каждый вызов `create` загружала бы Часть заново для каждой записи.
141
+ - **`state({ editor, options })`** — Состояние для `useRichTextToolState`. Её зовут при каждой транзакции и при каждом чтении хука, поэтому она должна быть быстрой и без побочных эффектов, а результат — данные без DOM-узлов. Примитивы, массивы и простые объекты сравниваются по значению, поэтому экран перерисовывается, только когда снимок изменился. `Date`, `Map`, `Set` и экземпляры классов сравниваются по ссылке: `state`, который создаёт их заново на каждый вызов, перерисует экран на каждой транзакции. Отдавайте простые данные (`date.toISOString()`) или ту же ссылку, пока данные не изменились. Нет `state` — у Инструмента нет Состояния, и хук отдаёт `null`.
142
+ - **`commands({ editor, options })`** — Команды для `handle.tool(name)`. Их зовут при каждом обращении к Хэндлу: только обёртки над движком, без подписок. Команда возвращает `boolean`, как у встроенных. Нет `commands` — `handle.tool(name)` отдаёт `null`.
143
+ - **Слияние Опций.** Вложенные простые объекты сливаются с умолчаниями, массивы и функции заменяются целиком — как `configure` у Tiptap v3. Значение `undefined` не в счёт: остаётся то, что было. Объект `defaults` не меняется, объект Опций общий для `create`, `state` и `commands`: читайте его, но не меняйте.
144
+ - **Опции читаются один раз**, при создании движка, вместе с набором. Чтобы сменить их, перемонтируйте Редактор ключом (`key`). Значения Опций своего Инструмента проверяет его автор: кит этого не делает.
145
+ - **Все правила своего Инструмента действуют** (ADR-0058): Префикс потребителя в имени, уникальность имени в `tools` (определение дважды, с `tool()` и без, — тоже ошибка набора), проверки набора, Загрузчик Части для движка. `defineTool` — сахар над той же формой `CustomTool`: отдельного пути сборки у него нет. Обычный объект `CustomTool` в `tools` работает как раньше, без Опций, Состояния и Команд.
146
+ - **Сервер.** `defineTool` и `tool()` живут в модуле без Tiptap и React в рантайме (Tiptap там — только типы), поэтому набор `tools` собирается в серверном модуле, рядом с остальной конфигурацией экрана. Определение содержит функции, как и `CustomTool`, поэтому клиентскому компоненту оно отдаётся вместе с модулем (импортом), а не пропом через границу RSC. Настроенный встроенный Инструмент состоит из данных: при данных в опциях его можно передать и пропом.
147
+
148
+ ### Карта `RichTextTools` и declaration merging
149
+
150
+ Типы всех трёх частей собраны в интерфейсе `RichTextTools`, как `Storage` и `Commands` у Tiptap: ключ — имя Инструмента, значение — `{ options; state; commands }`. Кит объявляет в нём `search` и `toc`, свой Инструмент потребитель дописывает сам:
151
+
152
+ ```ts
153
+ declare module "@olddevs/ui/editor" {
154
+ interface RichTextTools {
155
+ acme_note: {
156
+ options: { tone: "info" | "warning" };
157
+ state: { count: number };
158
+ commands: { clear(): boolean };
159
+ };
160
+ }
161
+ }
162
+ ```
163
+
164
+ - Из карты выводятся тип снимка в `useRichTextToolState`, тип Команд в `handle.tool()` и Опции `tool()` встроенного. Опции своего Инструмента `tool()` берёт из `defaults` его определения, поэтому поле `options` в карте описывает Опции для читателей карты (`RichTextTools["acme_note"]["options"]`), а `tool()` его не использует.
165
+ - **Типизация добровольна.** Инструмент, не дописанный в карту, работает: его Состояние и Команды имеют тип `unknown`.
166
+ - Файл с `declare module` должен быть модулем (в нём есть `import` или `export`); иначе объявление не дополняет карту, а заводит свой модуль. Положите его рядом с определением Инструмента.
167
+ - Карту не стоит путать с типизацией движка. Кто типизирует Часть для движка через `declare module "@tiptap/core"` (`Commands`, `Storage`), продолжает так делать: это слой движка и его мажор, а `RichTextTools` — слой кита.
168
+
169
+ ### Сквозной пример
170
+
171
+ Один набор `tools` с настройками и свой Инструмент в серверно-безопасном модуле:
172
+
173
+ ```ts
174
+ // editor-tools.ts: ни Tiptap, ни React в рантайме, модуль годится и для сервера.
175
+ import type { Editor } from "@tiptap/core";
176
+ import { RICH_TEXT_PRESETS, defineTool, tool } from "@olddevs/ui/editor";
177
+ import { MentionIcon } from "@olddevs/ui/icons";
178
+
179
+ // Загрузчик — одна функция на модуль: промис кэшируется на ней.
180
+ const loadEngine = () => import("./mention-engine.js").then((module) => module.engine);
181
+
182
+ function countMentions(editor: Editor): number {
183
+ let count = 0;
184
+ editor.state.doc.descendants((node) => {
185
+ if (node.type.name === "acme_mention") count += 1;
186
+ });
187
+ return count;
188
+ }
189
+
190
+ export const mention = defineTool({
191
+ name: "acme_mention",
192
+ // Умолчания: из них выводится тип Опций.
193
+ defaults: { labels: { insert: "Mention" }, limit: 5 },
194
+ create: (options) => ({
195
+ engine: loadEngine,
196
+ blocks: {
197
+ acme_mention: {
198
+ kind: "inline",
199
+ attributes: { acme_id: { kind: "pattern", pattern: "[0-9]+" } },
200
+ },
201
+ },
202
+ insertItems: [
203
+ {
204
+ section: "insert",
205
+ label: options.labels.insert,
206
+ icon: MentionIcon,
207
+ run: (chain) => chain.insertContent({ type: "acme_mention", attrs: { acme_id: null } }),
208
+ },
209
+ ],
210
+ }),
211
+ // Состояние: данные, которые экран читает хуком.
212
+ state: ({ editor }) => ({ count: countMentions(editor) }),
213
+ // Команды: тонкие обёртки над движком, возвращают boolean.
214
+ commands: ({ editor, options }) => ({
215
+ insert: (id: number) =>
216
+ countMentions(editor) < options.limit &&
217
+ editor.commands.insertContent({ type: "acme_mention", attrs: { acme_id: String(id) } }),
218
+ }),
219
+ });
220
+
221
+ // Типы Состояния и Команд для `useRichTextToolState` и `handle.tool()`.
222
+ declare module "@olddevs/ui/editor" {
223
+ interface RichTextTools {
224
+ acme_mention: {
225
+ options: { labels: { insert: string }; limit: number };
226
+ state: { count: number };
227
+ commands: { insert(id: number): boolean };
228
+ };
229
+ }
230
+ }
231
+
232
+ // Набор собирается один раз на модуль: `tools` читается при создании движка.
233
+ export const EDITOR_TOOLS = [
234
+ ...RICH_TEXT_PRESETS.full,
235
+ tool("search", { matchCase: true }),
236
+ tool("toc", { offsetVar: "--header-h", levels: [2, 3] }),
237
+ tool(mention, { limit: 10, labels: { insert: "Отметить коллегу" } }),
238
+ ];
239
+ ```
240
+
241
+ Экран читает Состояние всех трёх Инструментов и зовёт их Команды:
242
+
243
+ ```tsx
244
+ "use client";
245
+ import { useRef } from "react";
246
+ import { RichTextEditor, useRichTextToolState } from "@olddevs/ui/editor";
247
+ import type { RichTextEditorHandle } from "@olddevs/ui/editor";
248
+ import { EDITOR_TOOLS } from "./editor-tools";
249
+
250
+ export function ArticleBody({ html }: { html: string }) {
251
+ const ref = useRef<RichTextEditorHandle>(null);
252
+ const search = useRichTextToolState(ref, "search");
253
+ const toc = useRichTextToolState(ref, "toc");
254
+ const mentions = useRichTextToolState(ref, "acme_mention", (state) => state.count);
255
+
256
+ return (
257
+ <>
258
+ <nav aria-label="Разделы">
259
+ {toc?.headings.map((heading) => (
260
+ <button
261
+ key={heading.index}
262
+ type="button"
263
+ aria-current={heading.index === toc.current ? "location" : undefined}
264
+ onClick={() => ref.current?.tool("toc")?.jumpTo(heading.index)}
265
+ >
266
+ {heading.text}
267
+ </button>
268
+ ))}
269
+ </nav>
270
+ <button type="button" onClick={() => ref.current?.tool("search")?.open()}>
271
+ Найти
272
+ </button>
273
+ <button
274
+ type="button"
275
+ // Клик по кнопке вне поля не должен уводить фокус из Документа.
276
+ onMouseDown={(event) => event.preventDefault()}
277
+ onClick={() => ref.current?.tool("acme_mention")?.insert(1)}
278
+ >
279
+ Упомянуть ({mentions ?? 0})
280
+ </button>
281
+ <RichTextEditor
282
+ ref={ref}
283
+ aria-label="Текст статьи"
284
+ tools={EDITOR_TOOLS}
285
+ defaultValue={html}
286
+ status={
287
+ search?.open && search.query ? (
288
+ <span>
289
+ {search.total ? `${search.current + 1} из ${search.total}` : "Нет совпадений"}
290
+ </span>
291
+ ) : null
292
+ }
293
+ />
294
+ </>
295
+ );
296
+ }
297
+ ```
298
+
299
+ Что здесь важно. Запись `tool("toc", …)` включила Оглавление без строки `"toc"`, а `tool("search", …)` заменила Поиск из Пресета. `useRichTextToolState(ref, "acme_mention", …)` и `tool("acme_mention")` типизированы благодаря `declare module`; без него вернулся бы `unknown`. Опции `mention` в `EDITOR_TOOLS` — `limit: 10` вместо умолчания 5. Они читаются один раз, при создании движка, и `insert` видит именно их.
300
+
104
301
  ## Загрузчики, Готовность и Скелет
105
302
 
106
303
  Часть для движка можно отдать Загрузчиком — `engine: () => import("./engine.js").then((m) => m.engine)`: модуль с описанием тогда импортирует и сервер, а Tiptap уходит в отдельный чанк. Загрузка стартует с первого рендера в браузере, промис кэшируется на функции Загрузчика (поэтому Загрузчик — одна функция на модуль). **Готовность** — движок создан и Загрузчики всех включённых Инструментов, своих и тяжёлых встроенных (`codeBlock`, `table`), отработали. До неё поле — **Скелет**: рамка, место Панели и Документ, очищенный Схемой, без ввода и без прыжка высоты; на сервере — рамка без Документа. Сбой Загрузчика — ошибка поля с «Повторить», Документ остаётся виден. Подробно — [«Загрузчик Части для движка»](../components/inputs/rich-text-editor.md#загрузчик-части-для-движка) и [«Скелет»](../components/inputs/rich-text-editor.md#скелет).
@@ -120,10 +317,11 @@ import { sanitizeRichText } from "@olddevs/ui/editor/schema";
120
317
  - описание своего Инструмента (`CustomTool` и его поля, в том числе `trigger`), UI-записи и их Отведённые места, правила Схемы (`CustomBlock`, `CustomMark`, `CustomAttribute`), Признаки Блока;
121
318
  - HTML-соглашение (`data-block`, `data-mark`, `data-<префикс>_<имя>`);
122
319
  - методы Хэндла, `useRichTextEditorState`, колбэки `onReady`, `onFocus`, `onBlur`, проп `status`, команда `showNotice` и тип `FieldNotice`;
320
+ - Опции, Состояние и Команды (ADR-0069): карта `RichTextTools`, `tool()`, `defineTool()` и его типы (`ToolDefinition`, `ConfiguredTool`), `useRichTextToolState`, `handle.tool()`, а также поля Опций, Состояния и Команд `search` и `toc`;
123
321
  - третий аргумент `sanitizeRichText` и `richTextSchema`, проп `tools` и Остров у `RichTextView`, `markRange`;
124
322
  - имена встроенных Инструментов (`ToolName`) и сам факт Пресетов-массивов `RICH_TEXT_PRESETS` — но не их состав (ниже).
125
323
 
126
- **Не входят:** DOM и классы кита вокруг Отведённого места; id встроенных кнопок и пунктов; внутреннее описание встроенных Инструментов (надмножество публичной формы) — наружу оно не экспортируется; точное место записи внутри поверхности; вид Скелета; имена файлов и чанков в `dist`, как любой путь внутри пакета ([«Публичный контракт»](../conventions.md#публичный-контракт)). API сырого `editor` и Части для движка — это API Tiptap, он следует мажору движка.
324
+ **Не входят:** DOM и классы кита вокруг Отведённого места; id встроенных кнопок и пунктов; внутреннее описание встроенных Инструментов (надмножество публичной формы) — наружу оно не экспортируется; точное место записи внутри поверхности; вид Скелета; форма Настроенного Инструмента изнутри (запись непрозрачна, её поля читает кит); момент прокрутки после Команд Поиска (она происходит чуть позже вызова); имена файлов и чанков в `dist`, как любой путь внутри пакета ([«Публичный контракт»](../conventions.md#публичный-контракт)). API сырого `editor` и Части для движка — это API Tiptap, он следует мажору движка.
127
325
 
128
326
  **Сохранённый Документ не ломают ни minor, ни patch.** HTML-соглашение заморожено: `data-block` и `data-mark` расширяются только добавлением. Если форму придётся сменить, это мажор, и до следующего мажора кит читает обе формы — и разбором движка, и санитайзером.
129
327
 
@@ -140,6 +338,8 @@ import { sanitizeRichText } from "@olddevs/ui/editor/schema";
140
338
  | Удаление Инструмента из Пресета, переименование Инструмента | major |
141
339
  | Удаление публичного имени | major, после `@deprecated` в minor (ниже) |
142
340
  | Новое необязательное поле, новый вид записи, метод Хэндла или колбэк | minor |
341
+ | Новое поле Опций или Состояния, новая Команда, опция у другого встроенного Инструмента | minor |
342
+ | Удаление поля Опций, Состояния или Команды, смена его смысла | major |
143
343
  | Новый встроенный Инструмент в Пресете | minor, строка changeset выделена жирным |
144
344
  | Сдвиг Отведённого места внутри поверхности | minor со строкой changeset |
145
345
  | Подъём нижней границы Tiptap внутри мажора | minor |
@@ -149,7 +349,7 @@ import { sanitizeRichText } from "@olddevs/ui/editor/schema";
149
349
  - **Мажор Tiptap — мажор кита.** Кит поддерживает один мажор Tiptap за раз; движка нет в манифесте кита, и диапазон версий (`^3.31.4`) держит потребитель по странице [RichTextEditor](../components/inputs/rich-text-editor.md#установка).
150
350
  - **Пресет** — «то, что кит считает полным набором», а не замороженный список. Кому нужен стабильный состав, передаёт точный список имён.
151
351
  - **Ужесточение очистки** выходит заметкой в [migration-notes](migration-notes.md): что теперь срезается и как найти такие Документы. Ослабление очистки — отдельное решение, а не вопрос версии.
152
- - **Сначала пометка, потом удаление.** Замена выходит в minor, старое получает `@deprecated` и одно предупреждение в разработке, удаляется следующим мажором.
352
+ - **Сначала пометка, потом удаление.** Замена выходит в minor, старое получает `@deprecated` и одно предупреждение в разработке, удаляется следующим мажором. Так помечен `handle.openSearch()`: замена — `handle.tool("search")?.open()`, удаление — в 5.0, запись в [migration-notes](migration-notes.md).
153
353
 
154
354
  ## Рецепты
155
355
 
@@ -296,6 +496,8 @@ export const EDITOR_TOOLS = [
296
496
 
297
497
  Пустая подпись не роняет поле: в разработке — `console.error` с именем Инструмента и записи, а в имя и подсказку встаёт имя Инструмента.
298
498
 
499
+ Эту же фабрику без ручного слияния опций даёт `defineTool`: умолчания в `defaults`, настройка — `tool(definition, options)` (раздел «Опции, Состояние и Команды»). Фабрика остаётся рабочим вариантом для Инструмента без Состояния и Команд.
500
+
299
501
  ### `modulepreload` для ленивых чанков
300
502
 
301
503
  Предзагрузки кит не публикует. Чанки Частей для движка (своих Загрузчиков и встроенных `codeBlock` и `table` в `full`) начинают грузиться с первого рендера Редактора, и это время закрывает Скелет. Чтобы чанк пришёл вместе со страницей, поставьте на него `<link rel="modulepreload">` (в React 19 — `preloadModule(url)` из `react-dom`); URL с хешем берите из манифеста своего сборщика: имена чанков кита в контракт не входят. Пример — [RichTextEditor, «Загрузчик Части для движка»](../components/inputs/rich-text-editor.md#загрузчик-части-для-движка); цена сценариев «старт» и «полный» — [«Размер импорта»](../getting-started.md#размер-импорта).
@@ -303,6 +505,6 @@ export const EDITOR_TOOLS = [
303
505
  ## Ссылки
304
506
 
305
507
  - Страницы: [RichTextEditor](../components/inputs/rich-text-editor.md), [RichTextView](../components/content/rich-text-view.md), [словарь Редактора](../glossary.md#редактор), [migration-notes](migration-notes.md).
306
- - Решения: ADR-0058 — API расширения, ADR-0050 — Редактор.
307
- - Исходник: вход `src/editor/index.ts`, публичная форма своего Инструмента `src/editor/core/customTool.ts`, проверка массива `tools` и проводка `src/editor/core/customToolEngine.ts`, Хэндл `src/editor/core/handle.ts`, хук `src/editor/hooks/useRichTextEditorState.ts`, Сообщение поля `src/editor/core/fieldNotice.ts`, Схема и очистка `src/editor/core/schema.ts`, Острова `src/editor/core/viewIsland.ts`.
308
- - Тесты: `tests/rich-text-editor-custom-tool.test.tsx` (описание, Схема, проверка массива `tools`), `tests/rich-text-editor-custom-button.test.tsx`, `tests/rich-text-editor-custom-ui.test.tsx` и `tests/rich-text-editor-text-card.test.tsx` (UI-записи), `tests/rich-text-editor-handle.test.tsx` и `tests/rich-text-editor-handle-ssr.test.tsx` (Хэндл и хук), `tests/rich-text-editor-events.test.tsx` (колбэки, порядок вставки и drop), `tests/rich-text-editor-notice.test.tsx` (Строка статуса и Сообщение поля), `tests/rich-text-editor-loaders.test.tsx`, `tests/rich-text-editor-skeleton.test.tsx` и `tests/rich-text-editor-lazy-builtins.test.tsx` (Загрузчики, Скелет, ленивые встроенные), `tests/rich-text-editor-atom-text.test.tsx` (Текст атома), `tests/rich-text-editor-custom-markdown.test.tsx`, `tests/editor-sanitize-tools.test.tsx`, `tests/rich-text-view-island.test.tsx`, `tests/editor-entry.test.ts`; тестовые Инструменты — `tests/editor-test-tools.ts`.
508
+ - Решения: ADR-0058 — API расширения, ADR-0069 — Опции, Состояние и Команды Инструмента, ADR-0050 — Редактор.
509
+ - Исходник: вход `src/editor/index.ts`, публичная форма своего Инструмента `src/editor/core/customTool.ts`, проверка массива `tools` и проводка `src/editor/core/customToolEngine.ts`, Хэндл `src/editor/core/handle.ts`, хуки `src/editor/hooks/useRichTextEditorState.ts` и `src/editor/hooks/useRichTextToolState.ts`, Опции и карта `src/editor/core/toolOptions.ts`, типы Команд `src/editor/core/toolCommands.ts` и Состояния `src/editor/core/toolState.ts`, Сообщение поля `src/editor/core/fieldNotice.ts`, Схема и очистка `src/editor/core/schema.ts`, Острова `src/editor/core/viewIsland.ts`.
510
+ - Тесты: `tests/rich-text-editor-custom-tool.test.tsx` (описание, Схема, проверка массива `tools`), `tests/rich-text-editor-custom-button.test.tsx`, `tests/rich-text-editor-custom-ui.test.tsx` и `tests/rich-text-editor-text-card.test.tsx` (UI-записи), `tests/rich-text-editor-handle.test.tsx` и `tests/rich-text-editor-handle-ssr.test.tsx` (Хэндл и хук), `tests/rich-text-editor-events.test.tsx` (колбэки, порядок вставки и drop), `tests/rich-text-editor-notice.test.tsx` (Строка статуса и Сообщение поля), `tests/rich-text-editor-loaders.test.tsx`, `tests/rich-text-editor-skeleton.test.tsx` и `tests/rich-text-editor-lazy-builtins.test.tsx` (Загрузчики, Скелет, ленивые встроенные), `tests/rich-text-editor-atom-text.test.tsx` (Текст атома), `tests/rich-text-editor-custom-markdown.test.tsx`, `tests/editor-sanitize-tools.test.tsx`, `tests/rich-text-view-island.test.tsx`, `tests/editor-entry.test.ts`, `tests/rich-text-editor-tool-options.test.tsx`, `tests/rich-text-editor-tool-state.test.tsx`, `tests/rich-text-editor-tool-commands.test.tsx` и `tests/rich-text-editor-define-tool.test.tsx` (Опции, Состояние, Команды, `defineTool`), `tests/editor-tool-options-types.test.ts`, `tests/editor-tool-state-types.test.ts`, `tests/editor-tool-commands-types.test.ts` и `tests/editor-define-tool-types.test.ts` (типы и карта); тестовые Инструменты — `tests/editor-test-tools.ts`.
@@ -2,7 +2,7 @@
2
2
  category: api
3
3
  status: stable
4
4
  source: src/index.ts
5
- sourceHash: 0d3ab7842785
5
+ sourceHash: 902814b6f802
6
6
  ---
7
7
 
8
8
  # Compatibility and migration notes
@@ -82,6 +82,41 @@ A concrete check or codemod.
82
82
  5. Add or update a test for the previous boundary and the new contract; for
83
83
  interaction, check focus, the keyboard and accessible names.
84
84
 
85
+ ## 4.5.0 — `handle.openSearch()` → `handle.tool("search")?.open()`
86
+
87
+ **Status:** deprecation — minor. The method works throughout all 4.x releases and is removed in 5.0 (ADR-0058, item 12.7; ADR-0069).
88
+
89
+ ### What changed
90
+
91
+ The `RichTextEditor` Handle gets `tool(name)`, a single entry point to a Tool's commands. The `open` command of the
92
+ `search` Tool does what `openSearch` does and accepts more: `open({ replace?, query? })`, where `query` sets the
93
+ query at once. `handle.openSearch(options)` is marked `@deprecated` and warns once in the console in development.
94
+ New commands of built-in Tools go through `handle.tool()`, not through new Handle methods.
95
+
96
+ ### Who is affected
97
+
98
+ Everyone who calls `ref.current.openSearch(…)`, for example from a "Find" button in the screen header or a "find mentions" link.
99
+
100
+ ### Before → after
101
+
102
+ ```tsx
103
+ // before
104
+ ref.current?.openSearch({ replace: true });
105
+ // after
106
+ ref.current?.tool("search")?.open({ replace: true });
107
+ ```
108
+
109
+ ### Semantics and accessibility
110
+
111
+ Unchanged: the same panel, the same focus, the same shortcuts. The differences are small: `tool("search")` is `null`
112
+ without the `search` Tool in `tools` and before the engine exists, so a call through `?.` does nothing, just as
113
+ `openSearch` did; `open` returns a `boolean` (`false` in HTML mode) instead of `void`.
114
+
115
+ ### Action before updating
116
+
117
+ Nothing is required before 5.0. Search the code for `openSearch(` and replace the call. The development console
118
+ warning appears once per session, so it alone will not find every screen with the call.
119
+
85
120
  ## 4.0.0 — first public release
86
121
 
87
122
  **Status:** breaking change — the license and the registry change; the API does not.
@@ -81,6 +81,41 @@ source: src/index.ts
81
81
  5. Добавьте или обновите тест для прежней границы и нового контракта; для
82
82
  взаимодействия проверяйте фокус, клавиатуру и доступные имена.
83
83
 
84
+ ## 4.5.0 — `handle.openSearch()` → `handle.tool("search")?.open()`
85
+
86
+ **Статус:** deprecation — minor. Метод работает во всех выпусках 4.x и удаляется в 5.0 (ADR-0058, п. 12.7; ADR-0069).
87
+
88
+ ### Что изменилось
89
+
90
+ Хэндл `RichTextEditor` получает `tool(name)` — единый вход к Командам Инструмента. Команда `open` Инструмента
91
+ `search` делает то же, что `openSearch`, и принимает больше: `open({ replace?, query? })`, где `query` сразу
92
+ ставит запрос. `handle.openSearch(options)` помечен `@deprecated` и в разработке один раз предупреждает в консоли.
93
+ Новые Команды встроенных Инструментов идут через `handle.tool()`, а не новыми методами Хэндла.
94
+
95
+ ### Кого затрагивает
96
+
97
+ Всех, кто вызывает `ref.current.openSearch(…)`, например из кнопки «Найти» в шапке экрана или из ссылки «найти упоминания».
98
+
99
+ ### До → после
100
+
101
+ ```tsx
102
+ // было
103
+ ref.current?.openSearch({ replace: true });
104
+ // стало
105
+ ref.current?.tool("search")?.open({ replace: true });
106
+ ```
107
+
108
+ ### Семантика и доступность
109
+
110
+ Не меняются: та же панель, тот же фокус, те же сочетания. Различия мелкие: `tool("search")` — `null` без Инструмента
111
+ `search` в `tools` и до создания движка, поэтому вызов через `?.` ничего не делает, как раньше `openSearch`;
112
+ `open` возвращает `boolean` (`false` — в HTML-режиме) вместо `void`.
113
+
114
+ ### Действие до обновления
115
+
116
+ До 5.0 ничего не требуется. Найдите в коде `openSearch(` и замените вызов. Предупреждение в консоли разработки
117
+ появляется один раз за сеанс, поэтому по нему одному все экраны с этим вызовом не найти.
118
+
84
119
  ## 4.0.0 — первый публичный выпуск
85
120
 
86
121
  **Статус:** breaking change — смена лицензии и реестра; API без изменений.
@@ -5,14 +5,14 @@ category: content
5
5
  status: stable
6
6
  source: src/editor/features/toc/RichTextToc.tsx
7
7
  story: src/editor/features/toc/RichTextToc.stories.tsx
8
- sourceHash: 5eed0e4804e0
8
+ sourceHash: ece0addcd8ea
9
9
  ---
10
10
 
11
11
  # RichTextToc
12
12
 
13
13
  ## Summary for AI
14
14
 
15
- `RichTextToc` is the Table of contents of a Document, placed next to the View (`RichTextView`) or an Editor in `readOnly`. Collapsed, it is a strip of strokes, one per heading, with the stroke of the current section highlighted. Hovering the strip expands a list of headings; clicking an item scrolls to the heading and moves focus to it. The component is client-side and opt-in: the screen puts it in its own column and passes `target`, a ref to the View or to the Editor's Handle. Headings are read from the target's DOM; the Document has no anchors or `id` attributes, and the page address does not change. It lives in the `@olddevs/ui/editor` entry point. Decision: ADR-0066.
15
+ `RichTextToc` is the Table of contents of a Document, placed next to the View (`RichTextView`) or an Editor in `readOnly`. Collapsed, it is a strip of strokes, one per heading, with the stroke of the current section highlighted. Hovering the strip expands a list of headings; clicking an item scrolls to the heading and moves focus to it. The component is client-side and opt-in: the screen puts it in its own column and passes `target`, a ref to the View or to the Editor's Handle. Headings are read from the target's DOM; the Document has no anchors or `id` attributes, and the page address does not change. The `levels` prop keeps only the heading levels you need out of `h1`–`h4`. It lives in the `@olddevs/ui/editor` entry point. Decision: ADR-0066.
16
16
 
17
17
  ```tsx
18
18
  import { RichTextToc, RichTextView } from "@olddevs/ui/editor";
@@ -29,7 +29,7 @@ import { RichTextToc, RichTextView } from "@olddevs/ui/editor";
29
29
  - Navigation between application pages or sections of a site: use `SideNav` or `Tabs`, not the Table of contents of a Document.
30
30
  - A link to a section that can be shared: the Table of contents goes to a heading without an anchor (`#slug`), and the page address does not change. A link to a section would need its own ADR about `id` in the Schema.
31
31
  - A Table of contents as part of the Document itself, a Block inside the text: the Schema has no such Block.
32
- - The Table of contents belongs inside the Editor field and the screen has no column for it: use the Editor's `toc` Tool, the same strip in the left gutter of the frame, sticky under the Toolbar and in a field with `maxHeight` (the `RichTextEditor` page, "Table of contents" section).
32
+ - The Table of contents belongs inside the Editor field and the screen has no column for it: use the Editor's `toc` Tool, the same strip in the left gutter of the frame, sticky under the Toolbar and in a field with `maxHeight` (the `RichTextEditor` page, "Table of contents" section). There, a `tool("toc", options)` entry sets the same `offsetVar`, `levels` and `listWidth`, and the screen reads the headings and the current section with the `useRichTextToolState` hook. The standalone `RichTextToc` has no state or commands: it has no Handle.
33
33
 
34
34
  ## Minimal example
35
35
 
@@ -56,20 +56,22 @@ export function Article({ html }: { html: string }) {
56
56
 
57
57
  ## Public API
58
58
 
59
- | Prop | Meaning |
60
- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
61
- | `target` | Required. A ref to a `RichTextView` or to the Handle of a `RichTextEditor` whose headings the Table of contents shows. Headings are read after mounting, so there is no strip before hydration. |
62
- | `offsetVar` | The name of the CSS variable with the height of the screen's sticky header, `--topbar-h` by default (`TopBar` publishes it). A jump places the heading below the header, and the highlight counts a section from the header's bottom edge. |
63
- | `listWidth` | The width of the list: a CSS length or pixels, `240` by default. It is fixed rather than sized to the longest heading; a heading that does not fit is truncated with an ellipsis. |
64
- | `className` | Classes for the root. The remaining `div` props go to the root. |
59
+ | Prop | Meaning |
60
+ | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
61
+ | `target` | Required. A ref to a `RichTextView` or to the Handle of a `RichTextEditor` whose headings the Table of contents shows. Headings are read after mounting, so there is no strip before hydration. |
62
+ | `offsetVar` | The name of the CSS variable with the height of the screen's sticky header, `--topbar-h` by default (`TopBar` publishes it). A jump places the heading below the header, and the highlight counts a section from the header's bottom edge. |
63
+ | `listWidth` | The width of the list: a CSS length or pixels, `240` by default. It is fixed rather than sized to the longest heading; a heading that does not fit is truncated with an ellipsis. |
64
+ | `levels` | The heading levels that go into the Table of contents: a subset of `1`–`4`, all four by default. A heading of any other level is in neither the strip nor the list, and item indentation is counted from the largest remaining level. The set is read on every rebuild of the list, so changing it rebuilds the list. |
65
+ | `className` | Classes for the root. The remaining `div` props go to the root. |
65
66
 
66
67
  ### Which headings are included
67
68
 
68
69
  - `h1`–`h4` at the top level of the Document and inside a Wrapper.
69
70
  - Headings in the columns of a Layout are not included: two headings at the same height in neighboring columns confuse the highlight of the current section. Headings inside custom Blocks and their Islands are not included either: their content is part of the Block, not of the Document's structure.
70
- - With fewer than two headings the component renders nothing, and the screen's column stays empty.
71
+ - The `levels` prop narrows the set: `levels={[2, 3]}` keeps only `h2` and `h3`. Other levels are in neither the strip nor the list; an empty array leaves no headings at all.
72
+ - With fewer than two headings (after `levels`) the component renders nothing, and the screen's column stays empty.
71
73
 
72
- The indent of an item and the length of its stroke depend on the level **relative to the largest heading in the Document**: if the Document starts with `h2`, then `h2` has no indent and the longest stroke. The deeper the level, the shorter the stroke.
74
+ The indent of an item and the length of its stroke depend on the level **relative to the largest heading that made it into the Table of contents**: if the Document starts with `h2`, then `h2` has no indent and the longest stroke. The deeper the level, the shorter the stroke.
73
75
 
74
76
  ### A live Document
75
77