@bayonai/rich-text-editor 0.1.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/BEHAVIOR.md +396 -0
  2. package/CHANGELOG.md +22 -0
  3. package/README.md +25 -6
  4. package/dist/core/blockTree.d.ts +14 -0
  5. package/dist/core/blockTree.js +126 -0
  6. package/dist/core/blockTypes.d.ts +6 -0
  7. package/dist/core/blockTypes.js +5 -0
  8. package/dist/core/exportImport.d.ts +59 -0
  9. package/dist/core/exportImport.js +51 -0
  10. package/dist/core/features.d.ts +59 -0
  11. package/dist/core/features.js +57 -0
  12. package/dist/core/imageBlockDiagnostics.d.ts +4 -0
  13. package/dist/core/imageBlockDiagnostics.js +19 -0
  14. package/dist/core/proFeatures.d.ts +60 -0
  15. package/dist/core/proFeatures.js +64 -0
  16. package/dist/{richText.d.ts → core/richText.d.ts} +2 -0
  17. package/dist/core/richText.js +566 -0
  18. package/dist/core/types.d.ts +78 -0
  19. package/dist/index.d.ts +14 -8
  20. package/dist/index.js +8 -5
  21. package/dist/react/editor/RichTextBody.d.ts +28 -0
  22. package/dist/react/editor/RichTextBody.js +131 -0
  23. package/dist/react/editor/RichTextEditor.d.ts +138 -0
  24. package/dist/react/editor/RichTextEditor.js +2925 -0
  25. package/dist/react/editor/RichTextRenderedBlock.d.ts +20 -0
  26. package/dist/react/editor/RichTextRenderedBlock.js +162 -0
  27. package/dist/react/editor/RichTextRenderer.d.ts +13 -0
  28. package/dist/react/editor/RichTextRenderer.js +16 -0
  29. package/dist/react/{RichTextTitleInput.d.ts → editor/RichTextTitleInput.d.ts} +11 -1
  30. package/dist/react/{RichTextTitleInput.js → editor/RichTextTitleInput.js} +17 -2
  31. package/dist/react/editor/blockActions.d.ts +48 -0
  32. package/dist/react/editor/blockActions.js +495 -0
  33. package/dist/react/editor/editorHistory.d.ts +55 -0
  34. package/dist/react/editor/editorHistory.js +111 -0
  35. package/dist/react/{editorNavigation.d.ts → editor/editorNavigation.d.ts} +2 -0
  36. package/dist/react/{editorNavigation.js → editor/editorNavigation.js} +16 -0
  37. package/dist/react/editor/editorOperations.d.ts +10 -0
  38. package/dist/react/editor/editorOperations.js +3 -0
  39. package/dist/react/editor/editorSelection.d.ts +3 -0
  40. package/dist/react/editor/editorSelection.js +215 -0
  41. package/dist/react/{editorShortcuts.d.ts → editor/editorShortcuts.d.ts} +10 -0
  42. package/dist/react/{editorShortcuts.js → editor/editorShortcuts.js} +17 -1
  43. package/dist/react/{RichTextIcons.d.ts → icons/RichTextIcons.d.ts} +3 -0
  44. package/dist/react/{RichTextIcons.js → icons/RichTextIcons.js} +9 -0
  45. package/dist/react/index.d.ts +12 -9
  46. package/dist/react/index.js +7 -6
  47. package/dist/react/{EditorSessionProvider.d.ts → session/EditorSessionProvider.d.ts} +2 -2
  48. package/dist/react/{EditorSessionProvider.js → session/EditorSessionProvider.js} +3 -3
  49. package/dist/react/{UnsavedChangesDialog.js → session/UnsavedChangesDialog.js} +1 -1
  50. package/dist/react/styles/RichTextStyles.js +1362 -0
  51. package/dist/react/{BlockActionTool.d.ts → tools/BlockActionTool.d.ts} +1 -1
  52. package/dist/react/{BlockActionTool.js → tools/BlockActionTool.js} +6 -2
  53. package/dist/react/tools/LinkCreationInput.d.ts +9 -0
  54. package/dist/react/tools/LinkCreationInput.js +38 -0
  55. package/dist/react/{SelectionFormatToolbar.d.ts → tools/SelectionFormatToolbar.d.ts} +3 -2
  56. package/dist/react/{SelectionFormatToolbar.js → tools/SelectionFormatToolbar.js} +3 -3
  57. package/dist/react/tools/SpecialBlockOption.d.ts +9 -0
  58. package/dist/react/tools/SpecialBlockOption.js +8 -0
  59. package/dist/react/tools/SpecialBlockTool.d.ts +91 -0
  60. package/dist/react/tools/SpecialBlockTool.js +125 -0
  61. package/dist/react/{TranscriptionControl.d.ts → tools/TranscriptionControl.d.ts} +9 -0
  62. package/dist/react/{TranscriptionControl.js → tools/TranscriptionControl.js} +70 -9
  63. package/dist/react/tools/blockActionToolState.d.ts +41 -0
  64. package/dist/react/tools/blockActionToolState.js +177 -0
  65. package/dist/react/tools/imageBlockDiagnostics.d.ts +2 -0
  66. package/dist/react/tools/imageBlockDiagnostics.js +12 -0
  67. package/dist/{session.d.ts → session/session.d.ts} +1 -1
  68. package/dist-cjs/core/blockTree.js +137 -0
  69. package/dist-cjs/core/blockTypes.js +9 -0
  70. package/dist-cjs/core/exportImport.js +56 -0
  71. package/dist-cjs/core/features.js +62 -0
  72. package/dist-cjs/core/proFeatures.js +70 -0
  73. package/dist-cjs/core/richText.js +578 -0
  74. package/dist-cjs/index.js +22 -6
  75. package/dist-cjs/react/editor/RichTextBody.js +134 -0
  76. package/dist-cjs/react/editor/RichTextEditor.js +2956 -0
  77. package/dist-cjs/react/editor/RichTextRenderedBlock.js +166 -0
  78. package/dist-cjs/react/editor/RichTextRenderer.js +20 -0
  79. package/dist-cjs/react/{RichTextTitleInput.js → editor/RichTextTitleInput.js} +18 -2
  80. package/dist-cjs/react/editor/blockActions.js +518 -0
  81. package/dist-cjs/react/editor/editorHistory.js +120 -0
  82. package/dist-cjs/react/{editorNavigation.js → editor/editorNavigation.js} +17 -0
  83. package/dist-cjs/react/editor/editorOperations.js +6 -0
  84. package/dist-cjs/react/editor/editorSelection.js +219 -0
  85. package/dist-cjs/react/{editorShortcuts.js → editor/editorShortcuts.js} +17 -1
  86. package/dist-cjs/react/{RichTextIcons.js → icons/RichTextIcons.js} +12 -0
  87. package/dist-cjs/react/index.js +9 -7
  88. package/dist-cjs/react/{EditorSessionProvider.js → session/EditorSessionProvider.js} +3 -3
  89. package/dist-cjs/react/{UnsavedChangesDialog.js → session/UnsavedChangesDialog.js} +1 -1
  90. package/dist-cjs/react/styles/RichTextStyles.js +1365 -0
  91. package/dist-cjs/react/{BlockActionTool.js → tools/BlockActionTool.js} +6 -2
  92. package/dist-cjs/react/tools/LinkCreationInput.js +41 -0
  93. package/dist-cjs/react/{SelectionFormatToolbar.js → tools/SelectionFormatToolbar.js} +3 -3
  94. package/dist-cjs/react/tools/SpecialBlockOption.js +11 -0
  95. package/dist-cjs/react/tools/SpecialBlockTool.js +129 -0
  96. package/dist-cjs/react/{TranscriptionControl.js → tools/TranscriptionControl.js} +71 -9
  97. package/dist-cjs/react/tools/blockActionToolState.js +186 -0
  98. package/package.json +3 -2
  99. package/dist/react/RichTextBody.d.ts +0 -18
  100. package/dist/react/RichTextBody.js +0 -66
  101. package/dist/react/RichTextEditor.d.ts +0 -45
  102. package/dist/react/RichTextEditor.js +0 -1096
  103. package/dist/react/RichTextRenderedBlock.d.ts +0 -4
  104. package/dist/react/RichTextRenderedBlock.js +0 -36
  105. package/dist/react/RichTextRenderer.d.ts +0 -4
  106. package/dist/react/RichTextRenderer.js +0 -8
  107. package/dist/react/RichTextStyles.js +0 -719
  108. package/dist/react/SpecialBlockOption.d.ts +0 -7
  109. package/dist/react/SpecialBlockOption.js +0 -7
  110. package/dist/react/SpecialBlockTool.d.ts +0 -42
  111. package/dist/react/SpecialBlockTool.js +0 -50
  112. package/dist/react/blockActionToolState.d.ts +0 -18
  113. package/dist/react/blockActionToolState.js +0 -53
  114. package/dist/react/blockActions.d.ts +0 -8
  115. package/dist/react/blockActions.js +0 -111
  116. package/dist/richText.js +0 -297
  117. package/dist/types.d.ts +0 -34
  118. package/dist-cjs/react/RichTextBody.js +0 -69
  119. package/dist-cjs/react/RichTextEditor.js +0 -1108
  120. package/dist-cjs/react/RichTextRenderedBlock.js +0 -39
  121. package/dist-cjs/react/RichTextRenderer.js +0 -11
  122. package/dist-cjs/react/RichTextStyles.js +0 -722
  123. package/dist-cjs/react/SpecialBlockOption.js +0 -10
  124. package/dist-cjs/react/SpecialBlockTool.js +0 -54
  125. package/dist-cjs/react/blockActionToolState.js +0 -58
  126. package/dist-cjs/react/blockActions.js +0 -119
  127. package/dist-cjs/richText.js +0 -307
  128. /package/dist/{types.js → core/types.js} +0 -0
  129. /package/dist/{writingStats.d.ts → core/writingStats.d.ts} +0 -0
  130. /package/dist/{writingStats.js → core/writingStats.js} +0 -0
  131. /package/dist/react/{RichTextDocumentSurface.d.ts → editor/RichTextDocumentSurface.d.ts} +0 -0
  132. /package/dist/react/{RichTextDocumentSurface.js → editor/RichTextDocumentSurface.js} +0 -0
  133. /package/dist/react/{UnsavedChangesDialog.d.ts → session/UnsavedChangesDialog.d.ts} +0 -0
  134. /package/dist/react/{RichTextStyles.d.ts → styles/RichTextStyles.d.ts} +0 -0
  135. /package/dist/react/{richTextBlockStyles.d.ts → styles/richTextBlockStyles.d.ts} +0 -0
  136. /package/dist/react/{richTextBlockStyles.js → styles/richTextBlockStyles.js} +0 -0
  137. /package/dist/react/{specialBlockStyles.d.ts → styles/specialBlockStyles.d.ts} +0 -0
  138. /package/dist/react/{specialBlockStyles.js → styles/specialBlockStyles.js} +0 -0
  139. /package/dist/{saveControl.d.ts → session/saveControl.d.ts} +0 -0
  140. /package/dist/{saveControl.js → session/saveControl.js} +0 -0
  141. /package/dist/{session.js → session/session.js} +0 -0
  142. /package/dist/{sessionRegistry.d.ts → session/sessionRegistry.d.ts} +0 -0
  143. /package/dist/{sessionRegistry.js → session/sessionRegistry.js} +0 -0
  144. /package/dist-cjs/{types.js → core/types.js} +0 -0
  145. /package/dist-cjs/{writingStats.js → core/writingStats.js} +0 -0
  146. /package/dist-cjs/react/{RichTextDocumentSurface.js → editor/RichTextDocumentSurface.js} +0 -0
  147. /package/dist-cjs/react/{richTextBlockStyles.js → styles/richTextBlockStyles.js} +0 -0
  148. /package/dist-cjs/react/{specialBlockStyles.js → styles/specialBlockStyles.js} +0 -0
  149. /package/dist-cjs/{saveControl.js → session/saveControl.js} +0 -0
  150. /package/dist-cjs/{session.js → session/session.js} +0 -0
  151. /package/dist-cjs/{sessionRegistry.js → session/sessionRegistry.js} +0 -0
package/BEHAVIOR.md ADDED
@@ -0,0 +1,396 @@
1
+ # Rich Text Editor Behavior
2
+
3
+ This document describes the visible behavior and internal structure contracts for
4
+ `@bayonai/rich-text-editor`. It is intended for maintainers changing editor
5
+ behavior. Public package APIs and the persisted `RichTextBlock` shape should
6
+ remain stable unless a migration is explicitly planned.
7
+
8
+ ## Core Model
9
+
10
+ The saved document model is `RichTextBlock[]`.
11
+
12
+ Top-level blocks can be paragraphs, headings, quotes, code, dividers, images,
13
+ bullets, ordered rows, checkboxes, or toggles. Only tree rows are nestable:
14
+
15
+ - `bullet`
16
+ - `ordered`
17
+ - `checkbox`
18
+ - `toggle`
19
+
20
+ Tree rows carry child blocks in `children`. Toggles also carry `collapsed`.
21
+ Checkboxes also carry `checked`.
22
+
23
+ Paragraphs, headings, and quotes currently stay flat. Do not introduce generic
24
+ block nesting unless the editor model and persistence boundaries are planned
25
+ for it.
26
+
27
+ ## Source Responsibilities
28
+
29
+ - `src/core/types.ts` defines the persisted block shape.
30
+ - `src/core/richText.ts` owns sanitization, Markdown conversion, clipboard
31
+ encode/decode, and public non-React helpers.
32
+ - `src/react/editor/RichTextEditor.tsx` owns the editable DOM, semantic edit
33
+ handlers, DOM-to-block parsing, editor HTML serialization, selection
34
+ restoration, and keyboard behavior.
35
+ - `src/react/editor/RichTextBody.tsx` is the `contentEditable` shell and should
36
+ stay thin.
37
+ - `src/react/editor/blockActions.ts` owns pure block operations such as split,
38
+ delete, indent, outdent, reorder, and clipboard text for block actions.
39
+ - `src/react/editor/RichTextRenderedBlock.tsx` owns read-only rendering and must
40
+ stay visually aligned with editor rendering.
41
+ - `src/react/styles/RichTextStyles.tsx` owns package styles for both editable
42
+ and read-only surfaces.
43
+
44
+ When possible, add behavior in pure helpers first, cover it with focused tests,
45
+ then connect it to the editable DOM.
46
+
47
+ ## Component Behavior Inventory
48
+
49
+ The React package surface is intentionally small. Keep these responsibilities
50
+ stable so host apps can compose the editor without depending on private DOM
51
+ details.
52
+
53
+ - `RichTextEditor` is the full controlled editing experience. It composes the
54
+ title, document surface, editable body, selection toolbar, special block
55
+ insertion tool, block action handles, drag/drop state, stats, and optional
56
+ transcription. It must emit sanitized `RichTextBlock[]` updates through
57
+ `onContentBlocksChange` and title updates through `onTitleChange`.
58
+ - `RichTextDocumentSurface` provides the shared editor/read-only document
59
+ frame. It owns only the transparent vs panel background shell and should not
60
+ learn editor state or block semantics.
61
+ - `RichTextTitleInput` renders the editable auto-growing title textarea.
62
+ `Enter` and `ArrowDown` move focus to the start of the body. Validation text
63
+ is announced with `role="alert"` and the input exposes invalid/required ARIA
64
+ state.
65
+ - `RichTextReadTitle` renders the read-only title with the same textarea
66
+ sizing and package title styles as the editable title. Use it for view/edit
67
+ parity instead of duplicating title chrome in host apps.
68
+ - `RichTextBody` is the only `contentEditable` surface. It delegates semantic
69
+ Enter, Backspace, Delete, Tab, paste, toggle-collapse, and formatting shortcut
70
+ behavior to `RichTextEditor`; it should stay a thin event boundary.
71
+ - `RichTextRenderer` renders sanitized blocks for read-only views and injects
72
+ the shared package styles. When `onContentBlocksChange` is supplied, rendered
73
+ toggles are interactive and persist collapsed-state changes through the same
74
+ block shape.
75
+ - `RichTextRenderedBlocks` and `RichTextRenderedBlock` own read-only block
76
+ markup. They group adjacent tree rows into semantic `ul`/`ol` lists, render
77
+ nested children recursively, preserve checkbox checked state, and keep toggle
78
+ title/content structure aligned with the editor DOM.
79
+ - `RichTextStyleScope` injects the package CSS needed by editor, renderer,
80
+ title, transcription, block tools, and unsaved-change dialog surfaces. New
81
+ visible package components should reuse these classes rather than shipping
82
+ app-specific styles.
83
+ - `SelectionFormatToolbar` is the floating selection toolbar. It uses icon
84
+ buttons with labels/tooltips for bold, italic, link, title, quote, and code,
85
+ and it must keep mouse-down from stealing the active text selection.
86
+ - `SpecialBlockTool` and `SpecialBlockOption` render the floating add-block
87
+ control for empty insertion targets. The action catalog is the single source
88
+ of truth for special block labels, icons, inserted HTML, and initial focus
89
+ behavior.
90
+ - `BlockActionTool` renders each visible block handle and its copy, cut, delete,
91
+ and close icon controls. It owns pointer capture/suppressed-click behavior for
92
+ drag handles, but block movement and clipboard semantics stay in pure helpers.
93
+ - `TranscriptionControl` owns browser `SpeechRecognition` integration. It must
94
+ remain browser-local, expose clear ready/recording/success/error/unavailable
95
+ states, stop recording on unmount, and report final transcript text through
96
+ `onTranscript` only. It must treat mobile Safari as unavailable even when
97
+ `webkitSpeechRecognition` exists, because those sessions can get stuck.
98
+ - `EditorSessionProvider` owns the shared unsaved-change registry, beforeunload
99
+ guard, and pending-exit dialog. Use `useEditorSession` for a controlled
100
+ editor's dirty/save/reset state and `useEditorExitGuard` for route/navigation
101
+ guards.
102
+ - `UnsavedChangesDialog` is the default pending-exit UI. It lets users stay,
103
+ discard and leave, or save and leave; backdrop clicks stay on the page unless
104
+ a save is in progress, and save errors are announced.
105
+ - `RichTextIcons` are package-local presentational icons. They should stay
106
+ side-effect free and accessible labels should live on the buttons that render
107
+ them.
108
+
109
+ ## Baseline Patterns Intention
110
+
111
+ - Treat every visible row as a block. Paragraphs, headings, quotes, code,
112
+ media placeholders, dividers, list rows, checkboxes, and toggles should all
113
+ feel independently selectable, movable, copyable, and deletable when that
114
+ action is supported.
115
+ - Keep creation shortcuts lightweight and Markdown-like. A user should be able
116
+ to create common structure from the keyboard without opening menus: list
117
+ markers, checkbox syntax, numbered rows, headings, quotes, toggles, dividers,
118
+ and code blocks should remain discoverable through text shortcuts.
119
+ - Prefer contextual chrome over persistent controls. Block handles, insertion
120
+ tools, drag affordances, and selection formatting should appear near the
121
+ active or hovered content and stay quiet when the user is simply reading or
122
+ typing.
123
+ - Preserve keyboard-first editing. Enter creates the next natural row,
124
+ Shift+Enter creates an intentional soft break, Tab and Shift+Tab change list
125
+ hierarchy, Backspace exits or removes structure at the beginning of a block,
126
+ and common formatting shortcuts should not require the mouse.
127
+ - Keep block transforms conceptually reversible. When a row changes from one
128
+ type to another, preserve its visible text and caret position where practical
129
+ instead of treating the action as delete plus recreate.
130
+ - Make toggles manual and predictable. Toggle state changes only when the user
131
+ activates the toggle control or an explicit shortcut. Collapsing hides child
132
+ content visually but must not mutate, reorder, or discard those children.
133
+ - Keep drag/drop scoped to obvious block movement. Moving a block should move
134
+ its subtree, show a precise destination indicator, and avoid accidental
135
+ structural changes when the pointer is released on an invalid or ambiguous
136
+ target.
137
+ - Keep read-only output visually faithful to editing output. Users should not
138
+ need to learn a different document shape when switching between edit and
139
+ view modes.
140
+ - Keep mobile editing uncluttered. Favor the document content and the current
141
+ action target; avoid always-visible heavy toolbars, wide controls, or
142
+ side-scrolling layouts.
143
+ - Use icon buttons with accessible labels and tooltips for compact editor
144
+ controls. Text labels belong in menus, dialogs, validation, and ambiguous
145
+ destructive flows where the extra clarity is worth the space.
146
+
147
+ ## Current Baseline Discrepancies
148
+
149
+ These are known places where the current editor does not fully meet the
150
+ baseline patterns above. Treat this section as implementation guidance for
151
+ future editor polish, not as permission to broaden unrelated bug fixes.
152
+
153
+ - Block transforms are only partial. Text shortcuts create structure, Backspace
154
+ can convert supported tree rows back to paragraphs, and the selection toolbar
155
+ can format selected text as heading, quote, or code. There is not yet a
156
+ general row-level transform menu for changing an existing block between
157
+ paragraph, heading, quote, list row, checkbox, toggle, code, divider, or image
158
+ while preserving text, children, and caret placement.
159
+ - The shortcut set is intentionally smaller than a full Notion-style shortcut
160
+ map. The editor supports common patterns such as `# `, `> `, `>> `, `- `,
161
+ `* `, `1. `, `[]`, `[x]`, divider markers, and fenced-code starts, but it
162
+ does not expose multiple heading levels, slash commands, or Notion-exact
163
+ toggle/list shortcuts.
164
+ - The floating special-block insertion tool does not expose every basic block
165
+ type. It currently includes image, quote, title, bullet list, toggle, code
166
+ block, embedded HTML, and divider. Ordered rows and checkboxes are keyboard
167
+ shortcuts only unless a host builds additional chrome.
168
+ - Keyboard behavior covers core editing but not full block navigation. There is
169
+ no dedicated block-selection mode, no keyboard command for opening the block
170
+ action menu, no shortcut to toggle a checkbox or collapse a toggle from
171
+ anywhere in the row, and no expand/collapse-all toggle command.
172
+ - Read-only rendering is close to edit-mode structure, but it is not a perfect
173
+ mirror. Empty toggle content is omitted in read-only output, URL-backed image
174
+ blocks render their image, upload-progress placeholders stay editor-only, and
175
+ read-only block actions are not available.
176
+ - Mobile simplicity is an intention rather than a fully verified contract. The
177
+ editor uses contextual controls, but viewport-level checks should verify that
178
+ block handles, selection toolbar, special-block tool, transcription control,
179
+ and nested toggle/list rows do not create horizontal scrolling or obscure the
180
+ active text target on small screens.
181
+
182
+ ## Tree Row Structure
183
+
184
+ Bullets, ordered rows, checkboxes, and toggles are all tree rows. Shared tree
185
+ row behavior should use shared helpers rather than type-specific branches.
186
+
187
+ Shared concepts:
188
+
189
+ - row detection: `getTreeRowEditorParts(...)`
190
+ - label focus: `focusTreeRowLabelAtTextOffset(...)`
191
+ - Enter splitting: `splitTreeRowMarkdownAtTextOffset(...)` and
192
+ `splitTreeRowBlock(...)`
193
+ - Backspace paragraph exit:
194
+ `getTreeRowBackspaceParagraphMarkdown(...)` and
195
+ `getTreeRowBackspaceFocusPositionForParagraph(...)`
196
+ - nesting: `indentBlock(...)`, `outdentBlock(...)`, `reorderBlock(...)`
197
+
198
+ Type-specific behavior should be limited to actual type differences:
199
+
200
+ - bullet marker vs ordered counter vs checkbox input
201
+ - checkbox `checked` state
202
+ - toggle `collapsed` state and content box
203
+ - clipboard prefixes such as `- `, `1. `, `[ ] `, and `[x] `
204
+
205
+ ## Cursor Behavior Guidelines
206
+
207
+ Prefer visually consistent behavior over a technically pure model
208
+ interpretation.
209
+
210
+ Selection and toolbar behavior:
211
+
212
+ - Text selection shows the formatting toolbar above block handles and hover
213
+ zones.
214
+ - While a selection toolbar is active, formatting controls take priority over
215
+ block drag controls.
216
+
217
+ Backspace at the beginning of a tree-row label:
218
+
219
+ - Empty bullet exits to a paragraph containing `*`, with the caret after `*`.
220
+ - Empty ordered row exits to a paragraph containing `1.`, with the caret after
221
+ `1.`.
222
+ - Empty checkbox exits to an empty paragraph, with the caret at the start.
223
+ - Non-empty bullet, ordered row, or checkbox exits to a paragraph containing the
224
+ same text, with the caret at the start of that text. This keeps the caret
225
+ visually where it was before the marker/input was removed.
226
+ - Empty toggle title removes the toggle only when its content is empty.
227
+ - Toggle content must not backspace into the title. The title and content box
228
+ are a strict pair.
229
+ - Backspace in an empty text block inside toggle content deletes that block
230
+ when there is a previous content sibling to focus. It does nothing at the top
231
+ of an otherwise empty content box.
232
+ - Backspace immediately below a toggle must not pull the block into the toggle.
233
+ Toggle title and content are not allowed to merge with adjacent blocks.
234
+
235
+ Delete in an empty editable block:
236
+
237
+ - Forward Delete removes the empty current block and moves focus to the next
238
+ visible block when one exists.
239
+ - If no next focus target exists, Delete does not remove the final editable
240
+ fallback block.
241
+
242
+ Enter inside a tree-row label:
243
+
244
+ - Splits at the browser-visible cursor offset.
245
+ - Preserves intentional trailing spaces on the previous row.
246
+ - Moves children to the new visual sibling when a parent row is split.
247
+ - Empty tree rows become a default paragraph.
248
+ - Paragraph-like blocks inside toggle content split inside the same toggle
249
+ content wrapper. Enter should not break them out of the toggle.
250
+ - Shift+Enter stays inside the current row and inserts a visible soft break.
251
+
252
+ Tab and Shift+Tab:
253
+
254
+ - Use the shared block tree operations.
255
+ - Preserve following child sequences when outdenting.
256
+ - Keep focus at the same visible label offset when practical.
257
+
258
+ Placeholders and empty hit targets:
259
+
260
+ - Empty checkbox labels are visually empty and must not use a `Task`
261
+ placeholder.
262
+ - Empty checkbox labels still need a stable same-line editable hit area beside
263
+ the checkbox input.
264
+ - Empty list and toggle labels may show calm placeholders without selecting
265
+ placeholder text.
266
+ - Empty toggle content shows the `Toggle content` placeholder inside its content
267
+ wrapper.
268
+
269
+ ## Toggle Guidelines
270
+
271
+ A toggle is one strict structure:
272
+
273
+ 1. title row
274
+ 2. content wrapper
275
+
276
+ The content wrapper should always exist while editing, including when empty.
277
+ It is an independent nested document area and a drop target for dragged blocks.
278
+ Collapsed toggles hide content visually but keep child blocks in document data.
279
+
280
+ Backspace should not allow content rows to become title rows or adjacent root
281
+ blocks to be absorbed into a toggle. Structural changes must preserve the
282
+ title/content pairing.
283
+
284
+ All direct toggle content blocks render inside one shared content wrapper. Do
285
+ not render a separate bordered content box per child block.
286
+
287
+ Creating a toggle focuses the beginning of the title for both the special block
288
+ action and `>>` shortcut-created toggles.
289
+
290
+ Drag and drop inside toggle content should use the same block movement helpers
291
+ as the rest of the editor. Dropping onto the content wrapper places the dragged
292
+ block inside the toggle; dropping among rows inside the wrapper targets those
293
+ rows first.
294
+
295
+ Toggling collapsed state should preserve scroll position and should never drop
296
+ children.
297
+
298
+ ## Block Actions and Drag
299
+
300
+ Every visible block row should have its own block handle when hovered or active,
301
+ including paragraphs inside toggle content and individual checkbox items.
302
+ Empty paragraphs are still visible block rows: they should expose a drag handle
303
+ and, when they are also valid insertion targets, the special block plus control.
304
+ Those controls should sit in separate gutter slots so neither hides the other.
305
+
306
+ Handle visibility and placement:
307
+
308
+ - Deeply nested rows keep their hover lane aligned with their visual row.
309
+ - Checkbox handles use a small raised anchor to align with the checkbox input.
310
+ - Toggle handles use the normal row anchor.
311
+ - Block action menus include copy, cut, delete, and close icon controls.
312
+ - Deleting from the block action menu preserves scroll position.
313
+
314
+ Drag behavior:
315
+
316
+ - Dragging a block moves the whole block subtree.
317
+ - Before and after drops match the target sibling's visual indentation level.
318
+ - Moving the pointer into a compatible indentation lane drops inside the target
319
+ row.
320
+ - Moving the pointer to a shallower indentation lane drops beside the visual
321
+ ancestor.
322
+ - The drop indicator should draw at the indentation level that the drop will
323
+ produce.
324
+ - Toggle content boxes are valid drop targets, but row targets inside them take
325
+ priority.
326
+ - Invalid drag targets show the drop indicator in red. Releasing onto the
327
+ dragged block itself, one of its descendants, or no meaningful target must not
328
+ mutate the document or trigger a content change.
329
+
330
+ ## Clipboard and Persistence
331
+
332
+ Clipboard round trips should prefer the structured rich-text encoding when
333
+ available. Plain-text clipboard export should remain readable and compatible
334
+ with common Markdown-like input:
335
+
336
+ - bullet: `- text`
337
+ - ordered: `1. text`
338
+ - unchecked checkbox: `[ ] text`
339
+ - checked checkbox: `[x] text`
340
+ - toggle: `> title`
341
+
342
+ Persistence boundaries must preserve nested `children`, checkbox `checked`, and
343
+ toggle `collapsed`. If the saved block shape changes, update package tests,
344
+ application mappers, and Functions mappers together.
345
+
346
+ Pasting behavior:
347
+
348
+ - Rich-text clipboard payloads should decode back into the same block structure
349
+ when possible.
350
+ - Pasting into an empty editable block replaces that block instead of creating
351
+ an extra blank row.
352
+ - Pasting into an empty toggle content placeholder inserts the pasted blocks as
353
+ toggle children.
354
+ - Plain-text shortcuts such as `[]`, `* `, `1. `, and `>>` should continue to
355
+ work inside toggle content.
356
+
357
+ Typography and whitespace:
358
+
359
+ - Interior repeated spaces and intentional trailing spaces should be preserved
360
+ through editor storage and rendering.
361
+ - Spacer entities inserted by the browser should be dropped when they are only
362
+ editor artifacts.
363
+ - Enter splitting should use browser-visible text offsets, including soft
364
+ breaks and emoji.
365
+ - List-like rows should remain compact vertically.
366
+
367
+ ## Transcription
368
+
369
+ Hosts may enable browser-native transcription with `transcriptionEnabled`.
370
+ The editor inserts final transcript text into the current body selection with
371
+ readable spacing. Mobile Safari should show the unavailable state instead of
372
+ starting `webkitSpeechRecognition`, which can leave the browser in a stuck
373
+ recording session.
374
+
375
+ If a host needs the transcription control in external chrome, it should pass
376
+ `transcriptionPortalContainer`. The control must stay React-owned through a
377
+ portal; host components must not move `.bayon-rte-transcription` with direct DOM
378
+ operations such as `appendChild`.
379
+
380
+ ## Testing and Build Expectations
381
+
382
+ For behavior changes, add focused tests near the helper or component being
383
+ changed. Prefer pure helper tests for tree operations before DOM integration.
384
+
385
+ Common verification commands:
386
+
387
+ ```powershell
388
+ pnpm --filter @bayonai/rich-text-editor test -- --run
389
+ pnpm --filter @bayonai/rich-text-editor lint
390
+ pnpm --filter @bayonai/rich-text-editor build
391
+ pnpm exec tsc --noEmit -p tsconfig.json
392
+ git diff --check
393
+ ```
394
+
395
+ The repository tracks package build output. After changing rich-text package
396
+ source, rebuild the package so tracked output stays aligned.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - Added nested bullet, ordered, checkbox, and toggle block support with
6
+ recursive sanitization and editable/read-only rendering built on recursive
7
+ `ul`/`ol` plus `li` list DOM.
8
+ - Added Tab and Shift+Tab handling for indenting and outdenting nestable rows
9
+ while leaving paragraph, heading, quote, code, divider, and image blocks flat.
10
+ - Added `* ` and `1. ` shortcuts for unordered and ordered rows.
11
+ - Unified Enter behavior across bullet, ordered, checkbox, and toggle rows so
12
+ row splitting uses the caret position consistently.
13
+ - Fixed Enter at the end of title blocks so the following row starts as default
14
+ paragraph text.
15
+ - Fixed ArrowUp in body text so the browser moves to the previous visual line
16
+ instead of jumping focus to the title.
17
+ - Removed top-level list indentation and nested list borders while keeping
18
+ indentation on child list containers.
19
+ - Kept empty special-created list and toggle rows as placeholder rows instead
20
+ of selected placeholder text.
21
+ - Added editable toggle collapse and expand controls that preserve child
22
+ content while storing collapsed state on the toggle block.
23
+ - Aligned block action handles with indented nested rows.
24
+ - Added an editor operation boundary with document and selection snapshots to
25
+ prepare semantic editor actions for future undo/redo support.
26
+
5
27
  ## 0.1.2 - 2026-06-24
6
28
 
7
29
  - Added block-level action handles for non-empty editor blocks.
package/README.md CHANGED
@@ -14,10 +14,17 @@ The package has no Next.js, Firebase, or application-domain dependencies.
14
14
  Hosts provide controlled document values, optional metadata, persistence
15
15
  callbacks, and navigation adapters.
16
16
 
17
+ ## Maintainer Notes
18
+
19
+ Internal structure and behavior contracts are documented in
20
+ [`BEHAVIOR.md`](./BEHAVIOR.md). Read that file before changing tree-row,
21
+ toggle, cursor, clipboard, or persistence behavior.
22
+
17
23
  ## Editor Capabilities
18
24
 
19
25
  - Native rich-text body editing with headings, quotes, code, dividers,
20
- checkboxes, image placeholders, and Markdown-compatible text storage.
26
+ bullets, ordered bullets, checkboxes, toggles, image placeholders, and
27
+ Markdown-compatible text storage.
21
28
  - Empty, undesignated blocks expose the special block insertion tool.
22
29
  - Non-empty top-level blocks expose a compact gutter handle for block actions.
23
30
  - Dragging a block handle reorders blocks with a visible drop indicator.
@@ -26,12 +33,24 @@ callbacks, and navigation adapters.
26
33
  block only after the clipboard write succeeds.
27
34
  - Deleting the last block preserves one empty paragraph so the editor remains
28
35
  writable.
29
- - Checkbox rows can be reordered individually; copy, cut, and delete actions
30
- still apply to consecutive checkbox rows as one action group. Pressing Enter
31
- from a checkbox creates the next checkbox, and Backspace at the start of a
32
- checkbox row converts that row back to a paragraph.
36
+ - Checkbox rows can be created with the `[]` shortcut, reordered individually,
37
+ copied, cut, and deleted. Pressing Enter from a checkbox creates the next
38
+ checkbox, and Backspace at the start of a checkbox row converts that row back
39
+ to a paragraph.
40
+ - Bullet rows can be created with the `* ` shortcut, and ordered rows can be
41
+ created with the `1. ` shortcut. Empty list-like rows show placeholders
42
+ instead of selecting placeholder text.
43
+ - Bullet, ordered, checkbox, and toggle rows can hold nested children. Pressing
44
+ Tab indents a nestable row under the previous compatible row, and Shift+Tab
45
+ promotes it one level. Paragraph, heading, quote, code, divider, and image
46
+ blocks remain flat. Editable and read-only nested rows render as recursive
47
+ `ul`/`ol` plus `li` trees so list-like content keeps semantic list structure.
48
+ - Toggle rows render as collapsible sections in read-only output and preserve
49
+ their collapsed state in the stored block. Editable toggles can also be
50
+ collapsed and expanded without dropping child content.
33
51
  - Browser-native transcription can be enabled by hosts with the editor's
34
52
  `transcriptionEnabled` option. It defaults to enabled, uses the browser
35
53
  `SpeechRecognition` or `webkitSpeechRecognition` API when available, inserts
36
54
  final transcript text into the current editor body, and never uploads or
37
- stores audio.
55
+ stores audio. Mobile Safari is treated as unavailable because its prefixed
56
+ recognition sessions can get stuck and leave the editor unusable.
@@ -0,0 +1,14 @@
1
+ import { type NestableRichTextBlock } from "./blockTypes";
2
+ import type { RichTextBlock, RichTextBlockPath } from "./types";
3
+ export declare function findBlockPath(blocks: RichTextBlock[], blockId: string): RichTextBlockPath | null;
4
+ export declare function getBlockAtPath(blocks: RichTextBlock[], path: RichTextBlockPath): RichTextBlock | null;
5
+ export declare function getBlocksAtPath(blocks: RichTextBlock[], path: RichTextBlockPath): RichTextBlock[] | null;
6
+ export declare function removeBlockAtPath(blocks: RichTextBlock[], path: RichTextBlockPath): {
7
+ block: RichTextBlock;
8
+ blocks: RichTextBlock[];
9
+ } | null;
10
+ export declare function updateBlockAtPath(blocks: RichTextBlock[], path: RichTextBlockPath, updater: (block: RichTextBlock) => RichTextBlock): RichTextBlock[];
11
+ export declare function insertBlockAtPath(blocks: RichTextBlock[], parentPath: RichTextBlockPath, index: number, insertedBlocks: RichTextBlock[]): RichTextBlock[];
12
+ export declare function pathContains(parentPath: RichTextBlockPath, childPath: RichTextBlockPath): boolean;
13
+ export declare function withNestableBlockChildren(block: NestableRichTextBlock, children: RichTextBlock[]): NestableRichTextBlock;
14
+ export declare function withBlockChildren(block: RichTextBlock, children: RichTextBlock[]): RichTextBlock;
@@ -0,0 +1,126 @@
1
+ import { isNestableRichTextBlock } from "./blockTypes.js";
2
+ export function findBlockPath(blocks, blockId) {
3
+ for (let index = 0; index < blocks.length; index += 1) {
4
+ const block = blocks[index];
5
+ if (!block) {
6
+ continue;
7
+ }
8
+ if (block.id === blockId) {
9
+ return [index];
10
+ }
11
+ if (isNestableRichTextBlock(block) && block.children?.length) {
12
+ const childPath = findBlockPath(block.children, blockId);
13
+ if (childPath) {
14
+ return [index, ...childPath];
15
+ }
16
+ }
17
+ }
18
+ return null;
19
+ }
20
+ export function getBlockAtPath(blocks, path) {
21
+ let currentBlocks = blocks;
22
+ let currentBlock;
23
+ for (const index of path) {
24
+ currentBlock = currentBlocks[index];
25
+ if (!currentBlock) {
26
+ return null;
27
+ }
28
+ currentBlocks = isNestableRichTextBlock(currentBlock)
29
+ ? currentBlock.children ?? []
30
+ : [];
31
+ }
32
+ return currentBlock ?? null;
33
+ }
34
+ export function getBlocksAtPath(blocks, path) {
35
+ if (path.length === 0) {
36
+ return blocks;
37
+ }
38
+ const parent = getBlockAtPath(blocks, path);
39
+ return isNestableRichTextBlock(parent) ? parent.children ?? [] : null;
40
+ }
41
+ export function removeBlockAtPath(blocks, path) {
42
+ const [index, ...rest] = path;
43
+ if (index === undefined) {
44
+ return null;
45
+ }
46
+ if (rest.length === 0) {
47
+ const block = blocks[index];
48
+ if (!block) {
49
+ return null;
50
+ }
51
+ return {
52
+ block,
53
+ blocks: [...blocks.slice(0, index), ...blocks.slice(index + 1)],
54
+ };
55
+ }
56
+ const parent = blocks[index];
57
+ if (!isNestableRichTextBlock(parent)) {
58
+ return null;
59
+ }
60
+ const removed = removeBlockAtPath(parent.children ?? [], rest);
61
+ if (!removed) {
62
+ return null;
63
+ }
64
+ const updatedParent = withNestableBlockChildren(parent, removed.blocks);
65
+ return {
66
+ block: removed.block,
67
+ blocks: [
68
+ ...blocks.slice(0, index),
69
+ updatedParent,
70
+ ...blocks.slice(index + 1),
71
+ ],
72
+ };
73
+ }
74
+ export function updateBlockAtPath(blocks, path, updater) {
75
+ const [index, ...rest] = path;
76
+ if (index === undefined) {
77
+ return blocks;
78
+ }
79
+ const block = blocks[index];
80
+ if (!block) {
81
+ return blocks;
82
+ }
83
+ const updatedBlock = rest.length === 0
84
+ ? updater(block)
85
+ : isNestableRichTextBlock(block)
86
+ ? withNestableBlockChildren(block, updateBlockAtPath(block.children ?? [], rest, updater))
87
+ : block;
88
+ return [...blocks.slice(0, index), updatedBlock, ...blocks.slice(index + 1)];
89
+ }
90
+ export function insertBlockAtPath(blocks, parentPath, index, insertedBlocks) {
91
+ if (parentPath.length === 0) {
92
+ return [
93
+ ...blocks.slice(0, index),
94
+ ...insertedBlocks,
95
+ ...blocks.slice(index),
96
+ ];
97
+ }
98
+ return updateBlockAtPath(blocks, parentPath, (parent) => {
99
+ if (!isNestableRichTextBlock(parent)) {
100
+ return parent;
101
+ }
102
+ return withNestableBlockChildren(parent, [
103
+ ...(parent.children ?? []).slice(0, index),
104
+ ...insertedBlocks,
105
+ ...(parent.children ?? []).slice(index),
106
+ ]);
107
+ });
108
+ }
109
+ export function pathContains(parentPath, childPath) {
110
+ return parentPath.every((index, pathIndex) => childPath[pathIndex] === index);
111
+ }
112
+ export function withNestableBlockChildren(block, children) {
113
+ if (block.type === "toggle") {
114
+ return { ...block, children };
115
+ }
116
+ if (children.length > 0) {
117
+ return { ...block, children };
118
+ }
119
+ const { children: _children, ...rest } = block;
120
+ return rest;
121
+ }
122
+ export function withBlockChildren(block, children) {
123
+ return isNestableRichTextBlock(block)
124
+ ? withNestableBlockChildren(block, children)
125
+ : block;
126
+ }
@@ -0,0 +1,6 @@
1
+ import type { RichTextBlock } from "./types";
2
+ export type NestableRichTextBlock = Extract<RichTextBlock, {
3
+ type: "bullet" | "ordered" | "checkbox" | "toggle";
4
+ }>;
5
+ export declare const NESTABLE_RICH_TEXT_BLOCK_TYPES: Set<"toggle" | "checkbox" | "ordered" | "bullet">;
6
+ export declare function isNestableRichTextBlock(block: RichTextBlock | null | undefined): block is NestableRichTextBlock;
@@ -0,0 +1,5 @@
1
+ export const NESTABLE_RICH_TEXT_BLOCK_TYPES = new Set(["bullet", "ordered", "checkbox", "toggle"]);
2
+ export function isNestableRichTextBlock(block) {
3
+ return (!!block &&
4
+ NESTABLE_RICH_TEXT_BLOCK_TYPES.has(block.type));
5
+ }