@openeditor/react-native-prose-editor 0.0.2

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 (91) hide show
  1. package/LICENSE +160 -0
  2. package/README.md +180 -0
  3. package/android/build.gradle +69 -0
  4. package/android/consumer-rules.pro +18 -0
  5. package/android/src/androidTest/AndroidManifest.xml +8 -0
  6. package/android/src/androidTest/java/com/apollohg/editor/MeasureHeightInstrumentedTest.kt +53 -0
  7. package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceCollaborationInitialSyncTest.kt +241 -0
  8. package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceImeRegressionTest.kt +338 -0
  9. package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceOutsideTapTest.kt +789 -0
  10. package/android/src/androidTest/java/com/apollohg/editor/NativeDevicePerformanceTest.kt +350 -0
  11. package/android/src/androidTest/java/com/apollohg/editor/NativeEditorOutsideTapActivity.kt +5 -0
  12. package/android/src/main/assets/editor-icons/MaterialIcons.json +2236 -0
  13. package/android/src/main/assets/editor-icons/MaterialIcons.ttf +0 -0
  14. package/android/src/main/java/com/apollohg/editor/CaretGeometry.kt +50 -0
  15. package/android/src/main/java/com/apollohg/editor/EditorAddons.kt +135 -0
  16. package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +4527 -0
  17. package/android/src/main/java/com/apollohg/editor/EditorHeightBehavior.kt +14 -0
  18. package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +999 -0
  19. package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +474 -0
  20. package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +199 -0
  21. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +3137 -0
  22. package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +561 -0
  23. package/android/src/main/java/com/apollohg/editor/NativeProseViewerExpoView.kt +315 -0
  24. package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +1413 -0
  25. package/android/src/main/java/com/apollohg/editor/PositionBridge.kt +133 -0
  26. package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +315 -0
  27. package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +2172 -0
  28. package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +399 -0
  29. package/android/src/sharedTest/java/com/apollohg/editor/NativePerformanceSupport.kt +482 -0
  30. package/android/src/test/java/com/apollohg/editor/CaretGeometryTest.kt +137 -0
  31. package/android/src/test/java/com/apollohg/editor/EditorEditTextHardwareKeyTest.kt +321 -0
  32. package/android/src/test/java/com/apollohg/editor/EditorInputConnectionTest.kt +4202 -0
  33. package/android/src/test/java/com/apollohg/editor/NativeEditorExpoViewTest.kt +2581 -0
  34. package/android/src/test/java/com/apollohg/editor/NativeEditorModuleTest.kt +27 -0
  35. package/android/src/test/java/com/apollohg/editor/NativePerformanceTest.kt +197 -0
  36. package/android/src/test/java/com/apollohg/editor/NativeProseViewerExpoViewTest.kt +84 -0
  37. package/android/src/test/java/com/apollohg/editor/NativeToolbarTest.kt +544 -0
  38. package/android/src/test/java/com/apollohg/editor/PositionBridgeTest.kt +461 -0
  39. package/android/src/test/java/com/apollohg/editor/RenderBridgeTest.kt +2054 -0
  40. package/android/src/test/java/com/apollohg/editor/RichTextEditorViewTest.kt +1367 -0
  41. package/app.plugin.js +62 -0
  42. package/dist/EditorTheme.d.ts +113 -0
  43. package/dist/EditorTheme.js +29 -0
  44. package/dist/EditorToolbar.d.ts +191 -0
  45. package/dist/EditorToolbar.js +1095 -0
  46. package/dist/NativeEditorBridge.d.ts +325 -0
  47. package/dist/NativeEditorBridge.js +933 -0
  48. package/dist/NativeProseViewer.d.ts +53 -0
  49. package/dist/NativeProseViewer.js +480 -0
  50. package/dist/NativeRichTextEditor.d.ts +183 -0
  51. package/dist/NativeRichTextEditor.js +2574 -0
  52. package/dist/YjsCollaboration.d.ts +90 -0
  53. package/dist/YjsCollaboration.js +743 -0
  54. package/dist/addons.d.ts +100 -0
  55. package/dist/addons.js +82 -0
  56. package/dist/heightCache.d.ts +6 -0
  57. package/dist/heightCache.js +45 -0
  58. package/dist/index.d.ts +10 -0
  59. package/dist/index.js +30 -0
  60. package/dist/schemas.d.ts +37 -0
  61. package/dist/schemas.js +263 -0
  62. package/dist/useNativeEditor.d.ts +42 -0
  63. package/dist/useNativeEditor.js +124 -0
  64. package/expo-module.config.json +9 -0
  65. package/ios/EditorAddons.swift +327 -0
  66. package/ios/EditorCore.xcframework/Info.plist +44 -0
  67. package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
  68. package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
  69. package/ios/EditorLayoutManager.swift +774 -0
  70. package/ios/EditorTheme.swift +515 -0
  71. package/ios/Generated_editor_core.swift +1513 -0
  72. package/ios/NativeEditorExpoView.swift +3593 -0
  73. package/ios/NativeEditorModule.swift +624 -0
  74. package/ios/NativeProseViewerExpoView.swift +276 -0
  75. package/ios/PositionBridge.swift +558 -0
  76. package/ios/ReactNativeProseEditor.podspec +49 -0
  77. package/ios/RenderBridge.swift +1708 -0
  78. package/ios/RichTextEditorView.swift +6150 -0
  79. package/ios/Tests/NativePerformanceTests.swift +687 -0
  80. package/ios/Tests/PositionBridgeTests.swift +706 -0
  81. package/ios/Tests/RenderBridgeTests.swift +2236 -0
  82. package/ios/Tests/RichTextEditorViewTests.swift +6548 -0
  83. package/ios/editor_coreFFI/editor_coreFFI.h +1289 -0
  84. package/ios/editor_coreFFI/module.modulemap +7 -0
  85. package/ios/editor_coreFFI.h +904 -0
  86. package/ios/editor_coreFFI.modulemap +7 -0
  87. package/package.json +74 -0
  88. package/rust/android/arm64-v8a/libeditor_core.so +0 -0
  89. package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
  90. package/rust/android/x86_64/libeditor_core.so +0 -0
  91. package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +2563 -0
@@ -0,0 +1,1095 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_EDITOR_TOOLBAR_ITEMS = void 0;
4
+ exports.isEditorToolbarFocusPreservationActive = isEditorToolbarFocusPreservationActive;
5
+ exports.setActiveEditorToolbarFrameOwnerForEditor = setActiveEditorToolbarFrameOwnerForEditor;
6
+ exports.useEditorToolbarFrames = useEditorToolbarFrames;
7
+ exports.setEditorToolbarMentionState = setEditorToolbarMentionState;
8
+ exports._setEditorToolbarFrameForTests = _setEditorToolbarFrameForTests;
9
+ exports._resetEditorToolbarFrameRegistryForTests = _resetEditorToolbarFrameRegistryForTests;
10
+ exports._beginEditorToolbarInteractionForTests = _beginEditorToolbarInteractionForTests;
11
+ exports._endEditorToolbarInteractionForTests = _endEditorToolbarInteractionForTests;
12
+ exports.EditorToolbar = EditorToolbar;
13
+ const jsx_runtime_1 = require("react/jsx-runtime");
14
+ const vector_icons_1 = require("@expo/vector-icons");
15
+ const react_1 = require("react");
16
+ const react_native_1 = require("react-native");
17
+ const editorToolbarFrames = new Map();
18
+ const editorToolbarFrameListeners = new Set();
19
+ const editorToolbarMentionStateListeners = new Set();
20
+ let nextEditorToolbarRegistrationId = 1;
21
+ let activeEditorToolbarInteractions = 0;
22
+ let editorToolbarFocusPreserveUntil = 0;
23
+ let activeEditorToolbarFrameOwnerId = null;
24
+ let editorToolbarMentionState = null;
25
+ const EDITOR_TOOLBAR_FOCUS_PRESERVE_MS = 750;
26
+ function areToolbarFramesEqual(left, right) {
27
+ return (left?.x === right?.x &&
28
+ left?.y === right?.y &&
29
+ left?.width === right?.width &&
30
+ left?.height === right?.height);
31
+ }
32
+ function areToolbarFrameListsEqual(left, right) {
33
+ if (left.length !== right.length) {
34
+ return false;
35
+ }
36
+ return left.every((frame, index) => areToolbarFramesEqual(frame, right[index]));
37
+ }
38
+ function notifyEditorToolbarFrameListeners() {
39
+ editorToolbarFrameListeners.forEach((listener) => listener());
40
+ }
41
+ function notifyEditorToolbarMentionStateListeners() {
42
+ editorToolbarMentionStateListeners.forEach((listener) => listener());
43
+ }
44
+ function getEditorToolbarFramesSnapshot(ownerId) {
45
+ return Array.from(editorToolbarFrames.values())
46
+ .filter((registration) => registration.ownerId === ownerId ||
47
+ (registration.ownerId == null && activeEditorToolbarFrameOwnerId === ownerId))
48
+ .map((registration) => registration.frame);
49
+ }
50
+ function subscribeEditorToolbarMentionState(listener) {
51
+ editorToolbarMentionStateListeners.add(listener);
52
+ return () => {
53
+ editorToolbarMentionStateListeners.delete(listener);
54
+ };
55
+ }
56
+ function getEditorToolbarMentionStateSnapshot() {
57
+ return editorToolbarMentionState;
58
+ }
59
+ function useEditorToolbarMentionState() {
60
+ return (0, react_1.useSyncExternalStore)(subscribeEditorToolbarMentionState, getEditorToolbarMentionStateSnapshot, getEditorToolbarMentionStateSnapshot);
61
+ }
62
+ function registerEditorToolbarFrame(id, frame, ownerId) {
63
+ if (frame == null || frame.width <= 0 || frame.height <= 0) {
64
+ if (editorToolbarFrames.delete(id)) {
65
+ notifyEditorToolbarFrameListeners();
66
+ }
67
+ return;
68
+ }
69
+ const currentRegistration = editorToolbarFrames.get(id);
70
+ if (currentRegistration?.ownerId === ownerId &&
71
+ areToolbarFramesEqual(currentRegistration.frame, frame)) {
72
+ return;
73
+ }
74
+ editorToolbarFrames.set(id, { ownerId, frame });
75
+ notifyEditorToolbarFrameListeners();
76
+ }
77
+ function unregisterEditorToolbarFrame(id) {
78
+ if (editorToolbarFrames.delete(id)) {
79
+ notifyEditorToolbarFrameListeners();
80
+ }
81
+ }
82
+ function preserveEditorToolbarFocusForNextBlur() {
83
+ editorToolbarFocusPreserveUntil = Date.now() + EDITOR_TOOLBAR_FOCUS_PRESERVE_MS;
84
+ }
85
+ function beginEditorToolbarInteraction() {
86
+ activeEditorToolbarInteractions += 1;
87
+ preserveEditorToolbarFocusForNextBlur();
88
+ }
89
+ function endEditorToolbarInteraction() {
90
+ activeEditorToolbarInteractions = Math.max(0, activeEditorToolbarInteractions - 1);
91
+ preserveEditorToolbarFocusForNextBlur();
92
+ }
93
+ function isEditorToolbarFocusPreservationActive() {
94
+ return activeEditorToolbarInteractions > 0 || Date.now() <= editorToolbarFocusPreserveUntil;
95
+ }
96
+ function setActiveEditorToolbarFrameOwnerForEditor(ownerId, isActive) {
97
+ const nextOwnerId = isActive
98
+ ? ownerId
99
+ : activeEditorToolbarFrameOwnerId === ownerId
100
+ ? null
101
+ : activeEditorToolbarFrameOwnerId;
102
+ if (activeEditorToolbarFrameOwnerId === nextOwnerId) {
103
+ return;
104
+ }
105
+ activeEditorToolbarFrameOwnerId = nextOwnerId;
106
+ notifyEditorToolbarFrameListeners();
107
+ }
108
+ function useEditorToolbarFrames(ownerId) {
109
+ const [frames, setFrames] = (0, react_1.useState)(() => getEditorToolbarFramesSnapshot(ownerId));
110
+ (0, react_1.useEffect)(() => {
111
+ const listener = () => {
112
+ const nextFrames = getEditorToolbarFramesSnapshot(ownerId);
113
+ setFrames((currentFrames) => areToolbarFrameListsEqual(currentFrames, nextFrames) ? currentFrames : nextFrames);
114
+ };
115
+ editorToolbarFrameListeners.add(listener);
116
+ listener();
117
+ return () => {
118
+ editorToolbarFrameListeners.delete(listener);
119
+ };
120
+ }, [ownerId]);
121
+ return frames;
122
+ }
123
+ function setEditorToolbarMentionState(ownerId, state) {
124
+ if (state == null) {
125
+ if (editorToolbarMentionState?.ownerId !== ownerId) {
126
+ return;
127
+ }
128
+ editorToolbarMentionState = null;
129
+ notifyEditorToolbarMentionStateListeners();
130
+ return;
131
+ }
132
+ editorToolbarMentionState = {
133
+ ownerId,
134
+ ...state,
135
+ };
136
+ notifyEditorToolbarMentionStateListeners();
137
+ }
138
+ function _setEditorToolbarFrameForTests(id, frame, ownerId = null) {
139
+ registerEditorToolbarFrame(id, frame, ownerId);
140
+ }
141
+ function _resetEditorToolbarFrameRegistryForTests() {
142
+ editorToolbarFrames.clear();
143
+ editorToolbarMentionState = null;
144
+ activeEditorToolbarInteractions = 0;
145
+ editorToolbarFocusPreserveUntil = 0;
146
+ activeEditorToolbarFrameOwnerId = null;
147
+ notifyEditorToolbarFrameListeners();
148
+ notifyEditorToolbarMentionStateListeners();
149
+ }
150
+ function _beginEditorToolbarInteractionForTests() {
151
+ beginEditorToolbarInteraction();
152
+ }
153
+ function _endEditorToolbarInteractionForTests() {
154
+ endEditorToolbarInteraction();
155
+ }
156
+ function defaultIcon(id) {
157
+ return { type: 'default', id };
158
+ }
159
+ function resolveToolbarItemPlacement(placement) {
160
+ return placement ?? 'scroll';
161
+ }
162
+ exports.DEFAULT_EDITOR_TOOLBAR_ITEMS = [
163
+ { type: 'mark', mark: 'bold', label: 'Bold', icon: defaultIcon('bold') },
164
+ { type: 'mark', mark: 'italic', label: 'Italic', icon: defaultIcon('italic') },
165
+ { type: 'mark', mark: 'underline', label: 'Underline', icon: defaultIcon('underline') },
166
+ { type: 'mark', mark: 'strike', label: 'Strikethrough', icon: defaultIcon('strike') },
167
+ { type: 'blockquote', label: 'Blockquote', icon: defaultIcon('blockquote') },
168
+ { type: 'separator' },
169
+ { type: 'list', listType: 'bulletList', label: 'Bullet List', icon: defaultIcon('bulletList') },
170
+ {
171
+ type: 'list',
172
+ listType: 'orderedList',
173
+ label: 'Ordered List',
174
+ icon: defaultIcon('orderedList'),
175
+ },
176
+ {
177
+ type: 'command',
178
+ command: 'indentList',
179
+ label: 'Indent List',
180
+ icon: defaultIcon('indentList'),
181
+ },
182
+ {
183
+ type: 'command',
184
+ command: 'outdentList',
185
+ label: 'Outdent List',
186
+ icon: defaultIcon('outdentList'),
187
+ },
188
+ { type: 'node', nodeType: 'hardBreak', label: 'Line Break', icon: defaultIcon('lineBreak') },
189
+ {
190
+ type: 'node',
191
+ nodeType: 'horizontalRule',
192
+ label: 'Horizontal Rule',
193
+ icon: defaultIcon('horizontalRule'),
194
+ },
195
+ { type: 'separator' },
196
+ { type: 'command', command: 'undo', label: 'Undo', icon: defaultIcon('undo') },
197
+ { type: 'command', command: 'redo', label: 'Redo', icon: defaultIcon('redo') },
198
+ ];
199
+ const BUTTON_HIT = 44;
200
+ const BUTTON_VISIBLE = 32;
201
+ const TOOLBAR_PADDING_H = 12;
202
+ const TOOLBAR_PADDING_V = 4;
203
+ const MIN_TOOLBAR_HEIGHT = 40;
204
+ const MENU_MARGIN = 8;
205
+ const MENU_WIDTH = 192;
206
+ const KEYBOARD_FRAME_REMEASURE_DELAYS_MS = [50, 150, 300];
207
+ const ACTIVE_BG = 'rgba(0, 122, 255, 0.12)';
208
+ const ACTIVE_COLOR = '#007AFF';
209
+ const DEFAULT_COLOR = '#666666';
210
+ const DISABLED_COLOR = '#C7C7CC';
211
+ const SEPARATOR_COLOR = '#E5E5EA';
212
+ const TOOLBAR_BG = '#FFFFFF';
213
+ const TOOLBAR_BORDER = '#E5E5EA';
214
+ const TOOLBAR_RADIUS = 0;
215
+ const BUTTON_RADIUS = 6;
216
+ const MENU_BORDER = '#D1D1D6';
217
+ const MENU_SHADOW = '#000000';
218
+ const DEFAULT_GLYPH_ICONS = {
219
+ bold: 'B',
220
+ italic: 'I',
221
+ underline: 'U',
222
+ strike: 'S',
223
+ link: '🔗',
224
+ image: '🖼',
225
+ blockquote: '❝',
226
+ h1: 'H1',
227
+ h2: 'H2',
228
+ h3: 'H3',
229
+ h4: 'H4',
230
+ h5: 'H5',
231
+ h6: 'H6',
232
+ bulletList: '•≡',
233
+ orderedList: '1.',
234
+ indentList: '→',
235
+ outdentList: '←',
236
+ lineBreak: '↵',
237
+ horizontalRule: '—',
238
+ undo: '↩',
239
+ redo: '↪',
240
+ };
241
+ const DEFAULT_MATERIAL_ICONS = {
242
+ bold: 'format-bold',
243
+ italic: 'format-italic',
244
+ underline: 'format-underlined',
245
+ strike: 'strikethrough-s',
246
+ link: 'link',
247
+ image: 'image',
248
+ h1: 'title',
249
+ h2: 'title',
250
+ h3: 'title',
251
+ h4: 'title',
252
+ h5: 'title',
253
+ h6: 'title',
254
+ blockquote: 'format-quote',
255
+ bulletList: 'format-list-bulleted',
256
+ orderedList: 'format-list-numbered',
257
+ indentList: 'format-indent-increase',
258
+ outdentList: 'format-indent-decrease',
259
+ lineBreak: 'keyboard-return',
260
+ horizontalRule: 'horizontal-rule',
261
+ undo: 'undo',
262
+ redo: 'redo',
263
+ };
264
+ function resolveMentionSuggestionLabel(suggestion, trigger) {
265
+ return suggestion.label?.trim() || `${trigger}${suggestion.title}`;
266
+ }
267
+ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic, onToggleUnderline, onToggleStrike, onToggleBulletList, onToggleHeading, onToggleBlockquote, onToggleOrderedList, onIndentList, onOutdentList, onInsertHorizontalRule, onInsertLineBreak, onUndo, onRedo, onToggleMark, onToggleListType, onInsertNodeType, onRunCommand, onToolbarAction, onRequestLink, onRequestImage, toolbarItems = exports.DEFAULT_EDITOR_TOOLBAR_ITEMS, theme, showTopBorder, preserveEditorFocus = true, }) {
268
+ const marks = activeState.marks ?? {};
269
+ const nodes = activeState.nodes ?? {};
270
+ const commands = activeState.commands ?? {};
271
+ const allowedMarks = activeState.allowedMarks ?? [];
272
+ const insertableNodes = activeState.insertableNodes ?? [];
273
+ const rootRef = (0, react_1.useRef)(null);
274
+ const groupButtonRefs = (0, react_1.useRef)(new Map());
275
+ const { width: windowWidth, height: windowHeight } = (0, react_native_1.useWindowDimensions)();
276
+ const [expandedGroupKey, setExpandedGroupKey] = (0, react_1.useState)(null);
277
+ const [menuState, setMenuState] = (0, react_1.useState)(null);
278
+ const mentionState = useEditorToolbarMentionState();
279
+ const toolbarInteractionActiveRef = (0, react_1.useRef)(false);
280
+ const framePublishAnimationFramesRef = (0, react_1.useRef)([]);
281
+ const framePublishTimeoutsRef = (0, react_1.useRef)([]);
282
+ const registrationIdRef = (0, react_1.useRef)(null);
283
+ if (registrationIdRef.current == null) {
284
+ registrationIdRef.current = nextEditorToolbarRegistrationId++;
285
+ }
286
+ const isMarkActive = (0, react_1.useCallback)((mark) => !!marks[mark], [marks]);
287
+ const isInList = !!nodes['bulletList'] || !!nodes['orderedList'];
288
+ const canIndentList = isInList && !!commands['indentList'];
289
+ const canOutdentList = isInList && !!commands['outdentList'];
290
+ const shouldRenderMentionSuggestions = preserveEditorFocus && mentionState != null && mentionState.suggestions.length > 0;
291
+ const getActionForItem = (0, react_1.useCallback)((item) => {
292
+ switch (item.type) {
293
+ case 'mark':
294
+ if (onToggleMark) {
295
+ return () => onToggleMark(item.mark);
296
+ }
297
+ switch (item.mark) {
298
+ case 'bold':
299
+ return onToggleBold;
300
+ case 'italic':
301
+ return onToggleItalic;
302
+ case 'underline':
303
+ return onToggleUnderline;
304
+ case 'strike':
305
+ return onToggleStrike;
306
+ default:
307
+ return null;
308
+ }
309
+ case 'list':
310
+ if (onToggleListType) {
311
+ return () => onToggleListType(item.listType);
312
+ }
313
+ return item.listType === 'bulletList'
314
+ ? (onToggleBulletList ?? null)
315
+ : (onToggleOrderedList ?? null);
316
+ case 'link':
317
+ return onRequestLink ?? null;
318
+ case 'image':
319
+ return onRequestImage ?? null;
320
+ case 'heading':
321
+ return onToggleHeading ? () => onToggleHeading(item.level) : null;
322
+ case 'blockquote':
323
+ return onToggleBlockquote ?? null;
324
+ case 'node':
325
+ if (onInsertNodeType) {
326
+ return () => onInsertNodeType(item.nodeType);
327
+ }
328
+ switch (item.nodeType) {
329
+ case 'hardBreak':
330
+ return onInsertLineBreak ?? null;
331
+ case 'horizontalRule':
332
+ return onInsertHorizontalRule ?? null;
333
+ default:
334
+ return null;
335
+ }
336
+ case 'command':
337
+ if (onRunCommand) {
338
+ return () => onRunCommand(item.command);
339
+ }
340
+ switch (item.command) {
341
+ case 'indentList':
342
+ return onIndentList ?? null;
343
+ case 'outdentList':
344
+ return onOutdentList ?? null;
345
+ case 'undo':
346
+ return onUndo;
347
+ case 'redo':
348
+ return onRedo;
349
+ }
350
+ case 'action':
351
+ return onToolbarAction ? () => onToolbarAction(item.key) : null;
352
+ }
353
+ }, [
354
+ onIndentList,
355
+ onInsertHorizontalRule,
356
+ onInsertLineBreak,
357
+ onInsertNodeType,
358
+ onOutdentList,
359
+ onRedo,
360
+ onRunCommand,
361
+ onRequestImage,
362
+ onRequestLink,
363
+ onToggleBlockquote,
364
+ onToggleBold,
365
+ onToggleBulletList,
366
+ onToggleHeading,
367
+ onToggleItalic,
368
+ onToggleListType,
369
+ onToggleMark,
370
+ onToggleOrderedList,
371
+ onToggleStrike,
372
+ onToggleUnderline,
373
+ onToolbarAction,
374
+ onUndo,
375
+ ]);
376
+ const makeButtonKey = (0, react_1.useCallback)((item, index, prefix = '') => item.key != null
377
+ ? `${prefix}${item.key}`
378
+ : item.type === 'mark'
379
+ ? `${prefix}mark:${item.mark}:${index}`
380
+ : item.type === 'link'
381
+ ? `${prefix}link:${index}`
382
+ : item.type === 'image'
383
+ ? `${prefix}image:${index}`
384
+ : item.type === 'heading'
385
+ ? `${prefix}heading:${item.level}:${index}`
386
+ : item.type === 'blockquote'
387
+ ? `${prefix}blockquote:${index}`
388
+ : item.type === 'list'
389
+ ? `${prefix}list:${item.listType}:${index}`
390
+ : item.type === 'command'
391
+ ? `${prefix}command:${item.command}:${index}`
392
+ : item.type === 'node'
393
+ ? `${prefix}node:${item.nodeType}:${index}`
394
+ : `${prefix}action:${item.key}:${index}`, []);
395
+ const resolveButton = (0, react_1.useCallback)((item, index, prefix = '', groupKey, placement = 'scroll') => {
396
+ const action = getActionForItem(item);
397
+ if (!action) {
398
+ return null;
399
+ }
400
+ let isActive = false;
401
+ let isDisabled = false;
402
+ switch (item.type) {
403
+ case 'mark':
404
+ isActive = isMarkActive(item.mark);
405
+ isDisabled = !allowedMarks.includes(item.mark);
406
+ break;
407
+ case 'link':
408
+ isActive = isMarkActive('link');
409
+ isDisabled = !allowedMarks.includes('link') || !onRequestLink;
410
+ break;
411
+ case 'image':
412
+ isDisabled = !insertableNodes.includes('image') || !onRequestImage;
413
+ break;
414
+ case 'heading': {
415
+ const headingNodeType = `h${item.level}`;
416
+ isActive = !!nodes[headingNodeType];
417
+ isDisabled = !commands[`toggleHeading${item.level}`];
418
+ break;
419
+ }
420
+ case 'blockquote':
421
+ isActive = !!nodes['blockquote'];
422
+ isDisabled = !commands['toggleBlockquote'];
423
+ break;
424
+ case 'list':
425
+ isActive = !!nodes[item.listType];
426
+ isDisabled =
427
+ !commands[item.listType === 'bulletList' ? 'wrapBulletList' : 'wrapOrderedList'];
428
+ break;
429
+ case 'command':
430
+ switch (item.command) {
431
+ case 'indentList':
432
+ isDisabled = !canIndentList;
433
+ break;
434
+ case 'outdentList':
435
+ isDisabled = !canOutdentList;
436
+ break;
437
+ case 'undo':
438
+ isDisabled = !historyState.canUndo;
439
+ break;
440
+ case 'redo':
441
+ isDisabled = !historyState.canRedo;
442
+ break;
443
+ }
444
+ break;
445
+ case 'action':
446
+ isActive = !!item.isActive;
447
+ isDisabled = !!item.isDisabled || !onToolbarAction;
448
+ break;
449
+ case 'node':
450
+ isActive = !!nodes[item.nodeType];
451
+ isDisabled = !insertableNodes.includes(item.nodeType);
452
+ break;
453
+ }
454
+ return {
455
+ key: makeButtonKey(item, index, prefix),
456
+ label: item.label,
457
+ icon: item.icon,
458
+ action,
459
+ isActive,
460
+ isDisabled,
461
+ groupKey,
462
+ placement,
463
+ };
464
+ }, [
465
+ allowedMarks,
466
+ canIndentList,
467
+ canOutdentList,
468
+ commands,
469
+ getActionForItem,
470
+ historyState.canRedo,
471
+ historyState.canUndo,
472
+ insertableNodes,
473
+ isMarkActive,
474
+ makeButtonKey,
475
+ nodes,
476
+ onRequestImage,
477
+ onRequestLink,
478
+ onToolbarAction,
479
+ ]);
480
+ const compactRenderedItems = (entries) => entries.filter((entry, index, list) => {
481
+ if (entry.type !== 'separator') {
482
+ return true;
483
+ }
484
+ const previous = list[index - 1];
485
+ const next = list[index + 1];
486
+ return (previous != null &&
487
+ previous.type !== 'separator' &&
488
+ next != null &&
489
+ next.type !== 'separator');
490
+ });
491
+ const { startItems, scrollItems, endItems, groupsByKey } = (0, react_1.useMemo)(() => {
492
+ const startEntries = [];
493
+ const scrollEntries = [];
494
+ const endEntries = [];
495
+ const nextGroups = new Map();
496
+ const entriesForPlacement = (placement) => placement === 'start' ? startEntries : placement === 'end' ? endEntries : scrollEntries;
497
+ for (let index = 0; index < toolbarItems.length; index += 1) {
498
+ const item = toolbarItems[index];
499
+ const placement = resolveToolbarItemPlacement(item.placement);
500
+ const targetEntries = entriesForPlacement(placement);
501
+ if (item.type === 'separator') {
502
+ targetEntries.push({
503
+ type: 'separator',
504
+ key: item.key ?? `separator:${index}`,
505
+ placement,
506
+ });
507
+ continue;
508
+ }
509
+ if (item.type === 'group') {
510
+ const children = item.items
511
+ .map((child, childIndex) => resolveButton(child, childIndex, `${item.key}:`, item.key, placement))
512
+ .filter((child) => child != null);
513
+ if (children.length === 0) {
514
+ continue;
515
+ }
516
+ const presentation = item.presentation ?? 'expand';
517
+ const isExpanded = presentation === 'expand' && expandedGroupKey === item.key;
518
+ const isMenuOpen = presentation === 'menu' && menuState?.groupKey === item.key;
519
+ const group = {
520
+ key: item.key,
521
+ label: item.label,
522
+ icon: item.icon,
523
+ presentation,
524
+ placement,
525
+ children,
526
+ isActive: children.some((child) => child.isActive) || isExpanded || isMenuOpen,
527
+ isDisabled: children.every((child) => child.isDisabled),
528
+ isExpanded,
529
+ isOpen: isExpanded || isMenuOpen,
530
+ };
531
+ nextGroups.set(group.key, group);
532
+ targetEntries.push({ type: 'group', group });
533
+ if (group.isExpanded) {
534
+ targetEntries.push(...children.map((child) => ({ type: 'button', button: child })));
535
+ }
536
+ continue;
537
+ }
538
+ const button = resolveButton(item, index, '', undefined, placement);
539
+ if (button) {
540
+ targetEntries.push({ type: 'button', button });
541
+ }
542
+ }
543
+ return {
544
+ startItems: compactRenderedItems(startEntries),
545
+ scrollItems: compactRenderedItems(scrollEntries),
546
+ endItems: compactRenderedItems(endEntries),
547
+ groupsByKey: nextGroups,
548
+ };
549
+ }, [expandedGroupKey, menuState?.groupKey, resolveButton, toolbarItems]);
550
+ const resolvedShowTopBorder = showTopBorder ?? theme?.showTopBorder ?? true;
551
+ const publishToolbarFrame = (0, react_1.useCallback)(() => {
552
+ const registrationId = registrationIdRef.current;
553
+ const toolbar = rootRef.current;
554
+ if (!preserveEditorFocus || registrationId == null || !toolbar) {
555
+ if (registrationId != null) {
556
+ unregisterEditorToolbarFrame(registrationId);
557
+ }
558
+ return;
559
+ }
560
+ if (typeof toolbar.measureInWindow !== 'function') {
561
+ return;
562
+ }
563
+ toolbar.measureInWindow((x, y, width, height) => {
564
+ registerEditorToolbarFrame(registrationId, { x, y, width, height }, null);
565
+ });
566
+ }, [preserveEditorFocus]);
567
+ const cancelScheduledFramePublishes = (0, react_1.useCallback)(() => {
568
+ framePublishAnimationFramesRef.current.forEach((frame) => cancelAnimationFrame(frame));
569
+ framePublishAnimationFramesRef.current = [];
570
+ framePublishTimeoutsRef.current.forEach((timeout) => clearTimeout(timeout));
571
+ framePublishTimeoutsRef.current = [];
572
+ }, []);
573
+ const scheduleToolbarFramePublish = (0, react_1.useCallback)(() => {
574
+ if (!preserveEditorFocus) {
575
+ return;
576
+ }
577
+ cancelScheduledFramePublishes();
578
+ publishToolbarFrame();
579
+ framePublishAnimationFramesRef.current.push(requestAnimationFrame(publishToolbarFrame));
580
+ KEYBOARD_FRAME_REMEASURE_DELAYS_MS.forEach((delay) => {
581
+ framePublishTimeoutsRef.current.push(setTimeout(publishToolbarFrame, delay));
582
+ });
583
+ }, [cancelScheduledFramePublishes, preserveEditorFocus, publishToolbarFrame]);
584
+ const handleToolbarLayout = (0, react_1.useCallback)(() => {
585
+ requestAnimationFrame(publishToolbarFrame);
586
+ }, [publishToolbarFrame]);
587
+ (0, react_1.useEffect)(() => {
588
+ if (!preserveEditorFocus) {
589
+ const registrationId = registrationIdRef.current;
590
+ if (registrationId != null) {
591
+ unregisterEditorToolbarFrame(registrationId);
592
+ }
593
+ return;
594
+ }
595
+ const frame = requestAnimationFrame(publishToolbarFrame);
596
+ return () => cancelAnimationFrame(frame);
597
+ }, [
598
+ expandedGroupKey,
599
+ menuState?.groupKey,
600
+ preserveEditorFocus,
601
+ publishToolbarFrame,
602
+ startItems.length,
603
+ scrollItems.length,
604
+ endItems.length,
605
+ windowHeight,
606
+ windowWidth,
607
+ ]);
608
+ (0, react_1.useEffect)(() => {
609
+ const registrationId = registrationIdRef.current;
610
+ return () => {
611
+ cancelScheduledFramePublishes();
612
+ if (toolbarInteractionActiveRef.current) {
613
+ toolbarInteractionActiveRef.current = false;
614
+ endEditorToolbarInteraction();
615
+ }
616
+ if (registrationId != null) {
617
+ unregisterEditorToolbarFrame(registrationId);
618
+ }
619
+ };
620
+ }, [cancelScheduledFramePublishes]);
621
+ (0, react_1.useEffect)(() => {
622
+ if (!preserveEditorFocus) {
623
+ cancelScheduledFramePublishes();
624
+ return;
625
+ }
626
+ const subscriptions = [
627
+ react_native_1.Keyboard.addListener('keyboardDidShow', scheduleToolbarFramePublish),
628
+ react_native_1.Keyboard.addListener('keyboardDidHide', scheduleToolbarFramePublish),
629
+ react_native_1.Keyboard.addListener('keyboardDidChangeFrame', scheduleToolbarFramePublish),
630
+ ];
631
+ return () => {
632
+ subscriptions.forEach((subscription) => subscription.remove());
633
+ cancelScheduledFramePublishes();
634
+ };
635
+ }, [cancelScheduledFramePublishes, preserveEditorFocus, scheduleToolbarFramePublish]);
636
+ (0, react_1.useEffect)(() => {
637
+ if (expandedGroupKey != null && !groupsByKey.has(expandedGroupKey)) {
638
+ setExpandedGroupKey(null);
639
+ }
640
+ }, [expandedGroupKey, groupsByKey]);
641
+ (0, react_1.useEffect)(() => {
642
+ if (menuState != null && !groupsByKey.has(menuState.groupKey)) {
643
+ setMenuState(null);
644
+ }
645
+ }, [groupsByKey, menuState]);
646
+ (0, react_1.useEffect)(() => {
647
+ if (shouldRenderMentionSuggestions) {
648
+ setExpandedGroupKey(null);
649
+ setMenuState(null);
650
+ }
651
+ }, [shouldRenderMentionSuggestions]);
652
+ const handleButtonPress = (0, react_1.useCallback)((button) => {
653
+ button.action();
654
+ if (button.groupKey) {
655
+ setExpandedGroupKey((current) => (current === button.groupKey ? null : current));
656
+ }
657
+ setMenuState(null);
658
+ }, []);
659
+ const handleToolbarPressIn = (0, react_1.useCallback)(() => {
660
+ if (preserveEditorFocus && !toolbarInteractionActiveRef.current) {
661
+ toolbarInteractionActiveRef.current = true;
662
+ beginEditorToolbarInteraction();
663
+ }
664
+ }, [preserveEditorFocus]);
665
+ const handleToolbarPressOut = (0, react_1.useCallback)(() => {
666
+ if (preserveEditorFocus && toolbarInteractionActiveRef.current) {
667
+ toolbarInteractionActiveRef.current = false;
668
+ endEditorToolbarInteraction();
669
+ }
670
+ }, [preserveEditorFocus]);
671
+ const handleGroupPress = (0, react_1.useCallback)((group) => {
672
+ if (group.isDisabled) {
673
+ return;
674
+ }
675
+ if (group.presentation === 'expand') {
676
+ setMenuState(null);
677
+ setExpandedGroupKey((current) => (current === group.key ? null : group.key));
678
+ return;
679
+ }
680
+ const anchor = groupButtonRefs.current.get(group.key);
681
+ if (!anchor) {
682
+ return;
683
+ }
684
+ anchor.measureInWindow((x, y, width, height) => {
685
+ setExpandedGroupKey(null);
686
+ setMenuState((current) => current?.groupKey === group.key
687
+ ? null
688
+ : {
689
+ groupKey: group.key,
690
+ x,
691
+ y,
692
+ width,
693
+ height,
694
+ });
695
+ });
696
+ }, []);
697
+ const menuGroup = menuState != null ? (groupsByKey.get(menuState.groupKey) ?? null) : null;
698
+ const menuHeight = menuGroup ? menuGroup.children.length * 40 + 16 : 0;
699
+ const resolvedToolbarHeight = Math.max(theme?.height ?? BUTTON_VISIBLE + TOOLBAR_PADDING_V * 2, MIN_TOOLBAR_HEIGHT);
700
+ const resolvedButtonHeight = theme?.height == null
701
+ ? BUTTON_VISIBLE
702
+ : Math.max(28, Math.min(40, resolvedToolbarHeight - TOOLBAR_PADDING_V * 2));
703
+ const resolvedToolbarPaddingV = theme?.height == null
704
+ ? TOOLBAR_PADDING_V
705
+ : Math.max(4, (resolvedToolbarHeight - resolvedButtonHeight) / 2);
706
+ const resolvedSeparatorHeight = Math.max(16, resolvedButtonHeight - 12);
707
+ const menuTop = menuState == null
708
+ ? 0
709
+ : Math.max(MENU_MARGIN, Math.min(menuState.y + menuState.height + 8, windowHeight - menuHeight - MENU_MARGIN));
710
+ const menuLeft = menuState == null
711
+ ? 0
712
+ : Math.max(MENU_MARGIN, Math.min(menuState.x + menuState.width - MENU_WIDTH, windowWidth - MENU_WIDTH - MENU_MARGIN));
713
+ const renderButton = (button, onPress, options) => {
714
+ const activeColor = theme?.buttonActiveColor ?? ACTIVE_COLOR;
715
+ const defaultColor = theme?.buttonColor ?? DEFAULT_COLOR;
716
+ const disabledColor = theme?.buttonDisabledColor ?? DISABLED_COLOR;
717
+ const color = button.isActive
718
+ ? activeColor
719
+ : button.isDisabled
720
+ ? disabledColor
721
+ : defaultColor;
722
+ const anchorGroupKey = options?.anchorGroupKey;
723
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { ref: anchorGroupKey == null
724
+ ? undefined
725
+ : (node) => {
726
+ if (node) {
727
+ groupButtonRefs.current.set(anchorGroupKey, node);
728
+ }
729
+ else {
730
+ groupButtonRefs.current.delete(anchorGroupKey);
731
+ }
732
+ }, collapsable: false, style: styles.buttonAnchor, children: [(0, jsx_runtime_1.jsx)(react_native_1.TouchableOpacity, { onPressIn: handleToolbarPressIn, onPressOut: handleToolbarPressOut, onPress: onPress, disabled: button.isDisabled, style: [
733
+ styles.button,
734
+ {
735
+ height: resolvedButtonHeight,
736
+ borderRadius: theme?.buttonBorderRadius ?? BUTTON_RADIUS,
737
+ },
738
+ button.isActive && {
739
+ backgroundColor: theme?.buttonActiveBackgroundColor ?? ACTIVE_BG,
740
+ },
741
+ ], activeOpacity: 0.5, accessibilityRole: 'button', accessibilityLabel: button.label, accessibilityState: {
742
+ selected: button.isActive,
743
+ disabled: button.isDisabled,
744
+ expanded: options?.showsDisclosure ? options.expanded : undefined,
745
+ }, children: (0, jsx_runtime_1.jsx)(react_native_1.View, { children: (0, jsx_runtime_1.jsx)(ToolbarIcon, { icon: button.icon, color: color }) }) }), options?.showsDisclosure ? ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.groupDisclosure, { color }], children: '\u25BE' })) : null] }, button.key));
746
+ };
747
+ const renderSeparator = (key) => ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
748
+ styles.separator,
749
+ { height: resolvedSeparatorHeight },
750
+ theme?.separatorColor != null ? { backgroundColor: theme.separatorColor } : null,
751
+ ] }, key));
752
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { ref: rootRef, collapsable: false, onLayout: handleToolbarLayout, style: [
753
+ styles.container,
754
+ !resolvedShowTopBorder && styles.containerWithoutTopBorder,
755
+ {
756
+ minHeight: resolvedToolbarHeight,
757
+ paddingVertical: resolvedToolbarPaddingV,
758
+ },
759
+ theme?.backgroundColor != null ? { backgroundColor: theme.backgroundColor } : null,
760
+ theme?.borderColor != null
761
+ ? resolvedShowTopBorder
762
+ ? { borderTopColor: theme.borderColor }
763
+ : null
764
+ : null,
765
+ theme?.borderWidth != null
766
+ ? resolvedShowTopBorder
767
+ ? { borderTopWidth: theme.borderWidth }
768
+ : null
769
+ : null,
770
+ {
771
+ borderRadius: theme?.borderRadius ?? TOOLBAR_RADIUS,
772
+ },
773
+ ], children: [shouldRenderMentionSuggestions && mentionState != null ? ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.toolbarRow, children: [startItems.length > 0 ? ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.fixedSection, children: startItems.map((item) => {
774
+ if (item.type === 'separator') {
775
+ return renderSeparator(item.key);
776
+ }
777
+ if (item.type === 'group') {
778
+ return renderButton({
779
+ key: item.group.key,
780
+ label: item.group.label,
781
+ icon: item.group.icon,
782
+ isActive: item.group.isActive,
783
+ isDisabled: item.group.isDisabled,
784
+ }, () => handleGroupPress(item.group), {
785
+ anchorGroupKey: item.group.key,
786
+ showsDisclosure: true,
787
+ expanded: item.group.isOpen,
788
+ });
789
+ }
790
+ return renderButton(item.button, () => handleButtonPress(item.button));
791
+ }) })) : null, (0, jsx_runtime_1.jsx)(react_native_1.ScrollView, { testID: 'editor-toolbar-mention-suggestions', horizontal: true, showsHorizontalScrollIndicator: false, style: [
792
+ styles.scrollSection,
793
+ styles.mentionSuggestionsScroll,
794
+ {
795
+ backgroundColor: mentionState.theme?.popoverBackgroundColor ??
796
+ mentionState.theme?.backgroundColor ??
797
+ 'transparent',
798
+ borderColor: mentionState.theme?.popoverBorderColor ??
799
+ mentionState.theme?.borderColor ??
800
+ 'transparent',
801
+ borderWidth: mentionState.theme?.popoverBorderWidth ??
802
+ mentionState.theme?.borderWidth ??
803
+ 0,
804
+ borderRadius: mentionState.theme?.popoverBorderRadius ??
805
+ mentionState.theme?.borderRadius ??
806
+ 0,
807
+ },
808
+ mentionState.theme?.popoverShadowColor != null
809
+ ? {
810
+ shadowColor: mentionState.theme.popoverShadowColor,
811
+ shadowOpacity: 0.14,
812
+ shadowRadius: 12,
813
+ shadowOffset: { width: 0, height: 4 },
814
+ elevation: 8,
815
+ }
816
+ : null,
817
+ ], contentContainerStyle: styles.mentionSuggestionsContent, keyboardShouldPersistTaps: 'always', children: mentionState.suggestions.map((suggestion) => {
818
+ const label = resolveMentionSuggestionLabel(suggestion, mentionState.trigger);
819
+ const suggestionTheme = mentionState.suggestionThemes?.[suggestion.key] ?? mentionState.theme;
820
+ return ((0, jsx_runtime_1.jsx)(react_native_1.Pressable, { testID: `editor-toolbar-mention-suggestion-${suggestion.key}`, accessibilityRole: 'button', accessibilityLabel: label, onPressIn: handleToolbarPressIn, onPressOut: handleToolbarPressOut, onPress: () => mentionState.onSelectSuggestion(suggestion), style: ({ pressed }) => [
821
+ styles.mentionSuggestion,
822
+ {
823
+ backgroundColor: pressed
824
+ ? (suggestionTheme?.optionHighlightedBackgroundColor ??
825
+ 'rgba(0, 122, 255, 0.12)')
826
+ : (suggestionTheme?.backgroundColor ?? '#F2F2F7'),
827
+ borderColor: suggestionTheme?.borderColor ?? 'transparent',
828
+ borderWidth: suggestionTheme?.borderWidth ?? 0,
829
+ borderRadius: suggestionTheme?.borderRadius ?? 12,
830
+ },
831
+ ], children: ({ pressed }) => ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { numberOfLines: 1, style: [
832
+ styles.mentionSuggestionTitle,
833
+ {
834
+ fontWeight: suggestionTheme?.fontWeight ?? '600',
835
+ color: pressed
836
+ ? (suggestionTheme?.optionHighlightedTextColor ??
837
+ suggestionTheme?.optionTextColor ??
838
+ '#000000')
839
+ : (suggestionTheme?.optionTextColor ??
840
+ suggestionTheme?.textColor ??
841
+ '#000000'),
842
+ },
843
+ ], children: label }), suggestion.subtitle ? ((0, jsx_runtime_1.jsx)(react_native_1.Text, { numberOfLines: 1, style: [
844
+ styles.mentionSuggestionSubtitle,
845
+ {
846
+ color: suggestionTheme?.optionSecondaryTextColor ??
847
+ '#8E8E93',
848
+ },
849
+ ], children: suggestion.subtitle })) : null] })) }, suggestion.key));
850
+ }) }), endItems.length > 0 ? ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.fixedSection, children: endItems.map((item) => {
851
+ if (item.type === 'separator') {
852
+ return renderSeparator(item.key);
853
+ }
854
+ if (item.type === 'group') {
855
+ return renderButton({
856
+ key: item.group.key,
857
+ label: item.group.label,
858
+ icon: item.group.icon,
859
+ isActive: item.group.isActive,
860
+ isDisabled: item.group.isDisabled,
861
+ }, () => handleGroupPress(item.group), {
862
+ anchorGroupKey: item.group.key,
863
+ showsDisclosure: true,
864
+ expanded: item.group.isOpen,
865
+ });
866
+ }
867
+ return renderButton(item.button, () => handleButtonPress(item.button));
868
+ }) })) : null] })) : ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.toolbarRow, children: [startItems.length > 0 ? ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.fixedSection, children: startItems.map((item) => {
869
+ if (item.type === 'separator') {
870
+ return renderSeparator(item.key);
871
+ }
872
+ if (item.type === 'group') {
873
+ return renderButton({
874
+ key: item.group.key,
875
+ label: item.group.label,
876
+ icon: item.group.icon,
877
+ isActive: item.group.isActive,
878
+ isDisabled: item.group.isDisabled,
879
+ }, () => handleGroupPress(item.group), {
880
+ anchorGroupKey: item.group.key,
881
+ showsDisclosure: true,
882
+ expanded: item.group.isOpen,
883
+ });
884
+ }
885
+ return renderButton(item.button, () => handleButtonPress(item.button));
886
+ }) })) : null, (0, jsx_runtime_1.jsx)(react_native_1.ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, style: styles.scrollSection, contentContainerStyle: styles.scrollContent, keyboardShouldPersistTaps: 'always', onScrollBeginDrag: () => setMenuState(null), children: scrollItems.map((item) => {
887
+ if (item.type === 'separator') {
888
+ return renderSeparator(item.key);
889
+ }
890
+ if (item.type === 'group') {
891
+ return renderButton({
892
+ key: item.group.key,
893
+ label: item.group.label,
894
+ icon: item.group.icon,
895
+ isActive: item.group.isActive,
896
+ isDisabled: item.group.isDisabled,
897
+ }, () => handleGroupPress(item.group), {
898
+ anchorGroupKey: item.group.key,
899
+ showsDisclosure: true,
900
+ expanded: item.group.isOpen,
901
+ });
902
+ }
903
+ return renderButton(item.button, () => handleButtonPress(item.button));
904
+ }) }), endItems.length > 0 ? ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.fixedSection, children: endItems.map((item) => {
905
+ if (item.type === 'separator') {
906
+ return renderSeparator(item.key);
907
+ }
908
+ if (item.type === 'group') {
909
+ return renderButton({
910
+ key: item.group.key,
911
+ label: item.group.label,
912
+ icon: item.group.icon,
913
+ isActive: item.group.isActive,
914
+ isDisabled: item.group.isDisabled,
915
+ }, () => handleGroupPress(item.group), {
916
+ anchorGroupKey: item.group.key,
917
+ showsDisclosure: true,
918
+ expanded: item.group.isOpen,
919
+ });
920
+ }
921
+ return renderButton(item.button, () => handleButtonPress(item.button));
922
+ }) })) : null] })), !shouldRenderMentionSuggestions && menuState != null && menuGroup != null ? ((0, jsx_runtime_1.jsx)(react_native_1.Modal, { transparent: true, visible: true, animationType: 'fade', onRequestClose: () => setMenuState(null), children: (0, jsx_runtime_1.jsx)(react_native_1.Pressable, { style: styles.menuBackdrop, onPress: () => setMenuState(null), children: (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
923
+ styles.menuCard,
924
+ {
925
+ top: menuTop,
926
+ left: menuLeft,
927
+ backgroundColor: theme?.backgroundColor ?? TOOLBAR_BG,
928
+ borderColor: theme?.borderColor ?? MENU_BORDER,
929
+ },
930
+ ], children: menuGroup.children.map((button) => {
931
+ const activeColor = theme?.buttonActiveColor ?? ACTIVE_COLOR;
932
+ const defaultColor = theme?.buttonColor ?? DEFAULT_COLOR;
933
+ const disabledColor = theme?.buttonDisabledColor ?? DISABLED_COLOR;
934
+ const color = button.isActive
935
+ ? activeColor
936
+ : button.isDisabled
937
+ ? disabledColor
938
+ : defaultColor;
939
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.Pressable, { onPressIn: handleToolbarPressIn, onPressOut: handleToolbarPressOut, onPress: () => handleButtonPress(button), disabled: button.isDisabled, style: ({ pressed }) => [
940
+ styles.menuItem,
941
+ button.isActive && {
942
+ backgroundColor: theme?.buttonActiveBackgroundColor ?? ACTIVE_BG,
943
+ },
944
+ pressed &&
945
+ !button.isDisabled && {
946
+ opacity: 0.75,
947
+ },
948
+ ], accessibilityRole: 'button', accessibilityLabel: button.label, accessibilityState: {
949
+ selected: button.isActive,
950
+ disabled: button.isDisabled,
951
+ }, children: [(0, jsx_runtime_1.jsx)(ToolbarIcon, { icon: button.icon, color: color }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.menuLabel, { color }], children: button.label })] }, button.key));
952
+ }) }) }) })) : null] }));
953
+ }
954
+ function ToolbarIcon({ icon, color }) {
955
+ const materialIconName = resolveMaterialIconName(icon);
956
+ if (materialIconName) {
957
+ return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.iconContainer, children: (0, jsx_runtime_1.jsx)(vector_icons_1.MaterialIcons, { name: materialIconName, size: 20, color: color }) }));
958
+ }
959
+ const glyph = resolveGlyphText(icon) ?? '?';
960
+ return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.iconContainer, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.iconText, { color }], children: glyph }) }));
961
+ }
962
+ function resolveMaterialIconName(icon) {
963
+ switch (icon.type) {
964
+ case 'default':
965
+ return DEFAULT_MATERIAL_ICONS[icon.id];
966
+ case 'platform':
967
+ return icon.android?.type === 'material' ? icon.android.name : undefined;
968
+ case 'glyph':
969
+ return undefined;
970
+ }
971
+ }
972
+ function resolveGlyphText(icon) {
973
+ switch (icon.type) {
974
+ case 'default':
975
+ return DEFAULT_GLYPH_ICONS[icon.id];
976
+ case 'glyph':
977
+ return icon.text;
978
+ case 'platform':
979
+ return icon.fallbackText;
980
+ }
981
+ }
982
+ const styles = react_native_1.StyleSheet.create({
983
+ container: {
984
+ backgroundColor: TOOLBAR_BG,
985
+ borderTopWidth: react_native_1.StyleSheet.hairlineWidth,
986
+ borderTopColor: TOOLBAR_BORDER,
987
+ paddingVertical: TOOLBAR_PADDING_V,
988
+ overflow: 'hidden',
989
+ },
990
+ containerWithoutTopBorder: {
991
+ borderTopWidth: 0,
992
+ },
993
+ scrollContent: {
994
+ flexDirection: 'row',
995
+ alignItems: 'center',
996
+ paddingHorizontal: TOOLBAR_PADDING_H,
997
+ minWidth: '100%',
998
+ },
999
+ mentionSuggestionsContent: {
1000
+ paddingHorizontal: 12,
1001
+ paddingVertical: 4,
1002
+ alignItems: 'center',
1003
+ minWidth: '100%',
1004
+ },
1005
+ mentionSuggestionsScroll: {
1006
+ overflow: 'hidden',
1007
+ },
1008
+ toolbarRow: {
1009
+ flexDirection: 'row',
1010
+ alignItems: 'center',
1011
+ },
1012
+ fixedSection: {
1013
+ flexDirection: 'row',
1014
+ alignItems: 'center',
1015
+ flexShrink: 0,
1016
+ },
1017
+ scrollSection: {
1018
+ flex: 1,
1019
+ minWidth: 0,
1020
+ },
1021
+ mentionSuggestion: {
1022
+ minWidth: 88,
1023
+ minHeight: 40,
1024
+ marginRight: 8,
1025
+ paddingHorizontal: 12,
1026
+ paddingVertical: 8,
1027
+ justifyContent: 'center',
1028
+ },
1029
+ mentionSuggestionTitle: {
1030
+ fontSize: 14,
1031
+ fontWeight: '600',
1032
+ },
1033
+ mentionSuggestionSubtitle: {
1034
+ marginTop: 1,
1035
+ fontSize: 12,
1036
+ },
1037
+ buttonAnchor: {
1038
+ position: 'relative',
1039
+ },
1040
+ button: {
1041
+ width: BUTTON_HIT,
1042
+ height: BUTTON_VISIBLE,
1043
+ justifyContent: 'center',
1044
+ alignItems: 'center',
1045
+ borderRadius: BUTTON_RADIUS,
1046
+ },
1047
+ groupDisclosure: {
1048
+ position: 'absolute',
1049
+ right: 5,
1050
+ bottom: 2,
1051
+ fontSize: 9,
1052
+ fontWeight: '700',
1053
+ },
1054
+ separator: {
1055
+ width: react_native_1.StyleSheet.hairlineWidth,
1056
+ height: 20,
1057
+ marginHorizontal: 4,
1058
+ backgroundColor: SEPARATOR_COLOR,
1059
+ },
1060
+ iconContainer: {
1061
+ justifyContent: 'center',
1062
+ alignItems: 'center',
1063
+ },
1064
+ iconText: {
1065
+ fontSize: 16,
1066
+ fontWeight: '600',
1067
+ },
1068
+ menuBackdrop: {
1069
+ flex: 1,
1070
+ },
1071
+ menuCard: {
1072
+ position: 'absolute',
1073
+ width: MENU_WIDTH,
1074
+ borderRadius: 14,
1075
+ borderWidth: react_native_1.StyleSheet.hairlineWidth,
1076
+ paddingVertical: 8,
1077
+ shadowColor: MENU_SHADOW,
1078
+ shadowOpacity: 0.16,
1079
+ shadowRadius: 18,
1080
+ shadowOffset: { width: 0, height: 8 },
1081
+ elevation: 10,
1082
+ },
1083
+ menuItem: {
1084
+ minHeight: 40,
1085
+ paddingHorizontal: 12,
1086
+ flexDirection: 'row',
1087
+ alignItems: 'center',
1088
+ },
1089
+ menuLabel: {
1090
+ flex: 1,
1091
+ marginLeft: 10,
1092
+ fontSize: 14,
1093
+ fontWeight: '500',
1094
+ },
1095
+ });