@olddevs/ui 4.3.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 (182) hide show
  1. package/CHANGELOG.md +36 -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 +124 -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 -5
  24. package/dist/editor/core/handle.d.ts.map +1 -1
  25. package/dist/editor/core/handle.js +2 -13
  26. package/dist/editor/core/handle.js.map +1 -1
  27. package/dist/editor/core/handleWatchers.d.ts +36 -0
  28. package/dist/editor/core/handleWatchers.d.ts.map +1 -0
  29. package/dist/editor/core/handleWatchers.js +29 -0
  30. package/dist/editor/core/handleWatchers.js.map +1 -0
  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 +10 -3
  59. package/dist/editor/core/tools.d.ts.map +1 -1
  60. package/dist/editor/core/tools.js +2 -1
  61. package/dist/editor/core/tools.js.map +1 -1
  62. package/dist/editor/core/viewIsland.d.ts.map +1 -1
  63. package/dist/editor/core/viewIsland.js +2 -1
  64. package/dist/editor/core/viewIsland.js.map +1 -1
  65. package/dist/editor/features/builtins.d.ts +9 -4
  66. package/dist/editor/features/builtins.d.ts.map +1 -1
  67. package/dist/editor/features/builtins.js +36 -7
  68. package/dist/editor/features/builtins.js.map +1 -1
  69. package/dist/editor/features/search/SearchPanel.d.ts.map +1 -1
  70. package/dist/editor/features/search/SearchPanel.js +26 -12
  71. package/dist/editor/features/search/SearchPanel.js.map +1 -1
  72. package/dist/editor/features/search/revealMatch.d.ts +16 -0
  73. package/dist/editor/features/search/revealMatch.d.ts.map +1 -1
  74. package/dist/editor/features/search/revealMatch.js +22 -0
  75. package/dist/editor/features/search/revealMatch.js.map +1 -1
  76. package/dist/editor/features/search/searchPlugin.d.ts +8 -3
  77. package/dist/editor/features/search/searchPlugin.d.ts.map +1 -1
  78. package/dist/editor/features/search/searchPlugin.js +17 -6
  79. package/dist/editor/features/search/searchPlugin.js.map +1 -1
  80. package/dist/editor/features/search/searchSnapshot.d.ts +14 -0
  81. package/dist/editor/features/search/searchSnapshot.d.ts.map +1 -0
  82. package/dist/editor/features/search/searchSnapshot.js +17 -0
  83. package/dist/editor/features/search/searchSnapshot.js.map +1 -0
  84. package/dist/editor/features/search/searchTool.d.ts +3 -0
  85. package/dist/editor/features/search/searchTool.d.ts.map +1 -1
  86. package/dist/editor/features/search/searchTool.js +30 -1
  87. package/dist/editor/features/search/searchTool.js.map +1 -1
  88. package/dist/editor/features/toc/RichTextToc.d.ts +91 -0
  89. package/dist/editor/features/toc/RichTextToc.d.ts.map +1 -0
  90. package/dist/editor/features/toc/RichTextToc.js +417 -0
  91. package/dist/editor/features/toc/RichTextToc.js.map +1 -0
  92. package/dist/editor/features/toc/documentHeadings.d.ts +30 -0
  93. package/dist/editor/features/toc/documentHeadings.d.ts.map +1 -0
  94. package/dist/editor/features/toc/documentHeadings.js +20 -0
  95. package/dist/editor/features/toc/documentHeadings.js.map +1 -0
  96. package/dist/editor/features/toc/headings.d.ts +35 -0
  97. package/dist/editor/features/toc/headings.d.ts.map +1 -0
  98. package/dist/editor/features/toc/headings.js +27 -0
  99. package/dist/editor/features/toc/headings.js.map +1 -0
  100. package/dist/editor/features/toc/stickyOffset.d.ts +3 -0
  101. package/dist/editor/features/toc/stickyOffset.d.ts.map +1 -0
  102. package/dist/editor/features/toc/stickyOffset.js +14 -0
  103. package/dist/editor/features/toc/stickyOffset.js.map +1 -0
  104. package/dist/editor/features/toc/tocSnapshot.d.ts +10 -0
  105. package/dist/editor/features/toc/tocSnapshot.d.ts.map +1 -0
  106. package/dist/editor/features/toc/tocSnapshot.js +8 -0
  107. package/dist/editor/features/toc/tocSnapshot.js.map +1 -0
  108. package/dist/editor/features/toc/tocTool.d.ts +6 -0
  109. package/dist/editor/features/toc/tocTool.d.ts.map +1 -0
  110. package/dist/editor/features/toc/tocTool.js +21 -0
  111. package/dist/editor/features/toc/tocTool.js.map +1 -0
  112. package/dist/editor/hooks/useRichTextToolState.d.ts +43 -0
  113. package/dist/editor/hooks/useRichTextToolState.d.ts.map +1 -0
  114. package/dist/editor/hooks/useRichTextToolState.js +77 -0
  115. package/dist/editor/hooks/useRichTextToolState.js.map +1 -0
  116. package/dist/editor/index.d.ts +10 -0
  117. package/dist/editor/index.d.ts.map +1 -1
  118. package/dist/editor/index.js +4 -0
  119. package/dist/editor/index.js.map +1 -1
  120. package/dist/i18n/defaults.d.ts.map +1 -1
  121. package/dist/i18n/defaults.js +1 -0
  122. package/dist/i18n/defaults.js.map +1 -1
  123. package/dist/i18n/messages.d.ts +5 -0
  124. package/dist/i18n/messages.d.ts.map +1 -1
  125. package/dist/locales/ru.d.ts.map +1 -1
  126. package/dist/locales/ru.js +1 -0
  127. package/dist/locales/ru.js.map +1 -1
  128. package/docs/api/editor-extension.en.md +223 -23
  129. package/docs/api/editor-extension.md +224 -22
  130. package/docs/api/messages.en.md +743 -0
  131. package/docs/api/messages.md +1 -0
  132. package/docs/api/migration-notes.en.md +36 -1
  133. package/docs/api/migration-notes.md +35 -0
  134. package/docs/api/utilities.en.md +233 -0
  135. package/docs/components/content/rich-text-toc.en.md +132 -0
  136. package/docs/components/content/rich-text-toc.md +131 -0
  137. package/docs/components/index.en.md +3 -2
  138. package/docs/components/index.md +2 -1
  139. package/docs/components/inputs/rich-text-editor.en.md +277 -43
  140. package/docs/components/inputs/rich-text-editor.md +247 -11
  141. package/docs/conventions.en.md +318 -0
  142. package/docs/getting-started.en.md +7 -7
  143. package/docs/getting-started.md +6 -6
  144. package/docs/glossary.en.md +52 -47
  145. package/docs/glossary.md +51 -46
  146. package/docs/index.en.md +53 -0
  147. package/llms.txt +1 -0
  148. package/package.json +1 -1
  149. package/src/editor/RichTextEditor.tsx +197 -38
  150. package/src/editor/RichTextView.tsx +2 -1
  151. package/src/editor/core/containers.ts +8 -6
  152. package/src/editor/core/customTool.ts +8 -2
  153. package/src/editor/core/customToolEngine.ts +20 -1
  154. package/src/editor/core/extensions.ts +2 -1
  155. package/src/editor/core/handle.ts +26 -23
  156. package/src/editor/core/handleWatchers.ts +72 -0
  157. package/src/editor/core/nodeNames.ts +7 -0
  158. package/src/editor/core/schema.ts +5 -2
  159. package/src/editor/core/toolCommands.ts +85 -0
  160. package/src/editor/core/toolDefinition.ts +208 -0
  161. package/src/editor/core/toolDescriptor.ts +94 -3
  162. package/src/editor/core/toolOptions.ts +211 -0
  163. package/src/editor/core/toolState.ts +56 -0
  164. package/src/editor/core/tools.ts +12 -3
  165. package/src/editor/core/viewIsland.ts +5 -4
  166. package/src/editor/features/builtins.ts +72 -13
  167. package/src/editor/features/search/SearchPanel.tsx +38 -14
  168. package/src/editor/features/search/revealMatch.ts +45 -0
  169. package/src/editor/features/search/searchPlugin.ts +40 -11
  170. package/src/editor/features/search/searchSnapshot.ts +29 -0
  171. package/src/editor/features/search/searchTool.ts +51 -1
  172. package/src/editor/features/toc/RichTextToc.tsx +810 -0
  173. package/src/editor/features/toc/documentHeadings.ts +59 -0
  174. package/src/editor/features/toc/headings.ts +77 -0
  175. package/src/editor/features/toc/stickyOffset.ts +21 -0
  176. package/src/editor/features/toc/tocSnapshot.ts +24 -0
  177. package/src/editor/features/toc/tocTool.ts +41 -0
  178. package/src/editor/hooks/useRichTextToolState.ts +153 -0
  179. package/src/editor/index.ts +21 -0
  180. package/src/i18n/defaults.ts +1 -0
  181. package/src/i18n/messages.ts +5 -0
  182. package/src/locales/ru.ts +1 -0
@@ -1,5 +1,5 @@
1
1
  ---
2
- sourceHash: 8b34aab7b3be
2
+ sourceHash: 3aa1b5325b8c
3
3
  ---
4
4
 
5
5
  # Component catalog
@@ -29,7 +29,7 @@ page.
29
29
  ## Input fields
30
30
 
31
31
  - [TextControl](inputs/text-control.md): `TextControl` — the base text field: a single line or multi-line text, with an icon and a placeholder inside; the other input fields are built on it
32
- - [RichTextEditor](inputs/rich-text-editor.md): `RichTextEditor`, the `useRichTextEditorState` hook, the `RICH_TEXT_PRESETS` Presets, a custom Tool (`CustomTool`) — a rich text editor: headings, lists, links, inline formatting (bold, italic, color). Suitable for long articles and descriptions, and the value is stored as HTML. A separate entry point `@olddevs/ui/editor`; the consumer installs the Tiptap engine with the command from that page
32
+ - [RichTextEditor](inputs/rich-text-editor.md): `RichTextEditor`, the `useRichTextEditorState` and `useRichTextToolState` hooks, the `tool` and `defineTool` helpers, the `RICH_TEXT_PRESETS` Presets, a custom Tool (`CustomTool`) — a rich text editor: headings, lists, links, inline formatting (bold, italic, color). Suitable for long articles and descriptions, and the value is stored as HTML. A separate entry point `@olddevs/ui/editor`; the consumer installs the Tiptap engine with the command from that page
33
33
  - [Editor extension API](../api/editor-extension.md) — a reference for those who extend the Editor with custom Tools, buttons and Loaders: what can be plugged in and what will not change between versions
34
34
  - [NumberInput](inputs/number-input.md): `NumberInput` — a number field with "increase" and "decrease" buttons, bounds and formatting; an empty field yields `null`, not zero
35
35
  - [Choice controls](inputs/choice-controls.md): `Checkbox`, `CheckboxGroup`, `Radio`, `RadioGroup` — checkboxes and radio buttons: mark several independent options or choose exactly one from a set
@@ -141,6 +141,7 @@ Components built from primitives: the page frame, its navigation and header.
141
141
  - [SortableList](content/sortable-list.md): `SortableList` — a short list the user reorders by dragging: menu sections, scenario steps, priorities; lists sharing a `group` can exchange rows, which is how a kanban board is built
142
142
  - [EditableList](content/editable-list.md): `EditableList` — a list whose rows can be edited in place and reordered by dragging, and which can be added to at the bottom: a checklist, tasks; if you only need the order, `SortableList` is enough
143
143
  - [RichTextView](content/rich-text-view.md): `RichTextView` — displays ready-made rich text from the Editor without the Editor itself; a separate entry point `@olddevs/ui/editor`, suitable for a server component
144
+ - [RichTextToc](content/rich-text-toc.md): `RichTextToc` — the table of contents of a long Document next to the View or a `readOnly` Editor: a strip of strokes at the edge of the text, a list of headings on hover, a click goes to the heading; the `@olddevs/ui/editor` entry point
144
145
  - [VirtualGrid](content/virtual-grid.md): `VirtualGrid` — a grid of hundreds or thousands of previews that renders only the visible cells and lets you select them; for a dozen cards `Grid` is enough
145
146
  - [Avatar](content/avatar.md): `Avatar`, `AvatarGroup` — a round image of a person: a photo or, if there is none, initials; `AvatarGroup` shows several participants with a "+N" overflow count
146
147
  - [Data display](content/data-display.md): `Badge`, `Stat`, `DescriptionList`, `Table` — four ways to show data: a counter or a label, a key metric with an explanation, "property and value" pairs, a table of records of the same kind
@@ -24,7 +24,7 @@
24
24
  ## Поля ввода
25
25
 
26
26
  - [TextControl](inputs/text-control.md): `TextControl` — базовое текстовое поле: одна строка или многострочный текст, с иконкой и плейсхолдером внутри; на нём построены остальные поля ввода
27
- - [RichTextEditor](inputs/rich-text-editor.md): `RichTextEditor`, хук `useRichTextEditorState`, Пресеты `RICH_TEXT_PRESETS`, свой Инструмент (`CustomTool`) — редактор форматированного текста: заголовки, списки, ссылки, выделение слов (жирный, курсив, цвет). Подходит для длинных статей и описаний, значение хранится как HTML. Отдельный вход `@olddevs/ui/editor`; движок Tiptap потребитель ставит сам — командой со страницы
27
+ - [RichTextEditor](inputs/rich-text-editor.md): `RichTextEditor`, хуки `useRichTextEditorState` и `useRichTextToolState`, помощники `tool` и `defineTool`, Пресеты `RICH_TEXT_PRESETS`, свой Инструмент (`CustomTool`) — редактор форматированного текста: заголовки, списки, ссылки, выделение слов (жирный, курсив, цвет). Подходит для длинных статей и описаний, значение хранится как HTML. Отдельный вход `@olddevs/ui/editor`; движок Tiptap потребитель ставит сам — командой со страницы
28
28
  - [API расширения Редактора](../api/editor-extension.md) — справочник для тех, кто дописывает Редактор своими Инструментами, кнопками и Загрузчиками: что можно подключить и что не изменится между версиями
29
29
  - [NumberInput](inputs/number-input.md): `NumberInput` — поле для числа с кнопками «больше» и «меньше», границами и форматированием; пустое поле даёт `null`, а не ноль
30
30
  - [Choice controls](inputs/choice-controls.md): `Checkbox`, `CheckboxGroup`, `Radio`, `RadioGroup` — флажки и радиокнопки: отметить несколько независимых вариантов или выбрать ровно один из набора
@@ -137,6 +137,7 @@
137
137
  - [SortableList](content/sortable-list.md): `SortableList` — короткий список, который пользователь переставляет перетаскиванием: разделы меню, шаги сценария, приоритеты; списки одной `group` обмениваются строками: так собирается канбан-доска
138
138
  - [EditableList](content/editable-list.md): `EditableList` — список, строки которого можно править на месте и переставлять перетаскиванием, а сам список — пополнять снизу: чек-лист, задачи; если нужен только порядок, хватит `SortableList`
139
139
  - [RichTextView](content/rich-text-view.md): `RichTextView` — показ готового форматированного текста из Редактора без самого Редактора; отдельный вход `@olddevs/ui/editor`, годится серверному компоненту
140
+ - [RichTextToc](content/rich-text-toc.md): `RichTextToc` — Оглавление длинного Документа рядом с Просмотром или Редактором в `readOnly`: полоса штрихов у края текста, по наведению — список заголовков, клик ведёт к заголовку; вход `@olddevs/ui/editor`
140
141
  - [VirtualGrid](content/virtual-grid.md): `VirtualGrid` — сетка из сотен и тысяч превью, которая отрисовывает только видимые ячейки и позволяет их выбирать; для десятка карточек хватит `Grid`
141
142
  - [Avatar](content/avatar.md): `Avatar`, `AvatarGroup` — круглое изображение человека: фото или, если его нет, инициалы; `AvatarGroup` показывает нескольких участников со счётчиком остатка «+N»
142
143
  - [Data display](content/data-display.md): `Badge`, `Stat`, `DescriptionList`, `Table` — четыре способа показать данные: счётчик или метка, ключевой показатель с пояснением, пары «свойство и значение», таблица однотипных записей
@@ -7,6 +7,24 @@ exports:
7
7
  RichTextEditorHandle,
8
8
  RichTextDocument,
9
9
  useRichTextEditorState,
10
+ useRichTextToolState,
11
+ RichTextToolState,
12
+ tool,
13
+ defineTool,
14
+ ConfiguredTool,
15
+ ToolDefinition,
16
+ ToolDefinitionInput,
17
+ ToolDefinitionContext,
18
+ CustomToolBody,
19
+ RichTextTools,
20
+ SearchToolOptions,
21
+ TocToolOptions,
22
+ TocLevel,
23
+ SearchToolCommands,
24
+ TocToolCommands,
25
+ SearchToolState,
26
+ TocToolState,
27
+ TocToolHeading,
10
28
  Tool,
11
29
  ToolName,
12
30
  RichTextEditorPreset,
@@ -37,7 +55,7 @@ category: inputs
37
55
  status: stable
38
56
  source: src/editor/RichTextEditor.tsx
39
57
  story: src/editor/RichTextEditor.stories.tsx
40
- sourceHash: 95badb4a657c
58
+ sourceHash: b5a4f8fde844
41
59
  ---
42
60
 
43
61
  # RichTextEditor
@@ -55,7 +73,8 @@ sourceHash: 95badb4a657c
55
73
  - **Code block.** It has a header (language on the left, "Copy" on the right), a language (Python, TypeScript, JavaScript, Go, HTML, JSON, YAML, Markdown or plain text) and lowlight highlighting. Only `class="language-…"` is written into the Document, and for `RichTextView` the server helper from `@olddevs/ui/editor/highlight` highlights the Blocks.
56
74
  - **HTML mode.** The "HTML" button of the fixed Toolbar (the `source` Tool) swaps the Document for a field with its source HTML; leaving the mode, `blur`, form submission and `getHTML()` apply the edit through the Schema, like an incoming `value`.
57
75
  - **Feature set.** `preset="full" | "basic"` or an exact `tools` list; a disabled Tool removes both its button and its markup from the Schema.
58
- - **Handle (`ref`).** The screen drives the field through `ref`: `insertHTML`, `insertText`, `getText`, `getSelectedText`, `isEmpty`, `focus(position?)`, `blur`, `undo`, `redo`, `openSearch`; `getJSON()`/`setJSON()` export and load the Document as a lossless JSON envelope (see the "JSON" section), and `defaultValue` accepts the envelope too. The screen builds its own toolbar with the `useRichTextEditorState(ref, selector)` hook. Before Readiness (on the server, before mount, while Tool Loaders are running) the methods are silent: see the "Handle (`ref`) and a custom toolbar outside the field" section.
76
+ - **Tool options, state and commands.** The built-in `search` and `toc` Tools have options: a `tool("toc", { offsetVar: "--header-h", levels: [2, 3] })` entry sits in `tools` next to the names, switches the Tool on and configures it (for Search, the starting flags `matchCase`, `wholeWord` and `regex`; for the Table of contents, `offsetVar`, `levels` and `listWidth`). The `useRichTextToolState(ref, "search")` hook reads the state: for Search, whether the find panel is open, the query, the number of matches and the number of the current one; for the Table of contents, the headings and the current section. The hook returns `null` before mount, on the server, after the Editor unmounts and when the Tool is not in the set. Commands are called through the Handle: `ref.current?.tool("search")?.open()`, `ref.current?.tool("toc")?.jumpTo(2)`; each returns a `boolean`. Options are read once, when the engine is created. A custom Tool with the same three parts is described with `defineTool` (["Options, state and commands"](../../api/editor-extension.md#опции-состояние-и-команды)).
77
+ - **Handle (`ref`).** The screen drives the field through `ref`: `insertHTML`, `insertText`, `getText`, `getSelectedText`, `isEmpty`, `focus(position?)`, `blur`, `undo`, `redo`, `tool(name)`; `getJSON()`/`setJSON()` export and load the Document as a lossless JSON envelope (see the "JSON" section), and `defaultValue` accepts the envelope too. The screen builds its own toolbar with the `useRichTextEditorState(ref, selector)` hook. `openSearch` is deprecated (removed in 5.0): use `tool("search")?.open()` instead. Before Readiness (on the server, before mount, while Tool Loaders are running) the methods are silent: see the "Handle (`ref`) and a custom toolbar outside the field" section.
59
78
  - **Events.** The screen learns about field events through the `onReady()`, `onFocus()` and `onBlur()` callbacks (see the "Events" section).
60
79
  - **Skeleton and Loader.** Until Readiness the field shows a Skeleton: the frame and the Document as static HTML without input, with no height jump. The Skeleton's Document is `value` cleaned by the Schema, and only in the browser; on the server the Skeleton is a frame without a Document. The Engine part of a custom Tool can be passed as a Loader (`engine: () => import(…)`); if it fails, the result is a field error with "Retry".
61
80
  - **Status line and Field notice.** Below the field, inside the frame, are the Status line (`status`: the screen's React node at the start, the kit's counter at the end, no `role`) and the Field notice: the engine command `showNotice({ tone: "status" | "error", text })` / `showNotice(null)`. `status` is only announced, `error` is shown as a `role="alert"` line until the next edit; image messages go through the same channel.
@@ -305,43 +324,44 @@ The `link` Tool: the "Link" button on the Toolbar and `Ctrl/⌘+K` open a popove
305
324
 
306
325
  ## Tools and Presets
307
326
 
308
- `preset="full"` (default) is everything in the release. `preset="basic"` is for comments and short descriptions. `tools` is an exact list and takes precedence over a Preset; descriptions of custom Tools can be mixed in with the kit's names (see the "Custom Tool" section). The type of a `tools` element is `Tool` (a kit name or a `CustomTool`), the type of a kit name is `ToolName`; both are exported from `@olddevs/ui/editor`.
327
+ `preset="full"` (default) is everything in the release except the Table of contents `toc`: it changes the layout of the field, so it is switched on only explicitly. `preset="basic"` is for comments and short descriptions. `tools` is an exact list and takes precedence over a Preset; descriptions of custom Tools can be mixed in with the kit's names (see the "Custom Tool" section). The type of a `tools` element is `Tool` (a kit name, a Configured Tool from `tool(…)`, a `defineTool(…)` definition or a `CustomTool`), the type of a kit name is `ToolName`; both are exported from `@olddevs/ui/editor`.
309
328
 
310
329
  Presets are also exported as arrays of names — `RICH_TEXT_PRESETS.full` and `RICH_TEXT_PRESETS.basic` — so that you can assemble your own set from them: `tools={[...RICH_TEXT_PRESETS.full, mergeTagTool]}`. The arrays are frozen. A Preset's content is "what the kit considers the full set", not a frozen list: a new built-in Tool arrives in a Preset in a minor version. If you need a stable set, pass an exact list of names.
311
330
 
312
331
  **A disabled Tool removes both the button and the Schema extension.** Its Block or Mark cannot be created with a keyboard shortcut, a Markdown shortcut or a paste, and from `value` they arrive without Block markup, and the text stays.
313
332
 
314
- | `Tool` | Button | Markup | Shortcut | Markdown while typing | `basic` |
315
- | ----------------- | -------------------------------------- | ----------------------------------------- | ------------------------------------------------- | ---------------------- | ------- |
316
- | `heading` | "¶": Heading 2–4 | `h2`, `h3`, `h4` | `Ctrl/⌘+Alt+2…4` | `## `, `### `, `#### ` | — |
317
- | `heading1` | "¶": adds Heading 1 | `h1` | `Ctrl/⌘+Alt+1` | `# ` | — |
318
- | `history` | Undo, Redo | — | `Ctrl/⌘+Z`, `+Shift+Z` | — | yes |
319
- | `bold` | Bold | `strong` | `Ctrl/⌘+B` | `**text**` | yes |
320
- | `italic` | Italic | `em` | `Ctrl/⌘+I` | `*text*` | yes |
321
- | `underline` | "…": Underline | `u` | `Ctrl/⌘+U` | — | — |
322
- | `strike` | Strikethrough | `s` | `Ctrl/⌘+Shift+S` | `~~text~~` | yes |
323
- | `code` | "…": Inline code | `code` | `Ctrl/⌘+E` | `` `text` `` | yes |
324
- | `superscript` | "…": Superscript | `sup` | `Ctrl/⌘+.` | — | — |
325
- | `subscript` | "…": Subscript | `sub` | `Ctrl/⌘+,` | — | — |
326
- | `clearFormatting` | "…": Clear formatting | — (removes Marks) | `Ctrl/⌘+\` | — | — |
327
- | `link` | Link (address popover) | `a[href]` | `Ctrl/⌘+K` | `[text](url)` on paste | yes |
328
- | `color` | Color ▾ (Selection menu): "Text color" | `span[data-color]`, `span[style]` | — | — | — |
329
- | `highlight` | Highlight ▾ (Toolbar), "Color ▾" | `mark[data-color]`, `mark[style]` | — | — | — |
330
- | `align` | Alignment ▾ (menu) | `p`, `h2`–`h4` with `data-align` | `Ctrl/⌘+Shift+L`, `+E`, `+R` | — | — |
331
- | `bulletList` | Lists ▾: Bulleted list | `ul`, `li` | `Ctrl/⌘+Shift+8` | `- ` or `* ` | yes |
332
- | `orderedList` | Lists ▾: Numbered list | `ol`, `li` | `Ctrl/⌘+Shift+7` | `1. ` | yes |
333
- | `taskList` | Lists ▾: Checklist | `ul[data-type=taskList]` and items | `Ctrl/⌘+Shift+9` | `[ ] ` or `[x] ` | — |
334
- | `blockquote` | "…": Quote | `blockquote` | `Ctrl/⌘+Shift+B` | `> ` | yes |
335
- | `codeBlock` | "…": Code block | `pre`, `code class="language-*"` | `Ctrl/⌘+Alt+C` | ` ``` ` | — |
336
- | `horizontalRule` | "+": Divider | `hr` | — | `---` | — |
337
- | `table` | Table ▾ (menu) | `table`, `tbody`, `tr`, `th`, `td` | `Tab`, `Shift+Tab`, `Alt+↓` in a table | — | — |
338
- | `image` | Image (address popover) | `figure`, `img`, `figcaption` | — | `![alt](url)` on paste | — |
339
- | `emoji` | Emoji (popover with a grid) | — (plain text) | — | — | — |
340
- | `source` | HTML (mode toggle) | — (edits the Document's HTML) | — | — | — |
341
- | `search` | Find (find and replace panel) | — (searches the Document) | `Ctrl/⌘+F`, `Ctrl/⌘+Alt+F` | — | yes |
342
- | `layout` | "+": Layout | `div` with `data-block` and `data-layout` | `Tab`, `Shift+Tab`, arrow keys at the column edge | — | — |
343
- | `wrapper` | "+": Wrapper | `div[data-block=wrapper]` | `Enter` in an empty last paragraph — exit | — | — |
344
- | `insertMenu` | "+" Insert menu | — (Blocks of other Tools) | — | `/` | — |
333
+ | `Tool` | Button | Markup | Shortcut | Markdown while typing | `basic` |
334
+ | ----------------- | ---------------------------------------------------- | ----------------------------------------- | ------------------------------------------------- | ---------------------- | ------- |
335
+ | `heading` | "¶": Heading 2–4 | `h2`, `h3`, `h4` | `Ctrl/⌘+Alt+2…4` | `## `, `### `, `#### ` | — |
336
+ | `heading1` | "¶": adds Heading 1 | `h1` | `Ctrl/⌘+Alt+1` | `# ` | — |
337
+ | `history` | Undo, Redo | — | `Ctrl/⌘+Z`, `+Shift+Z` | — | yes |
338
+ | `bold` | Bold | `strong` | `Ctrl/⌘+B` | `**text**` | yes |
339
+ | `italic` | Italic | `em` | `Ctrl/⌘+I` | `*text*` | yes |
340
+ | `underline` | "…": Underline | `u` | `Ctrl/⌘+U` | — | — |
341
+ | `strike` | Strikethrough | `s` | `Ctrl/⌘+Shift+S` | `~~text~~` | yes |
342
+ | `code` | "…": Inline code | `code` | `Ctrl/⌘+E` | `` `text` `` | yes |
343
+ | `superscript` | "…": Superscript | `sup` | `Ctrl/⌘+.` | — | — |
344
+ | `subscript` | "…": Subscript | `sub` | `Ctrl/⌘+,` | — | — |
345
+ | `clearFormatting` | "…": Clear formatting | — (removes Marks) | `Ctrl/⌘+\` | — | — |
346
+ | `link` | Link (address popover) | `a[href]` | `Ctrl/⌘+K` | `[text](url)` on paste | yes |
347
+ | `color` | Color ▾ (Selection menu): "Text color" | `span[data-color]`, `span[style]` | — | — | — |
348
+ | `highlight` | Highlight ▾ (Toolbar), "Color ▾" | `mark[data-color]`, `mark[style]` | — | — | — |
349
+ | `align` | Alignment ▾ (menu) | `p`, `h2`–`h4` with `data-align` | `Ctrl/⌘+Shift+L`, `+E`, `+R` | — | — |
350
+ | `bulletList` | Lists ▾: Bulleted list | `ul`, `li` | `Ctrl/⌘+Shift+8` | `- ` or `* ` | yes |
351
+ | `orderedList` | Lists ▾: Numbered list | `ol`, `li` | `Ctrl/⌘+Shift+7` | `1. ` | yes |
352
+ | `taskList` | Lists ▾: Checklist | `ul[data-type=taskList]` and items | `Ctrl/⌘+Shift+9` | `[ ] ` or `[x] ` | — |
353
+ | `blockquote` | "…": Quote | `blockquote` | `Ctrl/⌘+Shift+B` | `> ` | yes |
354
+ | `codeBlock` | "…": Code block | `pre`, `code class="language-*"` | `Ctrl/⌘+Alt+C` | ` ``` ` | — |
355
+ | `horizontalRule` | "+": Divider | `hr` | — | `---` | — |
356
+ | `table` | Table ▾ (menu) | `table`, `tbody`, `tr`, `th`, `td` | `Tab`, `Shift+Tab`, `Alt+↓` in a table | — | — |
357
+ | `image` | Image (address popover) | `figure`, `img`, `figcaption` | — | `![alt](url)` on paste | — |
358
+ | `emoji` | Emoji (popover with a grid) | — (plain text) | — | — | — |
359
+ | `source` | HTML (mode toggle) | — (edits the Document's HTML) | — | — | — |
360
+ | `search` | Find (find and replace panel) | — (searches the Document) | `Ctrl/⌘+F`, `Ctrl/⌘+Alt+F` | — | yes |
361
+ | `layout` | "+": Layout | `div` with `data-block` and `data-layout` | `Tab`, `Shift+Tab`, arrow keys at the column edge | — | — |
362
+ | `wrapper` | "+": Wrapper | `div[data-block=wrapper]` | `Enter` in an empty last paragraph — exit | — | — |
363
+ | `insertMenu` | "+" Insert menu | — (Blocks of other Tools) | — | `/` | — |
364
+ | `toc` | Table of contents in the left gutter (not in `full`) | — (reads the headings) | — | — | — |
345
365
 
346
366
  A paragraph is always there; a soft line break is `Shift+Enter`. `insertMenu` and `emoji`, like `clearFormatting`, have no markup of their own: `insertMenu` provides access to other Tools' Blocks, and it is disabled where `/` is an ordinary character (paths, "and/or"); `basic` lacks it for the same reason. `emoji` inserts plain text — the Schema, the sanitizer and Markdown do not change because of it (see the "Emoji" section); `basic` lacks it: in a comment the OS's own palette is easier to reach, and a button would take up room in a short row. `source` is the HTML mode (see the "HTML mode" section): it has no markup of its own either, an HTML edit goes through the same Schema as `value`, and it cannot be used to get around the set of Tools; `basic` lacks it — in a comment an "HTML" button is superfluous unless the screen explicitly asks for it.
347
367
 
@@ -349,10 +369,38 @@ A paragraph is always there; a soft line break is `Shift+Enter`. `insertMenu` an
349
369
 
350
370
  Earlier, "no H1 in the Document" was a kit decision (the page heading sits outside the Editor); now it is the screen's decision — ADR-0053 revises the earlier decision, and ADR-0050 links to ADR-0053.
351
371
 
352
- A name enters the `Tool` type together with the Tool itself, not earlier: widening the input type does not break the consumer, whereas a name without an implementation would compile and do nothing. That is how `link` arrived — straight into the `basic` Preset too — and `color`, `highlight`, `align`, `table`, `image`, `emoji`, `layout`, `wrapper`, `source`; there are no reserved names now. Unknown names in `tools` (from JS without types) are dropped silently.
372
+ A name enters the `Tool` type together with the Tool itself, not earlier: widening the input type does not break the consumer, whereas a name without an implementation would compile and do nothing. That is how `link` arrived — straight into the `basic` Preset too — and `color`, `highlight`, `align`, `table`, `image`, `emoji`, `layout`, `wrapper`, `source`, `toc`; there are no reserved names now. Unknown names in `tools` (from JS without types) are dropped silently.
353
373
 
354
374
  `preset` and `tools` are read **when the engine is created**: the Schema of a live ProseMirror cannot be changed. To change the set on a mounted field, remount it with a key (`key`) — the edit history is lost in the process.
355
375
 
376
+ ### Options of a built-in Tool: `tool()`
377
+
378
+ Two built-in Tools have options: `search` and `toc`. You set them with the `tool(name, options)` helper, which returns a Configured Tool: an entry for `tools` that sits next to the names and the descriptions of custom Tools (ADR-0069).
379
+
380
+ ```tsx
381
+ import { RICH_TEXT_PRESETS, RichTextEditor, tool } from "@olddevs/ui/editor";
382
+
383
+ // Build the set once per module, not in a render: `tools` is read when the engine is created.
384
+ const tools = [
385
+ ...RICH_TEXT_PRESETS.full,
386
+ tool("toc", { offsetVar: "--header-h", levels: [2, 3] }),
387
+ tool("search", { matchCase: true }),
388
+ ];
389
+
390
+ <RichTextEditor tools={tools} defaultValue={html} />;
391
+ ```
392
+
393
+ - **An entry switches the Tool on.** `tool("toc", …)` enables `toc` without the `"toc"` string. If `tools` also holds the bare name (for example, `search` from `full`), the Configured Tool wins: there is still one Find button and one find panel.
394
+ - **Pass only what you change.** The remaining options come from the kit's defaults. Nested objects are merged and arrays are replaced whole: `levels: [2, 3]` means exactly these levels, not an addition to the default.
395
+ - **The position in the UI does not change.** A configured built-in Tool sits where it would sit by name; a custom Tool sits in `tools` order.
396
+ - **Types infer the options from the name.** `tool("toc", { matchCase: true })` and `tool("bold", {})` do not compile: `tool()` accepts only Tools that have options, which today means `search` and `toc`.
397
+ - **Two `tool()` entries with the same name** are a set error, like two custom Tools with the same name: a field error ("The editor couldn't start") and a `console.error` in development.
398
+ - **A wrong value** (from JS without types: `levels: [5]`, `offsetVar: "header"`) is replaced with the default, with a single `console.error` per Tool in development; the field keeps working.
399
+ - **Options are read once**, together with the `tools` set, when the engine is created. To change them on a mounted field, remount it with a key (`key`) — the edit history is lost in the process.
400
+ - **Server.** `tool()` pulls neither Tiptap nor React into the module, so a `tools` set that uses it can be built in a server module. A configured built-in Tool is made of data, and if its options are data, you can pass it to a client component as a prop.
401
+
402
+ The options of each Tool are described in its own section: "Find and replace" and "Table of contents". The options of a custom Tool are set by `defineTool` (["Options, state and commands"](../../api/editor-extension.md#опции-состояние-и-команды)).
403
+
356
404
  ## Custom Tool
357
405
 
358
406
  A custom Tool is the description of one Editor capability: its Blocks and Marks, Schema rules as data, and an Engine part (Tiptap extensions). It is passed in `tools` mixed with the kit's names and follows the same rules: **a Tool that is not passed removes its Blocks and Marks from the Schema** — they do not pass through `value`, paste or commands; a Block unwraps into its content and an Atom disappears. The decision is ADR-0058, sections 1 and 2.
@@ -442,7 +490,7 @@ An attribute cannot contain HTML: a value with `<` does not pass any rule, and t
442
490
 
443
491
  On an error the engine is not created: under the field's frame there is a `role="alert"` line "The editor couldn't start. The document is unchanged." (`editor.toolsFailed`), the hidden form field and `getHTML()` return the incoming value, `onChange` stays silent, and `ref.current.editor` is `null`. The cause is in `console.error` in development mode, with the Tool's name. This way data loss is visible before the first save, not on the server. **An unknown string in `tools` is still dropped silently**: the error applies only to descriptions of custom Tools.
444
492
 
445
- Buttons, Insert menu items, Block actions and Text cards are described in the sections below; a Tool can report an error to the author with the `showNotice` command (see the "Status line and Field notice" section). The whole extension API and the boundary of its stability (what falls under semver, what counts as breaking) are described on the [Editor extension API](../../api/editor-extension.md) page.
493
+ Buttons, Insert menu items, Block actions and Text cards are described in the sections below; a Tool can report an error to the author with the `showNotice` command (see the "Status line and Field notice" section). The whole extension API and the boundary of its stability (what falls under semver, what counts as breaking) are described on the [Editor extension API](../../api/editor-extension.md) page. If a Tool has settings and the screen needs its state and commands, describe it with `defineTool`: ["Options, state and commands"](../../api/editor-extension.md#опции-состояние-и-команды).
446
494
 
447
495
  ### Engine part Loader
448
496
 
@@ -1151,7 +1199,7 @@ The `required` check in the mode is evaluated against **the field's text** on ev
1151
1199
 
1152
1200
  ## Find and replace
1153
1201
 
1154
- The `search` Tool (present in `full` and in `basic`) is find and replace over the Document. The "Find" button (a magnifying glass icon) sits at the right edge of the fixed Toolbar, before the Shortcuts reference; `Ctrl/⌘+F` with focus in the Document opens the find panel (outside the field the browser's find works), `Ctrl/⌘+Alt+F` opens it with focus in the replace field, and `openSearch({ replace? })` on the Handle opens it from screen code.
1202
+ The `search` Tool (present in `full` and in `basic`) is find and replace over the Document. The "Find" button (a magnifying glass icon) sits at the right edge of the fixed Toolbar, before the Shortcuts reference; `Ctrl/⌘+F` with focus in the Document opens the find panel (outside the field the browser's find works), `Ctrl/⌘+Alt+F` opens it with focus in the replace field, and the `open` command of the Tool (`ref.current?.tool("search")?.open()`) opens it from screen code.
1155
1203
 
1156
1204
  ```tsx
1157
1205
  <RichTextEditor preset="basic" /> // the "Find" button and Ctrl/⌘+F are already there
@@ -1170,6 +1218,157 @@ The `search` Tool (present in `full` and in `basic`) is find and replace over th
1170
1218
 
1171
1219
  Message catalog strings: `editor.search` (the button and the `Ctrl/⌘+F` Shortcuts reference row), `searchReplace` (`Ctrl/⌘+Alt+F`), `searchPanel`, `searchField`, `replaceField`, `searchPrevious`, `searchNext`, `searchClose`, `searchMatchCase`, `searchWholeWord`, `searchRegex`, `replaceOne`, `replaceAll`, `searchCount({ current, total })`, `searchNoMatches`, `searchInvalid`.
1172
1220
 
1221
+ ### Find options
1222
+
1223
+ A `tool("search", options)` entry sets the **starting flags**: the panel opens with them, and the commands use them until the search is closed. The flags stay switchable in the panel, and the kit does not remember them between openings: closing the panel returns them to the starting values.
1224
+
1225
+ | Option | Meaning | Default |
1226
+ | ----------- | -------------------------------------------- | ------- |
1227
+ | `matchCase` | The panel opens with "Match case" on | `false` |
1228
+ | `wholeWord` | The panel opens with "Whole words" on | `false` |
1229
+ | `regex` | The panel opens with "Regular expression" on | `false` |
1230
+
1231
+ ```tsx
1232
+ // Searching code in Documents: case matters, so nobody has to switch the flag on each time.
1233
+ <RichTextEditor tools={[...RICH_TEXT_PRESETS.full, tool("search", { matchCase: true })]} />
1234
+ ```
1235
+
1236
+ ### Find state and commands
1237
+
1238
+ The state is read with `useRichTextToolState(ref, "search")`, and the commands are called on `ref.current?.tool("search")`. The general rules for both are in the "Handle (`ref`) and your own toolbar outside the field" section.
1239
+
1240
+ | State field | Meaning |
1241
+ | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1242
+ | `open` | The find panel is open. |
1243
+ | `query` | The query. Closing the panel removes the query and the highlighting; without a panel, the `setQuery` command sets it. |
1244
+ | `flags` | The flags `{ matchCase, wholeWord, regex }`. Closing the panel returns them to the starting values from the options. |
1245
+ | `total` | How many matches of the query are in the Document. |
1246
+ | `current` | The index of the current match, from 0; `-1` means no matches. |
1247
+ | `error` | The error text if the query is an invalid regular expression (the JavaScript engine's message, not localized); otherwise `null`. With an error there are no matches. |
1248
+
1249
+ The state has no match positions.
1250
+
1251
+ | Command | What it does |
1252
+ | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1253
+ | `open({ replace?, query? })` | Opens the panel, like the "Find" button and `Ctrl/⌘+F`. `query` puts the query in the field and in the Document highlighting at once; without it, the text selected within one Block becomes the query. `replace: true` puts focus in the replace field. `false` in HTML mode. |
1254
+ | `close()` | Closes the panel and removes the highlighting. Focus and the caret stay where they were, unlike `Escape` in the panel. A panel that is already closed is not an error. |
1255
+ | `setQuery(query, flags?)` | Sets the query. Flags not named in `flags` (and flags set to `undefined`) come from the open panel, or from the options when there is none. A non-empty query scrolls to the first match after the caret. `true` means the query was applied, even with no matches; `false` in HTML mode. |
1256
+ | `next()`, `previous()` | They select the next or previous match, wrapping around at either end, and scroll the field's scroll area or the window to it, even when focus is outside the Document; they do not take focus. `false` means no matches. |
1257
+ | `replace(text)` | Replaces the current match: one edit, one undo; the next match becomes current. `false` means no matches or a field the author cannot edit. |
1258
+ | `replaceAll(text)` | Replaces all matches: one edit, one undo. The conditions are the same as for `replace`. |
1259
+
1260
+ The query and the flags live in the engine, so `setQuery`, `next` and `replace` work without an open panel: you can build your own find interface on them. The panel and the commands share one search, so a replacement made from code shows up in the panel. Scrolling after `next`, `previous` and `setQuery` arrives a moment after the call: its code loads on first use and is not part of the Editor's starting chunk.
1261
+
1262
+ **A counter in the Status line.** While the find panel is open, the screen can show "3 of 12" next to the character counter. Closing the panel removes the query, and the counter disappears.
1263
+
1264
+ ```tsx
1265
+ import { useRef } from "react";
1266
+ import { RichTextEditor, useRichTextToolState } from "@olddevs/ui/editor";
1267
+ import type { RichTextEditorHandle } from "@olddevs/ui/editor";
1268
+
1269
+ function ArticleBody({ html }: { html: string }) {
1270
+ const ref = useRef<RichTextEditorHandle>(null);
1271
+ // `null` before mount and on the server, so the screen renders the same in both places.
1272
+ const search = useRichTextToolState(ref, "search");
1273
+
1274
+ return (
1275
+ <RichTextEditor
1276
+ ref={ref}
1277
+ aria-label="Article body"
1278
+ defaultValue={html}
1279
+ status={
1280
+ search?.open && search.query ? (
1281
+ <span>{search.total ? `${search.current + 1} of ${search.total}` : "No matches"}</span>
1282
+ ) : null
1283
+ }
1284
+ />
1285
+ );
1286
+ }
1287
+ ```
1288
+
1289
+ ## Table of contents
1290
+
1291
+ The `toc` Tool puts the Table of contents of the Document in the Editor's left gutter: the same strip of strokes and the same list as [`RichTextToc`](../content/rich-text-toc.md), but the Editor holds the place itself (ADR-0068). It is in neither Preset, `full` or `basic`: the Table of contents moves the text, and a kit update does not change the layout of screens that did not ask for it. Switch it on by name:
1292
+
1293
+ ```tsx
1294
+ <RichTextEditor tools={[...RICH_TEXT_PRESETS.full, "toc"]} defaultValue={html} />
1295
+ ```
1296
+
1297
+ A `tool("toc", options)` entry also switches the Tool on and configures it at the same time (see "Table of contents options" below).
1298
+
1299
+ - **Place.** A column to the left of the Document, inside the field frame. It exists only while there is a strip, from two `h1`–`h4` headings at the top level and in Wrappers: with the second heading the text moves right; without it the text stays at the edge, as without the Tool. The first stroke is level with the first line of the first heading.
1300
+ - **Stickiness.** The strip sticks under the fixed Toolbar (and under the app header: `--topbar-h`, ADR-0016); without a Toolbar, to the edge of the window. In a field with `maxHeight` or `resizable` it sticks to the top of the scrolling area, and the highlight and the jump count from there; the page does not scroll.
1301
+ - **The list** works as in `RichTextToc`: hover opens it to the right of the strip, Enter and Space open it from the keyboard, arrows move between items, Escape closes it; the current section is highlighted in the strip and in the list; the width is 240px, and a long heading is cut with an ellipsis. A click on an item does not close the list: moving the pointer away does.
1302
+ - **A jump while editing** puts the caret at the end of the heading, so you can type right away, as in the document outline of Google Docs; the Table of contents does the scrolling, and the heading flashes a fading background. In `readOnly` there is no caret, so focus goes to the heading itself.
1303
+ - **A live Document.** The list is rebuilt shortly after each edit: a new, renamed or removed heading shows in the strip. In HTML mode there is no Table of contents, because the Document is hidden.
1304
+ - **No Toolbar button.** To show or hide the Table of contents, the screen changes `tools` (and remounts the field with a `key`, as for any set of Tools).
1305
+
1306
+ Message catalog string: `editor.toc` ("Table of contents"), the name of the strip and the label of the list.
1307
+
1308
+ ### Table of contents options
1309
+
1310
+ A `tool("toc", options)` entry switches the Tool on and configures it. The standalone [`RichTextToc`](../content/rich-text-toc.md#public-api) takes the same `offsetVar`, `levels` and `listWidth`: the two share one core.
1311
+
1312
+ | Option | Meaning | Default |
1313
+ | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- |
1314
+ | `offsetVar` | The name of the CSS variable that holds the height of the screen's sticky header (ADR-0016), written in full with the two leading dashes: `--header-h`. A jump puts the heading under the header, and the highlight treats a section as current from the header's bottom edge. It does not move the Editor's own sticky Toolbar, which still sits under `--topbar-h`. | `--topbar-h` |
1315
+ | `levels` | The heading levels in the Table of contents: a subset of 1–4. A heading of any other level is in neither the strip nor the list, and item indentation is counted from the largest remaining level. An empty array and invalid values are replaced by the default. With fewer than two headings left, there is no gutter. | `[1, 2, 3, 4]` |
1316
+ | `listWidth` | The list width: pixels or a CSS length. Fixed rather than sized to the longest heading. | `240` |
1317
+
1318
+ ```tsx
1319
+ // The screen's header publishes its height in `--header-h`; the Table of contents needs only H2 and H3.
1320
+ <RichTextEditor
1321
+ tools={[...RICH_TEXT_PRESETS.full, tool("toc", { offsetVar: "--header-h", levels: [2, 3] })]}
1322
+ defaultValue={html}
1323
+ />
1324
+ ```
1325
+
1326
+ ### Table of contents state and commands
1327
+
1328
+ The state is read with `useRichTextToolState(ref, "toc")`, and the command is called on `ref.current?.tool("toc")`.
1329
+
1330
+ | State field | Meaning |
1331
+ | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1332
+ | `headings` | The headings in reading order, the same as in the strip and the list: top level and Wrappers, only the levels from `levels`, no empty ones. Each is `{ level, text, index }`: a level 1–4, the text without markup, and the index of the list item, from 0. The list is returned even with one heading, although there is no gutter then. |
1333
+ | `current` | The index of the current section in `headings`, from 0; `-1` means no headings. It follows scrolling. After the Document is edited, `headings` changes at once, while `current` catches up with a delay while the list is rebuilt. |
1334
+
1335
+ The headings are read from the Document, not from the DOM: the list is ready on the first frame without waiting for the lazy Table of contents UI, and it changes together with the edit. The state has no DOM nodes or ProseMirror positions.
1336
+
1337
+ There is one command, `jumpTo(index)`: go to a heading of the list, like a click on an item. The index is counted in the same list (with `levels` applied, from 0, empty headings skipped) and is taken from the state snapshot: right after the Document is edited, it leads to the heading that the snapshot names, even though the Table of contents UI rebuilds its list with a delay. While editing, the caret goes to the end of the heading; in `readOnly`, the heading itself gets focus; scrolling puts it under the sticky header (`offsetVar`) and the Toolbar, and the heading's highlight fades. `false` means there is no heading with that index, the Table of contents UI has not loaded yet, or HTML mode is on. Fewer than two headings is not a reason to refuse: there is no gutter, but the list exists, and the jump works.
1338
+
1339
+ **External navigation.** The screen builds breadcrumbs, a minimap or a list of sections next to the field from the state, instead of parsing the Document's DOM a second time.
1340
+
1341
+ ```tsx
1342
+ import type { RefObject } from "react";
1343
+ import { useRichTextToolState } from "@olddevs/ui/editor";
1344
+ import type { RichTextEditorHandle } from "@olddevs/ui/editor";
1345
+
1346
+ function SectionNav({ target }: { target: RefObject<RichTextEditorHandle | null> }) {
1347
+ const toc = useRichTextToolState(target, "toc");
1348
+ if (!toc) return null;
1349
+
1350
+ return (
1351
+ <nav aria-label="Sections">
1352
+ <ul>
1353
+ {toc.headings.map((heading) => (
1354
+ <li
1355
+ key={heading.index}
1356
+ aria-current={heading.index === toc.current ? "location" : undefined}
1357
+ >
1358
+ <button
1359
+ type="button"
1360
+ onClick={() => target.current?.tool("toc")?.jumpTo(heading.index)}
1361
+ >
1362
+ {heading.text}
1363
+ </button>
1364
+ </li>
1365
+ ))}
1366
+ </ul>
1367
+ </nav>
1368
+ );
1369
+ }
1370
+ ```
1371
+
1173
1372
  ## Status line and Field notice
1174
1373
 
1175
1374
  Under the field inside the frame there are two independent areas. The **Status line** is what the screen puts there itself: "Saved", a word count, a co-author. The **Field notice** is what the kit and Tools report: an upload result, a paste rejection. The first is a React node in the `status` prop, the second is data and an engine command.
@@ -1276,7 +1475,8 @@ import type { RichTextEditorHandle } from "@olddevs/ui/editor";
1276
1475
  | `focus(position?)` | `"end"` (the default), `"start"` or `"restore"`: return focus to where the selection was, for example after your own dialog or menu. |
1277
1476
  | `blur()` | Remove focus from the Document. |
1278
1477
  | `undo()`, `redo()` | Undo and redo an edit; they work with the `history` Tool enabled. |
1279
- | `openSearch({ replace? })` | Open the "Find and replace" panel, like the "Find" button and `Ctrl/⌘+F`; `replace: true` puts focus in the replace field. Does nothing without the `search` Tool, in HTML mode and before mount. |
1478
+ | `tool(name)` | The commands of a Tool: `tool("search")?.open()`, `tool("toc")?.jumpTo(2)`. `null` means the Tool is not in the set, has no commands, or the engine has not been created yet. See "Tool commands: `handle.tool()`". |
1479
+ | `openSearch({ replace? })` | **Deprecated**, removed in 5.0: the same as `tool("search")?.open({ replace })`; warns once in development. |
1280
1480
  | `canUndo()`, `canRedo()` | Whether `undo` and `redo` are available: `false` without `history`. |
1281
1481
  | `getHTML()`, `getMarkdown()`, `setMarkdown(md)`, `clear()` | See "Public API" and "Markdown". |
1282
1482
  | `getJSON()`, `setJSON(document): boolean` | The Document as a lossless JSON envelope, and replacing the Document with an envelope; `false` means the envelope was rejected. See "JSON". |
@@ -1344,6 +1544,35 @@ function Screen() {
1344
1544
 
1345
1545
  **History with an external toolbar.** The Handle provides `undo()` and `redo()` and their availability. For the `disabled` state of an "Undo" button, get availability from the hook's slice — `editor.can().undo?.() === true`: the slice re-renders the toolbar right after an edit. `ref.current.canUndo()` is a one-off read in a handler, and it is not subscribed to re-rendering. Without the `history` Tool the engine has no `undo` command (hence the `?.`), and both buttons stay unavailable.
1346
1546
 
1547
+ ### Tool state: `useRichTextToolState`
1548
+
1549
+ `useRichTextToolState(ref, name, selector?)` gives the screen the state of a Tool: a "3 of 12" counter from Find in the Status line, or external navigation built from the headings of the Table of contents. The hook sits next to `useRichTextEditorState` and follows the Handle's engine in the same way, but it returns not a slice of the Tiptap engine but a typed snapshot of the kit's contract. The state fields of the built-in Tools are in the "Find and replace" and "Table of contents" sections.
1550
+
1551
+ - **Name and type.** `name` is a Tool from the `tools` set; the snapshot type is inferred from the `RichTextTools` map: `search` returns `SearchToolState`, `toc` returns `TocToolState`. A name outside the map (a custom Tool without typing) gives `unknown`.
1552
+ - **Re-renders only when the snapshot changes.** The snapshot is recomputed on every engine transaction and on kit signals that the engine does not have: the find panel opening and closing, and the current section of the Table of contents changing on scroll. The screen re-renders when the result becomes different (plain data — primitives, arrays and plain objects — is compared by value, like `useRichTextEditorState`; `Date`, `Map`, `Set` and class instances are compared by reference), so typing that does not change the snapshot does not touch it. A custom Tool's `state` that creates such values anew on every call re-renders the screen on every transaction.
1553
+ - **A selector.** The third argument subscribes the screen to a slice: `useRichTextToolState(ref, "search", (state) => state.total)` re-renders it only when the number of matches changes. With `null`, the selector is not called.
1554
+ - **`null`** is what the hook returns before mount and in the screen's first render, on the server, after the Editor unmounts, and when the Tool is not in the set: Presets do not include `toc`, so you enable it by name or with a `tool("toc")` entry. This way the screen renders the same on the server and in the browser.
1555
+ - **A snapshot is data**: no DOM nodes, functions or ProseMirror positions. Do not mutate it.
1556
+ - **Cost.** The hook is not part of the `RichTextEditor` starting graph: only a screen that imports it pays for it.
1557
+
1558
+ The state of a custom Tool is declared with `defineTool` (the `state` field) and read with the same hook: ["Options, state and commands"](../../api/editor-extension.md#опции-состояние-и-команды).
1559
+
1560
+ ### Tool commands: `handle.tool()`
1561
+
1562
+ `ref.current?.tool(name)` returns the commands object of a Tool: `tool("search")?.open({ query: "TODO" })`, `tool("toc")?.jumpTo(2)`. The commands of the built-in Tools are described in the "Find and replace" and "Table of contents" sections.
1563
+
1564
+ ```tsx
1565
+ <button type="button" onClick={() => ref.current?.tool("search")?.open({ query: "TODO" })}>
1566
+ Find TODO
1567
+ </button>
1568
+ ```
1569
+
1570
+ - **The type is inferred from the `RichTextTools` map**, by the Tool's name. A name outside the map gives `unknown`.
1571
+ - **`null`** when the Tool is not in `tools`, has no commands, or the engine has not been created yet. That is why the commands of a disabled Tool are unavailable both in types (`?.`) and at run time.
1572
+ - **Every command returns a `boolean`:** done or not (HTML mode, no matches, no such heading, a field that cannot be edited). A refusal is `false`, as with `insertHTML`, not an exception.
1573
+ - **Take the object on every call** instead of keeping it between renders: it is cheap, and this way you always work with the live engine.
1574
+ - **The Handle gets no new method for each command.** The commands of the built-in Tools go through `tool(name)`, not through new methods; `openSearch` remains for compatibility only. Commands of Marks, Blocks and anything a Tool has not declared are still called through `ref.current.editor`.
1575
+
1347
1576
  ## Events
1348
1577
 
1349
1578
  The screen learns about the Editor through callback props; there is no event bus. The decision is recorded in ADR-0058, section 9.
@@ -1405,9 +1634,10 @@ export const pdfTool: CustomTool = { name: "acme_pdfDrop", engine: [PdfDrop] };
1405
1634
  - `maxHeight` — the height limit of the Document area (a number is pixels, a string is a CSS length), beyond it the Document scrolls inside the frame; `resizable` — a height handle at the area's bottom edge (a slider for keyboard users), and `maxHeight` with it is the starting height. See "Field height".
1406
1635
  - `surface` — `"theme"` (default) or `"paper"`: a light Paper in any theme, see "Surface". The type is `RichTextSurface`.
1407
1636
  - `status` — a React node of the Status line inside the field's frame, at the start of the counter row; the kit does not announce it. The engine command `showNotice({ tone, text } | null)` is the Field notice. Both are in "Status line and Field notice"; they follow the kit's semver.
1408
- - `tools` — kit names and descriptions of custom Tools (`CustomTool`, including their `buttons`, `insertItems`, `blockActions`, `textCards` and `trigger`), Presets as arrays — `RICH_TEXT_PRESETS`; see "Tools and Presets" and "Custom Tool". A description with an error is a field error.
1409
- - `ref` (`RichTextEditorHandle`): `getHTML()` — the current HTML, without waiting for the delay and without `onChange` (in HTML mode it first applies the field's edit — then `onChange` is called if the Document changed; the same for `getMarkdown()`); `getMarkdown()` and `setMarkdown(md)` — see "Markdown"; `getJSON()` and `setJSON(document)` — see "JSON"; `clear()` — clear the Document, and `onChange("")` notifies the screen; `focus(position?)`, `blur()`, `insertHTML`, `insertText`, `getText`, `getSelectedText`, `isEmpty`, `undo`, `redo`, `canUndo`, `canRedo` — see "Handle (`ref`) and your own toolbar outside the field"; `openSearch({ replace? })` — open the find panel, see "Find and replace"; `editor` — the underlying Tiptap `Editor`, its API **follows Tiptap's major version**, and before mount and on the server it is `null`. `focus()` without an argument puts the caret at the end of the Document (in HTML mode, in the HTML field).
1637
+ - `tools` — kit names, Configured Tools `tool(name, options)` for `search` and `toc`, `defineTool(…)` definitions and descriptions of custom Tools (`CustomTool`, including their `buttons`, `insertItems`, `blockActions`, `textCards` and `trigger`), Presets as arrays — `RICH_TEXT_PRESETS`; see "Tools and Presets" and "Custom Tool". A description with an error is a field error.
1638
+ - `ref` (`RichTextEditorHandle`): `getHTML()` — the current HTML, without waiting for the delay and without `onChange` (in HTML mode it first applies the field's edit — then `onChange` is called if the Document changed; the same for `getMarkdown()`); `getMarkdown()` and `setMarkdown(md)` — see "Markdown"; `getJSON()` and `setJSON(document)` — see "JSON"; `clear()` — clear the Document, and `onChange("")` notifies the screen; `focus(position?)`, `blur()`, `insertHTML`, `insertText`, `getText`, `getSelectedText`, `isEmpty`, `undo`, `redo`, `canUndo`, `canRedo` — see "Handle (`ref`) and your own toolbar outside the field"; `tool(name)` — the commands of a Tool, see "Tool commands: `handle.tool()`"; `openSearch({ replace? })` — deprecated, removed in 5.0, replaced by `tool("search")?.open({ replace })`; `editor` — the underlying Tiptap `Editor`, its API **follows Tiptap's major version**, and before mount and on the server it is `null`. `focus()` without an argument puts the caret at the end of the Document (in HTML mode, in the HTML field).
1410
1639
  - `useRichTextEditorState(ref, selector)` — a hook for your own toolbar outside the field, see "Handle (`ref`) and your own toolbar outside the field".
1640
+ - `useRichTextToolState(ref, name, selector?)` — the hook for Tool state (`search`, `toc` and custom Tools with `defineTool`): a data snapshot, `null` before mount, after unmount and without the Tool; see "Tool state: `useRichTextToolState`". It follows the kit's semver, as do `tool()`, `defineTool()`, `handle.tool()` and the `RichTextTools` map.
1411
1641
  - SSR: the engine is created only in the browser (`immediatelyRender: false`). On the server it renders a Skeleton without a Document — the frame and the space reserved for the Toolbar — and a hidden field with the incoming value. After hydration the Skeleton shows the Document cleaned by the Schema, and the editable area takes its place after the Loaders, without a height jump. The exception is a JSON envelope in `defaultValue`: only the engine builds its HTML, so with it the Skeleton is a frame without a Document, the hidden field is empty until Readiness, and the height changes once when the Document appears.
1412
1642
 
1413
1643
  ## Markdown
@@ -1528,12 +1758,16 @@ There are no other classes, no `style` (except the author's color), no `aria-*`
1528
1758
  - Changing `tools` on a mounted field expecting the Schema to be rebuilt: the set is read when the engine is created.
1529
1759
  - Conveying the meaning of a note by the Wrapper tone alone: a screen reader cannot see the background, a color-blind person cannot tell the tones apart, and in Markdown without alert support only a quote remains. Write it in words — "Warning: …" (WCAG 1.4.1).
1530
1760
  - Coloring text inside a Wrapper that has a tone with a palette color or a custom color: on a colored background its contrast may fall below 4.5:1 — the gate checks the Document's main and secondary text, not the author's color.
1761
+ - Changing `tool(…)` on a mounted field expecting new options: they are read when the engine is created, like `tools`. To change options, remount the field with a key.
1762
+ - Taking the number of Find matches or the headings of the Table of contents from `ref.current.editor`, from the Document's DOM or from internal plugin keys: that is not the kit's contract and it follows the Tiptap major. Use `useRichTextToolState` for this.
1763
+ - Calling `openSearch()` in new code: the method is deprecated and will disappear in 5.0. Use `tool("search")?.open()`.
1764
+ - Writing `tool("bold", …)` or options the Tool does not have in `tools`: the compiler stops it, and in JS without types the extra option is dropped with a `console.error`.
1531
1765
 
1532
1766
  ## Links
1533
1767
 
1534
- - Source: `src/editor/RichTextEditor.tsx`; the Toolbar is `src/editor/ui/EditorToolbar.tsx`, the formatting menus ("¶", "…", "Alignment", "Lists") are `src/editor/ui/FormatMenus.tsx` and `src/editor/core/blockTypes.ts`, the floating one is `src/editor/ui/FloatingToolbar.tsx`, the Selection menu is `src/editor/ui/SelectionBubble.tsx`, the shared display condition and position are `src/editor/ui/selectionMenu.ts`; the link is `src/editor/features/link/link.ts`, the Tool description `src/editor/features/link/linkTool.tsx` and the URL form `src/editor/features/link/LinkPopover.tsx`; emoji are the Tool description `src/editor/features/emoji/emojiTool.tsx` and the grid `src/editor/features/emoji/EmojiGrid.tsx`; buttons from Tool descriptions and the frame of their popovers are `src/editor/ui/ToolButtonView.tsx`, their shortcuts are `src/editor/core/toolButtons.ts`, the shortcuts of their Insert items and Block actions are `src/editor/core/toolEntryShortcuts.ts`; the image is `src/editor/features/image/image.ts` (node, upload, file paste), `src/editor/features/image/ImagePopover.tsx`, `src/editor/features/image/ImageToolbar.tsx`; the Field notice is the command `src/editor/core/fieldNotice.ts`, the file texts `src/editor/features/image/imageNotice.ts`, the live region, the error line and the Status line `src/editor/ui/FieldStatus.tsx`; a custom Tool is the public form `src/editor/core/customTool.ts`, the engine and set validation `src/editor/core/customToolEngine.ts`, the `tools` expansion `src/editor/features/builtins.ts`; the table is `src/editor/features/table/tables.ts` (nodes, merging, alignment, Markdown), `src/editor/features/table/tableActions.ts` (menu commands, sorting, size memory), `src/editor/features/table/TableMenuButton.tsx` (the "Table" menu), `src/editor/features/table/TableSizeGrid.tsx` and `src/editor/features/table/TableInsertDialog.tsx`; the Layout is `src/editor/features/layout/layout.ts` (nodes, commands, keyboard) and `src/editor/features/layout/columnLayouts.ts` (variants as data); the Wrapper is `src/editor/features/wrapper/wrapper.ts` (node, insertion, unwrapping, tone, keyboard, Markdown) and `src/editor/features/wrapper/wrapperTones.ts` (tones and GitHub alerts labels as data); the Document with Block containers is `src/editor/core/document.ts`; the Handle is `src/editor/core/handle.ts`, the hook is `src/editor/hooks/useRichTextEditorState.ts`; HTML mode is the state and applying in `src/editor/RichTextEditor.tsx`, HTML formatting in `src/editor/core/sourceFormat.ts`; Tools and Presets are `src/editor/core/tools.ts`; Block commands are `src/editor/core/blockCommands.ts`; the Block handle is `src/editor/ui/BlockHandle.tsx`; the shortcut table and labels are `src/editor/core/shortcuts.ts`, the Shortcuts reference is `src/editor/ui/ShortcutsButton.tsx`; the Glass of popup surfaces is `src/editor/styles/glass.ts`; the Insert menu is `src/editor/core/insertItems.ts` (the item registry), `src/editor/ui/SlashMenu.tsx`, `src/editor/ui/InsertMenuButton.tsx`, `src/editor/core/slashCommand.ts`; the Schema as data is `src/editor/core/schema.ts`; the palette and alignments are `src/editor/styles/palette.ts`, the color extensions are `src/editor/features/color/styleExtensions.ts`; the author's color is the form and parsing `src/editor/features/color/customColor.ts`, contrast `src/editor/features/color/contrast.ts`, the popover `src/editor/features/color/CustomColorPopover.tsx`; typography is `src/editor/styles/prose.ts`; the code block is `src/editor/features/code/codeBlock.ts` (extension, aliases, `Alt+F10`), `src/editor/features/code/CodeBlockView.tsx` (the header: language and "Copy"), `src/editor/features/code/CodeCopyButton.tsx` (the button with a tooltip), `src/editor/features/code/codeHeader.ts` (header classes shared with the View), the Paper (`surface="paper"`) is `src/editor/styles/surface.ts` and `src/editor/styles/tokens.ts`, `src/editor/features/code/codeLanguages.ts` (languages as data), `src/editor/features/code/lowlight.ts` (grammars), `src/editor/styles/codeTheme.ts` (the syntax highlighting theme), the server helper is `src/editor/features/code/highlight.ts`; find and replace is `src/editor/features/search/searchTool.ts` (the Tool), `src/editor/features/search/searchPlugin.ts` and `src/editor/features/search/SearchPanel.tsx`; JSON is `src/editor/core/jsonDocument.ts`; Field height is the resize handle `src/editor/ui/FieldResizer.tsx`; emoji (the set's data) is `src/editor/features/emoji/emoji.ts`; the character limit is `src/editor/core/maxLength.ts`; Markdown is `src/editor/core/markdown.ts`.
1535
- - Tests: `tests/rich-text-editor-text-card.test.tsx` (Text card), `tests/rich-text-editor.test.tsx`, `tests/rich-text-editor-handle.test.tsx` and `tests/rich-text-editor-handle-ssr.test.tsx` (Handle and hook), `tests/rich-text-editor-toolbar.test.tsx`, `tests/rich-text-editor-block-menu.test.tsx`, `tests/rich-text-editor-more-menu.test.tsx`, `tests/rich-text-editor-align-list-menus.test.tsx`, `tests/rich-text-editor-insert-menu.test.tsx`, `tests/rich-text-editor-schema.test.tsx`, `tests/rich-text-editor-markdown.test.tsx`, `tests/rich-text-editor-color.test.tsx`, `tests/rich-text-editor-custom-color.test.tsx`, `tests/rich-text-editor-link.test.tsx`, `tests/rich-text-editor-floating.test.tsx`, `tests/rich-text-editor-bubble.test.tsx`, `tests/rich-text-editor-glass.test.tsx`, `tests/rich-text-editor-table.test.tsx`, `tests/rich-text-editor-lazy-builtins.test.tsx` (lazy code block and table: `basic` does not load them, `full` shows a Skeleton until the chunks load), `tests/rich-text-editor-image.test.tsx`, `tests/rich-text-editor-notice.test.tsx`, `tests/rich-text-editor-code-block.test.tsx`, `tests/rich-text-editor-block-commands.test.tsx`, `tests/rich-text-editor-block-handle.test.tsx`, `tests/rich-text-editor-shortcuts.test.tsx`, `tests/rich-text-editor-layout.test.tsx`, `tests/rich-text-editor-wrapper.test.tsx`, `tests/rich-text-editor-wrapper-tone.test.tsx`, `tests/rich-text-editor-source.test.tsx`, `tests/rich-text-editor-custom-tool.test.tsx` (a custom Tool; test Tools are in `tests/editor-test-tools.ts`), `tests/rich-text-editor-custom-ui.test.tsx` (Insert items and Block actions of a custom Tool), `tests/rich-text-editor-trigger.test.tsx` (a custom Trigger character: opening, picking, the query, asynchronous items, set errors), `tests/rich-text-editor-custom-button.test.tsx` (buttons of a custom Tool), `tests/rich-text-editor-custom-markdown.test.tsx` (Markdown of a custom Block by default and by its own rules), `tests/rich-text-editor-atom-text.test.tsx` (Atom text, weight in `maxLength`, emptiness), `tests/rich-text-editor-empty-table.test.tsx` (an empty table is content), `tests/rich-text-editor-max-height.test.tsx` (`maxHeight` and `resizable`: what is inside the scroll area, the position of the Block handle during scrolling), `tests/rich-text-editor-events.test.tsx` (`onReady`, `onFocus`, `onBlur`, paste and drop order), `tests/rich-text-editor-loaders.test.tsx` (Engine part Loaders, "Retry"), `tests/rich-text-editor-skeleton.test.tsx` (Skeleton), `tests/rich-text-editor-emoji.test.tsx` (emoji), `tests/rich-text-editor-json.test.tsx` (JSON), `tests/editor-search.test.tsx`, `tests/editor-search-panel.test.tsx` and `tests/editor-search-replace.test.tsx` (find and replace), `tests/rich-text-paper.test.tsx`, `tests/rich-text-paper-css.test.tsx`, `tests/editor-entry.test.ts`, `tests/editor-schema-entry.test.ts`, `tests/editor-highlight-entry.test.ts`; in the browser — `tests-visual/interactions.spec.ts` ("RichTextEditor inside Dialog" — Escape in the style list, in the Insert menu, in the link popover and in the code block language menu; "RichTextEditor inside-dialog" and "inside-drawer" — the "…" menu and the color popovers with mouse and keyboard; "RichTextEditor insert menu"; "RichTextEditor block commands" — real `⌥⇧↑/↓`, `⌘]`, `⌘[` and `⌘⇧D`), `tests-visual/rich-text-editor-image.spec.ts` (file drag and width handles), `tests-visual/rich-text-editor-bubble.spec.ts` (the Selection menu in `Dialog`, `Drawer` and a scroll container, at the viewport edge, the inverse surface in three themes), `tests-visual/rich-text-editor-layout.spec.ts` (Layout in a `Dialog`: columns side by side and Tab across them, arrow keys, a stack on a phone), `tests-visual/rich-text-editor-wrapper.spec.ts` (Wrapper: a dashed outline in the Editor and no frame in `RichTextView`, the tone looks the same in both, exit with Enter, `↑` out of a Wrapper), `tests-visual/rich-text-editor-block-handle.spec.ts` (the handle menu in a `Dialog`: `Ctrl/⌘+/`, Escape by levels; the handle's position at the Block's first line), `tests-visual/rich-text-editor-trigger.spec.ts` (a custom Trigger character: the list at the caret, real typing of `@`, picking by mouse and Enter, `a@b` inside a word, axe in three themes), `tests-visual/rich-text-editor-source.spec.ts` (HTML mode: field height, growth with content, focus on entry and exit), `tests-visual/rich-text-editor-height.spec.ts` (Field height), `tests-visual/rich-text-editor-emoji.spec.ts` (emoji), `tests-visual/rich-text-editor-skeleton.spec.ts` (Skeleton).
1768
+ - Source: `src/editor/RichTextEditor.tsx`; the Toolbar is `src/editor/ui/EditorToolbar.tsx`, the formatting menus ("¶", "…", "Alignment", "Lists") are `src/editor/ui/FormatMenus.tsx` and `src/editor/core/blockTypes.ts`, the floating one is `src/editor/ui/FloatingToolbar.tsx`, the Selection menu is `src/editor/ui/SelectionBubble.tsx`, the shared display condition and position are `src/editor/ui/selectionMenu.ts`; the link is `src/editor/features/link/link.ts`, the Tool description `src/editor/features/link/linkTool.tsx` and the URL form `src/editor/features/link/LinkPopover.tsx`; emoji are the Tool description `src/editor/features/emoji/emojiTool.tsx` and the grid `src/editor/features/emoji/EmojiGrid.tsx`; buttons from Tool descriptions and the frame of their popovers are `src/editor/ui/ToolButtonView.tsx`, their shortcuts are `src/editor/core/toolButtons.ts`, the shortcuts of their Insert items and Block actions are `src/editor/core/toolEntryShortcuts.ts`; the image is `src/editor/features/image/image.ts` (node, upload, file paste), `src/editor/features/image/ImagePopover.tsx`, `src/editor/features/image/ImageToolbar.tsx`; the Field notice is the command `src/editor/core/fieldNotice.ts`, the file texts `src/editor/features/image/imageNotice.ts`, the live region, the error line and the Status line `src/editor/ui/FieldStatus.tsx`; a custom Tool is the public form `src/editor/core/customTool.ts`, the engine and set validation `src/editor/core/customToolEngine.ts`, the `tools` expansion and the options of built-in Tools `src/editor/features/builtins.ts`; `tool()` and the `RichTextTools` map `src/editor/core/toolOptions.ts`, the command types `src/editor/core/toolCommands.ts`, the state types `src/editor/core/toolState.ts`, the state hook `src/editor/hooks/useRichTextToolState.ts`, the Find and Table of contents descriptions `src/editor/features/search/searchTool.ts` and `src/editor/features/toc/tocTool.ts`; the table is `src/editor/features/table/tables.ts` (nodes, merging, alignment, Markdown), `src/editor/features/table/tableActions.ts` (menu commands, sorting, size memory), `src/editor/features/table/TableMenuButton.tsx` (the "Table" menu), `src/editor/features/table/TableSizeGrid.tsx` and `src/editor/features/table/TableInsertDialog.tsx`; the Layout is `src/editor/features/layout/layout.ts` (nodes, commands, keyboard) and `src/editor/features/layout/columnLayouts.ts` (variants as data); the Wrapper is `src/editor/features/wrapper/wrapper.ts` (node, insertion, unwrapping, tone, keyboard, Markdown) and `src/editor/features/wrapper/wrapperTones.ts` (tones and GitHub alerts labels as data); the Document with Block containers is `src/editor/core/document.ts`; the Handle is `src/editor/core/handle.ts`, the hook is `src/editor/hooks/useRichTextEditorState.ts`; HTML mode is the state and applying in `src/editor/RichTextEditor.tsx`, HTML formatting in `src/editor/core/sourceFormat.ts`; Tools and Presets are `src/editor/core/tools.ts`; Block commands are `src/editor/core/blockCommands.ts`; the Block handle is `src/editor/ui/BlockHandle.tsx`; the shortcut table and labels are `src/editor/core/shortcuts.ts`, the Shortcuts reference is `src/editor/ui/ShortcutsButton.tsx`; the Glass of popup surfaces is `src/editor/styles/glass.ts`; the Insert menu is `src/editor/core/insertItems.ts` (the item registry), `src/editor/ui/SlashMenu.tsx`, `src/editor/ui/InsertMenuButton.tsx`, `src/editor/core/slashCommand.ts`; the Schema as data is `src/editor/core/schema.ts`; the palette and alignments are `src/editor/styles/palette.ts`, the color extensions are `src/editor/features/color/styleExtensions.ts`; the author's color is the form and parsing `src/editor/features/color/customColor.ts`, contrast `src/editor/features/color/contrast.ts`, the popover `src/editor/features/color/CustomColorPopover.tsx`; typography is `src/editor/styles/prose.ts`; the code block is `src/editor/features/code/codeBlock.ts` (extension, aliases, `Alt+F10`), `src/editor/features/code/CodeBlockView.tsx` (the header: language and "Copy"), `src/editor/features/code/CodeCopyButton.tsx` (the button with a tooltip), `src/editor/features/code/codeHeader.ts` (header classes shared with the View), the Paper (`surface="paper"`) is `src/editor/styles/surface.ts` and `src/editor/styles/tokens.ts`, `src/editor/features/code/codeLanguages.ts` (languages as data), `src/editor/features/code/lowlight.ts` (grammars), `src/editor/styles/codeTheme.ts` (the syntax highlighting theme), the server helper is `src/editor/features/code/highlight.ts`; find and replace is `src/editor/features/search/searchTool.ts` (the Tool), `src/editor/features/search/searchPlugin.ts` and `src/editor/features/search/SearchPanel.tsx`; JSON is `src/editor/core/jsonDocument.ts`; Field height is the resize handle `src/editor/ui/FieldResizer.tsx`; emoji (the set's data) is `src/editor/features/emoji/emoji.ts`; the character limit is `src/editor/core/maxLength.ts`; Markdown is `src/editor/core/markdown.ts`.
1769
+ - Tests: `tests/rich-text-editor-text-card.test.tsx` (Text card), `tests/rich-text-editor.test.tsx`, `tests/rich-text-editor-handle.test.tsx` and `tests/rich-text-editor-handle-ssr.test.tsx` (Handle and hook), `tests/rich-text-editor-toolbar.test.tsx`, `tests/rich-text-editor-block-menu.test.tsx`, `tests/rich-text-editor-more-menu.test.tsx`, `tests/rich-text-editor-align-list-menus.test.tsx`, `tests/rich-text-editor-insert-menu.test.tsx`, `tests/rich-text-editor-schema.test.tsx`, `tests/rich-text-editor-markdown.test.tsx`, `tests/rich-text-editor-color.test.tsx`, `tests/rich-text-editor-custom-color.test.tsx`, `tests/rich-text-editor-link.test.tsx`, `tests/rich-text-editor-floating.test.tsx`, `tests/rich-text-editor-bubble.test.tsx`, `tests/rich-text-editor-glass.test.tsx`, `tests/rich-text-editor-table.test.tsx`, `tests/rich-text-editor-lazy-builtins.test.tsx` (lazy code block and table: `basic` does not load them, `full` shows a Skeleton until the chunks load), `tests/rich-text-editor-image.test.tsx`, `tests/rich-text-editor-notice.test.tsx`, `tests/rich-text-editor-code-block.test.tsx`, `tests/rich-text-editor-block-commands.test.tsx`, `tests/rich-text-editor-block-handle.test.tsx`, `tests/rich-text-editor-shortcuts.test.tsx`, `tests/rich-text-editor-layout.test.tsx`, `tests/rich-text-editor-wrapper.test.tsx`, `tests/rich-text-editor-wrapper-tone.test.tsx`, `tests/rich-text-editor-source.test.tsx`, `tests/rich-text-editor-custom-tool.test.tsx` (a custom Tool; test Tools are in `tests/editor-test-tools.ts`), `tests/rich-text-editor-custom-ui.test.tsx` (Insert items and Block actions of a custom Tool), `tests/rich-text-editor-trigger.test.tsx` (a custom Trigger character: opening, picking, the query, asynchronous items, set errors), `tests/rich-text-editor-custom-button.test.tsx` (buttons of a custom Tool), `tests/rich-text-editor-custom-markdown.test.tsx` (Markdown of a custom Block by default and by its own rules), `tests/rich-text-editor-atom-text.test.tsx` (Atom text, weight in `maxLength`, emptiness), `tests/rich-text-editor-empty-table.test.tsx` (an empty table is content), `tests/rich-text-editor-max-height.test.tsx` (`maxHeight` and `resizable`: what is inside the scroll area, the position of the Block handle during scrolling), `tests/rich-text-editor-events.test.tsx` (`onReady`, `onFocus`, `onBlur`, paste and drop order), `tests/rich-text-editor-loaders.test.tsx` (Engine part Loaders, "Retry"), `tests/rich-text-editor-skeleton.test.tsx` (Skeleton), `tests/rich-text-editor-emoji.test.tsx` (emoji), `tests/rich-text-editor-json.test.tsx` (JSON), `tests/editor-search.test.tsx`, `tests/editor-search-panel.test.tsx` and `tests/editor-search-replace.test.tsx` (find and replace), `tests/rich-text-editor-tool-options.test.tsx` and `tests/editor-tool-options-types.test.ts` (`tool()` options), `tests/rich-text-editor-tool-state.test.tsx`, `tests/rich-text-editor-tool-state-ssr.test.tsx` and `tests/editor-tool-state-types.test.ts` (state), `tests/rich-text-editor-tool-commands.test.tsx`, `tests/editor-search-commands-reveal.test.tsx` and `tests/editor-tool-commands-types.test.ts` (`handle.tool()` commands), `tests/rich-text-editor-define-tool.test.tsx`, `tests/editor-define-tool-types.test.ts` and `tests/editor-define-tool-server.test.tsx` (`defineTool`), `tests/rich-text-paper.test.tsx`, `tests/rich-text-paper-css.test.tsx`, `tests/editor-entry.test.ts`, `tests/editor-schema-entry.test.ts`, `tests/editor-highlight-entry.test.ts`; in the browser — `tests-visual/interactions.spec.ts` ("RichTextEditor inside Dialog" — Escape in the style list, in the Insert menu, in the link popover and in the code block language menu; "RichTextEditor inside-dialog" and "inside-drawer" — the "…" menu and the color popovers with mouse and keyboard; "RichTextEditor insert menu"; "RichTextEditor block commands" — real `⌥⇧↑/↓`, `⌘]`, `⌘[` and `⌘⇧D`), `tests-visual/rich-text-editor-image.spec.ts` (file drag and width handles), `tests-visual/rich-text-editor-bubble.spec.ts` (the Selection menu in `Dialog`, `Drawer` and a scroll container, at the viewport edge, the inverse surface in three themes), `tests-visual/rich-text-editor-layout.spec.ts` (Layout in a `Dialog`: columns side by side and Tab across them, arrow keys, a stack on a phone), `tests-visual/rich-text-editor-wrapper.spec.ts` (Wrapper: a dashed outline in the Editor and no frame in `RichTextView`, the tone looks the same in both, exit with Enter, `↑` out of a Wrapper), `tests-visual/rich-text-editor-block-handle.spec.ts` (the handle menu in a `Dialog`: `Ctrl/⌘+/`, Escape by levels; the handle's position at the Block's first line), `tests-visual/rich-text-editor-trigger.spec.ts` (a custom Trigger character: the list at the caret, real typing of `@`, picking by mouse and Enter, `a@b` inside a word, axe in three themes), `tests-visual/rich-text-editor-source.spec.ts` (HTML mode: field height, growth with content, focus on entry and exit), `tests-visual/rich-text-editor-height.spec.ts` (Field height), `tests-visual/rich-text-editor-emoji.spec.ts` (emoji), `tests-visual/rich-text-editor-skeleton.spec.ts` (Skeleton).
1536
1770
  - The extension API at a glance, "Stability" and recipes — [Editor extension API](../../api/editor-extension.md).
1537
1771
  - Viewing without editing — [RichTextView](../content/rich-text-view.md).
1538
- - Storybook: `Inputs/RichTextEditor`.
1772
+ - Storybook: `Inputs/RichTextEditor` (the `SearchStatusLine`, `TableOfContentsOptions`, `TableOfContentsExternalNavigation`, `ToolCommands`, `DefinedTool` and `DefinedToolConfigured` stories).
1539
1773
  - Code origin: minimal-tiptap (MIT); syntax highlighting theme colors — highlight.js (BSD-3-Clause); licenses are in the package `NOTICE`.