@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
@@ -2,20 +2,23 @@
2
2
  category: api
3
3
  status: stable
4
4
  source: src/editor/index.ts
5
- sourceHash: 2c7fdfa5b00e
5
+ sourceHash: 82b5217dd20e
6
6
  ---
7
7
 
8
8
  # Editor extension API
9
9
 
10
10
  ## Summary for AI
11
11
 
12
- A map of how a product extends `RichTextEditor` from `@olddevs/ui/editor`, and the stability boundary of this API. The unit of extension is a **custom Tool** (`CustomTool`): the description of one Editor capability, passed in `tools` mixed in with the kit's names. It carries Blocks and Marks with Schema rules (as data), an Engine part (Tiptap extensions, supplied directly or through a Loader) and UI contributions: buttons, Insert items, Block actions, Text cards. A custom Trigger character (`trigger`) is also available: `@` for mentions opens the same list as `/`. The screen's code controls the field through the Handle (`ref`) and the `useRichTextEditorState` hook, learns about Readiness and Field focus through the `onReady`, `onFocus` and `onBlur` callbacks, puts its own content into the Status line (`status`) and reports an error with the `showNotice` command. The same `tools` array is understood by server-side sanitizing (`sanitizeRichText`, `richTextSchema`) and by `RichTextView` with Islands. Everything listed follows the kit's semver from the first release, the HTML convention is frozen, and a Tiptap major is a kit major (section "Stability"). The details of each item live on the [RichTextEditor](../components/inputs/rich-text-editor.md) page; this page covers the map, the contract boundary and the recipes.
12
+ A map of how a product extends `RichTextEditor` from `@olddevs/ui/editor`, and the stability boundary of this API. The unit of extension is a **custom Tool** (`CustomTool`): the description of one Editor capability, passed in `tools` mixed in with the kit's names. It carries Blocks and Marks with Schema rules (as data), an Engine part (Tiptap extensions, supplied directly or through a Loader) and UI contributions: buttons, Insert items, Block actions, Text cards. A custom Trigger character (`trigger`) is also available: `@` for mentions opens the same list as `/`. The screen's code controls the field through the Handle (`ref`) and the `useRichTextEditorState` hook, learns about Readiness and Field focus through the `onReady`, `onFocus` and `onBlur` callbacks, puts its own content into the Status line (`status`) and reports an error with the `showNotice` command. A Tool has three typed parts, like `options`, `storage` and `commands` of a Tiptap extension: **options** (`tool(name, options)` for the built-in `search` and `toc`, `defineTool` for a custom Tool), **state** (the `useRichTextToolState` hook) and **commands** (`handle.tool(name)`); their types are collected in the extensible `RichTextTools` map, where a custom Tool adds itself through `declare module "@olddevs/ui/editor"`. The same `tools` array is understood by server-side sanitizing (`sanitizeRichText`, `richTextSchema`) and by `RichTextView` with Islands. Everything listed follows the kit's semver from the first release, the HTML convention is frozen, and a Tiptap major is a kit major (section "Stability"). The details of each item live on the [RichTextEditor](../components/inputs/rich-text-editor.md) page; this page covers the map, the contract boundary and the recipes.
13
13
 
14
14
  ```tsx
15
15
  import {
16
16
  RICH_TEXT_PRESETS,
17
17
  RichTextEditor,
18
+ defineTool,
19
+ tool,
18
20
  useRichTextEditorState,
21
+ useRichTextToolState,
19
22
  type CustomTool,
20
23
  type RichTextEditorHandle,
21
24
  } from "@olddevs/ui/editor";
@@ -29,12 +32,16 @@ The decision is ADR-0058, which supplements ADR-0050. Capitalized terms come fro
29
32
  - The Document needs a custom Block, Atom or Mark: a merge tag, an embed, a product card, a comment on a fragment. Their Schema, HTML, Markdown and display in `RichTextView` are described by one Tool.
30
33
  - Your own command in the Editor's interface: a button on the Toolbar, an Insert item in "/" and "+", a Block action in the Block handle menu, a Text card next to a fragment of text.
31
34
  - The screen controls the field: it inserts a template at the caret, renders its own panel, saves on focus loss, and shows "Saved" and the word count.
35
+ - A built-in Tool needs configuring (`toc` under a header with its own height variable, Find with case matching on) or you need to know what is happening inside it: a match counter in the Status line, an external navigation over the headings, a "Find" button in the screen header.
36
+ - A custom Tool with options whose state and commands the screen needs: `defineTool` instead of a hand-written factory.
32
37
  - The server stores a Document with custom Blocks and must sanitize it with the same set of rules as the Editor.
33
38
 
34
39
  ## When not to use
35
40
 
36
41
  - What you need is already among the kit's Tools (["Tools and Presets"](../components/inputs/rich-text-editor.md#инструменты-и-пресеты)): pass the name in `tools`, do not write a custom Tool.
37
42
  - To hide a built-in button or item, narrow `tools`. A built-in Tool cannot be replaced by name: turn it off and add a custom Tool under your own name.
43
+ - Only `search` and `toc` have options; `tool("bold", …)` does not compile. Options for other built-in Tools will be added one at a time, as consumers ask for them. You cannot change the behavior of a built-in Tool by substitution (there is no `extend()` as in Tiptap): the descriptions of built-in Tools are not exported.
44
+ - You cannot change the options of a mounted field: they are read once, when the engine is created, together with `tools`. Remount the field with a key.
38
45
  - You cannot put arbitrary React in the Toolbar row or in a menu: the kit renders the UI contributions itself. React is allowed only in the content of a popover (of a button, Insert item or Block action) and in a Text card.
39
46
  - You cannot intercept paste and drop through a prop: the Engine part of a custom Tool handles them (["Paste and drop order"](../components/inputs/rich-text-editor.md#порядок-вставки-и-drop)).
40
47
  - What is not provided until consumers ask for it (ADR-0058, section 13): `insertImage` and the selection's HTML in the Handle, `onSelectionChange`, `onPaste`, `onDrop`, `onEmpty`, `onDestroy`, history events, a signal about removed images, locked template Blocks, a "menu" UI contribution kind, Text cards and the Status line in `RichTextView`. Until then the screen has `ref.current.editor` and its events.
@@ -42,21 +49,26 @@ The decision is ADR-0058, which supplements ADR-0050. Capitalized terms come fro
42
49
 
43
50
  ## API map
44
51
 
45
- | What | Where in the API | Details |
46
- | -------------------------------------------- | ----------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
47
- | Custom Tool | `CustomTool` in `tools`; Presets — `RICH_TEXT_PRESETS` | ["Custom Tool"](../components/inputs/rich-text-editor.md#свой-инструмент) |
48
- | Schema rules and the HTML convention | `blocks` (`CustomBlock`), `marks` (`CustomMark`), `CustomAttribute`, `PrefixedName` | ["Custom Tool"](../components/inputs/rich-text-editor.md#свой-инструмент) |
49
- | Button | `buttons` (`CustomToolButton`, `CustomToolPopover`, `CustomToolLabel`) | ["Custom Tool's button"](../components/inputs/rich-text-editor.md#кнопка-своего-инструмента) |
50
- | Insert item, Block action | `insertItems` (`CustomInsertItem`), `blockActions` (`CustomBlockAction`) | ["Insert items and Block actions"](../components/inputs/rich-text-editor.md#пункты-вставки-и-действия-над-блоком) |
51
- | Trigger character | `trigger` (`CustomTrigger`, `TriggerItem`, `TriggerContext`) | ["Custom Trigger character"](../components/inputs/rich-text-editor.md#свой-символ-триггер) |
52
- | Text card | `textCards` (`CustomTextCard`), the `markRange` helper | ["Text card"](../components/inputs/rich-text-editor.md#карточка-у-текста) |
53
- | Handle and your own panel | `RichTextEditorHandle`, `useRichTextEditorState(ref, selector)` | ["Handle (`ref`) and your own toolbar outside the field"](../components/inputs/rich-text-editor.md#хэндл-ref-и-своя-панель-вне-поля) |
54
- | Events | `onReady`, `onFocus`, `onBlur`; paste and drop — by the Engine part | ["Events"](../components/inputs/rich-text-editor.md#события) |
55
- | Status line and Field notice | the `status` prop; the engine command `showNotice`, the `FieldNotice` type | ["Status line and Field notice"](../components/inputs/rich-text-editor.md#строка-статуса-и-сообщение-поля) |
56
- | Loader, Readiness and Skeleton | `engine: () => import(…)` | ["Engine part Loader"](../components/inputs/rich-text-editor.md#загрузчик-части-для-движка), ["Skeleton"](../components/inputs/rich-text-editor.md#скелет) |
57
- | Atom text: `getText`, `maxLength`, emptiness | `renderText` of the Engine part | ["Atom text"](../components/inputs/rich-text-editor.md#текст-атома) |
58
- | Server, Markdown, View | `sanitizeRichText(html, DOMPurify, tools)`, `richTextSchema(tools)`, `RichTextView` | ["Outside the Editor"](../components/inputs/rich-text-editor.md#вне-редактора-сервер-markdown-и-просмотр), [RichTextView, "Custom Tools: the third argument"](../components/content/rich-text-view.md#свои-инструменты-третий-аргумент) |
59
- | Island of a custom Block | `CustomBlock.island`, `CustomIslandProps`, the `tools` prop of `RichTextView` | [RichTextView, "Custom Block's Island"](../components/content/rich-text-view.md#остров-своего-блока) |
52
+ | What | Where in the API | Details |
53
+ | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
54
+ | Custom Tool | `CustomTool` in `tools`; Presets — `RICH_TEXT_PRESETS` | ["Custom Tool"](../components/inputs/rich-text-editor.md#свой-инструмент) |
55
+ | Schema rules and the HTML convention | `blocks` (`CustomBlock`), `marks` (`CustomMark`), `CustomAttribute`, `PrefixedName` | ["Custom Tool"](../components/inputs/rich-text-editor.md#свой-инструмент) |
56
+ | Button | `buttons` (`CustomToolButton`, `CustomToolPopover`, `CustomToolLabel`) | ["Custom Tool's button"](../components/inputs/rich-text-editor.md#кнопка-своего-инструмента) |
57
+ | Insert item, Block action | `insertItems` (`CustomInsertItem`), `blockActions` (`CustomBlockAction`) | ["Insert items and Block actions"](../components/inputs/rich-text-editor.md#пункты-вставки-и-действия-над-блоком) |
58
+ | Trigger character | `trigger` (`CustomTrigger`, `TriggerItem`, `TriggerContext`) | ["Custom Trigger character"](../components/inputs/rich-text-editor.md#свой-символ-триггер) |
59
+ | Text card | `textCards` (`CustomTextCard`), the `markRange` helper | ["Text card"](../components/inputs/rich-text-editor.md#карточка-у-текста) |
60
+ | Handle and your own panel | `RichTextEditorHandle`, `useRichTextEditorState(ref, selector)` | ["Handle (`ref`) and your own toolbar outside the field"](../components/inputs/rich-text-editor.md#хэндл-ref-и-своя-панель-вне-поля) |
61
+ | Options of a built-in Tool | `tool(name, options)` → `ConfiguredTool` for `search` and `toc`; `SearchToolOptions`, `TocToolOptions`, `TocLevel` | ["Options of a built-in Tool: `tool()`"](../components/inputs/rich-text-editor.md#опции-встроенного-инструмента-tool) |
62
+ | A custom Tool with options, state, commands | `defineTool({ name, defaults, create, state, commands })` → `ToolDefinition`, `tool(definition, options)` | ["Options, state and commands"](editor-extension.md#опции-состояние-и-команды) |
63
+ | Types of options, state and commands | the extensible `RichTextTools` interface | ["The `RichTextTools` map and declaration merging"](editor-extension.md#карта-richtexttools-и-declaration-merging) |
64
+ | Tool state | `useRichTextToolState(ref, name, selector?)`, `SearchToolState`, `TocToolState`, `TocToolHeading` | ["Tool state: `useRichTextToolState`"](../components/inputs/rich-text-editor.md#состояние-инструмента-userichtexttoolstate) |
65
+ | Tool commands | `handle.tool(name)`, `SearchToolCommands`, `TocToolCommands` | ["Tool commands: `handle.tool()`"](../components/inputs/rich-text-editor.md#команды-инструмента-handletool) |
66
+ | Events | `onReady`, `onFocus`, `onBlur`; paste and drop — by the Engine part | ["Events"](../components/inputs/rich-text-editor.md#события) |
67
+ | Status line and Field notice | the `status` prop; the engine command `showNotice`, the `FieldNotice` type | ["Status line and Field notice"](../components/inputs/rich-text-editor.md#строка-статуса-и-сообщение-поля) |
68
+ | Loader, Readiness and Skeleton | `engine: () => import(…)` | ["Engine part Loader"](../components/inputs/rich-text-editor.md#загрузчик-части-для-движка), ["Skeleton"](../components/inputs/rich-text-editor.md#скелет) |
69
+ | Atom text: `getText`, `maxLength`, emptiness | `renderText` of the Engine part | ["Atom text"](../components/inputs/rich-text-editor.md#текст-атома) |
70
+ | Server, Markdown, View | `sanitizeRichText(html, DOMPurify, tools)`, `richTextSchema(tools)`, `RichTextView` | ["Outside the Editor"](../components/inputs/rich-text-editor.md#вне-редактора-сервер-markdown-и-просмотр), [RichTextView, "Custom Tools: the third argument"](../components/content/rich-text-view.md#свои-инструменты-третий-аргумент) |
71
+ | Island of a custom Block | `CustomBlock.island`, `CustomIslandProps`, the `tools` prop of `RichTextView` | [RichTextView, "Custom Block's Island"](../components/content/rich-text-view.md#остров-своего-блока) |
60
72
 
61
73
  ## Custom Tool and the HTML convention
62
74
 
@@ -96,12 +108,195 @@ A Tool's `trigger` is not a UI contribution of a Slot but another way into the s
96
108
 
97
109
  ## Handle (`ref`), events, Status line
98
110
 
99
- - **The Handle** (`ref`) — insertion at the caret (`insertHTML`, `insertText` → `boolean`), reading (`getText`, `getSelectedText`, `isEmpty`, `getHTML`, `getMarkdown`, `getJSON`), focus (`focus("start" | "end" | "restore")`, `blur`), history (`undo`, `redo`, `canUndo`, `canRedo`), plus `clear`, `setMarkdown` and `setJSON` (a JSON envelope → `boolean`) and `openSearch({ replace? })`, which opens the find and replace panel. Insertion is an Author edit: a history step, `onChange` after a delay, the `maxLength` limit; past the limit, the insertion does not happen. Before Readiness the methods do not throw and change nothing: `insertHTML` and `insertText` return `false`. `editor` is the raw Tiptap for commands of Marks, Blocks and custom Tools; its API follows the Tiptap major.
111
+ - **The Handle** (`ref`) — insertion at the caret (`insertHTML`, `insertText` → `boolean`), reading (`getText`, `getSelectedText`, `isEmpty`, `getHTML`, `getMarkdown`, `getJSON`), focus (`focus("start" | "end" | "restore")`, `blur`), history (`undo`, `redo`, `canUndo`, `canRedo`), plus `clear`, `setMarkdown` and `setJSON` (a JSON envelope → `boolean`) and `tool(name)`, the commands of a Tool (see "Options, state and commands" below). `openSearch({ replace? })` is deprecated: it works as `tool("search")?.open({ replace })`, warns once in development and is removed in 5.0. Insertion is an Author edit: a history step, `onChange` after a delay, the `maxLength` limit; past the limit, the insertion does not happen. Before Readiness the methods do not throw and change nothing: `insertHTML` and `insertText` return `false`. `editor` is the raw Tiptap for commands of Marks, Blocks and custom Tools; its API follows the Tiptap major.
100
112
  - **`useRichTextEditorState(ref, selector)`** — the state of your own panel: re-renders only when the slice changes, `null` before the engine and on the server.
113
+ - **`useRichTextToolState(ref, name, selector?)`** — the state of a Tool: a data snapshot; the screen re-renders only when it changes, and the hook returns `null` before mount, on the server, after the Editor unmounts and without the Tool in the set.
101
114
  - **`onReady()`** — once per mount, when Readiness arrives; **`onFocus()`** and **`onBlur()`** — report focus on the field as a whole (Document, Toolbar, the kit's popovers, the HTML field); before `onBlur`, a pending `onChange` is flushed.
102
115
  - **`status`** — the screen's React node in the line under the Document, at the start; the kit's counter is at the end. The kit does not announce the line.
103
116
  - **`showNotice({ tone: "status" | "error", text } | null)`** — an engine command: one Field notice, a new one replaces the previous; `status` is only announced, `error` is visible as a `role="alert"` line until the next Author edit. The kit's image messages go through the same channel.
104
117
 
118
+ ## Options, state and commands
119
+
120
+ A Tool has three typed parts. They carry Tiptap's `options`, `storage` and `commands` over to the kit's contract, not the engine's (ADR-0069).
121
+
122
+ | Part | What it is | Who sets it | How the screen uses it |
123
+ | -------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------- | -------------------------------------------- |
124
+ | Options | What the screen sets before the engine exists | `tool(name, options)` for built-ins; `defaults` and `tool(definition, options)` for a custom Tool | — |
125
+ | State | A snapshot the screen reads | The kit for built-ins; the `state` field of `defineTool` | `useRichTextToolState(ref, name, selector?)` |
126
+ | Commands | What the screen calls | The kit for built-ins; the `commands` field of `defineTool` | `ref.current?.tool(name)?.command()` |
127
+
128
+ The Engine part still follows the Tiptap major (ADR-0058, item 12.4). The three new parts do not depend on Tiptap: their types and behavior follow the kit's semver.
129
+
130
+ ### `tool()`: a built-in Tool with options
131
+
132
+ `tool(name, options)` returns a Configured Tool: an opaque entry for `tools` (only the kit reads its fields). `search` has options (the starting flags) and so does `toc` (`offsetVar`, `levels`, `listWidth`); their values and an example are on the [RichTextEditor](../components/inputs/rich-text-editor.md#опции-встроенного-инструмента-tool) page. An entry switches the Tool on even without the name in `tools`, and a name from a Preset gives way to it. Two `tool()` entries with the same name are a set error.
133
+
134
+ `tool(definition, options)` configures a `defineTool` definition the same way; the options come from its `defaults`.
135
+
136
+ ### `defineTool()`: a custom Tool with options
137
+
138
+ `defineTool({ name, defaults, create, state?, commands? })` returns a definition: an element of `tools` that works both without `tool()` (with the defaults from `defaults`) and with it.
139
+
140
+ - **`defaults`** is the full set of options. The options type is inferred from it, so you do not declare it a second time. A literal widens to its base type (`tone: "info"` becomes `string`): for a union, give the value a type, `tone: "info" as "info" | "warning"`.
141
+ - **`create(options)`** returns the body of the description: the same shape as `CustomTool`, without the name (`CustomToolBody`): the Engine part, Schema rules, UI contributions. It is called once per `tools` entry (the definition itself, or the object that `tool(definition, options)` returned): the kit remembers the result for the pair of the definition and the entry's options object. Anything closed over in the `create` body (a counter, a cache) is therefore shared by every Editor that uses that entry. A `tool(…)` entry created in a component body is new on every render, and the Island in `RichTextView` remounts on every render: take the `tools` set out of the render, into a module constant or `useMemo`. Keep the Engine part Loader a module constant: the Loader's cache lives on the function itself, and a new function on every `create` call would load the part again for every entry.
142
+ - **`state({ editor, options })`** is the state for `useRichTextToolState`. It is called on every transaction and on every hook read, so it must be fast and free of side effects, and it must return data without DOM nodes. Primitives, arrays and plain objects are compared by value, so the screen re-renders only when the snapshot has changed. `Date`, `Map`, `Set` and class instances are compared by reference: a `state` that creates them anew on every call re-renders the screen on every transaction. Return plain data (`date.toISOString()`) or the same reference while the data has not changed. With no `state`, the Tool has no state and the hook returns `null`.
143
+ - **`commands({ editor, options })`** returns the commands for `handle.tool(name)`. It is called on every access through the Handle: thin wrappers over the engine, no subscriptions. A command returns a `boolean`, like the built-in ones. With no `commands`, `handle.tool(name)` returns `null`.
144
+ - **Merging options.** Nested plain objects are merged with the defaults, and arrays and functions are replaced whole, like `configure` in Tiptap v3. An `undefined` value is ignored: the existing value stays. The `defaults` object is not changed; the options object is shared by `create`, `state` and `commands`: read it, do not mutate it.
145
+ - **Options are read once**, when the engine is created, together with the set. To change them, remount the Editor with a key (`key`). The kit does not validate the values of a custom Tool's options; that is up to its author.
146
+ - **All the rules of a custom Tool apply** (ADR-0058): the Consumer prefix in the name, a unique name in `tools` (the same definition twice, with `tool()` and without, is also a set error), the set checks, the Engine part Loader. `defineTool` is sugar over the same `CustomTool` shape: it has no build path of its own. A plain `CustomTool` object in `tools` works as before, without options, state and commands.
147
+ - **Server.** `defineTool` and `tool()` live in a module with no Tiptap or React at run time (Tiptap appears there only as types), so the `tools` set can be built in a server module next to the rest of the screen's configuration. A definition holds functions, like `CustomTool`, so it reaches a client component together with its module (an import), not as a prop across the RSC boundary. A configured built-in Tool is made of data: with data in its options, it can be passed as a prop too.
148
+
149
+ ### The `RichTextTools` map and declaration merging
150
+
151
+ The types of all three parts are collected in the `RichTextTools` interface, like `Storage` and `Commands` in Tiptap: the key is the Tool's name, the value is `{ options; state; commands }`. The kit declares `search` and `toc` in it, and the consumer adds a custom Tool:
152
+
153
+ ```ts
154
+ declare module "@olddevs/ui/editor" {
155
+ interface RichTextTools {
156
+ acme_note: {
157
+ options: { tone: "info" | "warning" };
158
+ state: { count: number };
159
+ commands: { clear(): boolean };
160
+ };
161
+ }
162
+ }
163
+ ```
164
+
165
+ - The map gives the snapshot type to `useRichTextToolState`, the commands type to `handle.tool()` and the options to `tool()` for a built-in Tool. For a custom Tool, `tool()` takes the options from the `defaults` of its definition, so the `options` field in the map describes the options for readers of the map (`RichTextTools["acme_note"]["options"]`), and `tool()` does not use it.
166
+ - **Typing is optional.** A Tool not added to the map works: its state and commands have the type `unknown`.
167
+ - The file with `declare module` must be a module (it has an `import` or an `export`); otherwise the declaration does not augment the map but declares a module of its own. Put it next to the Tool's definition.
168
+ - Do not confuse the map with typing the engine. If you already type an Engine part through `declare module "@tiptap/core"` (`Commands`, `Storage`), keep doing so: that is the engine layer and follows its major, while `RichTextTools` is the kit layer.
169
+
170
+ ### An end-to-end example
171
+
172
+ One `tools` set with settings, and a custom Tool in a server-safe module:
173
+
174
+ ```ts
175
+ // editor-tools.ts: no Tiptap or React at run time, so the module is fine on the server too.
176
+ import type { Editor } from "@tiptap/core";
177
+ import { RICH_TEXT_PRESETS, defineTool, tool } from "@olddevs/ui/editor";
178
+ import { MentionIcon } from "@olddevs/ui/icons";
179
+
180
+ // The Loader is one function per module: the promise is cached on it.
181
+ const loadEngine = () => import("./mention-engine.js").then((module) => module.engine);
182
+
183
+ function countMentions(editor: Editor): number {
184
+ let count = 0;
185
+ editor.state.doc.descendants((node) => {
186
+ if (node.type.name === "acme_mention") count += 1;
187
+ });
188
+ return count;
189
+ }
190
+
191
+ export const mention = defineTool({
192
+ name: "acme_mention",
193
+ // Defaults: the options type is inferred from them.
194
+ defaults: { labels: { insert: "Mention" }, limit: 5 },
195
+ create: (options) => ({
196
+ engine: loadEngine,
197
+ blocks: {
198
+ acme_mention: {
199
+ kind: "inline",
200
+ attributes: { acme_id: { kind: "pattern", pattern: "[0-9]+" } },
201
+ },
202
+ },
203
+ insertItems: [
204
+ {
205
+ section: "insert",
206
+ label: options.labels.insert,
207
+ icon: MentionIcon,
208
+ run: (chain) => chain.insertContent({ type: "acme_mention", attrs: { acme_id: null } }),
209
+ },
210
+ ],
211
+ }),
212
+ // State: data the screen reads with a hook.
213
+ state: ({ editor }) => ({ count: countMentions(editor) }),
214
+ // Commands: thin wrappers over the engine that return a boolean.
215
+ commands: ({ editor, options }) => ({
216
+ insert: (id: number) =>
217
+ countMentions(editor) < options.limit &&
218
+ editor.commands.insertContent({ type: "acme_mention", attrs: { acme_id: String(id) } }),
219
+ }),
220
+ });
221
+
222
+ // The types of state and commands for `useRichTextToolState` and `handle.tool()`.
223
+ declare module "@olddevs/ui/editor" {
224
+ interface RichTextTools {
225
+ acme_mention: {
226
+ options: { labels: { insert: string }; limit: number };
227
+ state: { count: number };
228
+ commands: { insert(id: number): boolean };
229
+ };
230
+ }
231
+ }
232
+
233
+ // Build the set once per module: `tools` is read when the engine is created.
234
+ export const EDITOR_TOOLS = [
235
+ ...RICH_TEXT_PRESETS.full,
236
+ tool("search", { matchCase: true }),
237
+ tool("toc", { offsetVar: "--header-h", levels: [2, 3] }),
238
+ tool(mention, { limit: 10, labels: { insert: "Mention a colleague" } }),
239
+ ];
240
+ ```
241
+
242
+ The screen reads the state of all three Tools and calls their commands:
243
+
244
+ ```tsx
245
+ "use client";
246
+ import { useRef } from "react";
247
+ import { RichTextEditor, useRichTextToolState } from "@olddevs/ui/editor";
248
+ import type { RichTextEditorHandle } from "@olddevs/ui/editor";
249
+ import { EDITOR_TOOLS } from "./editor-tools";
250
+
251
+ export function ArticleBody({ html }: { html: string }) {
252
+ const ref = useRef<RichTextEditorHandle>(null);
253
+ const search = useRichTextToolState(ref, "search");
254
+ const toc = useRichTextToolState(ref, "toc");
255
+ const mentions = useRichTextToolState(ref, "acme_mention", (state) => state.count);
256
+
257
+ return (
258
+ <>
259
+ <nav aria-label="Sections">
260
+ {toc?.headings.map((heading) => (
261
+ <button
262
+ key={heading.index}
263
+ type="button"
264
+ aria-current={heading.index === toc.current ? "location" : undefined}
265
+ onClick={() => ref.current?.tool("toc")?.jumpTo(heading.index)}
266
+ >
267
+ {heading.text}
268
+ </button>
269
+ ))}
270
+ </nav>
271
+ <button type="button" onClick={() => ref.current?.tool("search")?.open()}>
272
+ Find
273
+ </button>
274
+ <button
275
+ type="button"
276
+ // A click on a button outside the field must not move focus out of the Document.
277
+ onMouseDown={(event) => event.preventDefault()}
278
+ onClick={() => ref.current?.tool("acme_mention")?.insert(1)}
279
+ >
280
+ Mention ({mentions ?? 0})
281
+ </button>
282
+ <RichTextEditor
283
+ ref={ref}
284
+ aria-label="Article body"
285
+ tools={EDITOR_TOOLS}
286
+ defaultValue={html}
287
+ status={
288
+ search?.open && search.query ? (
289
+ <span>{search.total ? `${search.current + 1} of ${search.total}` : "No matches"}</span>
290
+ ) : null
291
+ }
292
+ />
293
+ </>
294
+ );
295
+ }
296
+ ```
297
+
298
+ What to note here. The `tool("toc", …)` entry switches the Table of contents on without a `"toc"` string, and `tool("search", …)` replaces Find from the Preset. `useRichTextToolState(ref, "acme_mention", …)` and `tool("acme_mention")` are typed thanks to `declare module`; without it they return `unknown`. The options of `mention` in `EDITOR_TOOLS` are `limit: 10` instead of the default 5. They are read once, when the engine is created, and `insert` sees exactly those.
299
+
105
300
  ## Loaders, Readiness and Skeleton
106
301
 
107
302
  The Engine part can be supplied by a Loader — `engine: () => import("./engine.js").then((m) => m.engine)`: the server then imports the module with the description, and Tiptap goes into a separate chunk. Loading starts on the first render in the browser, and the promise is cached on the Loader function (so a Loader is one function per module). **Readiness** is when the engine is created and the Loaders of all enabled Tools — custom and the heavy built-in ones (`codeBlock`, `table`) — have run. Before it, the field is a **Skeleton**: the frame, the place of the Toolbar and the Document cleaned by the Schema, with no input and no height jump; on the server, the frame without the Document. A Loader failure is a field error with "Retry", and the Document stays visible. For details, see ["Engine part Loader"](../components/inputs/rich-text-editor.md#загрузчик-части-для-движка) and ["Skeleton"](../components/inputs/rich-text-editor.md#скелет).
@@ -121,10 +316,11 @@ The contract boundary per ADR-0058, item 12. The reviewer checks the changeset t
121
316
  - the description of a custom Tool (`CustomTool` and its fields, `trigger` included), UI contributions and their Slots, Schema rules (`CustomBlock`, `CustomMark`, `CustomAttribute`), Block traits;
122
317
  - the HTML convention (`data-block`, `data-mark`, `data-<prefix>_<name>`);
123
318
  - the Handle's methods, `useRichTextEditorState`, the `onReady`, `onFocus`, `onBlur` callbacks, the `status` prop, the `showNotice` command and the `FieldNotice` type;
319
+ - options, state and commands (ADR-0069): the `RichTextTools` map, `tool()`, `defineTool()` and its types (`ToolDefinition`, `ConfiguredTool`), `useRichTextToolState`, `handle.tool()`, and the options, state and command fields of `search` and `toc`;
124
320
  - the third argument of `sanitizeRichText` and `richTextSchema`, the `tools` prop and the Island of `RichTextView`, `markRange`;
125
321
  - the names of built-in Tools (`ToolName`) and the existence of the Preset arrays `RICH_TEXT_PRESETS` — but not their composition (below).
126
322
 
127
- **Not included:** the kit's DOM and classes around a Slot; the ids of built-in buttons and items; the internal description of built-in Tools (a superset of the public shape) — it is not exported; the exact place of a UI contribution inside a surface; the look of the Skeleton; file and chunk names in `dist`, like any path inside the package (["Public contract"](../conventions.md#публичный-контракт)). The API of the raw `editor` and of the Engine part is Tiptap's API, and it follows the engine's major.
323
+ **Not included:** the kit's DOM and classes around a Slot; the ids of built-in buttons and items; the internal description of built-in Tools (a superset of the public shape) — it is not exported; the exact place of a UI contribution inside a surface; the look of the Skeleton; the shape of a Configured Tool from the inside (the entry is opaque, only the kit reads its fields); the timing of the scroll after the Find commands (it happens a moment after the call); file and chunk names in `dist`, like any path inside the package (["Public contract"](../conventions.md#публичный-контракт)). The API of the raw `editor` and of the Engine part is Tiptap's API, and it follows the engine's major.
128
324
 
129
325
  **Neither a minor nor a patch breaks a saved Document.** The HTML convention is frozen: `data-block` and `data-mark` are extended only by addition. If the form has to change, that is a major, and until the next major the kit reads both forms — both in engine parsing and in the sanitizer.
130
326
 
@@ -141,6 +337,8 @@ The contract boundary per ADR-0058, item 12. The reviewer checks the changeset t
141
337
  | Removing a Tool from a Preset, renaming a Tool | major |
142
338
  | Removing a public name | major, after `@deprecated` in a minor (below) |
143
339
  | A new optional field, a new UI contribution kind, a Handle method or a callback | minor |
340
+ | A new option or state field, a new command, an option on another built-in Tool | minor |
341
+ | Removing an option, state or command field, changing its meaning | major |
144
342
  | A new built-in Tool in a Preset | minor, the changeset line is in bold |
145
343
  | A shift of a Slot inside a surface | minor with a changeset line |
146
344
  | Raising Tiptap's lower bound within a major | minor |
@@ -150,7 +348,7 @@ The contract boundary per ADR-0058, item 12. The reviewer checks the changeset t
150
348
  - **A Tiptap major is a kit major.** The kit supports one Tiptap major at a time; the engine is not in the kit manifest, and the consumer maintains the version range (`^3.31.4`) given on the [RichTextEditor](../components/inputs/rich-text-editor.md#установка) page.
151
349
  - **A Preset** is "what the kit considers a full set", not a frozen list. If you need a stable composition, pass an exact list of names.
152
350
  - **Tightening sanitizing** is released with a note in [migration-notes](migration-notes.md): what is now stripped and how to find such Documents. Loosening sanitizing is a separate decision, not a question of version.
153
- - **Deprecate first, remove later.** A replacement comes out in a minor, the old name gets `@deprecated` and one development warning, and is removed in the next major.
351
+ - **Deprecate first, remove later.** A replacement comes out in a minor, the old name gets `@deprecated` and one development warning, and is removed in the next major. This is how `handle.openSearch()` is marked: its replacement is `handle.tool("search")?.open()`, it is removed in 5.0, and there is an entry in [migration-notes](migration-notes.md).
154
352
 
155
353
  ## Recipes
156
354
 
@@ -297,6 +495,8 @@ export const EDITOR_TOOLS = [
297
495
 
298
496
  An empty label does not crash the field: in development there is a `console.error` with the name of the Tool and the UI contribution, and the Tool's name goes into the name and the tooltip.
299
497
 
498
+ `defineTool` gives you the same factory without merging the options by hand: the defaults go in `defaults`, and configuring is `tool(definition, options)` (the "Options, state and commands" section). The factory remains a working choice for a Tool with no state and no commands.
499
+
300
500
  ### `modulepreload` for lazy chunks
301
501
 
302
502
  The kit does not publish preloads. The chunks of Engine parts (of custom Loaders and of the built-in `codeBlock` and `table` in `full`) start loading on the Editor's first render, and the Skeleton is shown in the meantime. To have a chunk arrive with the page, add a `<link rel="modulepreload">` for it (in React 19, `preloadModule(url)` from `react-dom`); take the hashed URL from your bundler's manifest: the kit's chunk names are not part of the contract. For an example, see [RichTextEditor, "Engine part Loader"](../components/inputs/rich-text-editor.md#загрузчик-части-для-движка); the cost of the "start" and "full" scenarios is in ["Import size"](../getting-started.md#размер-импорта).
@@ -304,6 +504,6 @@ The kit does not publish preloads. The chunks of Engine parts (of custom Loaders
304
504
  ## Links
305
505
 
306
506
  - Pages: [RichTextEditor](../components/inputs/rich-text-editor.md), [RichTextView](../components/content/rich-text-view.md), [Editor glossary](../glossary.md#редактор), [migration-notes](migration-notes.md).
307
- - Decisions: ADR-0058 — the extension API, ADR-0050 — the Editor.
308
- - Source: the entry point `src/editor/index.ts`, the public form of a custom Tool `src/editor/core/customTool.ts`, validation of the `tools` array and wiring `src/editor/core/customToolEngine.ts`, the Handle `src/editor/core/handle.ts`, the hook `src/editor/hooks/useRichTextEditorState.ts`, the Field notice `src/editor/core/fieldNotice.ts`, the Schema and sanitizing `src/editor/core/schema.ts`, Islands `src/editor/core/viewIsland.ts`.
309
- - Tests: `tests/rich-text-editor-custom-tool.test.tsx` (description, Schema, `tools` array validation), `tests/rich-text-editor-custom-button.test.tsx`, `tests/rich-text-editor-custom-ui.test.tsx` and `tests/rich-text-editor-text-card.test.tsx` (UI contributions), `tests/rich-text-editor-handle.test.tsx` and `tests/rich-text-editor-handle-ssr.test.tsx` (the Handle and the hook), `tests/rich-text-editor-events.test.tsx` (callbacks, paste and drop order), `tests/rich-text-editor-notice.test.tsx` (Status line and Field notice), `tests/rich-text-editor-loaders.test.tsx`, `tests/rich-text-editor-skeleton.test.tsx` and `tests/rich-text-editor-lazy-builtins.test.tsx` (Loaders, Skeleton, lazy built-ins), `tests/rich-text-editor-atom-text.test.tsx` (Atom text), `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`; test Tools — `tests/editor-test-tools.ts`.
507
+ - Decisions: ADR-0058 — the extension API, ADR-0069 — Tool options, state and commands, ADR-0050 — the Editor.
508
+ - Source: the entry point `src/editor/index.ts`, the public form of a custom Tool `src/editor/core/customTool.ts`, validation of the `tools` array and wiring `src/editor/core/customToolEngine.ts`, the Handle `src/editor/core/handle.ts`, the hooks `src/editor/hooks/useRichTextEditorState.ts` and `src/editor/hooks/useRichTextToolState.ts`, options and the map `src/editor/core/toolOptions.ts`, the command types `src/editor/core/toolCommands.ts` and the state types `src/editor/core/toolState.ts`, the Field notice `src/editor/core/fieldNotice.ts`, the Schema and sanitizing `src/editor/core/schema.ts`, Islands `src/editor/core/viewIsland.ts`.
509
+ - Tests: `tests/rich-text-editor-custom-tool.test.tsx` (description, Schema, `tools` array validation), `tests/rich-text-editor-custom-button.test.tsx`, `tests/rich-text-editor-custom-ui.test.tsx` and `tests/rich-text-editor-text-card.test.tsx` (UI contributions), `tests/rich-text-editor-handle.test.tsx` and `tests/rich-text-editor-handle-ssr.test.tsx` (the Handle and the hook), `tests/rich-text-editor-events.test.tsx` (callbacks, paste and drop order), `tests/rich-text-editor-notice.test.tsx` (Status line and Field notice), `tests/rich-text-editor-loaders.test.tsx`, `tests/rich-text-editor-skeleton.test.tsx` and `tests/rich-text-editor-lazy-builtins.test.tsx` (Loaders, Skeleton, lazy built-ins), `tests/rich-text-editor-atom-text.test.tsx` (Atom text), `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` and `tests/rich-text-editor-define-tool.test.tsx` (options, state, commands, `defineTool`), `tests/editor-tool-options-types.test.ts`, `tests/editor-tool-state-types.test.ts`, `tests/editor-tool-commands-types.test.ts` and `tests/editor-define-tool-types.test.ts` (types and the map); test Tools — `tests/editor-test-tools.ts`.