@multiplatform.one/rich-text 6.3.0 → 6.4.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 (168) hide show
  1. package/dist/cjs/RichEditor.cjs +5 -0
  2. package/dist/cjs/RichEditor.native.js +22 -0
  3. package/dist/cjs/RichEditor.native.js.map +1 -1
  4. package/dist/cjs/RichTextEditor/index.cjs +2 -0
  5. package/dist/cjs/RichTextEditor/index.native.js +2 -0
  6. package/dist/cjs/RichTextEditor/index.native.js.map +1 -1
  7. package/dist/cjs/editor/SlashCommands.cjs +8 -1
  8. package/dist/cjs/editor/SlashCommands.native.js +14 -1
  9. package/dist/cjs/editor/SlashCommands.native.js.map +1 -1
  10. package/dist/cjs/editor/TentapEditor.native.js +15 -0
  11. package/dist/cjs/editor/TentapEditor.native.js.map +1 -1
  12. package/dist/cjs/editor/TiptapEditor.cjs +26 -230
  13. package/dist/cjs/editor/TiptapEditor.native.js +34 -280
  14. package/dist/cjs/editor/TiptapEditor.native.js.map +1 -1
  15. package/dist/cjs/editor/capabilities.cjs +37 -0
  16. package/dist/cjs/editor/capabilities.native.js +41 -0
  17. package/dist/cjs/editor/capabilities.native.js.map +1 -0
  18. package/dist/cjs/editor/extensions.cjs +275 -0
  19. package/dist/cjs/editor/extensions.native.js +328 -0
  20. package/dist/cjs/editor/extensions.native.js.map +1 -0
  21. package/dist/cjs/index.cjs +5 -2
  22. package/dist/cjs/index.native.js +4 -1
  23. package/dist/cjs/index.native.js.map +1 -1
  24. package/dist/cjs/toolbar/BubbleToolbar.cjs +9 -6
  25. package/dist/cjs/toolbar/BubbleToolbar.native.js +12 -7
  26. package/dist/cjs/toolbar/BubbleToolbar.native.js.map +1 -1
  27. package/dist/cjs/toolbar/FixedToolbar.cjs +6 -3
  28. package/dist/cjs/toolbar/MoreDropdown.cjs +17 -9
  29. package/dist/cjs/toolbar/MoreDropdown.native.js +21 -9
  30. package/dist/cjs/toolbar/MoreDropdown.native.js.map +1 -1
  31. package/dist/cjs/toolbar/TableBubble.cjs +15 -2
  32. package/dist/cjs/toolbar/TableBubble.native.js +17 -2
  33. package/dist/cjs/toolbar/TableBubble.native.js.map +1 -1
  34. package/dist/cjs/types.native.js.map +1 -1
  35. package/dist/esm/RichEditor.mjs +5 -0
  36. package/dist/esm/RichEditor.mjs.map +1 -1
  37. package/dist/esm/RichEditor.native.js +22 -0
  38. package/dist/esm/RichEditor.native.js.map +1 -1
  39. package/dist/esm/RichTextEditor/index.mjs +2 -0
  40. package/dist/esm/RichTextEditor/index.mjs.map +1 -1
  41. package/dist/esm/RichTextEditor/index.native.js +2 -0
  42. package/dist/esm/RichTextEditor/index.native.js.map +1 -1
  43. package/dist/esm/editor/SlashCommands.mjs +8 -1
  44. package/dist/esm/editor/SlashCommands.mjs.map +1 -1
  45. package/dist/esm/editor/SlashCommands.native.js +14 -1
  46. package/dist/esm/editor/SlashCommands.native.js.map +1 -1
  47. package/dist/esm/editor/TentapEditor.native.js +15 -0
  48. package/dist/esm/editor/TentapEditor.native.js.map +1 -1
  49. package/dist/esm/editor/TiptapEditor.mjs +28 -221
  50. package/dist/esm/editor/TiptapEditor.mjs.map +1 -1
  51. package/dist/esm/editor/TiptapEditor.native.js +36 -271
  52. package/dist/esm/editor/TiptapEditor.native.js.map +1 -1
  53. package/dist/esm/editor/capabilities.mjs +10 -0
  54. package/dist/esm/editor/capabilities.mjs.map +1 -0
  55. package/dist/esm/editor/capabilities.native.js +11 -0
  56. package/dist/esm/editor/capabilities.native.js.map +1 -0
  57. package/dist/esm/editor/extensions.mjs +236 -0
  58. package/dist/esm/editor/extensions.mjs.map +1 -0
  59. package/dist/esm/editor/extensions.native.js +286 -0
  60. package/dist/esm/editor/extensions.native.js.map +1 -0
  61. package/dist/esm/index.mjs +2 -1
  62. package/dist/esm/index.mjs.map +1 -1
  63. package/dist/esm/index.native.js +2 -1
  64. package/dist/esm/index.native.js.map +1 -1
  65. package/dist/esm/toolbar/BubbleToolbar.mjs +9 -6
  66. package/dist/esm/toolbar/BubbleToolbar.mjs.map +1 -1
  67. package/dist/esm/toolbar/BubbleToolbar.native.js +12 -7
  68. package/dist/esm/toolbar/BubbleToolbar.native.js.map +1 -1
  69. package/dist/esm/toolbar/FixedToolbar.mjs +6 -3
  70. package/dist/esm/toolbar/FixedToolbar.mjs.map +1 -1
  71. package/dist/esm/toolbar/MoreDropdown.mjs +17 -9
  72. package/dist/esm/toolbar/MoreDropdown.mjs.map +1 -1
  73. package/dist/esm/toolbar/MoreDropdown.native.js +21 -9
  74. package/dist/esm/toolbar/MoreDropdown.native.js.map +1 -1
  75. package/dist/esm/toolbar/TableBubble.mjs +15 -2
  76. package/dist/esm/toolbar/TableBubble.mjs.map +1 -1
  77. package/dist/esm/toolbar/TableBubble.native.js +17 -2
  78. package/dist/esm/toolbar/TableBubble.native.js.map +1 -1
  79. package/dist/esm/types.mjs.map +1 -1
  80. package/dist/esm/types.native.js.map +1 -1
  81. package/dist/jsx/RichEditor.mjs +5 -0
  82. package/dist/jsx/RichEditor.mjs.map +1 -1
  83. package/dist/jsx/RichEditor.native.js +22 -0
  84. package/dist/jsx/RichEditor.native.js.map +1 -1
  85. package/dist/jsx/RichTextEditor/index.mjs +2 -0
  86. package/dist/jsx/RichTextEditor/index.mjs.map +1 -1
  87. package/dist/jsx/RichTextEditor/index.native.js +2 -0
  88. package/dist/jsx/RichTextEditor/index.native.js.map +1 -1
  89. package/dist/jsx/editor/SlashCommands.mjs +8 -1
  90. package/dist/jsx/editor/SlashCommands.mjs.map +1 -1
  91. package/dist/jsx/editor/SlashCommands.native.js +14 -1
  92. package/dist/jsx/editor/SlashCommands.native.js.map +1 -1
  93. package/dist/jsx/editor/TentapEditor.native.js +15 -0
  94. package/dist/jsx/editor/TentapEditor.native.js.map +1 -1
  95. package/dist/jsx/editor/TiptapEditor.mjs +28 -221
  96. package/dist/jsx/editor/TiptapEditor.mjs.map +1 -1
  97. package/dist/jsx/editor/TiptapEditor.native.js +34 -280
  98. package/dist/jsx/editor/TiptapEditor.native.js.map +1 -1
  99. package/dist/jsx/editor/capabilities.mjs +10 -0
  100. package/dist/jsx/editor/capabilities.mjs.map +1 -0
  101. package/dist/jsx/editor/capabilities.native.js +41 -0
  102. package/dist/jsx/editor/capabilities.native.js.map +1 -0
  103. package/dist/jsx/editor/extensions.mjs +236 -0
  104. package/dist/jsx/editor/extensions.mjs.map +1 -0
  105. package/dist/jsx/editor/extensions.native.js +328 -0
  106. package/dist/jsx/editor/extensions.native.js.map +1 -0
  107. package/dist/jsx/index.js +2 -1
  108. package/dist/jsx/index.js.map +1 -1
  109. package/dist/jsx/index.mjs +2 -1
  110. package/dist/jsx/index.mjs.map +1 -1
  111. package/dist/jsx/index.native.js +4 -1
  112. package/dist/jsx/index.native.js.map +1 -1
  113. package/dist/jsx/toolbar/BubbleToolbar.mjs +9 -6
  114. package/dist/jsx/toolbar/BubbleToolbar.mjs.map +1 -1
  115. package/dist/jsx/toolbar/BubbleToolbar.native.js +12 -7
  116. package/dist/jsx/toolbar/BubbleToolbar.native.js.map +1 -1
  117. package/dist/jsx/toolbar/FixedToolbar.mjs +6 -3
  118. package/dist/jsx/toolbar/FixedToolbar.mjs.map +1 -1
  119. package/dist/jsx/toolbar/MoreDropdown.mjs +17 -9
  120. package/dist/jsx/toolbar/MoreDropdown.mjs.map +1 -1
  121. package/dist/jsx/toolbar/MoreDropdown.native.js +21 -9
  122. package/dist/jsx/toolbar/MoreDropdown.native.js.map +1 -1
  123. package/dist/jsx/toolbar/TableBubble.mjs +15 -2
  124. package/dist/jsx/toolbar/TableBubble.mjs.map +1 -1
  125. package/dist/jsx/toolbar/TableBubble.native.js +17 -2
  126. package/dist/jsx/toolbar/TableBubble.native.js.map +1 -1
  127. package/dist/jsx/types.mjs.map +1 -1
  128. package/dist/jsx/types.native.js.map +1 -1
  129. package/package.json +8 -8
  130. package/src/RichEditor.native.tsx +20 -0
  131. package/src/RichEditor.stories.tsx +32 -0
  132. package/src/RichEditor.tsx +5 -0
  133. package/src/RichTextEditor/index.tsx +14 -1
  134. package/src/editor/SlashCommands.tsx +10 -0
  135. package/src/editor/TentapEditor.native.tsx +13 -0
  136. package/src/editor/TiptapEditor.tsx +51 -279
  137. package/src/editor/capabilities.spec.ts +190 -0
  138. package/src/editor/capabilities.ts +76 -0
  139. package/src/editor/extensions.ts +315 -0
  140. package/src/index.ts +3 -0
  141. package/src/toolbar/BubbleToolbar.tsx +40 -28
  142. package/src/toolbar/FixedToolbar.tsx +20 -12
  143. package/src/toolbar/MoreDropdown.tsx +30 -6
  144. package/src/toolbar/TableBubble.tsx +65 -12
  145. package/src/types.ts +28 -1
  146. package/types/RichEditor.d.ts.map +1 -1
  147. package/types/RichEditor.native.d.ts.map +1 -1
  148. package/types/RichTextEditor/index.d.ts +7 -1
  149. package/types/RichTextEditor/index.d.ts.map +1 -1
  150. package/types/editor/SlashCommands.d.ts.map +1 -1
  151. package/types/editor/TentapEditor.native.d.ts.map +1 -1
  152. package/types/editor/TiptapEditor.d.ts.map +1 -1
  153. package/types/editor/capabilities.d.ts +29 -0
  154. package/types/editor/capabilities.d.ts.map +1 -0
  155. package/types/editor/extensions.d.ts +41 -0
  156. package/types/editor/extensions.d.ts.map +1 -0
  157. package/types/index.d.ts +2 -1
  158. package/types/index.d.ts.map +1 -1
  159. package/types/toolbar/BubbleToolbar.d.ts +4 -2
  160. package/types/toolbar/BubbleToolbar.d.ts.map +1 -1
  161. package/types/toolbar/FixedToolbar.d.ts +4 -2
  162. package/types/toolbar/FixedToolbar.d.ts.map +1 -1
  163. package/types/toolbar/MoreDropdown.d.ts +4 -2
  164. package/types/toolbar/MoreDropdown.d.ts.map +1 -1
  165. package/types/toolbar/TableBubble.d.ts +4 -1
  166. package/types/toolbar/TableBubble.d.ts.map +1 -1
  167. package/types/types.d.ts +23 -1
  168. package/types/types.d.ts.map +1 -1
@@ -0,0 +1,315 @@
1
+ /**
2
+ * Extension factory for the web Tiptap editor.
3
+ *
4
+ * One canonical list (LC-16) consumed by TiptapEditor AND by the
5
+ * capability specs, so what ships is exactly what is verified. With
6
+ * `capabilities: "markdown"` the non-markdown extensions are NOT
7
+ * registered — their commands do not exist on the editor (LC-52
8
+ * MARKDOWN-EXPRESSIBLE-EDITING), and StarterKit's underline sub-extension
9
+ * is disabled.
10
+ */
11
+
12
+ import { Extension, type Extensions } from "@tiptap/core";
13
+ import StarterKit from "@tiptap/starter-kit";
14
+ import Placeholder from "@tiptap/extension-placeholder";
15
+ import TextAlign from "@tiptap/extension-text-align";
16
+ import { Color } from "@tiptap/extension-color";
17
+ import { TextStyle } from "@tiptap/extension-text-style";
18
+ import { Highlight } from "@tiptap/extension-highlight";
19
+ import { Subscript } from "@tiptap/extension-subscript";
20
+ import { Superscript } from "@tiptap/extension-superscript";
21
+ import TaskList from "@tiptap/extension-task-list";
22
+ import TaskItem from "@tiptap/extension-task-item";
23
+ import Image from "@tiptap/extension-image";
24
+ import Typography from "@tiptap/extension-typography";
25
+ import CharacterCount from "@tiptap/extension-character-count";
26
+ import { Details, DetailsContent, DetailsSummary } from "@tiptap/extension-details";
27
+ import { Table } from "@tiptap/extension-table";
28
+ import { TableRow } from "@tiptap/extension-table-row";
29
+ import { TableHeader } from "@tiptap/extension-table-header";
30
+ import { TableCell } from "@tiptap/extension-table-cell";
31
+ import { common, createLowlight } from "lowlight";
32
+ import type { EditorCapabilities } from "../types";
33
+ import { MARKDOWN_EXCLUDED_SLASH_IDS } from "./capabilities";
34
+ import { SlashCommands } from "./SlashCommands";
35
+ import { MarkdownPaste } from "./MarkdownPaste";
36
+ import { BlockDragHandle } from "./BlockDragHandle";
37
+ import { MediaUpload } from "./media";
38
+ import { EmojiSuggestion } from "./emoji";
39
+ import { InlineMath } from "./mathematics";
40
+ import { CodeBlockWithPreview, PreviewChromeStore } from "./codeBlockPreview";
41
+ import { SuggestionChromeStore } from "./suggestionPopup";
42
+ import { t } from "../shared/t";
43
+
44
+ const lowlight = createLowlight(common);
45
+
46
+ /**
47
+ * Tab / Shift-Tab indentation inside code blocks.
48
+ * Inserts or removes 2 spaces at the cursor (or start of each selected line).
49
+ */
50
+ export const CodeBlockTabIndent = Extension.create({
51
+ name: "codeBlockTabIndent",
52
+ addKeyboardShortcuts() {
53
+ return {
54
+ Tab: ({ editor }) => {
55
+ if (!editor.isActive("codeBlock")) return false;
56
+ const { state, dispatch } = editor.view;
57
+ const { from, to } = state.selection;
58
+
59
+ if (from === to) {
60
+ dispatch(state.tr.insertText(" ", from, to));
61
+ return true;
62
+ }
63
+
64
+ const doc = state.doc;
65
+ const lines: { start: number }[] = [];
66
+ doc.nodesBetween(from, to, (node, pos) => {
67
+ if (node.isTextblock) {
68
+ lines.push({ start: pos + 1 });
69
+ }
70
+ });
71
+ if (lines.length === 0) {
72
+ dispatch(state.tr.insertText(" ", from, to));
73
+ return true;
74
+ }
75
+
76
+ let tr = state.tr;
77
+ let offset = 0;
78
+ for (const line of lines) {
79
+ const lineStart = line.start + offset;
80
+ tr = tr.insertText(" ", lineStart);
81
+ offset += 2;
82
+ }
83
+ dispatch(tr);
84
+ return true;
85
+ },
86
+ "Shift-Tab": ({ editor }) => {
87
+ if (!editor.isActive("codeBlock")) return false;
88
+ const { state, dispatch } = editor.view;
89
+ const { from, to, $from } = state.selection;
90
+
91
+ if (from === to) {
92
+ const lineStart = from - $from.parentOffset;
93
+ const text = state.doc.textBetween(lineStart, from);
94
+ const leadingSpaces = text.match(/^ {1,2}/)?.[0];
95
+ if (leadingSpaces) {
96
+ dispatch(state.tr.delete(lineStart, lineStart + leadingSpaces.length));
97
+ }
98
+ return true;
99
+ }
100
+
101
+ const doc = state.doc;
102
+ let tr = state.tr;
103
+ let offset = 0;
104
+ doc.nodesBetween(from, to, (node, pos) => {
105
+ if (node.isTextblock) {
106
+ const lineStart = pos + 1 + offset;
107
+ const text = tr.doc.textBetween(
108
+ lineStart,
109
+ Math.min(lineStart + node.content.size, tr.doc.content.size),
110
+ );
111
+ const leadingSpaces = text.match(/^ {1,2}/)?.[0];
112
+ if (leadingSpaces) {
113
+ tr = tr.delete(lineStart, lineStart + leadingSpaces.length);
114
+ offset -= leadingSpaces.length;
115
+ }
116
+ }
117
+ });
118
+ dispatch(tr);
119
+ return true;
120
+ },
121
+ };
122
+ },
123
+ });
124
+
125
+ /**
126
+ * Override Shift-Enter and Mod-Enter to create new paragraphs instead of
127
+ * hard breaks (<br>). Mirrors GitLab's approach: HardBreak node stays
128
+ * registered (so <br> in pasted HTML is preserved) but its keyboard
129
+ * shortcuts are replaced with the default enter (split paragraph) behavior.
130
+ */
131
+ export const ParagraphEnterKeys = Extension.create({
132
+ name: "paragraphEnterKeys",
133
+ priority: 1000,
134
+ addKeyboardShortcuts() {
135
+ return {
136
+ "Shift-Enter": ({ editor }) =>
137
+ editor.commands.first([
138
+ () => editor.commands.newlineInCode(),
139
+ () => editor.commands.splitListItem("listItem"),
140
+ () => editor.commands.createParagraphNear(),
141
+ () => editor.commands.liftEmptyBlock(),
142
+ () => editor.commands.splitBlock(),
143
+ ]),
144
+ "Mod-Enter": ({ editor }) =>
145
+ editor.commands.first([
146
+ () => editor.commands.newlineInCode(),
147
+ () => editor.commands.createParagraphNear(),
148
+ () => editor.commands.liftEmptyBlock(),
149
+ () => editor.commands.splitBlock(),
150
+ ]),
151
+ };
152
+ },
153
+ });
154
+
155
+ /**
156
+ * Tab/Shift-Tab indent/dedent and Alt+Arrow move for list items.
157
+ * Tab sinks (indents), Shift-Tab lifts (dedents).
158
+ * Alt+Up/Down moves the list item up or down among siblings.
159
+ */
160
+ export const ListItemKeys = Extension.create({
161
+ name: "listItemKeys",
162
+ addKeyboardShortcuts() {
163
+ return {
164
+ Tab: ({ editor }) => {
165
+ if (editor.isActive("codeBlock")) return false;
166
+ if (
167
+ editor.isActive("bulletList") ||
168
+ editor.isActive("orderedList") ||
169
+ editor.isActive("taskList")
170
+ ) {
171
+ return (
172
+ editor.commands.sinkListItem("listItem") || editor.commands.sinkListItem("taskItem")
173
+ );
174
+ }
175
+ return false;
176
+ },
177
+ "Shift-Tab": ({ editor }) => {
178
+ if (editor.isActive("codeBlock")) return false;
179
+ if (
180
+ editor.isActive("bulletList") ||
181
+ editor.isActive("orderedList") ||
182
+ editor.isActive("taskList")
183
+ ) {
184
+ return (
185
+ editor.commands.liftListItem("listItem") || editor.commands.liftListItem("taskItem")
186
+ );
187
+ }
188
+ return false;
189
+ },
190
+ };
191
+ },
192
+ });
193
+
194
+ // ===========================================================================
195
+ // Factory
196
+ // ===========================================================================
197
+
198
+ export interface BuildEditorExtensionsOptions {
199
+ /** Capability preset (default "full") */
200
+ capabilities?: EditorCapabilities;
201
+ /** Placeholder text (defaults to the translated "Start writing...") */
202
+ placeholder?: string;
203
+ /** Pre-built mention extension (ignored in markdown capabilities) */
204
+ mentionExtension?: Extensions[number] | null;
205
+ }
206
+
207
+ /** Build the production extension list for the given capability surface */
208
+ export function buildEditorExtensions({
209
+ capabilities = "full",
210
+ placeholder,
211
+ mentionExtension = null,
212
+ }: BuildEditorExtensionsOptions = {}): Extensions {
213
+ const markdown = capabilities === "markdown";
214
+
215
+ return [
216
+ StarterKit.configure({
217
+ heading: {
218
+ levels: [1, 2, 3, 4, 5, 6],
219
+ HTMLAttributes: { dir: "auto" },
220
+ },
221
+ paragraph: {
222
+ HTMLAttributes: { dir: "auto" },
223
+ },
224
+ link: {
225
+ openOnClick: false,
226
+ HTMLAttributes: {
227
+ rel: "noopener noreferrer",
228
+ target: "_blank",
229
+ },
230
+ },
231
+ bulletList: {
232
+ keepMarks: true,
233
+ },
234
+ orderedList: {
235
+ keepMarks: true,
236
+ },
237
+ codeBlock: false,
238
+ hardBreak: false,
239
+ // Markdown has no underline syntax — the mark must not exist
240
+ ...(markdown && { underline: false }),
241
+ }),
242
+ CodeBlockWithPreview.configure({
243
+ lowlight,
244
+ exitOnTripleEnter: true,
245
+ exitOnArrowDown: false,
246
+ }),
247
+ ParagraphEnterKeys,
248
+ CodeBlockTabIndent,
249
+ ListItemKeys,
250
+ // superscriptTwo/Three would mangle typed TeX (`$E=mc^2$` became
251
+ // `E=mc²` before the math input rule ran) — off, rest stays.
252
+ Typography.configure({
253
+ superscriptTwo: false,
254
+ superscriptThree: false,
255
+ }),
256
+ CharacterCount,
257
+ SlashCommands.configure({
258
+ excludeIds: markdown ? [...MARKDOWN_EXCLUDED_SLASH_IDS] : [],
259
+ }),
260
+ MarkdownPaste,
261
+ MediaUpload,
262
+ SuggestionChromeStore,
263
+ PreviewChromeStore,
264
+ // Emoji suggestions insert plain unicode characters (just text), so
265
+ // they stay available in markdown capabilities.
266
+ EmojiSuggestion,
267
+ ...(markdown
268
+ ? []
269
+ : [
270
+ InlineMath,
271
+ // Collapsible sections (GitLab <details> parity). persist: true
272
+ // is the extension's supported mode: its node view re-syncs the
273
+ // open state from node.attrs on every update, so ephemeral
274
+ // (persist: false) toggles get reverted by the next transaction.
275
+ // The open state therefore lives in the doc and serializes as
276
+ // `<details open>` — valid GitLab markdown.
277
+ Details.configure({ persist: true }),
278
+ DetailsSummary,
279
+ DetailsContent,
280
+ ]),
281
+ // Mention chips have no markdown representation — full surface only
282
+ ...(mentionExtension && !markdown ? [mentionExtension] : []),
283
+ BlockDragHandle,
284
+ Table.configure({ resizable: true }),
285
+ TableRow,
286
+ TableHeader,
287
+ TableCell,
288
+ Placeholder.configure({
289
+ placeholder: placeholder ?? t("Start writing..."),
290
+ }),
291
+ ...(markdown
292
+ ? []
293
+ : [
294
+ TextAlign.configure({
295
+ types: ["heading", "paragraph"],
296
+ defaultAlignment: "left",
297
+ }),
298
+ TextStyle,
299
+ Color,
300
+ Highlight.configure({
301
+ multicolor: true,
302
+ }),
303
+ Subscript,
304
+ Superscript,
305
+ ]),
306
+ TaskList,
307
+ TaskItem.configure({
308
+ nested: true,
309
+ }),
310
+ Image.configure({
311
+ inline: true,
312
+ allowBase64: true,
313
+ }),
314
+ ];
315
+ }
package/src/index.ts CHANGED
@@ -13,7 +13,10 @@ export type { RichTextEditorProps } from "./RichTextEditor";
13
13
  export type {
14
14
  RichEditorRef,
15
15
  RichEditorProps,
16
+ EditorCapabilities,
16
17
  EditorCommand,
17
18
  EditorState,
18
19
  RichTextMentionOptions,
19
20
  } from "./types";
21
+
22
+ export { MARKDOWN_COMMANDS, isCommandInCapabilities } from "./editor/capabilities";
@@ -14,8 +14,9 @@ import type { Editor } from "@tiptap/react";
14
14
  import { BubbleMenu } from "@tiptap/react/menus";
15
15
  import { useResolvedKnobs } from "@multiplatform.one/theme";
16
16
  import { XStack } from "tamagui";
17
- import type { EditorState, RichEditorRef } from "../types";
17
+ import type { EditorCapabilities, EditorState, RichEditorRef } from "../types";
18
18
  import { defaultEditorState } from "../types";
19
+ import { isCommandInCapabilities } from "../editor/capabilities";
19
20
  import { ToolbarButton } from "./ToolbarButton";
20
21
  import {
21
22
  BoldIcon,
@@ -42,6 +43,8 @@ export interface BubbleToolbarProps {
42
43
  editorRef: React.RefObject<RichEditorRef | null>;
43
44
  /** Current editor state */
44
45
  state?: EditorState;
46
+ /** Capability surface — markdown hides non-markdown controls (default "full") */
47
+ capabilities?: EditorCapabilities;
45
48
  }
46
49
 
47
50
  // ===========================================================================
@@ -52,8 +55,11 @@ export function BubbleToolbar({
52
55
  editor,
53
56
  editorRef,
54
57
  state = defaultEditorState,
58
+ capabilities = "full",
55
59
  }: BubbleToolbarProps) {
56
60
  const { knobProps } = useResolvedKnobs();
61
+ const has = (command: Parameters<typeof isCommandInCapabilities>[0]) =>
62
+ isCommandInCapabilities(command, capabilities);
57
63
 
58
64
  if (!editor) return null;
59
65
 
@@ -97,14 +103,16 @@ export function BubbleToolbar({
97
103
  <ItalicIcon>I</ItalicIcon>
98
104
  </ToolbarButton>
99
105
 
100
- <ToolbarButton
101
- active={state.isUnderline}
102
- onPress={() => editorRef.current?.executeCommand("underline")}
103
- aria-label={t("Underline")}
104
- tooltip={SHORTCUTS.underline}
105
- >
106
- <UnderlineIcon>U</UnderlineIcon>
107
- </ToolbarButton>
106
+ {has("underline") && (
107
+ <ToolbarButton
108
+ active={state.isUnderline}
109
+ onPress={() => editorRef.current?.executeCommand("underline")}
110
+ aria-label={t("Underline")}
111
+ tooltip={SHORTCUTS.underline}
112
+ >
113
+ <UnderlineIcon>U</UnderlineIcon>
114
+ </ToolbarButton>
115
+ )}
108
116
 
109
117
  <ToolbarButton
110
118
  active={state.isStrike}
@@ -124,27 +132,31 @@ export function BubbleToolbar({
124
132
  <InlineCodeIcon />
125
133
  </ToolbarButton>
126
134
 
127
- <ToolbarButton
128
- active={state.isSubscript}
129
- onPress={() => editorRef.current?.executeCommand("subscript")}
130
- aria-label={t("Subscript")}
131
- tooltip={SHORTCUTS.subscript}
132
- >
133
- <SubscriptIcon />
134
- </ToolbarButton>
135
+ {has("subscript") && (
136
+ <ToolbarButton
137
+ active={state.isSubscript}
138
+ onPress={() => editorRef.current?.executeCommand("subscript")}
139
+ aria-label={t("Subscript")}
140
+ tooltip={SHORTCUTS.subscript}
141
+ >
142
+ <SubscriptIcon />
143
+ </ToolbarButton>
144
+ )}
135
145
 
136
- <ToolbarButton
137
- active={state.isSuperscript}
138
- onPress={() => editorRef.current?.executeCommand("superscript")}
139
- aria-label={t("Superscript")}
140
- tooltip={SHORTCUTS.superscript}
141
- >
142
- <SuperscriptIcon />
143
- </ToolbarButton>
146
+ {has("superscript") && (
147
+ <ToolbarButton
148
+ active={state.isSuperscript}
149
+ onPress={() => editorRef.current?.executeCommand("superscript")}
150
+ aria-label={t("Superscript")}
151
+ tooltip={SHORTCUTS.superscript}
152
+ >
153
+ <SuperscriptIcon />
154
+ </ToolbarButton>
155
+ )}
144
156
 
145
- {/* Color pickers */}
146
- <TextColorPicker editorRef={editorRef} state={state} />
147
- <HighlightColorPicker editorRef={editorRef} state={state} />
157
+ {/* Color pickers — no markdown syntax (LC-52) */}
158
+ {has("textColor") && <TextColorPicker editorRef={editorRef} state={state} />}
159
+ {has("highlight") && <HighlightColorPicker editorRef={editorRef} state={state} />}
148
160
 
149
161
  {/* Link */}
150
162
  <LinkDialog editorRef={editorRef} state={state} />
@@ -10,8 +10,9 @@
10
10
 
11
11
  import { useCallback, useEffect, useRef } from "react";
12
12
  import { XStack } from "tamagui";
13
- import type { EditorState, RichEditorRef, ToolbarSection } from "../types";
13
+ import type { EditorCapabilities, EditorState, RichEditorRef, ToolbarSection } from "../types";
14
14
  import { defaultEditorState } from "../types";
15
+ import { isCommandInCapabilities } from "../editor/capabilities";
15
16
  import { ToolbarFrame } from "../styled";
16
17
  import { ToolbarButton } from "./ToolbarButton";
17
18
  import { ToolbarGroup } from "./ToolbarGroup";
@@ -53,6 +54,8 @@ export interface FixedToolbarProps {
53
54
  state?: EditorState;
54
55
  /** Toolbar sections to show (shows all when omitted) */
55
56
  sections?: ToolbarSection[];
57
+ /** Capability surface — markdown hides non-markdown controls (default "full") */
58
+ capabilities?: EditorCapabilities;
56
59
  /** Whether the toolbar has an error state */
57
60
  hasError?: boolean;
58
61
  /** Whether the editor is in fullscreen mode */
@@ -84,6 +87,7 @@ export function FixedToolbar({
84
87
  editorRef,
85
88
  state = defaultEditorState,
86
89
  sections,
90
+ capabilities = "full",
87
91
  hasError,
88
92
  fullscreen,
89
93
  onFullscreenToggle,
@@ -191,14 +195,16 @@ export function FixedToolbar({
191
195
  >
192
196
  <ItalicIcon>I</ItalicIcon>
193
197
  </ToolbarButton>
194
- <ToolbarButton
195
- active={state.isUnderline}
196
- onPress={() => executeCommand("underline")}
197
- aria-label={t("Underline")}
198
- tooltip={SHORTCUTS.underline}
199
- >
200
- <UnderlineIcon>U</UnderlineIcon>
201
- </ToolbarButton>
198
+ {isCommandInCapabilities("underline", capabilities) && (
199
+ <ToolbarButton
200
+ active={state.isUnderline}
201
+ onPress={() => executeCommand("underline")}
202
+ aria-label={t("Underline")}
203
+ tooltip={SHORTCUTS.underline}
204
+ >
205
+ <UnderlineIcon>U</UnderlineIcon>
206
+ </ToolbarButton>
207
+ )}
202
208
  <ToolbarButton
203
209
  active={state.isStrike}
204
210
  onPress={() => executeCommand("strike")}
@@ -209,8 +215,10 @@ export function FixedToolbar({
209
215
  </ToolbarButton>
210
216
  </ToolbarGroup>
211
217
 
212
- {/* Color */}
213
- <ToolbarGroup visible={visible("color")}>
218
+ {/* Color — no markdown syntax for color/highlight (LC-52) */}
219
+ <ToolbarGroup
220
+ visible={visible("color") && isCommandInCapabilities("textColor", capabilities)}
221
+ >
214
222
  <TextColorPicker editorRef={editorRef} state={state} />
215
223
  <HighlightColorPicker editorRef={editorRef} state={state} />
216
224
  </ToolbarGroup>
@@ -268,7 +276,7 @@ export function FixedToolbar({
268
276
  }
269
277
  showSeparator={false}
270
278
  >
271
- <MoreDropdown editorRef={editorRef} state={state} />
279
+ <MoreDropdown editorRef={editorRef} state={state} capabilities={capabilities} />
272
280
  </ToolbarGroup>
273
281
  </XStack>
274
282
  </div>
@@ -7,7 +7,8 @@ import type React from "react";
7
7
  import { DotsThreeIcon } from "@phosphor-icons/react";
8
8
  import { useRef, useState } from "react";
9
9
  import { DropdownMenu } from "@multiplatform.one/components";
10
- import type { EditorCommand, EditorState, RichEditorRef } from "../types";
10
+ import type { EditorCapabilities, EditorCommand, EditorState, RichEditorRef } from "../types";
11
+ import { isCommandInCapabilities } from "../editor/capabilities";
11
12
  import { ToolbarButton } from "./ToolbarButton";
12
13
  import {
13
14
  SubscriptIcon,
@@ -25,6 +26,8 @@ import { t } from "../shared/t";
25
26
  export interface MoreDropdownProps {
26
27
  editorRef: React.RefObject<RichEditorRef | null>;
27
28
  state: EditorState;
29
+ /** Capability surface — markdown drops sub/superscript and alignment */
30
+ capabilities?: EditorCapabilities;
28
31
  disabled?: boolean;
29
32
  }
30
33
 
@@ -130,7 +133,12 @@ function ItemSection({
130
133
  );
131
134
  }
132
135
 
133
- export function MoreDropdown({ editorRef, state, disabled }: MoreDropdownProps) {
136
+ export function MoreDropdown({
137
+ editorRef,
138
+ state,
139
+ capabilities = "full",
140
+ disabled,
141
+ }: MoreDropdownProps) {
134
142
  const [open, setOpen] = useState(false);
135
143
  const selectedRef = useRef(false);
136
144
 
@@ -142,6 +150,14 @@ export function MoreDropdown({ editorRef, state, disabled }: MoreDropdownProps)
142
150
  }
143
151
  };
144
152
 
153
+ // Sections whose commands markdown cannot express disappear whole (LC-52)
154
+ const inlineItems = INLINE_ITEMS.filter((item) =>
155
+ isCommandInCapabilities(item.command, capabilities),
156
+ );
157
+ const alignItems = ALIGN_ITEMS.filter((item) =>
158
+ isCommandInCapabilities(item.command, capabilities),
159
+ );
160
+
145
161
  return (
146
162
  <DropdownMenu open={open} onOpenChange={setOpen} placement="bottom-end">
147
163
  <DropdownMenu.Trigger asChild>
@@ -167,10 +183,18 @@ export function MoreDropdown({ editorRef, state, disabled }: MoreDropdownProps)
167
183
  }
168
184
  }}
169
185
  >
170
- <ItemSection items={INLINE_ITEMS} state={state} onSelect={handleSelect} />
171
- <DropdownMenu.Separator />
172
- <ItemSection items={ALIGN_ITEMS} state={state} onSelect={handleSelect} />
173
- <DropdownMenu.Separator />
186
+ {inlineItems.length > 0 && (
187
+ <>
188
+ <ItemSection items={inlineItems} state={state} onSelect={handleSelect} />
189
+ <DropdownMenu.Separator />
190
+ </>
191
+ )}
192
+ {alignItems.length > 0 && (
193
+ <>
194
+ <ItemSection items={alignItems} state={state} onSelect={handleSelect} />
195
+ <DropdownMenu.Separator />
196
+ </>
197
+ )}
174
198
  <ItemSection items={BLOCK_ITEMS} state={state} onSelect={handleSelect} />
175
199
  </DropdownMenu.Content>
176
200
  </DropdownMenu>