@olddevs/ui 4.6.0 → 4.6.1

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.
@@ -67,20 +67,20 @@ sourceHash: bba3a0af1f9c
67
67
 
68
68
  - **Entry point and engine.** It lives in its own entry point, `@olddevs/ui/editor`, not in the barrel, and the consumer installs the engine: it is not in the kit's manifest.
69
69
  - **Value and modes.** The value is an HTML string constrained by the Schema; an empty Document is `""`. The `value`/`onChange` and `defaultValue` modes are supported; `name` is for a native form; the label and error come from `FormField`.
70
- - **Height.** By default the field grows with the Document. `maxHeight` (pixels or a CSS length) limits the Document area: it scrolls inside the frame while the Toolbar and the Status line stay in place. `resizable` adds a height handle (a grip strip plus a slider you can use from the keyboard).
71
- - **Toolbar and Selection menu.** The Toolbar (`toolbar`) is either sticky above the field, floating above the selection, or the compact Selection menu on an inverse surface (`"bubble"`); on a touch screen it is always sticky. `Alt+F10` moves focus into it.
70
+ - **Height.** By default the field grows with the Document. `maxHeight` (pixels or a CSS length) limits the Document area: it scrolls inside the frame while the Toolbar and the Status bar stay in place. `resizable` adds a height handle (a grip strip plus a slider you can use from the keyboard).
71
+ - **Toolbar and Bubble menu.** The Toolbar (`toolbar`) is either sticky above the field, floating above the selection, or the compact Bubble menu on an inverse surface (`"bubble"`); on a touch screen it is always sticky. `Alt+F10` moves focus into it.
72
72
  - **Block handle.** A Block with the caret gets a "⋮" handle on the left with a menu of Block actions (add below, format, move up, move down, duplicate, delete; `Ctrl/⌘+/`).
73
73
  - **Link and image.** A link is edited in a popover on `Ctrl/⌘+K`. An image is a `<figure>` with a caption and a width of 50/75/100%, inserted by URL; with `onUploadImage(file) → Promise<url>` it can also come from the clipboard, by drag and drop, and through a file picker (base64 is never written into the Document).
74
74
  - **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.
75
- - **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`.
75
+ - **Source 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`.
76
76
  - **Feature set.** `preset="full" | "basic"` or an exact `tools` list; a disabled Tool removes both its button and its markup from the Schema.
77
77
  - **Tool options, state and commands.** The built-in `search`, `toc` and `details` 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`; for the collapsible Block, `persist`). 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#опции-состояние-и-команды)).
78
78
  - **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.
79
79
  - **Events.** The screen learns about field events through the `onReady()`, `onFocus()` and `onBlur()` callbacks (see the "Events" section).
80
- - **Collapsible block.** The `details` Tool (in `full`) is a Block made of a title and content: it is inserted with the "Collapsible block" item of the Insert menu, its HTML is the `details` and `summary` tags, and in the View it opens the browser's native way, without the engine. The `tool("details", { persist: true })` option writes the Block's open state into the Document (`open`); by default it does not. In Markdown the Block is raw HTML `<details><summary>`.
80
+ - **Collapsible block.** The `details` Tool (in `full`) is a Block made of a title and content: it is inserted with the "Collapsible block" item of the Slash menu, its HTML is the `details` and `summary` tags, and in the View it opens the browser's native way, without the engine. The `tool("details", { persist: true })` option writes the Block's open state into the Document (`open`); by default it does not. In Markdown the Block is raw HTML `<details><summary>`.
81
81
  - **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 Extensions of a custom Tool can be passed as a Loader (`engine: () => import(…)`); if it fails, the result is a field error with "Retry".
82
- - **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.
83
- - **Buttons of a custom Tool.** A custom Tool (`CustomTool` in `tools`) can carry buttons in the `buttons` field: a label (a string or a function that receives `{ locale }`), an icon, `can`, `isActive`, `isVisible`, `selectionMenu`, `shortcut` and exactly one of `run` and `popover`. The kit renders the button in one group before "Undo" and "Redo", binds the shortcut and shows it in the tooltip, in `aria-keyshortcuts` and in the Shortcuts reference.
82
+ - **Status bar and Field notice.** Below the field, inside the frame, are the Status bar (`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.
83
+ - **Buttons of a custom Tool.** A custom Tool (`CustomTool` in `tools`) can carry buttons in the `buttons` field: a label (a string or a function that receives `{ locale }`), an icon, `can`, `isActive`, `isVisible`, `selectionMenu`, `shortcut` and exactly one of `run` and `popover`. The kit renders the button in one group before "Undo" and "Redo", binds the shortcut and shows it in the tooltip, in `aria-keyshortcuts` and in the Keyboard shortcuts popover.
84
84
  - **Text card.** Another kind of UI contribution is a Text card (`textCards`): a range `rangeAt(state, pos)` (for a Mark, the ready-made `markRange("acme_comment")`) and a body `render({ editor, range, close })`. The kit opens it when the caret is in the range or on click, manages the frame (layer, anchor under the range, flip at the edge, Escape), does not take focus out of the Document, and moves focus into it with `Alt+F10`.
85
85
  - **Insert items and Block actions.** Two more kinds of UI contributions are Insert items (`insertItems`: "/", "+", and "Add ›" and "Format ›" of the Block handle) and Block actions (`blockActions`: a section of the Block handle menu before "Delete"), also with exactly one of `run` and `popover`. An item's popover opens at the caret, an action's at the Block.
86
86
  - **Paste and drop.** A custom Tool can affect paste and drop only through Extensions.
@@ -146,64 +146,64 @@ import { RichTextEditor } from "@olddevs/ui/editor";
146
146
 
147
147
  The fixed Toolbar sits above the field and **sticks on scroll** below the app header: the offset is the CSS variable `--topbar-h`, published by the kit's `TopBar` (ADR-0016, ADR-0019). Without a header the variable does not exist, and the Toolbar sticks to the viewport edge. The stickiness works inside the field's frame: the Toolbar leaves together with the field when the field is scrolled out entirely. By default the Editor has no scroll of its own — it grows with the Document; a height limit and a height handle are enabled by `maxHeight` and `resizable` (see the "Field height" section).
148
148
 
149
- - `toolbar="fixed"` (default) — the Toolbar above the field, and above a non-empty selection also the Selection menu, the same pill as in `toolbar="bubble"` (turn it off with `selectionMenu={false}`; it does not exist on a touch screen); `toolbar="floating"` — the Toolbar above a non-empty selection (below); `toolbar="bubble"` — the Selection menu instead of the Toolbar, a compact pill above the selection (below); `toolbar="none"` — no Toolbar, formatting is available only from the keyboard.
149
+ - `toolbar="fixed"` (default) — the Toolbar above the field, and above a non-empty selection also the Bubble menu, the same pill as in `toolbar="bubble"` (turn it off with `selectionMenu={false}`; it does not exist on a touch screen); `toolbar="floating"` — the Toolbar above a non-empty selection (below); `toolbar="bubble"` — the Bubble menu instead of the Toolbar, a compact pill above the selection (below); `toolbar="none"` — no Toolbar, formatting is available only from the keyboard.
150
150
  - In `readOnly` there is no Toolbar. In `disabled` the Toolbar is visible, but all buttons are disabled and do not take focus.
151
- - **Shortcuts reference** is the last Toolbar button, with a keyboard icon, at the right edge; only the fixed Toolbar has it (there is none above the selection or in `readOnly`, and with `toolbar="none"` there is no Toolbar at all). Details are in the "Shortcuts reference" section.
151
+ - **Keyboard shortcuts popover** is the last Toolbar button, with a keyboard icon, at the right edge; only the fixed Toolbar has it (there is none above the selection or in `readOnly`, and with `toolbar="none"` there is no Toolbar at all). Details are in the "Keyboard shortcuts popover" section.
152
152
  - **Contents and order — one compact row; the kit sets the order, not `tools`**. Groups are separated by a divider:
153
153
 
154
154
  | Group | Buttons and menus in order |
155
155
  | ----------- | ---------------------------------------------------------------------------------- |
156
- | Whole Block | "+" Insert menu · HTML · "¶" Block type · Alignment ▾ |
156
+ | Whole Block | "+" Slash menu · HTML · "¶" Block type · Alignment ▾ |
157
157
  | Marks | bold · italic · highlight · "…" (other Marks, code block, quote, clear formatting) |
158
158
  | Lists | Lists ▾ |
159
159
  | Insert | link · image · Table ▾ · emoji |
160
160
  | History | undo · redo |
161
161
 
162
- At the right edge are Find (the `search` Tool) and the Shortcuts reference. A button or menu without its Tool enabled is not rendered, and a group without buttons disappears together with its divider: in `basic` the row is bold, italic, "…" | Lists ▾ | link | undo, redo | find and the reference at the right edge (a "¶" with only "Text" is not rendered). The floating Toolbar (`toolbar="floating"`) has the same contents and order, only without Find and the reference; the Selection menu (`toolbar="bubble"`) has its own short set — no alignment, with its own "Lists ▾" and "Color" (below).
162
+ At the right edge are Find (the `search` Tool) and the Keyboard shortcuts popover. A button or menu without its Tool enabled is not rendered, and a group without buttons disappears together with its divider: in `basic` the row is bold, italic, "…" | Lists ▾ | link | undo, redo | find and the Keyboard shortcuts popover at the right edge (a "¶" with only "Text" is not rendered). The floating Toolbar (`toolbar="floating"`) has the same contents and order, only without Find and the Keyboard shortcuts popover; the Bubble menu (`toolbar="bubble"`) has its own short set — no alignment, with its own "Lists ▾" and "Color" (below).
163
163
 
164
- - There are no separate buttons on the Toolbar for alignment, lists, quote, horizontal rule and code block: alignment and lists are menus, quote is a checkbox in "…", the rule is an Insert menu item ("+" and `/`), the code block is an item in "…" (they are used rarely). When the Toolbar is narrower than its content it wraps onto the next row instead of running past the edge — **by whole groups**: "Undo · Redo" never split across rows, the divider sits at the end of a group, and a wrapped row starts with a button. The full row in a 640px column takes two rows: search and the reference go to the second one, to the right edge.
164
+ - There are no separate buttons on the Toolbar for alignment, lists, quote, horizontal rule and code block: alignment and lists are menus, quote is a checkbox in "…", the rule is a Slash menu item ("+" and `/`), the code block is an item in "…" (they are used rarely). When the Toolbar is narrower than its content it wraps onto the next row instead of running past the edge — **by whole groups**: "Undo · Redo" never split across rows, the divider sits at the end of a group, and a wrapped row starts with a button. The full row in a 640px column takes two rows: search and the Keyboard shortcuts popover go to the second one, to the right edge.
165
165
 
166
166
  ### The "¶" menu — line type
167
167
 
168
168
  The "¶" button opens the line type menu under the caret: **Text** (`Ctrl/⌘+Alt+0`), **Heading 1** (only with `heading1`, `Ctrl/⌘+Alt+1`), **Heading 2–4** (`Ctrl/⌘+Alt+2…4`). The shortcut is shown on every item: `⌘`, `⌥`, `⇧` joined with a plus on Mac and iOS (`⌘+⌥+2`), `Ctrl`, `Alt`, `Shift` elsewhere.
169
169
 
170
- - **There are no lists or quote in "¶"** (the owner's decision): lists have the "Lists ▾" menu (on the Toolbar and in the Selection menu), quote has the "Quote" checkbox in "…". They stay in the Block handle's "Format ›" and in the Insert menu ("/", "Turn into"): there a Block is turned into anything, rather than a line type being chosen.
170
+ - **There are no lists or quote in "¶"** (the owner's decision): lists have the "Lists ▾" menu (on the Toolbar and in the Bubble menu), quote has the "Quote" checkbox in "…". They stay in the Block handle's "Format ›" and in the Slash menu ("/", "Turn into"): there a Block is turned into anything, rather than a line type being chosen.
171
171
  - **The items come from the enabled Tools** (`heading`, `heading1`) plus the "¶" items of custom Tools; "Text" is always there as long as there is at least one other item. Without headings there is no "¶" button — that is the case in `basic`.
172
172
  - **The line under the caret decides**: a paragraph in a list item or in a quote is "Text", a heading in a quote is its level. This type is checked, and it also appears in the button's accessible name ("Block type: Heading 2") and **on the icon**: the heading level (H1–H4), the icon of a custom Tool's item, and "¶" for text (before that the icon was always "¶", ADR-0054). In a code block the button says "Code block", the icon is "¶", and no item is checked: the code block is an item of "…".
173
- - **A choice changes the line type in place** and keeps the text: a heading in a quote stays in the quote. Where the Schema does not allow this type for the line (a heading first in a list item), the Insert menu's "Turn into" takes over: the lists and quotes around the line are removed, the type is set. It is one edit, and `Ctrl/⌘+Z` undoes it whole. Choosing the current type changes nothing. Focus returns to the Document.
173
+ - **A choice changes the line type in place** and keeps the text: a heading in a quote stays in the quote. Where the Schema does not allow this type for the line (a heading first in a list item), the Slash menu's "Turn into" takes over: the lists and quotes around the line are removed, the type is set. It is one edit, and `Ctrl/⌘+Z` undoes it whole. Choosing the current type changes nothing. Focus returns to the Document.
174
174
 
175
175
  ### The "…" menu — More formatting
176
176
 
177
- The "…" button (tooltip and name — "More formatting", `editor.moreFormatting`) comes right after the "Highlight" button (in the Selection menu, after "Color") and opens the menu of the remaining formatting: **Inline code** (`Ctrl/⌘+E`), **Underline** (`Ctrl/⌘+U`), **Strikethrough** (`Ctrl/⌘+Shift+S`), **Superscript** (`Ctrl/⌘+.`), **Subscript** (`Ctrl/⌘+,`), **Code block** (`Ctrl/⌘+Alt+C`), **Quote** (`Ctrl/⌘+Shift+B`), **Clear formatting** (`Ctrl/⌘+\`). There are no colors in "…" — they have their own buttons (below). The shortcuts shown on the items come from the same table as the Toolbar tooltips and the reference. There are no shortcuts borrowed from familiar editors (`⌘H`, `⌘L`, `⌘⇧M`): macOS and the browser intercept them, and the label would promise something that does not fire.
177
+ The "…" button (tooltip and name — "More formatting", `editor.moreFormatting`) comes right after the "Highlight" button (in the Bubble menu, after "Color") and opens the menu of the remaining formatting: **Inline code** (`Ctrl/⌘+E`), **Underline** (`Ctrl/⌘+U`), **Strikethrough** (`Ctrl/⌘+Shift+S`), **Superscript** (`Ctrl/⌘+.`), **Subscript** (`Ctrl/⌘+,`), **Code block** (`Ctrl/⌘+Alt+C`), **Quote** (`Ctrl/⌘+Shift+B`), **Clear formatting** (`Ctrl/⌘+\`). There are no colors in "…" — they have their own buttons (below). The shortcuts shown on the items come from the same table as the Toolbar tooltips and the Keyboard shortcuts popover. There are no shortcuts borrowed from familiar editors (`⌘H`, `⌘L`, `⌘⇧M`): macOS and the browser intercept them, and the label would promise something that does not fire.
178
178
 
179
- - **The items come from the enabled Tools** (`code`, `underline`, `strike`, `superscript`, `subscript`, `codeBlock`, `blockquote`, `clearFormatting`). Without any of them there is no "…" button; with one, the menu has a single item. In `basic`, "…" has inline code, strikethrough and quote. A Mark that has a button in the row of the Toolbar or the Selection menu is not repeated in that menu's "…": the Selection menu has no underline in "…" because it is in the row.
179
+ - **The items come from the enabled Tools** (`code`, `underline`, `strike`, `superscript`, `subscript`, `codeBlock`, `blockquote`, `clearFormatting`). Without any of them there is no "…" button; with one, the menu has a single item. In `basic`, "…" has inline code, strikethrough and quote. A Mark that has a button in the row of the Toolbar or the Bubble menu is not repeated in that menu's "…": the Bubble menu has no underline in "…" because it is in the row.
180
180
  - **Marks are items with a check** (`menuitemcheckbox`, `aria-checked`): the Marks of the selection or caret are checked. A choice sets or removes the Mark in one edit (`Ctrl/⌘+Z` undoes it whole), closes the menu and returns focus to the Document. An item that does not apply here (a Mark inside a code block) is unavailable.
181
181
  - **The code block is a separate section right after the Marks** (before that it was a Toolbar button between "HTML" and "¶"). It is a Block, not a Mark, so it comes after a divider, but it is the same checkbox item with a shortcut: all the toggles of "…" are listed one after another, and "Clear formatting" comes after them.
182
182
  - The code block item is checked when the caret is in a code block; choosing it turns the selection's Blocks into a code block or back into text — in one edit, like `Ctrl/⌘+Alt+C`. It is unavailable where the engine does not allow a code block.
183
- - The same item is in the "…" menu of the Selection menu: the Selection menu had no code block button, and that is where turning selected lines into code is convenient.
183
+ - The same item is in the "…" menu of the Bubble menu: the Bubble menu had no code block button, and that is where turning selected lines into code is convenient.
184
184
  - **Quote is a checkbox next to the code block** (before that it was a "¶" item): checked when the caret is in a quote; choosing it wraps the selection's Blocks in a quote or removes it — in one edit, like `Ctrl/⌘+Shift+B`. Unavailable where the engine does not allow a quote. Present only if `blockquote` is enabled.
185
185
  - **On a narrow screen the submenu goes under the item** (narrower than 640px): there is no room at the side on a phone, so the submenu of the Block handle "⋮" and the "Table" menu (together with the size grid) open under their item on top of the parent menu. On a wide screen they open at the side, as before.
186
- - There are no separate buttons on the Toolbar for inline code, underline, strikethrough, superscript, subscript, code block and clearing: "…" replaces them. The menu is built in the shared formatting menu module (`MoreFormatMenu` in `FormatMenus.tsx`) — the same "…" appears in the Selection menu too.
187
- - The row of frequently used Marks is shared by the Toolbar and the Selection menu (`rowMarks.ts`): on the Toolbar it is bold and italic, in the Selection menu also underline.
186
+ - There are no separate buttons on the Toolbar for inline code, underline, strikethrough, superscript, subscript, code block and clearing: "…" replaces them. The menu is built in the shared formatting menu module (`MoreFormatMenu` in `FormatMenus.tsx`) — the same "…" appears in the Bubble menu too.
187
+ - The row of frequently used Marks is shared by the Toolbar and the Bubble menu (`rowMarks.ts`): on the Toolbar it is bold and italic, in the Bubble menu also underline.
188
188
 
189
189
  ### The "Highlight" and "Color" buttons
190
190
 
191
191
  Color is chosen by eye, from dots, in one click — as in Tiptap's Notion-like template. Before that, text color and highlight were submenus of "…", and the "Highlight" button applied yellow in one click.
192
192
 
193
- **Which button goes where** (the owner's decision — no duplicates): on the Toolbar (fixed and floating) there is only "Highlight", in the Selection menu there is only "Color", which also has the highlight grid. Without the `color` Tool, the Selection menu has "Highlight" — otherwise there would be no way to set a highlight there. **The trade-off of this decision:** text color is chosen only from the Selection menu. Buttons cannot set it where there is no Selection menu: with `selectionMenu={false}`, with `toolbar="floating"` and on a touch screen (there the fixed Toolbar replaces the Selection menu). The Handle and engine commands remain.
193
+ **Which button goes where** (the owner's decision — no duplicates): on the Toolbar (fixed and floating) there is only "Highlight", in the Bubble menu there is only "Color", which also has the highlight grid. Without the `color` Tool, the Bubble menu has "Highlight" — otherwise there would be no way to set a highlight there. **The trade-off of this decision:** text color is chosen only from the Bubble menu. Buttons cannot set it where there is no Bubble menu: with `selectionMenu={false}`, with `toolbar="floating"` and on a touch screen (there the fixed Toolbar replaces the Bubble menu). The Handle and engine commands remain.
194
194
 
195
195
  - **"Highlight"** (the `highlight` Tool, tooltip — `editor.highlightToggle`): a click opens a row of nine highlights by hue — gray, brown, orange, yellow, green, blue, purple, pink, red — and "Remove highlight" (a crossed-out circle with a tooltip, `editor.highlightRemove`). The button's name states the current highlight ("Highlight: Pink", "Highlight: Custom #ffe066", "Highlight: None"), so it is audible without opening the popover; while the selection has a highlight, the button looks pressed. A click on the button itself no longer applies yellow: yellow is set by clicking its dot.
196
196
  - **"Color"** (the `color` Tool, `editor.color`): the letter "A" in the current text color and an arrow. The popover has two grids: **"Text color"** (`editor.textColorSection`: six tones, a dot with the letter "A" in that color, and "Remove color" as the last cell, `editor.colorRemove`) and **"Highlight color"** (`editor.highlightSection`: the same nine highlights, and "Remove highlight" as the last cell). The highlight grid is there only if `highlight` is enabled too. Removal is a crossed-out circle with a tooltip at the end of the grid, as in "Highlight", not a white dot at the start: that one looked like one more color and was checked when there is no color. With no color, nothing in the grid is checked, and focus on open is on the first dot. Next to each grid's heading is **"Custom color…"**: it opens a `ColorPicker` popover (see the "Custom color" section below).
197
197
  - **Roles — as in a WAI-ARIA Authoring Practices (APG) menu, not a radio group:** the popover is a `menu`, the grids are a `group` with a heading, the dots and "Custom color…" are `menuitemradio` with `aria-checked` (the current one is marked with a ring around the dot), "Remove color" and "Remove highlight" are `menuitem`. In a radio group an arrow key would choose right away and close the popover; here the arrow keys only move between dots: `←` `→` — the neighboring dot, `↑` `↓` — the same column in the row above or below (five per row), `Home` and `End` — the first and the last; Enter, Space and click apply.
198
198
  - **Focus.** On open, it is on the checked color (or the first one). A choice applies the color in one edit (`Ctrl/⌘+Z` undoes it whole), closes the popover and returns focus to the Document, to the previous selection. Escape closes only the popover and returns focus to the button — in both `Dialog` and `Drawer` the dialog or drawer stays open (ADR-0041).
199
- - **In the Selection menu** the button itself holds the popover: while it is open, the button has `aria-expanded="true"` and the Selection menu does not hide. In HTML mode and on inline code the buttons are unavailable (`aria-disabled`) and do not open the popover. In `basic` neither button exists: neither `color` nor `highlight` is part of it.
199
+ - **In the Bubble menu** the button itself holds the popover: while it is open, the button has `aria-expanded="true"` and the Bubble menu does not hide. In Source mode and on inline code the buttons are unavailable (`aria-disabled`) and do not open the popover. In `basic` neither button exists: neither `color` nor `highlight` is part of it.
200
200
 
201
201
  ### The "Alignment" and "Lists" menus
202
202
 
203
203
  Both are menus from the same shared module as "¶" and "…" (`AlignMenu` and `ListMenu` in `FormatMenus.tsx`): radio items (`menuitemradio`) with the current value checked and shortcut labels from the shared table. The button's icon is the icon of the current value, its name is the value in words; a choice returns focus to the Document and is undone by a single `Ctrl/⌘+Z`.
204
204
 
205
205
  - **Alignment ▾** (the `align` Tool): **Align left** (`Ctrl/⌘+Shift+L`), **Align center** (`Ctrl/⌘+Shift+E`), **Align right** (`Ctrl/⌘+Shift+R`). The button's name is "Alignment: Align center"; for a Block without `data-align`, "Align left" is checked, and so it is for a selection with mixed alignment. Choosing the checked item changes nothing and puts no empty edit into history; choosing "Align left" restores the default. **There is no justify** — the decision is below, in "Color, highlight and alignment" (WCAG 1.4.8).
206
- - **Lists ▾** (any of `bulletList`, `orderedList`, `taskList`): **Bulleted list** (`Ctrl/⌘+Shift+8`), **Numbered list** (`Ctrl/⌘+Shift+7`), **Checklist** (`Ctrl/⌘+Shift+9`) — the same kinds as in "Format ›" of the Block handle, only from the enabled Tools. The kind of the nearest list around the caret is checked (also in a quote inside an item), the button's name is "List: numbered", and outside a list it is "Lists" with the generic list icon. Choosing another kind works the same way as the Insert menu's "Turn into": the Block leaves the list and the quote and is wrapped again in one edit. **Choosing the checked kind again removes the list**, like the old toggle button. A menu with a single enabled kind stays a menu.
206
+ - **Lists ▾** (any of `bulletList`, `orderedList`, `taskList`): **Bulleted list** (`Ctrl/⌘+Shift+8`), **Numbered list** (`Ctrl/⌘+Shift+7`), **Checklist** (`Ctrl/⌘+Shift+9`) — the same kinds as in "Format ›" of the Block handle, only from the enabled Tools. The kind of the nearest list around the caret is checked (also in a quote inside an item), the button's name is "List: numbered", and outside a list it is "Lists" with the generic list icon. Choosing another kind works the same way as the Slash menu's "Turn into": the Block leaves the list and the quote and is wrapped again in one edit. **Choosing the checked kind again removes the list**, like the old toggle button. A menu with a single enabled kind stays a menu.
207
207
  - Without the `align` Tool there is no alignment menu; without any list kind there is no "Lists" menu. In `basic` there is "Lists" with two items and no alignment.
208
208
 
209
209
  ### Floating Toolbar
@@ -214,55 +214,55 @@ Both are menus from the same shared module as "¶" and "…" (`AlignMenu` and `L
214
214
  - **The Toolbar lives in the Editor's DOM**, not in a portal: it travels with the text in any scroll container (window, drawer, card). So an ancestor with `overflow: hidden` clips it at its edge — do not put the Editor in such a container right against the top, or use `toolbar="fixed"`.
215
215
  - The layer is the `OVERLAY_LAYER.popover` step, like popovers. The Toolbar does not take part in the Escape stack: there is nothing to close in it, and Escape stays with the dialog or drawer around the Editor.
216
216
 
217
- ### Selection menu: `selectionMenu` and `toolbar="bubble"`
217
+ ### Bubble menu: `selectionMenu` and `toolbar="bubble"`
218
218
 
219
- By default the Selection menu also appears on top of the fixed Toolbar (`toolbar="fixed"`, the `selectionMenu` prop, `true` by default): the Toolbar at the top is for everything; the Selection menu keeps the mouse near the text. In that case `Alt+F10` moves focus to the Toolbar. `toolbar="bubble"` puts the **Selection menu** in place of the Toolbar — a compact pill above the selected text, as in Notion and Medium. It sits on an **inverse surface**: dark in the light theme, light in `dark` and `dim` — so it blends neither with the text nor with the field under it.
219
+ By default the Bubble menu also appears on top of the fixed Toolbar (`toolbar="fixed"`, the `selectionMenu` prop, `true` by default): the Toolbar at the top is for everything; the Bubble menu keeps the mouse near the text. In that case `Alt+F10` moves focus to the Toolbar. `toolbar="bubble"` puts the **Bubble menu** in place of the Toolbar — a compact pill above the selected text, as in Notion and Medium. It sits on an **inverse surface**: dark in the light theme, light in `dark` and `dim` — so it blends neither with the text nor with the field under it.
220
220
 
221
- - **The set is defined by the kit and is not configurable:** "¶" (line type, the same menu as on the Toolbar) · Lists ▾ (the same menu as on the Toolbar) · bold · italic · underline · Color ▾ (both grids — text and highlight; without `color`, "Highlight") · "…" (the other Marks and the code block — the same menu as on the Toolbar, but without underline: it is in the row here) · link (the same popover as `Ctrl/⌘+K`) · emoji (the same grid). A button of a disabled Tool is not rendered: `basic` has no emoji, and with `tools={["italic", "link"]}` two buttons remain. There is no undo, redo, Shortcuts reference or rare Marks in the Selection menu — it is about the selected text.
221
+ - **The set is defined by the kit and is not configurable:** "¶" (line type, the same menu as on the Toolbar) · Lists ▾ (the same menu as on the Toolbar) · bold · italic · underline · Color ▾ (both grids — text and highlight; without `color`, "Highlight") · "…" (the other Marks and the code block — the same menu as on the Toolbar, but without underline: it is in the row here) · link (the same popover as `Ctrl/⌘+K`) · emoji (the same grid). A button of a disabled Tool is not rendered: `basic` has no emoji, and with `tools={["italic", "link"]}` two buttons remain. There is no undo, redo, Keyboard shortcuts popover or rare Marks in the Bubble menu — it is about the selected text.
222
222
  - **Bold, italic and underline are toggles** with `aria-pressed`; the tooltip names the shortcut from the same table as the Toolbar's, and the button announces it with `aria-keyshortcuts`.
223
- - **Display and position are as in the floating Toolbar:** the Selection menu appears when the selection has settled, and hides on an empty selection, on a selected image (it has its own panel) and when focus has left the Editor. At the edge of the viewport or a scroll container it flips below the selection and shifts inward. In `readOnly` there is no Selection menu.
223
+ - **Display and position are as in the floating Toolbar:** the Bubble menu appears when the selection has settled, and hides on an empty selection, on a selected image (it has its own panel) and when focus has left the Editor. At the edge of the viewport or a scroll container it flips below the selection and shifts inward. In `readOnly` there is no Bubble menu.
224
224
  - **A touch screen gets the fixed Toolbar**, for the same reason as with `floating`: the system selection menu of iOS and Android appears in the same place.
225
- - **Menus and popovers opened from the Selection menu use the regular popover surface**, not the inverse one: "¶", "…", the link field and the emoji grid look the same as when opened from the Toolbar. Escape in an open menu closes only the menu and returns focus to its button in the Selection menu; inside `Dialog` and `Drawer` the dialog or drawer stays open.
226
- - **The Selection menu lives in the Editor's DOM**, like the floating Toolbar: it travels with the text in a scroll container, and an ancestor with `overflow: hidden` clips it at its edge. The layer is `OVERLAY_LAYER.popover`.
227
- - The Selection menu's name is `editor.selectionToolbar` ("Selection formatting" in English, "Форматирование выделения" in Russian): the screen reader tells it apart from the Toolbar.
228
- - **The colors are inverse surface tokens** `--surface-inverse`, `--surface-inverse-hover`, `--surface-inverse-pressed`, `--text-on-inverse` and the focus outline `--accent-on-inverse` ([palette.md](../../palette.md#инверсная-поверхность)). On the Paper (`surface="paper"`) the Selection menu stays in the app theme, like the Toolbar.
225
+ - **Menus and popovers opened from the Bubble menu use the regular popover surface**, not the inverse one: "¶", "…", the link field and the emoji grid look the same as when opened from the Toolbar. Escape in an open menu closes only the menu and returns focus to its button in the Bubble menu; inside `Dialog` and `Drawer` the dialog or drawer stays open.
226
+ - **The Bubble menu lives in the Editor's DOM**, like the floating Toolbar: it travels with the text in a scroll container, and an ancestor with `overflow: hidden` clips it at its edge. The layer is `OVERLAY_LAYER.popover`.
227
+ - The Bubble menu's name is `editor.selectionToolbar` ("Selection formatting" in English, "Форматирование выделения" in Russian): the screen reader tells it apart from the Toolbar.
228
+ - **The colors are inverse surface tokens** `--surface-inverse`, `--surface-inverse-hover`, `--surface-inverse-pressed`, `--text-on-inverse` and the focus outline `--accent-on-inverse` ([palette.md](../../palette.md#инверсная-поверхность)). On the Paper (`surface="paper"`) the Bubble menu stays in the app theme, like the Toolbar.
229
229
 
230
230
  ### Glass on popup surfaces
231
231
 
232
- Everything the Editor shows above the Document sits on **Glass** — a semi-transparent background of its surface that blurs what is behind it: the floating Toolbar and the image panel, the Selection menu, menu lists ("¶", "…", alignment, lists, table, Insert menu, Block handle, code block language), popovers (link, image, custom color, emoji, Shortcuts reference, popovers of the buttons, Insert items and Block actions of a custom Tool) and the Text card. The background is 82% of the surface token in `color-mix`; the blur is 14px, as in toasts (ADR-0010); tooltips keep their own 85% and 10px.
232
+ Everything the Editor shows above the Document sits on **Glass** — a semi-transparent background of its surface that blurs what is behind it: the floating Toolbar and the image panel, the Bubble menu, menu lists ("¶", "…", alignment, lists, table, Slash menu, Block handle, code block language), popovers (link, image, custom color, emoji, Keyboard shortcuts popover, popovers of the buttons, Insert items and Block actions of a custom Tool) and the Text card. The background is 82% of the surface token in `color-mix`; the blur is 14px, as in toasts (ADR-0010); tooltips keep their own 85% and 10px.
233
233
 
234
- - **The color is the token of its own surface:** the Toolbar and popovers use `--bg-1`, menu lists use `--surface-elevated`, the Selection menu uses `--surface-inverse`. The theme changes the token, and the Glass changes with it.
234
+ - **The color is the token of its own surface:** the Toolbar and popovers use `--bg-1`, menu lists use `--surface-elevated`, the Bubble menu uses `--surface-inverse`. The theme changes the token, and the Glass changes with it.
235
235
  - **A solid background under `prefers-reduced-transparency: reduce` and `prefers-contrast: more`:** under a semi-transparent background, text contrast depends on what lies beneath the menu, and a user who asked the system to remove transparency or raise contrast gets the old solid surface without blur.
236
236
  - **A solid background on iOS:** in Safari on iOS 27 the menu's Glass background was not rendered at all, and the items ended up on top of the Document text. So on iOS (the condition `@supports (-webkit-touch-callout: none)`, which only WebKit on iOS understands) the Glass is replaced with a solid surface without blur; desktop Safari, Chrome and Firefox keep the Glass.
237
237
  - **The Editor only.** The kit's `Menu` and `Popover` outside the Editor stay solid: the Editor passes the Glass to them as a class, and the public API did not change.
238
238
 
239
239
  ### Keyboard: `Alt+F10`
240
240
 
241
- `Alt+F10` (`⌥F10` on Mac) from the Document moves focus to the first button of the Toolbar — fixed, floating or the Selection menu; the key shows the floating Toolbar and the Selection menu at once, without waiting for the delay. The shortcut is the same as in CKEditor and TinyMCE: APG has none of its own, and one shortcut for all editors is easier to remember. Tab is not an option here — in the Document it indents a list item.
241
+ `Alt+F10` (`⌥F10` on Mac) from the Document moves focus to the first button of the Toolbar — fixed, floating or the Bubble menu; the key shows the floating Toolbar and the Bubble menu at once, without waiting for the delay. The shortcut is the same as in CKEditor and TinyMCE: APG has none of its own, and one shortcut for all editors is easier to remember. Tab is not an option here — in the Document it indents a list item.
242
242
 
243
243
  - Inside the Toolbar, the arrow keys, Home and End work as usual. A button that has fired returns focus to the Document, to the same selection; if nothing was pressed, `Shift+Tab` moves focus back: the floating Toolbar sits in the DOM right after the Document.
244
244
  - The Toolbar does not intercept Escape: in a dialog or drawer it closes it (see "Accessibility and forms").
245
- - On an empty selection of the floating Toolbar and the Selection menu, and with `toolbar="none"`, the key does nothing and passes on.
245
+ - On an empty selection of the floating Toolbar and the Bubble menu, and with `toolbar="none"`, the key does nothing and passes on.
246
246
  - The Toolbar announces the shortcut with `aria-keyshortcuts="Alt+F10"` — the screen reader names it on the Toolbar itself.
247
- - From the HTML mode field, `Alt+F10` moves focus to the "HTML" button: the other Toolbar buttons are `aria-disabled` there, and it is the first one that works (Enter on it switches back to visual mode, and the arrow keys move on through the Toolbar). See the "HTML mode" section.
247
+ - From the Source mode field, `Alt+F10` moves focus to the "HTML" button: the other Toolbar buttons are `aria-disabled` there, and it is the first one that works (Enter on it switches back to visual mode, and the arrow keys move on through the Toolbar). See the "Source mode" section.
248
248
  - **An open Text card** takes priority over the Toolbar: `Alt+F10` moves focus into it, Escape returns the caret; while the Text card is hidden or closed, the key moves focus to the Toolbar (see the "Text card" section).
249
249
  - With the caret in a code block, `Alt+F10` moves focus first to that Block's language button, then to "Copy", then to the Toolbar (see the "Code block" section). Likewise, on a selected image the key moves focus to its panel.
250
250
 
251
- ## Shortcuts reference
251
+ ## Keyboard shortcuts popover
252
252
 
253
253
  The button with a keyboard icon ("Keyboard shortcuts") is the last one on the fixed Toolbar, at the right edge. It opens the kit's `Popover` with a "command — shortcut" list that is **read-only**: the rows are not clickable, this is not a command palette.
254
254
 
255
- - **There is no button on a phone.** On a touch screen without hover (`@media (hover: none) and (pointer: coarse)` — a phone, a tablet without a trackpad) there are no shortcuts, so the reference button is hidden, and menu items ("¶", "…", table, Insert menu, Block handle) do not show shortcut labels. A tablet with a trackpad and a laptop with a touch screen keep the tooltips.
255
+ - **There is no button on a phone.** On a touch screen without hover (`@media (hover: none) and (pointer: coarse)` — a phone, a tablet without a trackpad) there are no shortcuts, so the Keyboard shortcuts button is hidden, and menu items ("¶", "…", table, Slash menu, Block handle) do not show shortcut labels. A tablet with a trackpad and a laptop with a touch screen keep the tooltips.
256
256
  - **The markup is a table** with the column headers "Command" and "Shortcut"; the shortcut is the kit's `Kbd` (`<kbd>`). A screen reader reads a row as a "command — shortcut" pair.
257
- - **Custom buttons.** The shortcut of a custom Tool's button (the `shortcut` field) goes into the reference as a separate group without a heading after the kit's groups, in `tools` order; the label uses the `UiProvider` locale. Keyboard shortcuts bound by a custom Tool's Extensions do not appear here — see the "Custom Tool's button" section.
257
+ - **Custom buttons.** The shortcut of a custom Tool's button (the `shortcut` field) goes into the Keyboard shortcuts popover as a separate group without a heading after the kit's groups, in `tools` order; the label uses the `UiProvider` locale. Keyboard shortcuts bound by a custom Tool's Extensions do not appear here — see the "Custom Tool's button" section.
258
258
  - **Group order:** selection (the first `⌘A` selects the Block's text, the second the whole Document), undo and redo, Block commands (duplicate, up, down, handle menu), clear formatting, Marks (bold, italic, strikethrough, underline, inline code, superscript and subscript), Block type (text, headings, quote, code block), lists and item indent (`⌘]` and `⌘`), alignment, link, search (`Ctrl/⌘+F`, `Ctrl/⌘+Alt+F`), moving to the Toolbar (`Alt+F10`).
259
259
  - **The set depends on `tools`.** A row exists only for the command of an enabled Tool: `basic` has no headings, underline, superscript and subscript, alignment, checklist or code block, and the item indent is there with any of the three lists. Heading 1 only with `heading` and `heading1` together. Commands without a Tool of their own are always there: selection, duplicating and reordering a Block, the Block handle menu, `Alt+F10`.
260
260
  - **Custom Tools.** The shortcuts of their Insert items and Block actions (the `shortcut` field) form a group without a heading after the kit's groups, one group per Tool, in `tools` order; a row's label is the UI contribution's label in the current locale.
261
261
  - **Width follows the content:** the popover is as wide as the longest row, with no empty gap between the command and the shortcut; the screen edge limits it, and a long list scrolls.
262
262
  - **Labels.** Keys are joined with a plus on all platforms: `⌘+⇧+Z` on Apple and `Ctrl+Shift+Z` elsewhere. The macOS run-together form (`⌘⌥⇧↑`) reads as one word in a long list. The Apple detection is the same as ProseMirror's, that is, the one the engine uses to decide which key is `mod`. `Kbd` renders the symbols `⌘ ⌥ ⇧ ⌃` in a proportional font: a monospace stack has no `⇧`, and the fallback glyph comes out thin.
263
- - **One command table is the source of truth.** The tooltips and `aria-keyshortcuts` of Toolbar buttons, the labels in the Insert menu and the reference read shortcuts from one table (`src/editor/core/shortcuts.ts`: command id, Tool, keyboard shortcut, a label from the message catalog). The extensions bind the keyboard shortcuts, and the table labels them; so that a label never promises what is not there, `tests/rich-text-editor-shortcuts.test.tsx` presses every shortcut of the table in a live Editor and checks the result.
264
- - **Closing.** Escape and a click outside close the popover and return focus to the button (if the click itself put focus on a field or button, it is not taken away). Inside `Dialog` and `Drawer`, Escape closes only the reference, not the dialog or drawer ([ADR-0041 (ADR-0041)).
265
- - **Not shown:** in `readOnly`, with `toolbar="none"`, above the selection in `toolbar="floating"` and in the Selection menu (`toolbar="bubble"`); on a touch screen they are replaced by the fixed Toolbar, but the reference button is hidden there by the rule at the start of the section: on a phone and on a tablet without a trackpad it is absent.
263
+ - **One command table is the source of truth.** The tooltips and `aria-keyshortcuts` of Toolbar buttons, the labels in the Slash menu and the Keyboard shortcuts popover read shortcuts from one table (`src/editor/core/shortcuts.ts`: command id, Tool, keyboard shortcut, a label from the message catalog). The extensions bind the keyboard shortcuts, and the table labels them; so that a label never promises what is not there, `tests/rich-text-editor-shortcuts.test.tsx` presses every shortcut of the table in a live Editor and checks the result.
264
+ - **Closing.** Escape and a click outside close the popover and return focus to the button (if the click itself put focus on a field or button, it is not taken away). Inside `Dialog` and `Drawer`, Escape closes only the Keyboard shortcuts popover, not the dialog or drawer ([ADR-0041 (ADR-0041)).
265
+ - **Not shown:** in `readOnly`, with `toolbar="none"`, above the selection in `toolbar="floating"` and in the Bubble menu (`toolbar="bubble"`); on a touch screen they are replaced by the fixed Toolbar, but the Keyboard shortcuts button is hidden there by the rule at the start of the section: on a phone and on a tablet without a trackpad it is absent.
266
266
 
267
267
  Message catalog strings: `editor.shortcuts` (the name of the button and popover), `shortcutsCommand` and `shortcutsKeys` (column headers), and the labels of commands without a button of their own — `selectBlock`, `selectDocument`, `duplicateBlock`, `moveBlockUp`, `moveBlockDown`, `blockActions`, `indentListItem`, `outdentListItem`, `focusToolbar`.
268
268
 
@@ -283,14 +283,14 @@ A click or `Ctrl/⌘+/` from the Document opens a menu — the kit's `Menu`:
283
283
  - **The Block as a whole.** The handle sits at a Document Block: a paragraph, a heading, a whole list, a checklist, a quote, a code block, a table, an image, a rule. With the caret in a list's second item, the handle is at the list's first line, and the menu actions apply to the whole list; "Format › Heading 2" turns all its items into headings. A selection across several Blocks has no handle.
284
284
  - **Shortcut labels only when the shortcut does the same thing.** For a top-level paragraph or heading the items are labeled: `⌥+⇧+↑/↓`, `⌘+⇧+D`, the "¶" shortcuts in "Format ›". In a list item `⌥⇧↑` moves the item, not the list, so there are no labels; in a table `⌥⇧↑/↓` belong to rows, so "Up" and "Down" have no label, while `⌘⇧D` copies the whole table, so "Duplicate" has one. "Add ›" has no labels: the shortcuts change the current Block, and the item puts a new one.
285
285
  - **One edit to undo**, and focus after the action is in the Document. Escape closes the submenu, then the menu, and returns focus to the Document; inside `Dialog` and `Drawer` the dialog or drawer does not close (ADR-0041).
286
- - **Keyboard.** The handle is outside the Tab order (in the Document Tab indents a list item); `Ctrl/⌘+/` moves focus to it — like "Block actions" in Notion, and browsers and systems leave this shortcut free in a field. The row is in the Shortcuts reference; the handle has `aria-keyshortcuts`.
286
+ - **Keyboard.** The handle is outside the Tab order (in the Document Tab indents a list item); `Ctrl/⌘+/` moves focus to it — like "Block actions" in Notion, and browsers and systems leave this shortcut free in a field. The row is in the Keyboard shortcuts popover; the handle has `aria-keyshortcuts`.
287
287
  - **Tool actions.** Custom Block actions (`blockActions` of a Tool description, like "Unwrap" of the Wrapper) appear as a separate section before "Delete": only for Blocks whose names are listed in `blocks`, and filtered by a visibility predicate; `run` receives the Block's position. Custom Insert items go into "Add ›" and "Format ›" (see the "Custom Tool" section).
288
288
  - **There is no dragging by the handle** (the owner's decision).
289
- - **Layout** (see the "Layout" section). A column is a container of Blocks, like the Document: a Block in a column has its own handle at the column's left edge, and its actions do not leave the column. In "Add ›" the Layout is a submenu of six variants; a Block in a column has the "Layout ›" item (a variant of its Layout), and for a Layout selected as a whole, "Format ›" holds these six variants with the current one checked.
289
+ - **Column layout** (see the "Column layout" section). A column is a container of Blocks, like the Document: a Block in a column has its own handle at the column's left edge, and its actions do not leave the column. In "Add ›" the Column layout is a submenu of six variants; a Block in a column has the "Layout ›" item (a variant of its Column layout), and for a Column layout selected as a whole, "Format ›" holds these six variants with the current one checked.
290
290
  - **Wrapper** (see the "Wrapper" section). A Wrapper is also a container of Blocks: a Block in it has its own handle at the Wrapper's left edge, and its actions do not leave it. The items **"Tone ›"** and **"Unwrap"** are available for a Block directly in a Wrapper and for the Wrapper itself selected as a whole (Backspace on the Block under it); for the Wrapper, "Up", "Down", "Duplicate" and "Delete" apply to the Wrapper together with its content, and "Format ›" is unavailable — it has no Block type.
291
291
  - **Placement.** The handle lives in the Editor's DOM and travels with the text in any scroll container, in `Dialog` and `Drawer`; the sticky Toolbar and popovers cover it. The position is recomputed on every edit and on resize.
292
292
 
293
- Message catalog strings: `editor.blockActions` (the name of the handle, menu and reference row), `blockAdd`, `blockFormat`, `blockMoveUp`, `blockMoveDown`, `blockDuplicate`, `blockDelete`. Icons — `MoreVerticalIcon`, `AddIcon`, `ParagraphIcon`, `ArrowUpIcon`, `ArrowDownIcon`, `DuplicateIcon`, `DeleteIcon` from `@olddevs/ui/icons`.
293
+ Message catalog strings: `editor.blockActions` (the name of the handle, menu and Keyboard shortcuts popover row), `blockAdd`, `blockFormat`, `blockMoveUp`, `blockMoveDown`, `blockDuplicate`, `blockDelete`. Icons — `MoreVerticalIcon`, `AddIcon`, `ParagraphIcon`, `ArrowUpIcon`, `ArrowDownIcon`, `DuplicateIcon`, `DeleteIcon` from `@olddevs/ui/icons`.
294
294
 
295
295
  ## Keyboard: Block commands
296
296
 
@@ -347,7 +347,7 @@ Presets are also exported as arrays of names — `RICH_TEXT_PRESETS.full` and `R
347
347
  | `subscript` | "…": Subscript | `sub` | `Ctrl/⌘+,` | — | — |
348
348
  | `clearFormatting` | "…": Clear formatting | — (removes Marks) | `Ctrl/⌘+\` | — | — |
349
349
  | `link` | Link (address popover) | `a[href]` | `Ctrl/⌘+K` | `[text](url)` on paste | yes |
350
- | `color` | Color ▾ (Selection menu): "Text color" | `span[data-color]`, `span[style]` | — | — | — |
350
+ | `color` | Color ▾ (Bubble menu): "Text color" | `span[data-color]`, `span[style]` | — | — | — |
351
351
  | `highlight` | Highlight ▾ (Toolbar), "Color ▾" | `mark[data-color]`, `mark[style]` | — | — | — |
352
352
  | `align` | Alignment ▾ (menu) | `p`, `h2`–`h4` with `data-align` | `Ctrl/⌘+Shift+L`, `+E`, `+R` | — | — |
353
353
  | `bulletList` | Lists ▾: Bulleted list | `ul`, `li` | `Ctrl/⌘+Shift+8` | `- ` or `* ` | yes |
@@ -364,10 +364,10 @@ Presets are also exported as arrays of names — `RICH_TEXT_PRESETS.full` and `R
364
364
  | `layout` | "+": Layout | `div` with `data-block` and `data-layout` | `Tab`, `Shift+Tab`, arrow keys at the column edge | — | — |
365
365
  | `wrapper` | "+": Wrapper | `div[data-block=wrapper]` | `Enter` in an empty last paragraph — exit | — | — |
366
366
  | `details` | "+": Collapsible block | `details`, `summary`, `div[data-type=detailsContent]` | `Enter` in the title and at the empty end of the content | — | — |
367
- | `insertMenu` | "+" Insert menu | — (Blocks of other Tools) | — | `/` | — |
367
+ | `insertMenu` | "+" Slash menu | — (Blocks of other Tools) | — | `/` | — |
368
368
  | `toc` | Table of contents in the left gutter (not in `full`) | — (reads the headings) | — | — | — |
369
369
 
370
- 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.
370
+ 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 Source mode (see the "Source 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.
371
371
 
372
372
  **The level-one heading is `heading1`** (an add-on to `heading`): `full` has it, `basic` does not. With it the Block levels are 1–4: `Ctrl/⌘+Alt+1` and `# ` with a space give H1, `<h1>` from `value`, `defaultValue` and paste stays H1, and the "¶" menu on the Toolbar offers "Heading 1". Without it the old behavior holds: `# ` while typing stays text, `Ctrl/⌘+Alt+1` does nothing, and `<h1>` in incoming HTML becomes a paragraph. Disable `heading1` on a screen where the page heading already sits above the Editor: a second H1 breaks the page structure. Without the `heading` Tool there is no H1, even if `heading1` is passed.
373
373
 
@@ -448,7 +448,7 @@ export const mergeTagTool: CustomTool = {
448
448
  | `blocks` | Block Schema rules — by Node name (`CustomBlock`) |
449
449
  | `marks` | Mark Schema rules — by Mark name (`CustomMark`) |
450
450
  | `buttons` | Toolbar buttons — see the "Custom Tool's button" section (`CustomToolButton`) |
451
- | `insertItems` | Insert menu items (`CustomInsertItem`) — see the "Insert items and Block actions" section |
451
+ | `insertItems` | Slash menu items (`CustomInsertItem`) — see the "Insert items and Block actions" section |
452
452
  | `blockActions` | Block actions in the Block handle menu (`CustomBlockAction`) — the same section |
453
453
  | `textCards` | Text cards (`CustomTextCard`) — see the "Text card" section |
454
454
  | `trigger` | A custom Trigger character (`CustomTrigger`) — see the "Custom Trigger character" section |
@@ -468,11 +468,11 @@ The kit completes the Node and Mark from the Extensions: it sets the group (`blo
468
468
  | `CustomBlock` field | Value |
469
469
  | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
470
470
  | `kind` | `"block"` — block-level (`div`) or `"inline"` — inline (`span`). Required |
471
- | `place` | `"anywhere"` (default) — wherever a Block can appear; `"top"` — only the top level of the Document, like a Layout |
471
+ | `place` | `"anywhere"` (default) — wherever a Block can appear; `"top"` — only the top level of the Document, like a Column layout |
472
472
  | `attributes` | Custom attributes: name → value kind (`CustomAttribute`) |
473
473
  | `container` | The "container" Block trait: Blocks inside get their own Block handle, as in the Wrapper |
474
474
  | `turnInto` | The "can be turned into" Block trait: the Block handle's "Format ›" is available for this Block too |
475
- | `plainAtom` | The "Atom without a formatting menu" Block trait: a selected Block has no Selection menu and no floating Toolbar above it |
475
+ | `plainAtom` | The "Atom without a formatting menu" Block trait: a selected Block has no Bubble menu and no floating Toolbar above it |
476
476
  | `island` | The Block's Island in `RichTextView` — a client component from a module with `"use client"` (`CustomIslandProps`); the Editor does not read the field |
477
477
 
478
478
  `CustomMark` has only `attributes`. The kind of an attribute value (`CustomAttribute`):
@@ -483,7 +483,7 @@ The kit completes the Node and Mark from the Extensions: it sets the group (`blo
483
483
 
484
484
  An attribute cannot contain HTML: a value with `<` does not pass any rule, and the content of a non-editable Block is stored as the Document's markup inside the Block.
485
485
 
486
- **Block traits** are off by default: an ordinary Block is not a container, has no "Turn into", and has the Selection menu. It has a Block handle, like any Block: "Add ›", "Format ›" (unavailable), "Up", "Down", "Duplicate", "Delete". The kit copies the traits into the node spec itself; their vocabulary belongs to the kit, not to Tiptap.
486
+ **Block traits** are off by default: an ordinary Block is not a container, has no "Turn into", and has the Bubble menu. It has a Block handle, like any Block: "Add ›", "Format ›" (unavailable), "Up", "Down", "Duplicate", "Delete". The kit copies the traits into the node spec itself; their vocabulary belongs to the kit, not to Tiptap.
487
487
 
488
488
  **A failed validation when the set is assembled is a field error.** The set is validated once, when the engine is created; with Loaders, after they have run, based on what they returned. It is an error if:
489
489
 
@@ -494,7 +494,7 @@ An attribute cannot contain HTML: a value with `<` does not pass any rule, and t
494
494
 
495
495
  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.
496
496
 
497
- 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#опции-состояние-и-команды).
497
+ Buttons, Slash 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 bar 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#опции-состояние-и-команды).
498
498
 
499
499
  ### Extensions Loader
500
500
 
@@ -585,7 +585,7 @@ export const stampTool: CustomTool = {
585
585
  | `can` | Required. Whether the button is available now. An unavailable one is `aria-disabled`, stays in the arrow-key order, and a click or shortcut does nothing |
586
586
  | `isActive` | If present, the button is a toggle with `aria-pressed`; if absent, a one-off action without `aria-pressed` |
587
587
  | `isVisible` | A visibility predicate that depends on context: the button is hidden while it returns `false`. If absent, always visible |
588
- | `selectionMenu` | `true` — the button also appears in the Selection menu (`toolbar="bubble"`) |
588
+ | `selectionMenu` | `true` — the button also appears in the Bubble menu (`toolbar="bubble"`) |
589
589
  | `shortcut` | A shortcut: `mod`, `alt`, `shift` and a key — `["mod", "shift", "Y"]` |
590
590
  | `run` | `(editor) => void` — runs on a click or the shortcut |
591
591
  | `popover` | `({ editor, close }) => ReactNode` — a popover instead of an action. Exactly one of `run` and `popover` |
@@ -593,13 +593,13 @@ export const stampTool: CustomTool = {
593
593
  - **Placement.** Custom buttons appear on the Toolbar as one group before "Undo" and "Redo", after the kit's groups, in `tools` order and `buttons` array order. The fixed and the floating Toolbar show the same set. The kit sets the order of its own buttons; a built-in one can be hidden only by disabling its Tool, and to get a short Toolbar, narrow `tools`. A disabled Tool removes both its buttons and its shortcut.
594
594
  - **State.** `can`, `isActive` and `isVisible` are read on every transaction, like the kit's buttons, so they can use the Document's state: `editor.isActive("acme_pin")`. In a non-editable field (`readOnly`, `disabled`) the kit disables the buttons itself.
595
595
  - **Popover.** The kit manages the frame: the `Popover` layer, an anchor at the button, the Escape stack (inside a `Dialog`, Escape closes the popover, not the dialog), and the frame's name is the button's label. The content is mounted again each time the popover opens. `close` closes the popover the same way as Escape. Closing returns focus and selection to the Document, not to the button; a click elsewhere does not touch focus. A popover triggered by the shortcut opens at the caret, not at the button: the button may not be on screen at all (`toolbar="none"`).
596
- - **The shortcut is the single source of truth.** The kit itself binds the shortcut to `run` (or opens the popover), writes it into the tooltip with `Kbd`, into the button's `aria-keyshortcuts` and into the Shortcuts reference: custom rows appear there as a separate group without a heading after the kit's groups, in `tools` order. An unavailable button does not take the key, and the key passes on into the Document. Keyboard shortcuts bound by the Extensions (`addKeyboardShortcuts`) do not appear in the reference: set the button's shortcut with the `shortcut` field, not with an extension.
596
+ - **The shortcut is the single source of truth.** The kit itself binds the shortcut to `run` (or opens the popover), writes it into the tooltip with `Kbd`, into the button's `aria-keyshortcuts` and into the Keyboard shortcuts popover: custom rows appear there as a separate group without a heading after the kit's groups, in `tools` order. An unavailable button does not take the key, and the key passes on into the Document. Keyboard shortcuts bound by the Extensions (`addKeyboardShortcuts`) do not appear in the Keyboard shortcuts popover: set the button's shortcut with the `shortcut` field, not with an extension.
597
597
  - **Label.** The function receives `{ locale }` (the `UiProvider` locale), and the label re-renders when it changes; in a single-language app a string is enough. Label overrides go through the Tool factory's options (for example, a `labels` field; the recipe is [Extension API, "Labels through factory options"](../../api/editor-extension.md#подписи-через-опции-фабрики)); the `UiProvider` message catalog is not extended for the consumer's own Tools. If the label is empty, development mode logs a `console.error` with the Tool's and button's names, and the Tool's name is used in place of the label in the button's name, the tooltip and the popover's name.
598
598
  - **Menu buttons** (with radio items or a submenu) are not available to custom Tools: a popover is enough for choosing from a list.
599
599
 
600
600
  ## Insert items and Block actions
601
601
 
602
- A custom Tool adds an item to the Insert menu and an action to the Block handle menu **by description, without editing the menu**: the kit renders, the consumer describes (ADR-0058, sections 5 and 6). A disabled Tool removes its items and actions everywhere.
602
+ A custom Tool adds an item to the Slash menu and an action to the Block handle menu **by description, without editing the menu**: the kit renders, the consumer describes (ADR-0058, sections 5 and 6). A disabled Tool removes its items and actions everywhere.
603
603
 
604
604
  ```tsx
605
605
  import { ColumnLayoutIcon } from "@olddevs/ui/icons";
@@ -634,15 +634,15 @@ export const calloutTool: CustomTool = {
634
634
 
635
635
  **Insert item (`CustomInsertItem`).**
636
636
 
637
- | Field | What it is |
638
- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
639
- | `section` | `"insert"` ("Insert" — a new Block after the current one) or `"turnInto"` ("Turn into" — change the current Block, keeping its text). Required |
640
- | `label` | The label: a string or a function that receives `{ locale }` (the `UiProvider` locale), as for a button. Required |
641
- | `icon` | A component with the `IconProps` contract from `@olddevs/ui/icons` or your own. Required: there is no fallback icon |
642
- | `aliases` | An array of strings in any language — search words in addition to the label |
643
- | `shortcut` | A shortcut in the Toolbar's notation: `["mod", "alt", "N"]`. It is the only source of the key: the kit binds it and shows it in the item's row and in the Shortcuts reference |
644
- | `run` | `(chain, editor) => chain` — a chain command: return the chain with the Block's command; the kit calls `.run()` |
645
- | `popover` | `({ editor, close }) => ReactNode` — a popover at the caret instead of a command. Exactly one of `run` and `popover` is required |
637
+ | Field | What it is |
638
+ | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
639
+ | `section` | `"insert"` ("Insert" — a new Block after the current one) or `"turnInto"` ("Turn into" — change the current Block, keeping its text). Required |
640
+ | `label` | The label: a string or a function that receives `{ locale }` (the `UiProvider` locale), as for a button. Required |
641
+ | `icon` | A component with the `IconProps` contract from `@olddevs/ui/icons` or your own. Required: there is no fallback icon |
642
+ | `aliases` | An array of strings in any language — search words in addition to the label |
643
+ | `shortcut` | A shortcut in the Toolbar's notation: `["mod", "alt", "N"]`. It is the only source of the key: the kit binds it and shows it in the item's row and in the Keyboard shortcuts popover |
644
+ | `run` | `(chain, editor) => chain` — a chain command: return the chain with the Block's command; the kit calls `.run()` |
645
+ | `popover` | `({ editor, close }) => ReactNode` — a popover at the caret instead of a command. Exactly one of `run` and `popover` is required |
646
646
 
647
647
  - **Where the item appears.** It shows up in "/", the Toolbar's "+", and the Block handle's "Add ›" and "Format ›" automatically: the `turnInto` section goes into both "Format ›" and "¶"; "Add ›" holds the items of both sections (a new empty Block below, as with built-ins). Custom items appear **at the end of their section**, in `tools` order; inside a Tool, the array order. Without an enabled `insertMenu` the item does not appear, as with built-ins.
648
648
  - **One edit, one undo.** The section decides what happens to the Document, and `run` decides which Block: in `insert` the kit prepares an empty paragraph (the current one if empty, otherwise a new one after the Block), in `turnInto` it removes the list or quote around the Block. The preparation and `run` go in one transaction.
@@ -663,13 +663,13 @@ export const calloutTool: CustomTool = {
663
663
  | `isVisible` | `(editor, pos) => boolean` — a predicate that decides visibility from the context; if absent, the action is visible for all its `blocks` |
664
664
  | `shortcut` | A shortcut in the Toolbar's notation: it acts on the Block under the caret and only where the action is visible in the handle menu; on another Block the key goes to the Document |
665
665
 
666
- Custom actions appear in the Block handle menu **in their own section before "Delete"**, in `tools` order. An action with a `popover` opens the popover **at the Block** — the popover, Escape and focus return are the same as for a button, and the popover's accessible name is the action's label: a choice in the handle menu opens it at the handle's Block, a shortcut at the Block under the caret. `pos` follows Document edits while the popover is open. The shortcut is in the item's row (as with built-ins, and only when the shortcut does the same thing as the item) and in the Shortcuts reference: the Tool's rows are a separate group without a heading after the kit's groups.
666
+ Custom actions appear in the Block handle menu **in their own section before "Delete"**, in `tools` order. An action with a `popover` opens the popover **at the Block** — the popover, Escape and focus return are the same as for a button, and the popover's accessible name is the action's label: a choice in the handle menu opens it at the handle's Block, a shortcut at the Block under the caret. `pos` follows Document edits while the popover is open. The shortcut is in the item's row (as with built-ins, and only when the shortcut does the same thing as the item) and in the Keyboard shortcuts popover: the Tool's rows are a separate group without a heading after the kit's groups.
667
667
 
668
668
  **Tool set validation** adds field errors for UI contributions: `insertItems`/`blockActions` is not an array or an item is not an object; no label or icon; not exactly one of `run` and `popover`; the section is not `insert`/`turnInto`; `aliases` is not an array of strings; `blocks` is empty; `isVisible` is not a function; the shortcut is shorter than two keys (a modifier and a key).
669
669
 
670
670
  ## Custom Trigger character
671
671
 
672
- A custom Tool opens the same list as "/" with **its own character**: `@` for mentions, `#` for tags. The decision is ADR-0064. The kit renders the list — the same component as the Insert menu: a popover at the caret, search by what is typed, arrow keys, `Enter`, `Escape`, the "Nothing found" row; focus stays in the Document. The consumer describes the character, the items on demand and what happens when an item is picked.
672
+ A custom Tool opens the same list as "/" with **its own character**: `@` for mentions, `#` for tags. The decision is ADR-0064. The kit renders the list — the same component as the Slash menu: a popover at the caret, search by what is typed, arrow keys, `Enter`, `Escape`, the "Nothing found" row; focus stays in the Document. The consumer describes the character, the items on demand and what happens when an item is picked.
673
673
 
674
674
  ```tsx
675
675
  import { RichTextEditor, type CustomTool } from "@olddevs/ui/editor";
@@ -721,9 +721,9 @@ const mentionTool: CustomTool = {
721
721
  - **When it opens.** The same rules as "/": the character is at the start of a Block or after a space (a non-breaking one counts as a space), and the query ends at a space. Inside a word (`a@b.com`), in a code block and in inline code the list does not open. A space ends the query: the list closes and the text stays.
722
722
  - **Keyboard.** As with "/": `↓`/`↑` move through the items, wrapping around, `Enter` picks, `Escape` closes and keeps what was typed (it does not reopen on the same character), `Enter` on an empty list is an ordinary line break. An `Enter` that confirms IME composition is handled by the IME, not by the list.
723
723
  - **Asynchronous items.** While a promise is pending, the previous list (or nothing) stays, and the "Nothing found" row does not flicker. The result of a stale query is discarded. A rejected promise gives an empty list and, in development, a `console.error`.
724
- - **Independence from "/".** The trigger works without `insertMenu` in `tools`; only one list is open at a time, because the query begins with exactly one trigger character. It opens only while editing: in `readOnly`, `disabled` and HTML mode there is no list.
724
+ - **Independence from "/".** The trigger works without `insertMenu` in `tools`; only one list is open at a time, because the query begins with exactly one trigger character. It opens only while editing: in `readOnly`, `disabled` and Source mode there is no list.
725
725
  - **Accessibility.** The field stays `role="textbox"` and while the list is open gets `aria-controls` and `aria-activedescendant`, as with "/"; the list is `role="listbox"` named by `label`.
726
- - **Tool set errors** (the field shows an error, and the cause is reported with `console.error` in development): two Tools with one `char`; a `char` that is a space, `/`, or not one printable character; `items` or `onSelect` that are not functions; a `label` that is neither a string nor a function. A custom Trigger character does not collide with built-in ones: `/` is taken by the Insert menu.
726
+ - **Tool set errors** (the field shows an error, and the cause is reported with `console.error` in development): two Tools with one `char`; a `char` that is a space, `/`, or not one printable character; `items` or `onSelect` that are not functions; a `label` that is neither a string nor a function. A custom Trigger character does not collide with built-in ones: `/` is taken by the Slash menu.
727
727
  - **A mention is the consumer's node.** The kit does not know what is inserted: a mention Atom, a link, a tag — `onSelect` decides. The Atom is described like any custom Block (Extensions, Schema rules, `renderText` — Atom text), so a mention reaches `getText()`, Markdown and `maxLength`.
728
728
 
729
729
  ## Text card
@@ -769,7 +769,7 @@ export const commentTool: CustomTool = {
769
769
 
770
770
  **`markRange(markName)`** is a ready-made `rangeAt` for a Mark: the continuous stretch of text with this Mark and the same values of its attributes around `pos`. **The edges are included**: a caret right after a word or before it is still in the range (otherwise a click on the first letter would not open the Text card); between two stretches, the one that ends at `pos` wins, so text typed next continues that stretch. For a Block or any other range, write your own `rangeAt` — it reads only `state` and is called on every transaction, so it must be fast and free of side effects. The kit discards a range that lies outside the Document, has `from > to`, or consists of values that are not numbers; an exception in `rangeAt` does not crash the field, but writes `console.error` in development. `markRange` is also in the lightweight `@olddevs/ui/editor/schema` entry point: the description of a custom Tool with a Text card is data, and server sanitization reads it too, so the description module needs neither Tiptap nor React.
771
771
 
772
- - **When it is open.** The caret (a collapsed selection) is inside the range, or a click on it. **A Text card does not open on hover.** An expanded selection closes the Text card: the Selection menu works there. The range is recomputed on every transaction: the Text card follows the range when you edit, and closes when the range disappears. A field out of focus (except `readOnly`) does not show the Text card, and neither do `disabled` and HTML mode.
772
+ - **When it is open.** The caret (a collapsed selection) is inside the range, or a click on it. **A Text card does not open on hover.** An expanded selection closes the Text card: the Bubble menu works there. The range is recomputed on every transaction: the Text card follows the range when you edit, and closes when the range disappears. A field out of focus (except `readOnly`) does not show the Text card, and neither do `disabled` and Source mode.
773
773
  - **One Text card.** No more than one is open: **the narrowest range** under the caret wins, and with equal width, the contribution that comes earlier, that is, the Tool earlier in `tools` (inside a Tool, earlier in the array).
774
774
  - **The kit controls the card.** The layer and the Escape stack work as for the kit's `Popover` (ADR-0041: Escape closes the Text card, not the `Dialog` or `Drawer` around the field); the card is anchored under the range, **flips above the range at the bottom edge of the viewport**, shifts away from the side edges and looks like the kit's popover. There is no heading and no buttons defined by data: the body is your React node, mounted again each time the card opens.
775
775
  - **Non-modal.** Focus stays in the Document, you can keep typing, and the Text card follows the range. **`Alt+F10` moves focus into an open Text card** (to the body's first focusable element, otherwise to the card itself), as it moves focus to the panel of a selected image. **Escape returns the caret to its place** and hides the Text card.
@@ -781,7 +781,7 @@ export const commentTool: CustomTool = {
781
781
 
782
782
  ## Code block
783
783
 
784
- The `codeBlock` Tool: the "Code block" checkbox item of the "…" menu and an Insert menu item, `Ctrl/⌘+Alt+C` and a ` ``` ` fence at the start of a line. A Block has a language and syntax highlighting — the decision of ADR-0051: lowlight with highlight.js grammars, only in the `@olddevs/ui/editor` entry point (the kit's `CodeView` still does not highlight code, ADR-0039).
784
+ The `codeBlock` Tool: the "Code block" checkbox item of the "…" menu and a Slash menu item, `Ctrl/⌘+Alt+C` and a ` ``` ` fence at the start of a line. A Block has a language and syntax highlighting — the decision of ADR-0051: lowlight with highlight.js grammars, only in the `@olddevs/ui/editor` entry point (the kit's `CodeView` still does not highlight code, ADR-0039).
785
785
 
786
786
  **Languages.** Plain text (no language), Python, TypeScript, JavaScript, Go, HTML, JSON, YAML and Markdown — data in one module, `src/editor/features/code/codeLanguages.ts`, and the names used in the Document are `RICH_TEXT_SCHEMA.codeLanguages`. In HTML the language is a class on `<code>`: `<pre><code class="language-python">…</code></pre>`; a Block without a language has no class. Highlighting is **not written** into the Document: the `span class="hljs-…"` tokens are Editor decorations and are absent from `getHTML()`.
787
787
 
@@ -812,7 +812,7 @@ The language button has `aria-keyshortcuts="Alt+F10"`. **In `readOnly` and `disa
812
812
 
813
813
  ## Images
814
814
 
815
- The `image` Tool (in `full`, not in `basic`): the "Image" button on the Toolbar, the "Image" item in the Insert menu (`/img`) and Markdown `![alt](url)` on paste. An image is written into the Document as a Block:
815
+ The `image` Tool (in `full`, not in `basic`): the "Image" button on the Toolbar, the "Image" item in the Slash menu (`/img`) and Markdown `![alt](url)` on paste. An image is written into the Document as a Block:
816
816
 
817
817
  ```html
818
818
  <figure>
@@ -844,7 +844,7 @@ The `image` Tool (in `full`, not in `basic`): the "Image" button on the Toolbar,
844
844
  ```
845
845
 
846
846
  - **With an upload handler (`onUploadImage`)** an image can be pasted from the clipboard (a screenshot, "Copy image" in the browser), dropped into the field as a file, or chosen with the "Upload from device" button in the popover. Several files are uploaded one by one and land in file order.
847
- - **While the promise is pending**, a placeholder "Uploading photo.png…" appears where the image will be (with a preview where the browser supports `blob:`), and the Document has **no** image: not in `getHTML()`, not in `onChange`, not in the form. The upload progress is announced by the Field notice's live region (`role="status"`, see "Status line and Field notice").
847
+ - **While the promise is pending**, a placeholder "Uploading photo.png…" appears where the image will be (with a preview where the browser supports `blob:`), and the Document has **no** image: not in `getHTML()`, not in `onChange`, not in the form. The upload progress is announced by the Field notice's live region (`role="status"`, see "Status bar and Field notice").
848
848
  - **On success** the image takes the placeholder's place in one edit: `Ctrl/⌘+Z` removes exactly that image. **On failure** (the promise is rejected or the URL is outside `http`/`https`, for example `data:` or a relative one), the placeholder disappears, the Document does not change, nothing is added to the edit history, and under the field there is a `role="alert"` line: "Couldn't upload photo.png. The document is unchanged." The next edit clears it. This is a Field notice: file messages go through the same channel as `showNotice`, and a new one replaces the previous one.
849
849
  - **Without an upload handler**, files pasted from the clipboard or dropped into the field are not accepted: under the field there is a hint to insert the image by URL. "Copy image" from the browser still works — alongside the file the clipboard holds HTML with the image URL, and the image is inserted by URL if the protocol policy allows it (`http`/`https`).
850
850
  - **What is not uploaded.** A non-image file (`notes.pdf`) is rejected with a message. A paste from Word and from pages where the clipboard holds HTML with text next to the file goes through ordinary HTML parsing: Word also puts a snapshot of the selection as a picture on the clipboard, and uploading it would be a mistake.
@@ -955,7 +955,7 @@ The `table` Tool is part of `preset="full"`, not of `basic`. It needs the `@tipt
955
955
  | Sort › | unavailable | "Sort ascending", "Sort descending" — by the column under the caret (rules below). In a table with merged cells both items are unavailable, and the submenu's section label explains why |
956
956
  | Delete table | unavailable | Removes the whole table; the item is red (`danger`) and separated by a divider |
957
957
 
958
- A table inserted from the menu or the dialog lands as a new Block after the current one; in an empty paragraph, it takes the paragraph's place; from inside a table, it goes **after** that table: the Editor does not create nested tables. The "Table" item of the Insert menu (`/table`, `/grid`, "+") still inserts 3×3 immediately, without a size choice and without remembering it.
958
+ A table inserted from the menu or the dialog lands as a new Block after the current one; in an empty paragraph, it takes the paragraph's place; from inside a table, it goes **after** that table: the Editor does not create nested tables. The "Table" item of the Slash menu (`/table`, `/grid`, "+") still inserts 3×3 immediately, without a size choice and without remembering it.
959
959
 
960
960
  **Keyboard.** To open the menu, press `Alt+F10` (it moves focus into the Toolbar), use the arrow keys to reach "Table ▾", then press `Enter` or `↓`.
961
961
 
@@ -1003,18 +1003,18 @@ The grid follows the APG `grid` pattern: `role="grid"` → `row` → `gridcell`.
1003
1003
  **The output.** `<table><tbody><tr><th>…</th></tr><tr><td>…</td></tr></tbody></table>`, a cell is `<th><p>…</p></th>`. A cell has only `colspan`, `rowspan` and `data-align`; no `style`, `colwidth`, `class` or `scope`; `thead`, `colgroup` and `caption` are not written (the header row is an ordinary first row of `th`). The standard Tiptap extension writes `style="min-width: …"` on the table, a `colgroup` with column widths, `colwidth` and `style="text-align"` on cells — the kit turns this off (`src/editor/features/table/tables.ts`), and such attributes are dropped on load and paste; the alignment in them is kept as `data-align`.
1004
1004
 
1005
1005
  - **Column widths are not configurable.** There are no resize handles in v1 (the Tiptap option `resizable: false`): the browser computes the width from the content.
1006
- - **Rows with a different number of cells** (external HTML with an error or with a merge over the limit) are evened out with empty cells when the Editor starts — without `onChange`: this is not an Author edit.
1006
+ - **Rows with a different number of cells** (external HTML with an error or with a merge over the limit) are evened out with empty cells when the Editor starts — without `onChange`: this is not a User edit.
1007
1007
  - **A cell holds any Schema Blocks** (paragraphs, lists, a quote, a code block), not only paragraphs: if a cell accepted only one paragraph, inserting a list would cut the table in two.
1008
1008
  - **A disabled `table`** removes the button, the menu item and the extension: a table from a paste and from `value` arrives as the text of the cells, as paragraphs.
1009
1009
  - **Scrolling on a narrow screen.** A table wider than its block scrolls horizontally inside itself (`display: block; overflow-x: auto` on `<table>`, `prose.ts`) and does not stretch the page; the minimum column width is 6 rem. There is no wrapping element: `RichTextView` renders the saved HTML as is, and scrolling must work on a bare `<table>`. Details and the trade-off are on the [RichTextView](../content/rich-text-view.md#таблицы) page.
1010
1010
 
1011
1011
  Live examples are the stories `TableMenu`, `TableMerged`, `TableSortAndAlign`, `TableRussian`, `TableNarrow`, `TableInsertSlash` and, for the grid alone, `TableSizeGrid`, `TableSizeGridFields`, `TableSizeGridRussian`.
1012
1012
 
1013
- ## Insert menu
1013
+ ## Slash menu
1014
1014
 
1015
1015
  A list of Blocks that opens **on `/` in the text** and **on the "+" button**, the first button on the Toolbar (the `insertMenu` Tool, included in `full`). It has two sections of items:
1016
1016
 
1017
- - **"Insert"** — a new Block after the current one: code block, divider, table, image, Layout (two columns; the other variants are in the Block handle), Wrapper. An image opens the URL popover and lands in place of a prepared empty paragraph. If the caret is in an empty paragraph, the Block takes its place; otherwise it goes as a new Block right after the current top-level Block, not inside a quote or a list; with an image selected, after it. The current Block's text is not touched. The Block handle's "Add ›" inserts a Block the same way, except that it always adds a new Block below, even from an empty paragraph.
1017
+ - **"Insert"** — a new Block after the current one: code block, divider, table, image, Column layout (two columns; the other variants are in the Block handle), Wrapper. An image opens the URL popover and lands in place of a prepared empty paragraph. If the caret is in an empty paragraph, the Block takes its place; otherwise it goes as a new Block right after the current top-level Block, not inside a quote or a list; with an image selected, after it. The current Block's text is not touched. The Block handle's "Add ›" inserts a Block the same way, except that it always adds a new Block below, even from an empty paragraph.
1018
1018
  - **"Turn into"** — change the current Block's type, keeping the text: paragraph, H2–H4 (and H1 with `heading1`), bulleted, numbered and checklist, quote, code block. The Block first leaves its list or quote, then becomes the chosen type: "Turn a list item into a heading" gives a heading outside the list.
1019
1019
 
1020
1020
  An item has an icon, a label and a reminder: a Markdown shortcut (`##`, `-`, `>`, `---`), and where there is none, a keyboard shortcut. Items exist only for enabled Tools: a disabled `codeBlock` disappears from both sections, and a section without items is not shown. A menu without items other than the paragraph does not appear at all.
@@ -1033,23 +1033,23 @@ A custom Trigger character (`@`) opens a similar list, independently of "/" —
1033
1033
 
1034
1034
  "+" on the Toolbar is an ordinary menu button (the kit's `Menu`): it supports the arrow keys, `Enter`, `Escape` and the first letter of the label; an item applies to the Block under the caret or to the selection, after which focus returns to the Document.
1035
1035
 
1036
- ## Layout
1036
+ ## Column layout
1037
1037
 
1038
- The `layout` Tool (present in `full`, not in `basic`) is a Block of columns. It is inserted with the **"Layout"** item of the Insert menu (`/layout` or "+"): two equal columns, the caret in the first. Six variants — **Single column, Two columns, Three columns, Four columns, 60/40, 40/60** — are chosen in the Block handle menu: "Add › Layout ›" inserts a Layout under the Block (from a column, under the whole Layout), and the variant is switched with "Layout ›" in the handle of a Block in a column and with "Format ›" in the handle of a Layout selected as a whole (Backspace on the Block under it); in both, the current variant is checked. The Layout has no button of its own on the Toolbar.
1038
+ The `layout` Tool (present in `full`, not in `basic`) is a Block of columns. It is inserted with the **"Layout"** item of the Slash menu (`/layout` or "+"): two equal columns, the caret in the first. Six variants — **Single column, Two columns, Three columns, Four columns, 60/40, 40/60** — are chosen in the Block handle menu: "Add › Layout ›" inserts a Column layout under the Block (from a column, under the whole Column layout), and the variant is switched with "Layout ›" in the handle of a Block in a column and with "Format ›" in the handle of a Column layout selected as a whole (Backspace on the Block under it); in both, the current variant is checked. The Column layout has no button of its own on the Toolbar.
1039
1039
 
1040
- - **The Blocks of a column have their own handle**, at the left edge of the column at the level of the Block's first line (for the right column, in the gap between the columns). For them a column works like the Document: "Add ›" puts a new Block below in the same column, "Move up" and "Move down" do not leave it, "Delete" on the only Block leaves an empty column, "Format ›" changes the type of the Block itself. The Layout's variant is a separate "Layout ›" item: the caret is almost always in a column, and the handle of the whole Layout is rarely visible.
1040
+ - **The Blocks of a column have their own handle**, at the left edge of the column at the level of the Block's first line (for the right column, in the gap between the columns). For them a column works like the Document: "Add ›" puts a new Block below in the same column, "Move up" and "Move down" do not leave it, "Delete" on the only Block leaves an empty column, "Format ›" changes the type of the Block itself. The Column layout's variant is a separate "Layout ›" item: the caret is almost always in a column, and the handle of the whole Column layout is rarely visible.
1041
1041
 
1042
- - **A column is a separate area.** It holds any Blocks except another Layout: there is no nesting at the Schema level, so you cannot create a nested Layout by pasting or from `value`. A Layout is only a top-level Block: the "Layout" item from a column, list or quote puts a new Layout under the current top-level Block (from a column, under the whole Layout), and a Layout from the clipboard pasted into a column, list, quote or cell unwraps into its Blocks. The other "Insert" items from a column land under the current Block **in the same column**; moving (`Alt+Shift+↑/↓`) and duplicating a Block do not leave the column either.
1042
+ - **A column is a separate area.** It holds any Blocks except another Column layout: there is no nesting at the Schema level, so you cannot create a nested Column layout by pasting or from `value`. A Column layout is only a top-level Block: the "Layout" item from a column, list or quote puts a new Column layout under the current top-level Block (from a column, under the whole Column layout), and a Column layout from the clipboard pasted into a column, list, quote or cell unwraps into its Blocks. The other "Insert" items from a column land under the current Block **in the same column**; moving (`Alt+Shift+↑/↓`) and duplicating a Block do not leave the column either.
1043
1043
  - **In the Editor the columns are outlined with a gray dashed line**; in `RichTextView` there is no outline — the border is for the author, not the reader. The variant sets the column widths, and dragging does not change them.
1044
- - **Changing the variant does not lose text.** If there are fewer columns, the Blocks of the extra columns move to the end of the last remaining one (empty extra columns simply go away); if there are more, empty ones are added. Insertion and a variant change are one edit, undone with one `Ctrl/⌘+Z`. "Single column" is a Layout of one column; to remove the whole Layout, use "Delete" on its handle or `Backspace` on the Block under it (the first press selects the Layout, the second deletes it).
1045
- - **A narrow screen.** Columns sit side by side only when the Document itself is wide enough: two columns from 30 rem (480 px), three from 40 rem (640 px), four from 48 rem (768 px); at a smaller width they stack in reading order. The threshold is based on the Document's width (a container query), not the window's width, so in a narrow `Drawer` on a wide monitor the columns also stack. The root of a Document with a Layout becomes a container (`container-type: inline-size`); in a shrink-to-fit context (a flex-row item without a width), give the field or `RichTextView` a width.
1044
+ - **Changing the variant does not lose text.** If there are fewer columns, the Blocks of the extra columns move to the end of the last remaining one (empty extra columns simply go away); if there are more, empty ones are added. Insertion and a variant change are one edit, undone with one `Ctrl/⌘+Z`. "Single column" is a Column layout of one column; to remove the whole Column layout, use "Delete" on its handle or `Backspace` on the Block under it (the first press selects the Column layout, the second deletes it).
1045
+ - **A narrow screen.** Columns sit side by side only when the Document itself is wide enough: two columns from 30 rem (480 px), three from 40 rem (640 px), four from 48 rem (768 px); at a smaller width they stack in reading order. The threshold is based on the Document's width (a container query), not the window's width, so in a narrow `Drawer` on a wide monitor the columns also stack. The root of a Document with a Column layout becomes a container (`container-type: inline-size`); in a shrink-to-fit context (a flex-row item without a width), give the field or `RichTextView` a width.
1046
1046
 
1047
1047
  | Key in a column | What it does |
1048
1048
  | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1049
1049
  | `Tab` / `Shift+Tab` | To the start of the next / previous column. In a list, table and code block, Tab keeps its own behavior; from the outermost column it goes to the browser — focus leaves the Editor |
1050
- | `→` / `←` at the text edge | To the start of the neighboring column on the right / to the end of the one on the left; from the outermost one, out of the Layout |
1051
- | `↓` / `↑` at the bottom / top line | Out of the Layout — to the Block under it / above it; at the Document edge, a gap cursor (a caret between two Blocks where an ordinary caret cannot go) |
1052
- | `Backspace` at the start of a column | Columns do not merge and text does not leave the Layout; an empty heading, as everywhere, becomes text |
1050
+ | `→` / `←` at the text edge | To the start of the neighboring column on the right / to the end of the one on the left; from the outermost one, out of the Column layout |
1051
+ | `↓` / `↑` at the bottom / top line | Out of the Column layout — to the Block under it / above it; at the Document edge, a gap cursor (a caret between two Blocks where an ordinary caret cannot go) |
1052
+ | `Backspace` at the start of a column | Columns do not merge and text does not leave the Column layout; an empty heading, as everywhere, becomes text |
1053
1053
 
1054
1054
  **The markup** is a `div` with `data-block`; the widths are set by the variant in `data-layout`, with no `style` (the column shares in percent, joined by hyphens):
1055
1055
 
@@ -1060,20 +1060,20 @@ The `layout` Tool (present in `full`, not in `basic`) is a Block of columns. It
1060
1060
  </div>
1061
1061
  ```
1062
1062
 
1063
- The `data-layout` values are `100`, `50-50`, `33-33-33`, `25-25-25-25`, `60-40`, `40-60` (`RICH_TEXT_SCHEMA.layouts`). HTML from another editor with `div[data-block="column"]` columns and widths in `style="flex-basis: 40%"` is read as the Layout of the nearest variant, and `style` is dropped. If the number of columns disagrees with the variant, the Editor adjusts the variant to the number of columns. Without the `layout` Tool, a Layout from `value` and paste unwraps into Blocks one after another.
1063
+ The `data-layout` values are `100`, `50-50`, `33-33-33`, `25-25-25-25`, `60-40`, `40-60` (`RICH_TEXT_SCHEMA.layouts`). HTML from another editor with `div[data-block="column"]` columns and widths in `style="flex-basis: 40%"` is read as the Column layout of the nearest variant, and `style` is dropped. If the number of columns disagrees with the variant, the Editor adjusts the variant to the number of columns. Without the `layout` Tool, a Column layout from `value` and paste unwraps into Blocks one after another.
1064
1064
 
1065
- **Markdown does not support columns**: `getMarkdown()` exports the columns' Blocks one after another, left to right, and `setMarkdown()` does not create a Layout, even from raw HTML. The strings are `editor.layout` and `editor.layoutVariant` in the English and Russian message catalogs; the item's icon is `ColumnLayoutIcon`. The decision is ADR-0056.
1065
+ **Markdown does not support columns**: `getMarkdown()` exports the columns' Blocks one after another, left to right, and `setMarkdown()` does not create a Column layout, even from raw HTML. The strings are `editor.layout` and `editor.layoutVariant` in the English and Russian message catalogs; the item's icon is `ColumnLayoutIcon`. The decision is ADR-0056.
1066
1066
 
1067
1067
  ## Wrapper
1068
1068
 
1069
- The `wrapper` Tool (present in `full`, not in `basic`) is a Block that gathers a group of Blocks, like the Wrapper in Redactor. Without a tone the Wrapper has no look of its own: it keeps Blocks together — they can be moved, duplicated and deleted as one Block. With a tone it becomes a note or a warning, like alerts in GitHub and callouts in Notion (see the "Tone" subsection below). It is inserted with the **"Wrapper"** item of the Insert menu (`/wrapper` or "+") and with **"Add › Wrapper"** in the Block handle: an empty Wrapper with one paragraph lands under the current Block, and the caret goes into the paragraph inside. The Wrapper has no button of its own on the Toolbar.
1069
+ The `wrapper` Tool (present in `full`, not in `basic`) is a Block that gathers a group of Blocks, like the Wrapper in Redactor. Without a tone the Wrapper has no look of its own: it keeps Blocks together — they can be moved, duplicated and deleted as one Block. With a tone it becomes a note or a warning, like alerts in GitHub and callouts in Notion (see the "Tone" subsection below). It is inserted with the **"Wrapper"** item of the Slash menu (`/wrapper` or "+") and with **"Add › Wrapper"** in the Block handle: an empty Wrapper with one paragraph lands under the current Block, and the caret goes into the paragraph inside. The Wrapper has no button of its own on the Toolbar.
1070
1070
 
1071
- - **Inside — any Blocks:** a paragraph, a heading, lists, a quote, a code block, a table, an image, a divider, but not another Wrapper or a Layout. There is no nesting at the Schema level, so you cannot create a nested Wrapper or Layout by pasting or from `value`. A Wrapper pasted into a Wrapper, list, quote or cell unwraps into its Blocks, and a Layout inside a Wrapper unwraps into the columns' Blocks.
1072
- - **Placement.** A Wrapper can appear in the Document and in a Layout column. From a column, the "Wrapper" item puts it under the current Block in the same column; from a Wrapper, list, quote or cell, under their Block. Insertion is one edit, undone with one `Ctrl/⌘+Z`.
1071
+ - **Inside — any Blocks:** a paragraph, a heading, lists, a quote, a code block, a table, an image, a divider, but not another Wrapper or a Column layout. There is no nesting at the Schema level, so you cannot create a nested Wrapper or Column layout by pasting or from `value`. A Wrapper pasted into a Wrapper, list, quote or cell unwraps into its Blocks, and a Column layout inside a Wrapper unwraps into the columns' Blocks.
1072
+ - **Placement.** A Wrapper can appear in the Document and in a column of a Column layout. From a column, the "Wrapper" item puts it under the current Block in the same column; from a Wrapper, list, quote or cell, under their Block. Insertion is one edit, undone with one `Ctrl/⌘+Z`.
1073
1073
  - **The Blocks of a Wrapper have their own handle** at the left edge of the Wrapper. For them a Wrapper works like the Document: "Add ›" puts a new Block below in the same Wrapper, "Move up", "Move down" and `Alt+Shift+↑/↓` do not leave it, and "Delete" on the only Block leaves an empty paragraph.
1074
1074
  - **The Wrapper's own handle** appears when it is selected as a whole (`Backspace` on the Block under it): "Move up", "Move down", "Duplicate" and "Delete" act on it together with its content.
1075
1075
  - **"Unwrap"** is a Block action of the `wrapper` Tool: an item of the handle menu on the Wrapper itself and on a Block directly in it, in the Tools' actions section before "Delete". The Blocks take the Wrapper's place, and the caret stays in its text. One edit, one undo.
1076
- - **In the Editor a Wrapper without a tone is outlined with a gray dashed line** and inner padding — the same look as Layout columns; in `RichTextView` there is no outline, and the spacing between Blocks is as in the Document: the border is for the author, not the reader. A Wrapper with a tone is visible to the reader too — with the same look as in the Editor.
1076
+ - **In the Editor a Wrapper without a tone is outlined with a gray dashed line** and inner padding — the same look as the columns of a Column layout; in `RichTextView` there is no outline, and the spacing between Blocks is as in the Document: the border is for the author, not the reader. A Wrapper with a tone is visible to the reader too — with the same look as in the Editor.
1077
1077
 
1078
1078
  | Key in a Wrapper | What it does |
1079
1079
  | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -1112,7 +1112,7 @@ It is chosen in the **"Tone ›"** submenu of the handle menu — on a Block dir
1112
1112
 
1113
1113
  - **The names are the kit's `Tone` vocabulary**, not the menu words: `data-tone="warning"` goes to the database, and neither the menu label nor the interface language changes it. The theme substitutes the color, as with `data-color`: a saved Document reads in `dark`, `light`, `dim`, with any accent and brand. The values are the roles `--rte-tone-<tone>-background` and `--rte-tone-<tone>-border` (`src/editor/styles/tokens.ts`): in a theme they resolve to the kit's status tokens, on the Paper (`surface="paper"`) to the primitives of the light theme. The Schema does not support a custom tone color (`style`).
1114
1114
  - **"Important" is a neutral tone**, and it has two departures from `TONE_TOKENS.neutral`. The background is `bg-3`, not `bg-2`: the Editor's field itself sits on `bg-2`, and in the field "Important" would be left without a background; `bg-3` is the app background that the text tokens are calibrated for, and in weight it is closer to the soft backgrounds of the other tones in dark themes than `bg-4` is. In the light theme this background is almost white — the stripe carries the tone there. The stripe is `fg-3`, not the decorative `line`: it carries meaning and holds 3:1.
1115
- - **Contrast** (`npm run contrast`, the color pairs for Wrapper tones): the Document's text on each background is at least 4.5:1 (the worst is `warning`, 7.15 in the dark theme on `surface-elevated`; on the Paper 13.3), the stripe is at least 3:1 both against the app background around the Wrapper (the worst is 4.51 for "Note" on the Paper) and against its own fill (the worst is 3.68, "Note" on the Paper; in the app theme 4.60). This holds in all themes, accents and brands, and on the Paper.
1115
+ - **Contrast** (`npm run contrast`, the color pairs for Callout tones): the Document's text on each background is at least 4.5:1 (the worst is `warning`, 7.15 in the dark theme on `surface-elevated`; on the Paper 13.3), the stripe is at least 3:1 both against the app background around the Wrapper (the worst is 4.51 for "Note" on the Paper) and against its own fill (the worst is 3.68, "Note" on the Paper; in the app theme 4.60). This holds in all themes, accents and brands, and on the Paper.
1116
1116
  - **Secondary text in a Wrapper with a tone uses higher-contrast roles.** The `fg-1`–`fg-3` and `accent-strong` steps are calibrated for app backgrounds, and on the colored tone backgrounds the contrast of a quote would be 4.23:1, of a link 3.97, of a caption and gray text 3.59. So inside a Wrapper with a tone a link, a quote (and the language label in the code block header), an image caption, a done checklist item, gray text color (`data-color="muted"`) and the empty caption placeholder use the roles `--rte-tone-link`, `-secondary`, `-muted`, `-muted-foreground`:
1117
1117
 
1118
1118
  | What | Role outside a tone | In the app theme | On the Paper (`paper`) |
@@ -1124,9 +1124,9 @@ It is chosen in the **"Tone ›"** submenu of the handle menu — on a Block dir
1124
1124
 
1125
1125
  In the app theme, on all five backgrounds only `fg-0` holds 4.5:1 (`fg-1` gives 4.23 on "Note"), so inside a tone secondary text uses the main text's colors: a quote is still told apart by the stripe, a done item by strikethrough, a caption by font size, a link by underline (a Document link always has it). On the Paper, `cloud-2` and `cloud-3` pass on the tone backgrounds too and stay; the link (the accent gave 3.68) and the placeholder are moved to higher-contrast roles. The worst pair is 7.15:1 in a theme (`warning`, dark theme), 5.30:1 on the Paper (caption and placeholder on "Note"). Outside a Wrapper and in a Wrapper without a tone, the roles do not change. The gate does not measure the author's text color — palette and custom color — on a tone background: that is the author's choice.
1126
1126
 
1127
- - **An unknown tone is dropped** — both on parsing (`value`, paste, HTML mode) and in `sanitizeRichText`: `data-tone="purple"`, `"Accent"` and `data-tone` on any other tag are removed, and the Wrapper stays without a tone. A Wrapper in a Wrapper, list, quote or cell is unwrapped into its Blocks, and its tone goes away. If unsanitized HTML with an unrecognized tone reaches `RichTextView`, it renders the Wrapper without any tone styling.
1127
+ - **An unknown tone is dropped** — both on parsing (`value`, paste, Source mode) and in `sanitizeRichText`: `data-tone="purple"`, `"Accent"` and `data-tone` on any other tag are removed, and the Wrapper stays without a tone. A Wrapper in a Wrapper, list, quote or cell is unwrapped into its Blocks, and its tone goes away. If unsanitized HTML with an unrecognized tone reaches `RichTextView`, it renders the Wrapper without any tone styling.
1128
1128
  - **A tone is styling.** The saved HTML has no ARIA roles (`note`, `alert`) and no `aria-*` attributes: the screen reader reads the Wrapper's Blocks like the Blocks around it. So the meaning of a note is stated **in words** in its text — "Warning: …", "Tip: …": the background color must not be the only way the meaning is conveyed (WCAG 1.4.1).
1129
- - **HTML mode** shows `data-tone` on a Wrapper as is; the field text passes through the Schema, like `value`.
1129
+ - **Source mode** shows `data-tone` on a Wrapper as is; the field text passes through the Schema, like `value`.
1130
1130
 
1131
1131
  **Markdown — GitHub alerts.** `getMarkdown()` exports a Wrapper with a tone as a quote whose first line holds the alert keyword (`[!NOTE]`), followed by its Blocks inside the quote:
1132
1132
 
@@ -1144,9 +1144,9 @@ The strings are `editor.wrapper` ("Wrapper"), `editor.unwrap` ("Unwrap"), `edito
1144
1144
 
1145
1145
  ## Collapsible block
1146
1146
 
1147
- The `details` Tool (present in `full`, not in `basic`) is a Block made of a title and content that the reader opens and closes: details under a heading, like a spoiler on GitHub or a toggle in Notion. The engine is the Details extension from Tiptap, and the API follows it (ADR-0048). It is inserted with the **"Collapsible block"** item of the Insert menu (`/collapsible`, `/details` or "+") and with **"Add › Collapsible block"** in the Block handle: an empty open Block with the caret in the title. Insertion is one edit, undone with one `Ctrl/⌘+Z`.
1147
+ The `details` Tool (present in `full`, not in `basic`) is a Block made of a title and content that the reader opens and closes: details under a heading, like a spoiler on GitHub or a toggle in Notion. The engine is the Details extension from Tiptap, and the API follows it (ADR-0048). It is inserted with the **"Collapsible block"** item of the Slash menu (`/collapsible`, `/details` or "+") and with **"Add › Collapsible block"** in the Block handle: an empty open Block with the caret in the title. Insertion is one edit, undone with one `Ctrl/⌘+Z`.
1148
1148
 
1149
- - **Title and content.** The title (`summary`) is one line of text with Marks, with no line breaks or Blocks. The content is any Blocks: a paragraph, a heading, lists, a quote, a code block, a table, an image, a divider, another collapsible Block. There are no Layouts or Wrappers inside: there is no nesting at the Schema level, and a Layout or a Wrapper from `value` or paste unwraps into its Blocks. The Block itself stands wherever any Block can: in the Document, a Layout column, a Wrapper, a quote, a cell and the content of another Block. The Insert menu puts it under the handle's Block: from a paragraph, under it; from a list or a quote, under the whole list or quote; from a Wrapper, a column or the content of another Block, inside them.
1149
+ - **Title and content.** The title (`summary`) is one line of text with Marks, with no line breaks or Blocks. The content is any Blocks: a paragraph, a heading, lists, a quote, a code block, a table, an image, a divider, another collapsible Block. There are no Column layouts or Wrappers inside: there is no nesting at the Schema level, and a Column layout or a Wrapper from `value` or paste unwraps into its Blocks. The Block itself stands wherever any Block can: in the Document, a column of a Column layout, a Wrapper, a quote, a cell and the content of another Block. The Slash menu puts it under the handle's Block: from a paragraph, under it; from a list or a quote, under the whole list or quote; from a Wrapper, a column or the content of another Block, inside them.
1150
1150
  - **Opening.** In the Editor, to the left of the title there is a button with a chevron: a real `button` named from the catalog ("Details: {title}") with `aria-expanded`. It is in the Tab order (from the Document, Tab leads to the button of the nearest Block below the caret), Enter and Space open and close the Block, and focus stays on the button. Collapsed content is hidden from the caret as well: the arrow keys do not take it inside. In `readOnly` the Block still opens and closes.
1151
1151
  - **The `persist` option.** `tool("details", { persist: true })`, `false` by default (as in Tiptap). Without it the open state lives only in the display: opening does not reach the Document, the undo history or `onChange`, `open` is not written to the HTML and is not read from `value`, and a Block opened during editing is not published open without anyone asking. With `persist: true` an open Block writes `open=""`, `open` is read from `value`, paste and `setMarkdown()`, and opening or closing is an edit of the Document (`onChange`, undo). A Block inserted from the menu is open with any `persist`. The options are read once, when the engine is created; an invalid value (not a boolean) is replaced by the default with a `console.error` in development.
1152
1152
  - **The Block handle works like a Wrapper's.** The handle at the title is the handle of the whole Block: "Move up", "Move down", "Duplicate" and "Delete" take it with its content. The Blocks in the content have their own handle at the left edge of the content: "Add ›" puts a new Block below in the content, "Move up", "Move down" and `Alt+Shift+↑/↓` do not leave it. There is no "Tone ›" for the Block. **"Remove collapsing"** is a Block action of the `details` Tool: an item on the Block itself and on a Block directly in its content, in the Tools' actions section before "Delete". The title becomes a paragraph, the content Blocks follow it, and the caret stays in its text. One edit, one undo.
@@ -1186,7 +1186,7 @@ Run the gates, then the build.
1186
1186
  </details>
1187
1187
  ```
1188
1188
 
1189
- The blank line after `summary` makes GitHub read the content as Markdown, and the Block collapses there. The title is inline Markdown on one line: GitHub will show its asterisks as they are, but the `getMarkdown()` → `setMarkdown()` round trip loses nothing; `<` in a title is written as `&lt;`. With `persist` an open Block is written as `<details open>`. `setMarkdown()` reads such a Block back — with nested Blocks, lists and code, where `</details>` inside a fence is not taken for the closing tag; `<details open>` without `persist` gives a closed Block. A tag with no matching `</details>` stays text. This differs from a Layout and a Wrapper without a tone, which unwrap in Markdown: a collapsible Block has a common form in Markdown, and columns do not. Without the `details` Tool the same lines unwrap into Blocks in a row (an empty paragraph may remain after the closing tag).
1189
+ The blank line after `summary` makes GitHub read the content as Markdown, and the Block collapses there. The title is inline Markdown on one line: GitHub will show its asterisks as they are, but the `getMarkdown()` → `setMarkdown()` round trip loses nothing; `<` in a title is written as `&lt;`. With `persist` an open Block is written as `<details open>`. `setMarkdown()` reads such a Block back — with nested Blocks, lists and code, where `</details>` inside a fence is not taken for the closing tag; `<details open>` without `persist` gives a closed Block. A tag with no matching `</details>` stays text. This differs from a Column layout and a Wrapper without a tone, which unwrap in Markdown: a collapsible Block has a common form in Markdown, and columns do not. Without the `details` Tool the same lines unwrap into Blocks in a row (an empty paragraph may remain after the closing tag).
1190
1190
 
1191
1191
  The strings are `editor.details` ("Collapsible block"), `editor.unwrapDetails` ("Remove collapsing") and `editor.detailsToggle` ("Details: {title}", "Details: no title" for a Block without a title) in the message catalog (English and Russian); the icons are `DetailsIcon` for the insert item and `UnwrapIcon` for "Remove collapsing". The decision is ADR-0048 ("for the future": there was no customer at release).
1192
1192
 
@@ -1213,16 +1213,16 @@ The `emoji` Tool (present in `full`, not in `basic`) is the "Emoji" button on th
1213
1213
 
1214
1214
  Inside `Dialog` and `Drawer`, you can choose an emoji with both the mouse and the keyboard, and Escape closes only the popover: Radix layers share one stack (ADR-0041). The strings are `editor.emoji` (the button's name), `editor.emojiGrid` (the grid's name), `editor.emojiCategory` (category labels) and `editor.emojiName` (the name of each emoji) — in the message catalog, in English and Russian.
1215
1215
 
1216
- ## HTML mode
1216
+ ## Source mode
1217
1217
 
1218
- The `source` Tool (present in `full`, not in `basic`) is the "HTML" toggle button on the **fixed** Toolbar, in the "Whole Block" group right after "+", as in Redactor. It replaces the Document with a monospace field holding the current Document's HTML; pressing it again switches back to the visual mode. The mode has no keyboard shortcut; the Shortcuts reference is available in the mode.
1218
+ The `source` Tool (present in `full`, not in `basic`) is the "HTML" toggle button on the **fixed** Toolbar, in the "Whole Block" group right after "+", as in Redactor. It replaces the Document with a monospace field holding the current Document's HTML; pressing it again switches back to the visual mode. The mode has no keyboard shortcut; the Keyboard shortcuts popover is available in the mode.
1219
1219
 
1220
1220
  ```tsx
1221
1221
  <RichTextEditor preset="full" value={html} onChange={setHtml} /> // the "HTML" button is already on the Toolbar
1222
1222
  <RichTextEditor tools={["bold", "italic", "link", "history", "source"]} /> // or explicitly in tools
1223
1223
  ```
1224
1224
 
1225
- **What is visible.** The HTML is formatted: a top-level Block on its own line, the children of Block containers (list items, table rows and cells, Layout columns, parts of a `figure`, a checklist item) indented by two spaces, a Block's content on its tag's line (text whitespace is unchanged), the content of `pre` as is. The formatting is lossless: applying unchanged text produces the same Document. An empty Document opens as an empty field, not as `<p></p>`. There is no syntax highlighting.
1225
+ **What is visible.** The HTML is formatted: a top-level Block on its own line, the children of Block containers (list items, table rows and cells, the columns of a Column layout, parts of a `figure`, a checklist item) indented by two spaces, a Block's content on its tag's line (text whitespace is unchanged), the content of `pre` as is. The formatting is lossless: applying unchanged text produces the same Document. An empty Document opens as an empty field, not as `<p></p>`. There is no syntax highlighting.
1226
1226
 
1227
1227
  **Look.** The field is **always dark, using the kit's colors**, not the code block's syntax highlighting palette: in `dark` and `dim`, the theme's deepest background with its text and border; in `light` and on the Paper (`paper`), the same values of the dark theme (the variables `--rte-source-background`, `--rte-source-foreground`, `--rte-source-border`, which can be overridden). On the Paper (`paper`), the light sheet stays under the dark field: the sheet's contents change, not the sheet itself. The corner radius is as for code, the font is monospace 13 px, as in `CodeView`; the padding is as for the Document area. On entry the height is at least the Document's height, then the field grows with its content and can be stretched vertically by hand. With `maxHeight` or `resizable` the field lives in the same scroll area as the Document (see the "Field height" section). Text selection in the field also comes from the dark theme for the current accent (`--selection-dark`), with the field's light text: the light theme's light selection was unreadable on the dark field.
1228
1228
 
@@ -1239,19 +1239,19 @@ If the text did not change since entry or the last time it was applied, or parsi
1239
1239
 
1240
1240
  The `required` check in the mode is evaluated against **the field's text** on every change, not the applied Document: the browser validates the form before the `submit` event, so `form.requestSubmit()` with focus in the HTML field lets through text typed into the field while the Document is empty, and blocks the form when the field has been cleared but the Document is not empty. The text is parsed by the same Schema as on applying, and it is empty exactly when the Document would be empty: `<p></p>` or only tags the Schema drops block submission, `<hr>` does not. Without `required` the text is not parsed while typing.
1241
1241
 
1242
- **External replacements in the mode.** A new `value` (not equal to the last one emitted), `setMarkdown()` and `clear()` replace the Document, and the field's text is overwritten with the formatting of the new Document: the screen that holds `value` outranks an unsaved edit. The field is not reset when the `value` it emitted comes back as a prop.
1242
+ **Programmatic replacements in the mode.** A new `value` (not equal to the last one emitted), `setMarkdown()` and `clear()` replace the Document, and the field's text is overwritten with the formatting of the new Document: the screen that holds `value` outranks an unsaved edit. The field is not reset when the `value` it emitted comes back as a prop.
1243
1243
 
1244
1244
  - **`maxLength`** does not truncate the text when it is applied — like `setMarkdown`: the counter shows the excess. In the mode the counter reflects the last applied state.
1245
1245
  - **`disabled`** and `<fieldset disabled>` disable the HTML field and the button; the mode stays open.
1246
- - **What is off in the mode.** The other Toolbar buttons and menus are `aria-disabled` (they stay in the arrow-key order but do not fire); there is no Block handle, Selection menu or `/` Insert menu. The engine stays mounted and hidden: the edit history is preserved.
1247
- - **Where the button is absent.** The button is absent from the floating Toolbar and the Selection menu (`toolbar="floating"`, `"bubble"`): they appear above a selection in the Document, and in this mode there is no Document; with `toolbar="none"` and in `readOnly` there is no Toolbar. On a touch screen the Toolbar is always fixed — so the mode exists there.
1246
+ - **What is off in the mode.** The other Toolbar buttons and menus are `aria-disabled` (they stay in the arrow-key order but do not fire); there is no Block handle, Bubble menu or `/` Slash menu. The engine stays mounted and hidden: the edit history is preserved.
1247
+ - **Where the button is absent.** The button is absent from the floating Toolbar and the Bubble menu (`toolbar="floating"`, `"bubble"`): they appear above a selection in the Document, and in this mode there is no Document; with `toolbar="none"` and in `readOnly` there is no Toolbar. On a touch screen the Toolbar is always fixed — so the mode exists there.
1248
1248
  - **SSR.** The mode is internal state, not a prop; it always starts visual, so the server output is the same as without the `source` Tool.
1249
1249
 
1250
1250
  `RichTextView`, the Schema, the `sanitizeRichText` sanitizer and the allowlist are not changed by the mode: it does not widen the Schema: HTML from the field goes through it the same way as an incoming `value`. The strings are `editor.source` (the button's name, "HTML") and `editor.sourceField` (the field's name, "HTML source" in English, "HTML-код документа" in Russian) in the message catalog, in English and Russian; the icon is `SourceIcon`.
1251
1251
 
1252
1252
  ## Find and replace
1253
1253
 
1254
- 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.
1254
+ 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 Keyboard shortcuts popover; `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.
1255
1255
 
1256
1256
  ```tsx
1257
1257
  <RichTextEditor preset="basic" /> // the "Find" button and Ctrl/⌘+F are already there
@@ -1268,7 +1268,7 @@ The `search` Tool (present in `full` and in `basic`) is find and replace over th
1268
1268
  - **Accessibility.** The panel is a `role="search"` region named "Find and replace"; the counter is announced through a live region with `aria-live="polite"` ("3 of 12", "No matches"), the fields have labels, and the switch is `role="switch"`.
1269
1269
  - **Weight.** The match highlighting plugin and the shortcuts are in the engine from the start; the find panel (fields, checkboxes, switch) is a lazy chunk that loads the first time the panel opens and does not delay Readiness.
1270
1270
 
1271
- 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`.
1271
+ Message catalog strings: `editor.search` (the button and the `Ctrl/⌘+F` Keyboard shortcuts popover row), `searchReplace` (`Ctrl/⌘+Alt+F`), `searchPanel`, `searchField`, `replaceField`, `searchPrevious`, `searchNext`, `searchClose`, `searchMatchCase`, `searchWholeWord`, `searchRegex`, `replaceOne`, `replaceAll`, `searchCount({ current, total })`, `searchNoMatches`, `searchInvalid`.
1272
1272
 
1273
1273
  ### Find options
1274
1274
 
@@ -1300,18 +1300,18 @@ The state is read with `useRichTextToolState(ref, "search")`, and the commands a
1300
1300
 
1301
1301
  The state has no match positions.
1302
1302
 
1303
- | Command | What it does |
1304
- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1305
- | `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. |
1306
- | `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. |
1307
- | `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. |
1308
- | `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. |
1309
- | `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. |
1310
- | `replaceAll(text)` | Replaces all matches: one edit, one undo. The conditions are the same as for `replace`. |
1303
+ | Command | What it does |
1304
+ | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1305
+ | `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 Source mode. |
1306
+ | `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. |
1307
+ | `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 Source mode. |
1308
+ | `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. |
1309
+ | `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. |
1310
+ | `replaceAll(text)` | Replaces all matches: one edit, one undo. The conditions are the same as for `replace`. |
1311
1311
 
1312
1312
  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.
1313
1313
 
1314
- **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.
1314
+ **A counter in the Status bar.** 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.
1315
1315
 
1316
1316
  ```tsx
1317
1317
  import { useRef } from "react";
@@ -1352,7 +1352,7 @@ A `tool("toc", options)` entry also switches the Tool on and configures it at th
1352
1352
  - **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.
1353
1353
  - **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.
1354
1354
  - **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.
1355
- - **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.
1355
+ - **A live Document.** The list is rebuilt shortly after each edit: a new, renamed or removed heading shows in the strip. In Source mode there is no Table of contents, because the Document is hidden.
1356
1356
  - **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).
1357
1357
 
1358
1358
  Message catalog string: `editor.toc` ("Table of contents"), the name of the strip and the label of the list.
@@ -1386,7 +1386,7 @@ The state is read with `useRichTextToolState(ref, "toc")`, and the command is ca
1386
1386
 
1387
1387
  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.
1388
1388
 
1389
- 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.
1389
+ 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 Source 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.
1390
1390
 
1391
1391
  **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.
1392
1392
 
@@ -1421,11 +1421,11 @@ function SectionNav({ target }: { target: RefObject<RichTextEditorHandle | null>
1421
1421
  }
1422
1422
  ```
1423
1423
 
1424
- ## Status line and Field notice
1424
+ ## Status bar and Field notice
1425
1425
 
1426
- 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.
1426
+ Under the field inside the frame there are two independent areas. The **Status bar** 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.
1427
1427
 
1428
- ### Status line: `status`
1428
+ ### Status bar: `status`
1429
1429
 
1430
1430
  ```tsx
1431
1431
  <RichTextEditor
@@ -1445,9 +1445,9 @@ Under the field inside the frame there are two independent areas. The **Status l
1445
1445
  - **The container has no `role` and no `aria-live`: the kit does not announce the line.** A word count changes on every edit, and reading it aloud would mean talking over typing. If you need "Saved" to be announced, wrap only that in `role="status"` (the example above; with autosave on `onBlur`, see [the recipe on the extension API page](../../api/editor-extension.md#сохранено-в-строке-статуса)). Do not wrap the whole line in `role="status"`.
1446
1446
  - It is visible in `readOnly` and in `disabled` alike: it is part of the field's frame, not of editing. With `disabled` the frame is muted as a whole, like other fields, together with the line.
1447
1447
  - The counter keeps its `id` and stays in the field's `aria-describedby` while visible; the screen's content is not part of the field's description.
1448
- - Take the data from your own state: the word count from `value`/`onChange`, "Saved" from the server's response. Tools have no access to the Status line: it has no owning Tool.
1448
+ - Take the data from your own state: the word count from `value`/`onChange`, "Saved" from the server's response. Tools have no access to the Status bar: it has no owning Tool.
1449
1449
  - The default color is the second text step; the size is 11 px, with monospace digits. Set your own color on your own node: the third text step does not reach a 4.5:1 contrast ratio on the field's background.
1450
- - `RichTextView` has no Status line: it shows sanitized HTML without an engine.
1450
+ - `RichTextView` has no Status bar: it shows sanitized HTML without an engine.
1451
1451
 
1452
1452
  ### Field notice: `showNotice`
1453
1453
 
@@ -1461,8 +1461,8 @@ ref.current?.editor?.commands.showNotice({ tone: "error", text: "Couldn't insert
1461
1461
  ref.current?.editor?.commands.showNotice(null);
1462
1462
  ```
1463
1463
 
1464
- - **`status`** is invisible and only announced: the text goes to a `role="status"` live region outside the Document. Visually, the upload progress is shown by a placeholder in the Document, and live regions inside `contenteditable` are not read everywhere. It stays until it is replaced or cleared; an Author edit does not touch it.
1465
- - **`error`** is visible as a `role="alert"` line under the field, above the Status line, **until the next Author edit**, a new notice or `showNotice(null)`. An edit is any change to the Document; an external `value` replacement and `clear()` do not count as an edit; `setMarkdown()` and `setJSON()` clear the error. Show an error when the Document did not change as a result: without the line it would look like "nothing happened".
1464
+ - **`status`** is invisible and only announced: the text goes to a `role="status"` live region outside the Document. Visually, the upload progress is shown by a placeholder in the Document, and live regions inside `contenteditable` are not read everywhere. It stays until it is replaced or cleared; a User edit does not touch it.
1465
+ - **`error`** is visible as a `role="alert"` line under the field, above the Status bar, **until the next User edit**, a new notice or `showNotice(null)`. An edit is any change to the Document; a programmatic `value` replacement and `clear()` do not count as an edit; `setMarkdown()` and `setJSON()` clear the error. Show an error when the Document did not change as a result: without the line it would look like "nothing happened".
1466
1466
  - The notice has no durations, close buttons or spinners of its own: it goes away on its own only after an edit; otherwise it is cleared by a command.
1467
1467
  - **The command does not change the Document**: no transaction, no history step, no `onChange`. `editor.can().showNotice(…)` does not show a notice.
1468
1468
  - A notice without text (`text: ""`) is the same as `null`. The kit does not translate the text: it is shown as is, without markup. The kit's own messages (files) go through the `editor.*` message catalog, and the strings of custom Tools come from their own message catalog.
@@ -1479,10 +1479,10 @@ By default the Editor grows with the Document, and a long text stretches the pag
1479
1479
  ```
1480
1480
 
1481
1481
  - **Units.** A number is pixels, a string is any CSS length (`"24rem"`, `"60vh"`). There is no row-count option like `maxRows` in `TextControl`: Blocks have different heights.
1482
- - **What scrolls.** Only the Document area: the Document, its Skeleton, the HTML mode field and the Block handle. The fixed Toolbar above it and the Status line with the Field notice under it stay in place, the frame does not move. The scrollbar sits at the frame.
1482
+ - **What scrolls.** Only the Document area: the Document, its Skeleton, the Source mode field and the Block handle. The fixed Toolbar above it and the Status bar with the Field notice under it stay in place, the frame does not move. The scrollbar sits at the frame.
1483
1483
  - **`resizable` with `maxHeight`** — `maxHeight` becomes the **starting height**, not a limit: the field stretches both taller and shorter. Two systems that control height at the same time produce a field that jumps on every edit. The same reasoning applies to `autosize` in `TextControl`. Without `maxHeight` the start is the Document's height. The area does not shrink below 128 px (a few lines).
1484
1484
  - **The height handle is the kit's own, not the browser's corner.** The browser renders the native `resize: vertical` as its own corner, which ignores the theme: in a dark one it is a white square on the frame. The kit's handle is a strip at the area's bottom edge with a grip in the middle, styled like the `SplitPanel` strip: the grip is `--line`, on hover `--fg-2`, while dragging and on focus the accent. The visible strip is 10 px, the hit zone 26 px (WCAG 2.5.8). For keyboard users there is a slider "Resize the editor" (`editor.resizeField`, the value is `editor.fieldHeight`): the Up and Down arrow keys move it by 16 px, with Shift by a pixel; dragging with the mouse moves focus to it, so you can fine-tune the height with the arrow keys. A double click on the strip returns the starting height. A height set by dragging persists until the field unmounts: saving it between visits is the screen's job.
1485
- - **The Block handle and popup surfaces travel with the text.** The "⋮" handle sits inside the area and scrolls together with the Block; beyond the area's edge it is clipped and does not overlap the Toolbar. The floating Toolbar, the Selection menu and the image panel listen to the area's scroll, not the page's; at the area's edge they flip below the selection. The Text card and the "/" Insert menu follow the range and the caret the same way: their anchor is tied to the Document (`contextElement`).
1485
+ - **The Block handle and popup surfaces travel with the text.** The "⋮" handle sits inside the area and scrolls together with the Block; beyond the area's edge it is clipped and does not overlap the Toolbar. The floating Toolbar, the Bubble menu and the image panel listen to the area's scroll, not the page's; at the area's edge they flip below the selection. The Text card and the "/" Slash menu follow the range and the caret the same way: their anchor is tied to the Document (`contextElement`).
1486
1486
  - **Without the props nothing changes:** the area is a plain element without styles, the field grows with the Document, and the Toolbar sticks to the top of the browser window.
1487
1487
 
1488
1488
  ## Paper: `surface`
@@ -1495,14 +1495,14 @@ By default the Editor grows with the Document, and a long text stretches the pag
1495
1495
 
1496
1496
  **When to use the Paper.** A Document that will leave the app with a light look: a print or PDF preparation screen, an email editor, a document template. The author sees the text as it will be read, and the author's color does not lose contrast when the theme changes. For notes, comments and task descriptions the Paper is not needed: the Document lives in the app and follows its theme.
1497
1497
 
1498
- **What stays in the app theme.** The Paper is only the editable area. The Toolbar (fixed and floating), the Insert menu, the link, image and color popovers, the table menu, dialogs, the labels and counter under the field, the field's frame and the focus ring around it are in the app theme: this is the interface, not the Document. The Paper's edge is the app theme's `--line` frame and `--shadow-1` shadow, so the Paper stands apart on both a dark and a light background.
1498
+ **What stays in the app theme.** The Paper is only the editable area. The Toolbar (fixed and floating), the Slash menu, the link, image and color popovers, the table menu, dialogs, the labels and counter under the field, the field's frame and the focus ring around it are in the app theme: this is the interface, not the Document. The Paper's edge is the app theme's `--line` frame and `--shadow-1` shadow, so the Paper stands apart on both a dark and a light background.
1499
1499
 
1500
1500
  **How it is built.** The Paper carries a nested `data-theme="light"` — through it the highlights (`--marker-*`) and the syntax highlighting theme take light values. The kit tokens (`--fg-0`, surfaces, `--accent-strong`) are declared only on `:root[data-theme]` and do not understand a nested theme, so the Document reads not them but the `--rte-*` roles (`src/editor/styles/tokens.ts`): in a theme they map to kit tokens, on the Paper to the same `--od-*` primitives that the light theme assigns. A test checks the match with the light theme by parsing `styles.css`.
1501
1501
 
1502
1502
  - **The accent on the Paper is the app theme's fill** (`--accent`), not the light `accent-strong`: for a brand and the violet accent the light step exists only in a light app. The fill is designed for white text on top of it, so used as text on the white Paper it meets 4.5:1 with any brand. In a light app the link, the `accent` color and focus on the Paper use exactly the light `accent-strong`.
1503
1503
  - **Focus inside the Paper** (the code block language button, checklist checkboxes) is an outline of the same accent: the dark theme's light `accent-strong` does not reach 3:1 contrast on white.
1504
1504
  - **The code block header on the Paper** is light: the background and text come from the syntax highlighting theme selected by the nested `data-theme`, and the border and line are the Paper's roles. The language button, the language label and "Copy" use the `--rte-secondary` role, hover is `--rte-control-hover`, not the tokens of a `ghost` button: those would have stayed with dark theme values, and a light icon would end up on a white header.
1505
- - **The Wrapper tone on the Paper** has its own `--rte-tone-*` roles with light theme values (see the "Wrapper" section, "Tone"): the app theme's status `*-soft` and `*-strong` would be dark on a white Paper.
1505
+ - **The Callout tone on the Paper** has its own `--rte-tone-*` roles with light theme values (see the "Wrapper" section, "Tone"): the app theme's status `*-soft` and `*-strong` would be dark on a white Paper.
1506
1506
  - **Contrast** is checked by `npm run contrast`: every Paper pair against the Paper itself, in each theme × accent × brand.
1507
1507
  - `RichTextView` with the same `surface` shows the Document on the same Paper.
1508
1508
 
@@ -1536,7 +1536,7 @@ import type { RichTextEditorHandle } from "@olddevs/ui/editor";
1536
1536
 
1537
1537
  **Insertion.** `insertHTML` and `insertText` return `true` if the Document changed and `false` if not: there is no engine yet, the field is read-only or disabled, the insertion would go past `maxLength`, or the Schema dropped everything inserted. The HTML goes through the Schema like an incoming `value`: a Block of a disabled Tool is dropped (the text inside it stays as an ordinary paragraph), and if nothing is left, nothing is inserted. **An insertion that goes past `maxLength` is not performed at all** and returns `false`: a template cut off in the middle is worse than a refusal; what to tell the user is up to the screen. The caret is placed after the inserted content; the method does not take focus itself — if the insertion comes from a button or a dialog, start with `focus("restore")`. In a field that has never been focused, the caret is at the start of the Document.
1538
1538
 
1539
- **Author edit and External replacement.** Insertion, `undo` and `redo` count as Author edits: a separate history step (undo removes the insertion alone, not together with the typing before it), `onChange` fires after `changeDelay`, and the `maxLength` limit applies. `clear()`, `setMarkdown()`, `setJSON()` and a new `value` count as External replacements, and the limit does not truncate them; `onChange` fires immediately for `clear()`, `setMarkdown()` and `setJSON()`, but is not called for a new `value`. In `readOnly` and in a disabled field, insertion returns `false`, and `undo` and `redo` do nothing.
1539
+ **User edit and Programmatic replacement.** Insertion, `undo` and `redo` count as User edits: a separate history step (undo removes the insertion alone, not together with the typing before it), `onChange` fires after `changeDelay`, and the `maxLength` limit applies. `clear()`, `setMarkdown()`, `setJSON()` and a new `value` count as Programmatic replacements, and the limit does not truncate them; `onChange` fires immediately for `clear()`, `setMarkdown()` and `setJSON()`, but is not called for a new `value`. In `readOnly` and in a disabled field, insertion returns `false`, and `undo` and `redo` do nothing.
1540
1540
 
1541
1541
  **Calling a method before Readiness is safe** — on the server, before mount and while Tool Loaders are running (see the "Custom Tool" section). The engine is created in the browser after the first render and the Loaders, so a call at this moment neither throws nor queues.
1542
1542
 
@@ -1546,7 +1546,7 @@ import type { RichTextEditorHandle } from "@olddevs/ui/editor";
1546
1546
 
1547
1547
  On the server `ref.current` is empty until the Editor mounts, so write `ref.current?.…`.
1548
1548
 
1549
- **HTML mode.** `getText()` applies the HTML field's edit, like `getHTML()`; insertion applies it, inserts into the Document and overwrites the field; `focus` and `blur` act on the HTML field; `undo` and `redo` do nothing (a `textarea` has its own undo); `getSelectedText()` returns `""`.
1549
+ **Source mode.** `getText()` applies the HTML field's edit, like `getHTML()`; insertion applies it, inserts into the Document and overwrites the field; `focus` and `blur` act on the HTML field; `undo` and `redo` do nothing (a `textarea` has its own undo); `getSelectedText()` returns `""`.
1550
1550
 
1551
1551
  ### Your own toolbar: `useRichTextEditorState`
1552
1552
 
@@ -1592,13 +1592,13 @@ function Screen() {
1592
1592
  }
1593
1593
  ```
1594
1594
 
1595
- **Recipe: the buttons of an external toolbar call `preventDefault` on `mousedown`.** A click on a button moves focus to it, the field loses focus, and a command without `.focus()` in the chain would go astray. `preventDefault` on `mousedown` keeps focus in the Document, so the selection does not disappear either visually or for the floating Toolbar and the Selection menu, which hide when focus is lost. ProseMirror's selection survives losing focus even without this, but the visibility of the floating Toolbar, the Selection menu and the image panel depends on focus. The rule "a click on a custom toolbar does not clear the selection" is not part of the public contract: this is a recipe, not a property of the field. A button that opens its own dialog, where focus inevitably moves away, calls `ref.current?.focus("restore")` when the dialog closes.
1595
+ **Recipe: the buttons of an external toolbar call `preventDefault` on `mousedown`.** A click on a button moves focus to it, the field loses focus, and a command without `.focus()` in the chain would go astray. `preventDefault` on `mousedown` keeps focus in the Document, so the selection does not disappear either visually or for the floating Toolbar and the Bubble menu, which hide when focus is lost. ProseMirror's selection survives losing focus even without this, but the visibility of the floating Toolbar, the Bubble menu and the image panel depends on focus. The rule "a click on a custom toolbar does not clear the selection" is not part of the public contract: this is a recipe, not a property of the field. A button that opens its own dialog, where focus inevitably moves away, calls `ref.current?.focus("restore")` when the dialog closes.
1596
1596
 
1597
1597
  **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.
1598
1598
 
1599
1599
  ### Tool state: `useRichTextToolState`
1600
1600
 
1601
- `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.
1601
+ `useRichTextToolState(ref, name, selector?)` gives the screen the state of a Tool: a "3 of 12" counter from Find in the Status bar, 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.
1602
1602
 
1603
1603
  - **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`.
1604
1604
  - **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.
@@ -1621,7 +1621,7 @@ The state of a custom Tool is declared with `defineTool` (the `state` field) and
1621
1621
 
1622
1622
  - **The type is inferred from the `RichTextTools` map**, by the Tool's name. A name outside the map gives `unknown`.
1623
1623
  - **`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.
1624
- - **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.
1624
+ - **Every command returns a `boolean`:** done or not (Source mode, no matches, no such heading, a field that cannot be edited). A refusal is `false`, as with `insertHTML`, not an exception.
1625
1625
  - **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.
1626
1626
  - **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`.
1627
1627
 
@@ -1642,14 +1642,14 @@ The screen learns about the Editor through callback props; there is no event bus
1642
1642
  ```
1643
1643
 
1644
1644
  - **`onReady()`** takes no arguments and is called **once per mount**, when the engine is created. By then `ref` is already usable: in the callback you can insert a template or set focus. It fires in `readOnly` and in `disabled` too; a re-render, a `placeholder` change or a change of the callback itself do not cause a second call. An error in the `tools` set (there is no engine, see "Custom Tool") and server rendering do not call `onReady`. Readiness means the engine is created **and all Tool Loaders have run** (see the "Custom Tool" section): `onReady` waits for them, and on a Loader failure it is not called — and after "Retry" it is called once, when the engine is created.
1645
- - **`onFocus()` and `onBlur()`** report Field focus and take no arguments (the DOM event is not passed through). Here the field means the whole field: the Document, the Toolbar, popovers and kit dialogs (link, color, image, "Insert table"), the HTML mode field. Moving between them does not lose Field focus: from the text to the Toolbar, to the link popover and back, neither `onBlur` nor `onFocus` fires — as with a composite form field. The Document's focus (the engine's `focus` and `blur`) is different: a click on the Toolbar removes it, while Field focus stays.
1645
+ - **`onFocus()` and `onBlur()`** report Field focus and take no arguments (the DOM event is not passed through). Here the field means the whole field: the Document, the Toolbar, popovers and kit dialogs (link, color, image, "Insert table"), the Source mode field. Moving between them does not lose Field focus: from the text to the Toolbar, to the link popover and back, neither `onBlur` nor `onFocus` fires — as with a composite form field. The Document's focus (the engine's `focus` and `blur`) is different: a click on the Toolbar removes it, while Field focus stays.
1646
1646
  - **Before `onBlur` a pending `onChange` is flushed**, wherever focus moves away from (the Document, the Toolbar, a popover), so autosave on `onBlur` gets the fresh Document. `onBlur` fires a tick after focus leaves: in that time focus could have come back into the field. If the Editor is unmounted with focus inside, `onBlur` is not called (a pending `onChange` is still delivered).
1647
1647
 
1648
1648
  ### Paste and drop order
1649
1649
 
1650
1650
  Only the Extensions of a custom Tool can intercept pasting from the clipboard and dropping — through the ProseMirror plugins `transformPastedHTML`, `transformPasted`, `handlePaste` and `handleDrop` in `engine`. The Editor has no hooks or props of its own named `onPaste` and `onDrop`.
1651
1651
 
1652
- - **The handlers of custom Tools see the event before the built-in ones** — Markdown paste, image upload and Layout — wherever a custom Tool appears in `tools`. A `handlePaste` or `handleDrop` that returns `true` takes the whole event: the built-in ones never get it. This way a link to a video can be turned into a custom Block before Markdown parses it.
1652
+ - **The handlers of custom Tools see the event before the built-in ones** — Markdown paste, image upload and Column layout — wherever a custom Tool appears in `tools`. A `handlePaste` or `handleDrop` that returns `true` takes the whole event: the built-in ones never get it. This way a link to a video can be turned into a custom Block before Markdown parses it.
1653
1653
  - **Schema parsing and the limit always apply.** What a paste brought beyond the Schema (`<script>`, `onclick`, unknown tags, values that break the attribute rules) is dropped, as with an incoming `value`; HTML cleanup (`transformPastedHTML`) happens before the Schema and cannot weaken it. A transaction of your own handler that takes the text beyond `maxLength` is **rejected entirely**; tail trimming exists only in the built-in paste from the clipboard (a transaction flagged `paste`).
1654
1654
  - **Files other than images are handled by a custom Tool** in `handlePaste` and `handleDrop`: its own upload function and its own upload URL, set through the options of its extension. The Editor has no general `onUploadFile`. A file that no handler claims reaches the kit: an image is uploaded through `onUploadImage`, and anything else gets the message "`name` is not an image" (`editor.imageNotImage` in the message catalog).
1655
1655
 
@@ -1685,9 +1685,9 @@ export const pdfTool: CustomTool = { name: "acme_pdfDrop", engine: [PdfDrop] };
1685
1685
  - `onUploadImage(file) → Promise<string>` — the image uploader, see "Images". Without it files are not accepted.
1686
1686
  - `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".
1687
1687
  - `surface` — `"theme"` (default) or `"paper"`: a light Paper in any theme, see "Surface". The type is `RichTextSurface`.
1688
- - `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.
1688
+ - `status` — a React node of the Status bar 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 bar and Field notice"; they follow the kit's semver.
1689
1689
  - `tools` — kit names, Configured Tools `tool(name, options)` for `search`, `toc` and `details`, `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.
1690
- - `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).
1690
+ - `ref` (`RichTextEditorHandle`): `getHTML()` — the current HTML, without waiting for the delay and without `onChange` (in Source 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 Source mode, in the HTML field).
1691
1691
  - `useRichTextEditorState(ref, selector)` — a hook for your own toolbar outside the field, see "Handle (`ref`) and your own toolbar outside the field".
1692
1692
  - `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.
1693
1693
  - 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.
@@ -1702,14 +1702,14 @@ The Markdown engine is the `@tiptap/markdown` package (the same version range as
1702
1702
  - **When a paste stays text.** If the clipboard holds any other HTML next to the text (a copy from a browser, Word, Google Docs), the usual HTML parsing applies — Markdown is not looked for in it. Pasting "as plain text" (`Ctrl/⌘+Shift+V`) and pasting into code also give the original characters. If nothing the Schema can accept is left of the pasted Markdown (with `table` disabled, a lone table; with `codeBlock` disabled, a lone code block), the original text is pasted rather than nothing.
1703
1703
  - **Tables.** A GFM table (`| a | b |` and a delimiter row, at least `---` in a cell for paste recognition) becomes a table, with the first row as the header; `getMarkdown()` exports the table back, always with a header row (without one, with an empty one). Multiline cell content goes into Markdown as one line, with line breaks as `<br>`; `|` in a cell is escaped. **Column alignment is preserved in both directions:** `:---:` and `---:` become `data-align` on the column's cells on paste and `setMarkdown`, and `getMarkdown()` writes them back; `:---` is the default and gives no attribute. **Markdown cannot express cell merging:** `getMarkdown()` spreads a merged cell over the grid — the text stays in its top-left position, the covered positions become empty cells, so the columns below do not shift; the merge itself is lost, and `setMarkdown(getMarkdown())` returns the table without it.
1704
1704
  - **`getMarkdown()`** is the Document as Markdown, right now, without waiting for `changeDelay`. An empty Document and a call before mount (there is no engine on the server) give `""`.
1705
- - **`setMarkdown(md)`** replaces the Document with the parsed Markdown. Like `clear()`, this is an External replacement: `onChange` receives the new HTML immediately and only once, and the `maxLength` limit does not truncate it. Before mount it does nothing.
1706
- - **The Schema wins.** The parsed result goes through the Editor's Schema: everything not in it now is dropped, `setMarkdown` included. In this version of the kit the following are lost: images with a URL outside `http`/`https` (they are removed entirely, with their `alt`), links with a URL outside the `http`/`https`/`mailto` policy (the link text stays) and raw HTML inside Markdown (it is removed entirely, except the `details` and `summary` of a collapsible Block). Without the `taskList` Tool, checklists become ordinary list items, without `codeBlock` the code block is removed, and without `table` the table is removed. Markdown never yields a Layout or a Wrapper without a tone: their Blocks follow one another. A collapsible Block is read from raw HTML `<details><summary>` and exported as the same (see the "Collapsible block" section). A Wrapper with a tone comes only from a GitHub alert (`> [!NOTE]` and four more values, see the "Wrapper" section, "Tone"), and `getMarkdown()` exports it the same way. With `heading1`, `#` gives H1, and `getMarkdown` writes H1 as `#`. Without it, `#` is demoted to H2; `#####` is demoted to H4 in any case. What exactly is accepted is decided by the set of Tools, not by this list.
1705
+ - **`setMarkdown(md)`** replaces the Document with the parsed Markdown. Like `clear()`, this is a Programmatic replacement: `onChange` receives the new HTML immediately and only once, and the `maxLength` limit does not truncate it. Before mount it does nothing.
1706
+ - **The Schema wins.** The parsed result goes through the Editor's Schema: everything not in it now is dropped, `setMarkdown` included. In this version of the kit the following are lost: images with a URL outside `http`/`https` (they are removed entirely, with their `alt`), links with a URL outside the `http`/`https`/`mailto` policy (the link text stays) and raw HTML inside Markdown (it is removed entirely, except the `details` and `summary` of a collapsible Block). Without the `taskList` Tool, checklists become ordinary list items, without `codeBlock` the code block is removed, and without `table` the table is removed. Markdown never yields a Column layout or a Wrapper without a tone: their Blocks follow one another. A collapsible Block is read from raw HTML `<details><summary>` and exported as the same (see the "Collapsible block" section). A Wrapper with a tone comes only from a GitHub alert (`> [!NOTE]` and four more values, see the "Wrapper" section, "Tone"), and `getMarkdown()` exports it the same way. With `heading1`, `#` gives H1, and `getMarkdown` writes H1 as `#`. Without it, `#` is demoted to H2; `#####` is demoted to H4 in any case. What exactly is accepted is decided by the set of Tools, not by this list.
1707
1707
  - **Custom Tools.** Custom Markdown rules live in the Extensions; without them a custom Block is exported as its content, and an Atom as its Atom text (see the "Custom Tool" section, "Outside the Editor").
1708
1708
  - **What is lost on export.** Markdown does not express text color and highlight color, Block alignment, underline, an image's width and caption, table cell merging and a list inside a cell: an image is exported as `![alt](url)`, `getMarkdown()` omits the rest, and `setMarkdown(getMarkdown())` returns the Document without them. This is how the format works: for lossless storage use HTML, and for a lossless file use the JSON envelope (see the "JSON" section).
1709
1709
 
1710
1710
  ## JSON
1711
1711
 
1712
- A JSON envelope is the Document for a file, a template or a transfer between products **without loss**: it keeps everything Markdown loses (color, highlight, alignment, underline, an image with width and caption, cell merging, Layout, a Wrapper with a tone). The envelope is an exchange format **alongside HTML, not instead of it**: `value`, `onChange`, the hidden form field, `RichTextView` and server sanitization still work with HTML (ADR-0061).
1712
+ A JSON envelope is the Document for a file, a template or a transfer between products **without loss**: it keeps everything Markdown loses (color, highlight, alignment, underline, an image with width and caption, cell merging, Column layout, a Wrapper with a tone). The envelope is an exchange format **alongside HTML, not instead of it**: `value`, `onChange`, the hidden form field, `RichTextView` and server sanitization still work with HTML (ADR-0061).
1713
1713
 
1714
1714
  ```ts
1715
1715
  import type { RichTextDocument } from "@olddevs/ui/editor";
@@ -1720,8 +1720,8 @@ const opened: boolean = ref.current?.setJSON(JSON.parse(fileText)) ?? false;
1720
1720
  ```
1721
1721
 
1722
1722
  - **Shape.** `format` is always `"olddevs-rich-text"`, `version` is the tree format number (currently `1`), `doc` is the Document tree with a root node `"doc"`. The node and attribute names in `doc` are the format's contract: if the Schema renames them, the kit bumps `version` and reads old files itself. Do not check or build `doc` by hand: use what `getJSON()` returned.
1723
- - **`getJSON()`** is the Document as an envelope, right now, without waiting for `changeDelay`. In HTML mode the HTML field's edit is applied first, as with `getHTML()`. Before mount and on the server it gives `null`.
1724
- - **`setJSON(document): boolean`** replaces the Document with an envelope. The parameter is `unknown`: pass the result of `JSON.parse` of the file directly, and the Editor checks the shape. Like `setMarkdown()`, this is an External replacement: `onChange` receives the new HTML immediately and only once, the `maxLength` limit does not truncate it, the method does not take focus, and in HTML mode the field's text is rewritten. `false` means the envelope was rejected or there is no engine yet; the Document is untouched, and what to tell the user ("the file does not fit") is up to the screen.
1723
+ - **`getJSON()`** is the Document as an envelope, right now, without waiting for `changeDelay`. In Source mode the HTML field's edit is applied first, as with `getHTML()`. Before mount and on the server it gives `null`.
1724
+ - **`setJSON(document): boolean`** replaces the Document with an envelope. The parameter is `unknown`: pass the result of `JSON.parse` of the file directly, and the Editor checks the shape. Like `setMarkdown()`, this is a Programmatic replacement: `onChange` receives the new HTML immediately and only once, the `maxLength` limit does not truncate it, the method does not take focus, and in Source mode the field's text is rewritten. `false` means the envelope was rejected or there is no engine yet; the Document is untouched, and what to tell the user ("the file does not fit") is up to the screen.
1725
1725
  - **What is rejected entirely:** not an object, an unrecognized `format`, a different `version` (including its absence), a `doc` without a root `"doc"` node, and a tree from which nodes cannot be built (a malformed node, an empty text node). A bare Tiptap tree from `editor.getJSON()` is rejected too: without an envelope an Editor file cannot be told from any other JSON.
1726
1726
  - **The Schema wins — for JSON too.** An envelope goes through the Schema as strictly as HTML: a Block of a disabled Tool unwraps into its text, a Mark that is not in the Schema is removed, and attributes are checked by **the same rules as for HTML**. A dangerous link or image URL, a color outside the palette, an author's color not in the `#rrggbb` form, `data-*` outside the Schema's list, and values of custom Tools outside their rules are dropped in the Document itself, not only on output. The author's whitespace is not collapsed by the `getJSON()` → `setJSON()` round trip.
1727
1727
  - **`defaultValue` with an envelope** is the initial Document from JSON, uncontrolled mode only (`value` wins). It passes the same check as `setJSON()`, is applied when the engine appears, without `onChange` and without an undo step; a rejected one gives an empty Document and one error in the development console. Until Readiness, with an envelope the Skeleton is a frame without a Document, `getHTML()` is `""`, and `isEmpty()` evaluates the envelope's tree by the `required` rule.
@@ -1736,11 +1736,11 @@ The allowlist of this release with `preset="full"` (disabled Tools narrow it):
1736
1736
 
1737
1737
  <!-- allow-list -->
1738
1738
 
1739
- | What | Allowed |
1740
- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
1741
- | Blocks | `p`, `h1`, `h2`, `h3`, `h4`, `ul`, `ol`, `li`, `blockquote`, `hr`, `pre`, `table`, `tbody`, `tr`, `th`, `td`; `label`, `input`, `span` — only inside a checklist item; `div` — there too, and also the Layout and its columns (a Layout only at the top level, a column only directly in it) and the Wrapper (at the top level and directly in a column, not in another Wrapper); `details` — a collapsible Block, with `summary` and a content `div` inside it (both only directly in `details`; outside it the tags are removed); `figure` — an image, with `img` and an optional `figcaption` inside it (they do not exist outside `figure`) |
1742
- | Marks | `strong`, `em`, `u`, `s`, `code`, `sup`, `sub`, `mark` (only with a color), `br`, `a` |
1743
- | Attributes | `ol[start]` — only when numbering does not start at 1; `ul[data-type]`, `li[data-type]`, `li[data-checked]` — a checklist; `input[type]`, `input[checked]` — its checkbox; `code[class]` — the code block language, only `language-python`, `language-typescript`, `language-javascript`, `language-go`, `language-html`, `language-json`, `language-yaml` or `language-markdown` (no class without a language); `span[data-color]`, `mark[data-color]` — text color and highlight, only names from the Editor's own palette — text color on `span`, highlight on `mark`; `span[style]`, `mark[style]` — an arbitrary author's color, only `color:#rrggbb` and `background-color:#rrggbb`, and not together with data-color; `p[data-align]`, `h1[data-align]`, `h2[data-align]`, `h3[data-align]`, `h4[data-align]` — Block alignment, center or right; `th[colspan]`, `td[colspan]` — horizontal merge, an integer from 2 to 20; `th[rowspan]`, `td[rowspan]` — vertical, from 2 to 50; `th[data-align]`, `td[data-align]` — column alignment, center or right, on each of its cells; `a[href]` — only http, https or mailto; `a[rel]` — always `noopener noreferrer`; `img[src]` — only absolute http or https, no data and blob; `img[alt]` — always, empty for an image without a description; `img[data-width]` — 50, 75 or 100; `div[data-block]` — the Layout (`layout`), its column (`column`) and the Wrapper (`wrapper`); `div[data-layout]` — the Layout variant, column shares in percent: `100`, `50-50`, `33-33-33`, `25-25-25-25`, `60-40` or `40-60`; `div[data-tone]` — the Wrapper tone, only on it: `neutral`, `accent`, `success`, `warning` or `danger` (no attribute without a tone); `div[data-type]` — the content wrapper of a collapsible Block, only the value "detailsContent" and only directly in `details`; `details[open]` — an open Block, only an empty "open", written with the "persist" option |
1739
+ | What | Allowed |
1740
+ | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1741
+ | Blocks | `p`, `h1`, `h2`, `h3`, `h4`, `ul`, `ol`, `li`, `blockquote`, `hr`, `pre`, `table`, `tbody`, `tr`, `th`, `td`; `label`, `input`, `span` — only inside a checklist item; `div` — there too, and also the Column layout and its columns (a Column layout only at the top level, a column only directly in it) and the Wrapper (at the top level and directly in a column, not in another Wrapper); `details` — a collapsible Block, with `summary` and a content `div` inside it (both only directly in `details`; outside it the tags are removed); `figure` — an image, with `img` and an optional `figcaption` inside it (they do not exist outside `figure`) |
1742
+ | Marks | `strong`, `em`, `u`, `s`, `code`, `sup`, `sub`, `mark` (only with a color), `br`, `a` |
1743
+ | Attributes | `ol[start]` — only when numbering does not start at 1; `ul[data-type]`, `li[data-type]`, `li[data-checked]` — a checklist; `input[type]`, `input[checked]` — its checkbox; `code[class]` — the code block language, only `language-python`, `language-typescript`, `language-javascript`, `language-go`, `language-html`, `language-json`, `language-yaml` or `language-markdown` (no class without a language); `span[data-color]`, `mark[data-color]` — text color and highlight, only names from the Editor's own palette — text color on `span`, highlight on `mark`; `span[style]`, `mark[style]` — an arbitrary author's color, only `color:#rrggbb` and `background-color:#rrggbb`, and not together with data-color; `p[data-align]`, `h1[data-align]`, `h2[data-align]`, `h3[data-align]`, `h4[data-align]` — Block alignment, center or right; `th[colspan]`, `td[colspan]` — horizontal merge, an integer from 2 to 20; `th[rowspan]`, `td[rowspan]` — vertical, from 2 to 50; `th[data-align]`, `td[data-align]` — column alignment, center or right, on each of its cells; `a[href]` — only http, https or mailto; `a[rel]` — always `noopener noreferrer`; `img[src]` — only absolute http or https, no data and blob; `img[alt]` — always, empty for an image without a description; `img[data-width]` — 50, 75 or 100; `div[data-block]` — the Column layout (`layout`), its column (`column`) and the Wrapper (`wrapper`); `div[data-layout]` — the Column layout variant, column shares in percent: `100`, `50-50`, `33-33-33`, `25-25-25-25`, `60-40` or `40-60`; `div[data-tone]` — the Callout tone, only on it: `neutral`, `accent`, `success`, `warning` or `danger` (no attribute without a tone); `div[data-type]` — the content wrapper of a collapsible Block, only the value "detailsContent" and only directly in `details`; `details[open]` — an open Block, only an empty "open", written with the "persist" option |
1744
1744
 
1745
1745
  Tiptap writes a checklist item like this: `<li data-type="taskItem" data-checked="true"><label><input type="checkbox" checked><span></span></label><div><p>…</p></div></li>`. A sanitizer should let `input`, `label`, `span` and `div` through only inside such an item, and `input` only with `type="checkbox"`.
1746
1746
 
@@ -1756,11 +1756,11 @@ There are no other classes, no `style` (except the author's color), no `aria-*`
1756
1756
 
1757
1757
  - The editable area is `role="textbox"` with `aria-multiline`.
1758
1758
  - **The Toolbar** is `role="toolbar"` named from the message catalog (`editor.toolbar`, "Formatting") with **one Tab stop**: Tab enters the Toolbar, the Left and Right arrow keys move between the buttons, Home and End go to the edges, and the next Tab moves focus to the Document. The Toolbar remembers where focus was. It is built on the kit's `Toolbar`.
1759
- - Buttons are icons with `aria-label`; toggle buttons (bold, italic, underline in the Selection menu, link, image) announce their state with **`aria-pressed`** — a screen reader announces whether italic is on at the caret. One-off actions (undo, redo) are ordinary buttons. A button of a custom Tool announces `aria-pressed` only if it has `isActive`, and its shortcut is in the tooltip and in `aria-keyshortcuts`; an empty label is replaced with the Tool's name. An action unavailable right now is `aria-disabled`, not `disabled`: it stays in the arrow-key order.
1759
+ - Buttons are icons with `aria-label`; toggle buttons (bold, italic, underline in the Bubble menu, link, image) announce their state with **`aria-pressed`** — a screen reader announces whether italic is on at the caret. One-off actions (undo, redo) are ordinary buttons. A button of a custom Tool announces `aria-pressed` only if it has `isActive`, and its shortcut is in the tooltip and in `aria-keyshortcuts`; an empty label is replaced with the Tool's name. An action unavailable right now is `aria-disabled`, not `disabled`: it stays in the arrow-key order.
1760
1760
  - A hover and focus tooltip names the action and the shortcut; the shortcut is also announced with `aria-keyshortcuts` (`Control+B` or `Meta+B` on Apple). On Apple the label is `⌘+⇧+S`, elsewhere `Ctrl+Shift+S`, using the same check the engine uses to decide which key is `mod`.
1761
- - **The Insert menu on `/`** follows the `combobox` pattern from the WAI-ARIA Authoring Practices Guide (APG): focus stays in the field, the list is `role="listbox"` with `role="group"` sections, and the field points to it with `aria-controls` and to the current row with `aria-activedescendant` (the row has `aria-selected`). The field announces `aria-autocomplete="list"`. `aria-expanded` is not set on the field: ARIA 1.2 does not allow it on `role="textbox"`, and the role of a multiline Document cannot be changed to `combobox`. A row's name is its label; the `##` reminder is not read to the screen reader.
1761
+ - **The Slash menu on `/`** follows the `combobox` pattern from the WAI-ARIA Authoring Practices Guide (APG): focus stays in the field, the list is `role="listbox"` with `role="group"` sections, and the field points to it with `aria-controls` and to the current row with `aria-activedescendant` (the row has `aria-selected`). The field announces `aria-autocomplete="list"`. `aria-expanded` is not set on the field: ARIA 1.2 does not allow it on `role="textbox"`, and the role of a multiline Document cannot be changed to `combobox`. A row's name is its label; the `##` reminder is not read to the screen reader.
1762
1762
  - The list on `/` is a layer of the kit's `Popover` (Radix): Escape closes only it, not the `Dialog` or `Drawer` around the field, and what was typed stays as text. "+" is the kit's `Menu` with `aria-haspopup`, and Escape in it behaves the same way (ADR-0041).
1763
- - **The Shortcuts reference** is the kit's `Popover`: a button with `aria-haspopup="dialog"` and `aria-expanded`, the popover is `role="dialog"` named "Keyboard shortcuts", with a `table` inside having column headers (`columnheader`) and a row header (`rowheader`) for each command, and each shortcut is marked up with `<kbd>`. Escape and a click outside close the popover and return focus to the button; in a `Dialog`, only the popover (ADR-0041). Tests: `tests/rich-text-editor-shortcuts.test.tsx`, in the browser `tests-visual/interactions.spec.ts` ("RichTextEditor inside Dialog: Escape closes the shortcuts reference").
1763
+ - **The Keyboard shortcuts popover** is the kit's `Popover`: a button with `aria-haspopup="dialog"` and `aria-expanded`, the popover is `role="dialog"` named "Keyboard shortcuts", with a `table` inside having column headers (`columnheader`) and a row header (`rowheader`) for each command, and each shortcut is marked up with `<kbd>`. Escape and a click outside close the popover and return focus to the button; in a `Dialog`, only the popover (ADR-0041). Tests: `tests/rich-text-editor-shortcuts.test.tsx`, in the browser `tests-visual/interactions.spec.ts` ("RichTextEditor inside Dialog: Escape closes the shortcuts reference").
1764
1764
  - **The floating Toolbar** is the same `role="toolbar"` with one Tab stop and the same arrow keys; it appears above a non-empty selection, and on a touch screen it is replaced by the fixed one. To move from the Document to any Toolbar, press `Alt+F10` (see the "Keyboard: `Alt+F10`" section).
1765
1765
  - **The link popover** is `role="dialog"` named "Link" (`editor.link`); the field is labeled (`editor.linkAddress`), the URL error (`editor.linkInvalid`) is associated with the field through `aria-describedby` and sets `aria-invalid`. The "Link" button is a toggle with `aria-pressed` when the caret is in a link.
1766
1766
  - **Image.** The popover is `role="dialog"` named "Image" (`editor.image`); the "Image address" and "Alternative text" fields are labeled, the second one's hint is associated through `aria-describedby`, and the URL error (`editor.imageInvalid`) sets `aria-invalid`. The width presets are a `role="group"` "Width" of toggle buttons with `aria-pressed`. The selected image's panel is a `role="toolbar"` "Image" with one Tab stop. The width handles are for the mouse only (`aria-hidden`, no focus): the same width is in the popover and on the panel. Upload progress is a `role="status"` live region outside the Document (live regions inside `contenteditable` are not read everywhere), and a failure is a visible `role="alert"` line under the field: this is the Field notice, the same one that `showNotice` shows.
@@ -1771,16 +1771,16 @@ There are no other classes, no `style` (except the author's color), no `aria-*`
1771
1771
  - **The "Alignment" and "Lists" menus** are like "¶": a button with `aria-haspopup="menu"` and the current value in its name ("Alignment: Align center", "List: numbered"), radio items with `aria-checked`, shortcuts labeled in the items; Enter, Space and `↓` open, Escape closes only the menu and returns focus to the button. Alignment and lists are menus rather than three toggle buttons each in a row: a value announced in the button's name and a check in the menu convey the same as `aria-pressed`, and the row is four buttons shorter — ADR-0054.
1772
1772
  - **The "Table" menu** is the kit's `Menu` with submenus (`SubmenuTrigger`): a button with `aria-haspopup="menu"`, items with `›` having `aria-haspopup` and `aria-expanded`, "Header row" is `menuitemcheckbox`, column alignment is `menuitemradio`, and unavailable items get `aria-disabled` and stay in the arrow-key order. The "Table ›" submenu is a react-aria popover with a grid (the `grid` pattern from APG, `aria-activedescendant`, the size in words in a live region). Escape closes one level at a time, and in a `Dialog` the dialog goes last (ADR-0041). The "Insert table" dialog is the kit's `Dialog`: focus on open is in the "Columns" field, and on close it goes back to the Document. Tests: `tests/rich-text-editor-table-menu.test.tsx`, in the browser `tests-visual/rich-text-editor-table-menu.spec.ts`.
1773
1773
  - **Emoji.** The button is `aria-haspopup="dialog"`; the popover is `role="dialog"` named "Emoji" (`editor.emoji`) with a `role="grid"` ("Choose an emoji") where focus stays on the grid and `aria-activedescendant` names the current cell, as in the table size grid. Categories are a `rowgroup` with a name ("Favorites", "Smileys"…), an emoji is a `gridcell` named from the message catalog (`editor.emojiName`: "Fire", "Red heart"), not by the symbol: screen readers pronounce symbols differently, and sometimes stay silent. The symbol in the cell is hidden from the screen reader. Escape closes the popover and returns focus to the Document.
1774
- - **Layout.** Columns go in the markup left to right — the screen reader reads them in the same order, and they stack in the same order on a narrow screen. A column has no role of its own: it is a group of Document Blocks, not a widget. A Tab press that the Document handles (columns, list, table) does not reach the focus trap of the `Dialog` and `Drawer` around the field; a Tab press that it lets through (the last column on Tab, the first on Shift+Tab; a paragraph outside a list) moves focus on — there is no keyboard trap.
1774
+ - **Column layout.** Columns go in the markup left to right — the screen reader reads them in the same order, and they stack in the same order on a narrow screen. A column has no role of its own: it is a group of Document Blocks, not a widget. A Tab press that the Document handles (columns, list, table) does not reach the focus trap of the `Dialog` and `Drawer` around the field; a Tab press that it lets through (the last column on Tab, the first on Shift+Tab; a paragraph outside a list) moves focus on — there is no keyboard trap.
1775
1775
  - **Wrapper.** It has no role of its own: it is a group of Document Blocks, not a widget, and the screen reader reads its Blocks in order, like the Blocks around it. `Tab` in a Wrapper is not intercepted — there is no keyboard trap; `Enter` in an empty last paragraph and the arrow keys at its edge move the caret out of a Wrapper. **A tone** adds no roles or `aria-*` — it is styling only: the meaning of a note ("Warning: …") is written by the author in words (WCAG 1.4.1). The "Tone ›" submenu is the kit's `Menu`: radio items with `aria-checked`, and the submenu item's name includes the current tone.
1776
1776
  - **Collapsible block.** The opening button is a real `button` named from the catalog (`editor.detailsToggle`: "Details: {title}") with `aria-expanded`; it is in the Tab order, Enter and Space open the Block, and focus stays on the button. The content of a collapsed Block is hidden from both the screen reader and the caret. In `RichTextView` the Block is a native `details` and `summary`: the kit adds no role or name, the browser opens it, from the keyboard and without JavaScript. `Tab` in the Block is not intercepted — there is no keyboard trap.
1777
- - **HTML mode.** The "HTML" button is a toggle with `aria-pressed`, in the Toolbar's arrow-key order like any button. The field is a native `textarea` named from the message catalog ("HTML source", `editor.sourceField`); the hint and error of `FormField` go into its `aria-describedby`, merged the same way as for the Document; `aria-invalid` mirrors the Document. Autocorrections are off: `spellcheck="false"`, `autocapitalize`, `autocorrect` and `autocomplete` are `off`, `dir="ltr"`. Tab and `Shift+Tab` are native: the field inserts no tab character, and there is no keyboard trap; undo inside the field is the ordinary text field undo. Entering puts focus in the field (caret at the start), leaving with the button puts it in the Document (caret at the end). `Alt+F10` moves focus from the field to the "HTML" button, as from the Document to the Toolbar: the Toolbar announces this shortcut in the mode too. The other Toolbar buttons in the mode are `aria-disabled`, in the arrow-key order. A click on the `FormField` label and a `required` failure move focus into the HTML field. Tests: `tests/rich-text-editor-source.test.tsx`, in the browser `tests-visual/rich-text-editor-source.spec.ts`.
1778
- - **Field notice and Status line.** A `role="status"` live region is present in every Editor and is empty while there is no `status` notice; an error is `role="alert"` under the field. The Status line is a container without `role` and `aria-live`: the kit does not announce it, and the screen wraps "Saved" in `role="status"` itself. The line is visible with `readOnly` and `disabled`, and the counter stays in `aria-describedby`. Tests: `tests/rich-text-editor-notice.test.tsx`; in the showcase — `StatusLine`, `StatusLineReadOnly`, `FieldNotice`.
1777
+ - **Source mode.** The "HTML" button is a toggle with `aria-pressed`, in the Toolbar's arrow-key order like any button. The field is a native `textarea` named from the message catalog ("HTML source", `editor.sourceField`); the hint and error of `FormField` go into its `aria-describedby`, merged the same way as for the Document; `aria-invalid` mirrors the Document. Autocorrections are off: `spellcheck="false"`, `autocapitalize`, `autocorrect` and `autocomplete` are `off`, `dir="ltr"`. Tab and `Shift+Tab` are native: the field inserts no tab character, and there is no keyboard trap; undo inside the field is the ordinary text field undo. Entering puts focus in the field (caret at the start), leaving with the button puts it in the Document (caret at the end). `Alt+F10` moves focus from the field to the "HTML" button, as from the Document to the Toolbar: the Toolbar announces this shortcut in the mode too. The other Toolbar buttons in the mode are `aria-disabled`, in the arrow-key order. A click on the `FormField` label and a `required` failure move focus into the HTML field. Tests: `tests/rich-text-editor-source.test.tsx`, in the browser `tests-visual/rich-text-editor-source.spec.ts`.
1778
+ - **Field notice and Status bar.** A `role="status"` live region is present in every Editor and is empty while there is no `status` notice; an error is `role="alert"` under the field. The Status bar is a container without `role` and `aria-live`: the kit does not announce it, and the screen wraps "Saved" in `role="status"` itself. The line is visible with `readOnly` and `disabled`, and the counter stays in `aria-describedby`. Tests: `tests/rich-text-editor-notice.test.tsx`; in the showcase — `StatusLine`, `StatusLineReadOnly`, `FieldNotice`.
1779
1779
  - **Text card.** `role="dialog"` without `aria-modal`, named by the contribution's label (`label`); focus on open stays in the Document. A `role="status"` live region announces the appearance with the phrase `editor.textCardShown` including the label and the `Alt+F10` shortcut; `Alt+F10` moves focus into the Text card, and Escape returns the caret and hides it. See the "Text card" section.
1780
1780
  - A checklist item's checkbox is named from the message catalog (`editor.taskCheckbox`): "Done: <item text>".
1781
1781
  - **Name.** The browser associates `<label for>` only with native form controls, and `contenteditable` is not one of them. So `FormField` publishes the `id` of its label through context, and the `RichTextEditor` inside it references the label through `aria-labelledby` and puts focus in the Document on a click on the label. For this the Editor needs no `id` and `FormField` needs no `htmlFor` (but set `htmlFor` out of habit — it does no harm). The Editor does not touch the label's DOM, it only listens for the click. An explicit `aria-label` and `aria-labelledby` outrank the label. If none of this is set, the name comes from the message catalog (`editor.label`) — there is no nameless field, but such a name says nothing about the content: give your own.
1782
1782
  - The `FormField` hint and error are announced together with the field: their `id` values go into `aria-describedby` and are **merged** with its own description from the prop. An error sets `aria-invalid` and styles the frame as invalid; an explicit `aria-invalid` prop outranks it.
1783
- - **`required` works like the native one.** An empty Document (no text, no Atom — `hr`, an image, a custom Atom — and no table) blocks submission of an **ordinary** `<form>`: the browser shows a validation message with the text from the message catalog (`editor.required`, follows the kit's locale), and focus goes to the Editor. As soon as the Document has content, the block is lifted immediately, without waiting for `changeDelay`. In HTML mode, emptiness is decided by the HTML field's text parsed by the Schema — also immediately, while typing (see the "HTML mode" section). `readOnly` and `disabled` are not checked, as with native fields. The asterisk on the label comes only from `FormField required`.
1783
+ - **`required` works like the native one.** An empty Document (no text, no Atom — `hr`, an image, a custom Atom — and no table) blocks submission of an **ordinary** `<form>`: the browser shows a validation message with the text from the message catalog (`editor.required`, follows the kit's locale), and focus goes to the Editor. As soon as the Document has content, the block is lifted immediately, without waiting for `changeDelay`. In Source mode, emptiness is decided by the HTML field's text parsed by the Schema — also immediately, while typing (see the "Source mode" section). `readOnly` and `disabled` are not checked, as with native fields. The asterisk on the label comes only from `FormField required`.
1784
1784
  - How it works: a hidden `<input type="hidden" name>` is **excluded from constraint validation by the specification**, and `contenteditable` is not form-associated, so next to it there is an invisible text field without `name` — it carries `setCustomValidity`, while the hidden field still puts the value into `FormData`.
1785
1785
  - In the kit's `Form` (`noValidate`) the browser validates no field — there the consumer signals that a field is required through `FormField error`, as for `TextControl`.
1786
1786
  - `readOnly` and `disabled` are announced with `aria-readonly` and `aria-disabled`.
@@ -1798,18 +1798,18 @@ There are no other classes, no `style` (except the author's color), no `aria-*`
1798
1798
  - Using the author's color where the Document is shown in several themes: unlike the palette, it does not follow the theme.
1799
1799
  - Setting `changeDelay={0}` "just in case" on long Documents: the form gets fresh HTML before submission anyway.
1800
1800
  - Setting a link with a `ref.editor` command that bypasses the popover, using a relative URL or a scheme outside `http`/`https`/`mailto`: in HTML such a link goes out without `href`.
1801
- - Putting an Editor with `toolbar="floating"` or `"bubble"` in a container with `overflow: hidden` right against its top edge: the Toolbar or the Selection menu above the first line is clipped.
1801
+ - Putting an Editor with `toolbar="floating"` or `"bubble"` in a container with `overflow: hidden` right against its top edge: the Toolbar or the Bubble menu above the first line is clipped.
1802
1802
  - Repainting the Editor's popup surfaces with a solid color through the consumer's `className` or CSS: the Glass is removed by the system's transparency and contrast setting, and a hand-made background stops respecting that setting.
1803
- - Overriding the Selection menu's token colors with your own: the inverse pair is checked for contrast in all themes and brands, and a hand-picked color is not. A brand of your own overrides `--accent-on-inverse` in its own CSS rule, like `--accent-strong`.
1803
+ - Overriding the Bubble menu's token colors with your own: the inverse pair is checked for contrast in all themes and brands, and a hand-picked color is not. A brand of your own overrides `--accent-on-inverse` in its own CSS rule, like `--accent-strong`.
1804
1804
  - Calling `ref.editor` methods as part of the kit's contract — Tiptap commands follow the engine's major versions; common tasks are covered by the Handle (`insertHTML`, `getText`, `focus`, `undo`), and `editor` is the escape hatch for commands of Marks, Blocks and custom Tools. The exception is `showNotice`: it is a kit command and follows semver.
1805
- - Building the buttons of your own toolbar without `preventDefault` on `mousedown`: a click takes focus out of the field, and the floating Toolbar together with the Selection menu disappear.
1805
+ - Building the buttons of your own toolbar without `preventDefault` on `mousedown`: a click takes focus out of the field, and the floating Toolbar together with the Bubble menu disappear.
1806
1806
  - Reading `ref.current.editor.isActive(...)` during render of your own toolbar instead of `useRichTextEditorState`: the toolbar is not notified of a selection change and does not re-render.
1807
- - Wrapping the whole Status line in `role="status"`: the word count would be read aloud on every edit. Announce only what is needed (`Saved`).
1807
+ - Wrapping the whole Status bar in `role="status"`: the word count would be read aloud on every edit. Announce only what is needed (`Saved`).
1808
1808
  - Rendering your own error line under the field, duplicating the Field notice (paste and upload errors): `showNotice` already provides a line with `role="alert"`, and your own copy would be announced twice.
1809
1809
  - Wrapping short labels and headings in `RichTextEditor`: it is a field for a Document, not for a line.
1810
1810
  - Hiding a button with a style while leaving the Tool in `tools`: the Schema still lets it through — from a paste and by shortcut. Disable the Tool, not the button.
1811
1811
  - Changing `tools` on a mounted field expecting the Schema to be rebuilt: the set is read when the engine is created.
1812
- - 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).
1812
+ - Conveying the meaning of a note by the Callout 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).
1813
1813
  - 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.
1814
1814
  - 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.
1815
1815
  - 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.
@@ -1818,8 +1818,8 @@ There are no other classes, no `style` (except the author's color), no `aria-*`
1818
1818
 
1819
1819
  ## Links
1820
1820
 
1821
- - 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 collapsible Block is `src/editor/features/details/details.ts` (nodes, insertion, unwrapping, keyboard), `detailsMarkdown.ts` (Markdown as raw HTML) and `detailsTool.ts` (the Tool description); 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`.
1822
- - 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-details.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` (Extensions 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-details.spec.ts` (the collapsible Block: opening in the Editor and in `RichTextView`, the button from the keyboard), `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).
1821
+ - 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 Bubble 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 bar `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 Column 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 collapsible Block is `src/editor/features/details/details.ts` (nodes, insertion, unwrapping, keyboard), `detailsMarkdown.ts` (Markdown as raw HTML) and `detailsTool.ts` (the Tool description); 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`; Source 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 Keyboard shortcuts popover is `src/editor/ui/ShortcutsButton.tsx`; the Glass of popup surfaces is `src/editor/styles/glass.ts`; the Slash 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`.
1822
+ - 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-details.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` (Extensions 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 Slash 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 Bubble 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` (Column 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-details.spec.ts` (the collapsible Block: opening in the Editor and in `RichTextView`, the button from the keyboard), `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` (Source 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).
1823
1823
  - The extension API at a glance, "Stability" and recipes — [Editor extension API](../../api/editor-extension.md).
1824
1824
  - Viewing without editing — [RichTextView](../content/rich-text-view.md).
1825
1825
  - Storybook: `Inputs/RichTextEditor` (the `SearchStatusLine`, `TableOfContentsOptions`, `TableOfContentsExternalNavigation`, `ToolCommands`, `DefinedTool` and `DefinedToolConfigured` stories).