@bendyline/squisq-editor-react 1.5.3 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -10
- package/dist/index.d.ts +1114 -52
- package/dist/index.js +18446 -8050
- package/dist/index.js.map +1 -1
- package/dist/styles/fa-brands-400-AHOAZHCU.woff2 +0 -0
- package/dist/styles/fa-regular-400-VRZYIBIZ.woff2 +0 -0
- package/dist/styles/fa-solid-900-MDEYK55F.woff2 +0 -0
- package/dist/styles/fa-v4compatibility-ETEVP6IB.woff2 +0 -0
- package/dist/styles/index.css +13867 -0
- package/package.json +15 -7
- package/src/BlockCardView.tsx +121 -0
- package/src/BlockPreviewPanel.tsx +69 -0
- package/src/BlockPropertiesPopover.tsx +191 -0
- package/src/EditorContext.tsx +165 -16
- package/src/EditorShell.tsx +265 -144
- package/src/FolderView.tsx +131 -0
- package/src/Icon.tsx +26 -0
- package/src/ImageEditor.tsx +69 -22
- package/src/OutlinePanel.tsx +64 -7
- package/src/PlainHtmlPreview.tsx +30 -3
- package/src/PreviewControls.tsx +491 -122
- package/src/PreviewPanel.tsx +15 -9
- package/src/RawEditor.tsx +179 -17
- package/src/RecorderEntry.tsx +9 -16
- package/src/TemplateAnnotation.ts +44 -0
- package/src/TemplatePicker.tsx +329 -54
- package/src/ThemeCustomizerPanel.tsx +30 -336
- package/src/ThemePicker.tsx +112 -3
- package/src/TimelineBlockPreview.tsx +37 -0
- package/src/TimelineTrack.tsx +671 -0
- package/src/Toolbar.tsx +544 -180
- package/src/Tooltip.tsx +22 -4
- package/src/TransitionPicker.tsx +351 -0
- package/src/VersionHistoryPanel.tsx +4 -16
- package/src/ViewMenuPanel.tsx +17 -14
- package/src/WysiwygEditor.tsx +161 -65
- package/src/__tests__/blockProperties.test.ts +92 -0
- package/src/__tests__/blockRange.test.ts +105 -0
- package/src/__tests__/buildPreviewDocTransition.test.ts +73 -0
- package/src/__tests__/codeContextSectionView.test.tsx +95 -0
- package/src/__tests__/codeContextZoneManager.test.ts +127 -0
- package/src/__tests__/createShapeLayer.test.ts +46 -0
- package/src/__tests__/diffContextSections.test.ts +39 -0
- package/src/__tests__/drawingShapeRoundTrip.test.ts +49 -0
- package/src/__tests__/editorShellCodeContext.test.tsx +86 -0
- package/src/__tests__/editorShellProps.test.tsx +96 -0
- package/src/__tests__/embeddedMedia.test.ts +48 -0
- package/src/__tests__/headingTransition.test.ts +138 -0
- package/src/__tests__/layoutChildRoundTrip.test.ts +71 -0
- package/src/__tests__/plainHtmlPreview.test.tsx +10 -8
- package/src/__tests__/previewControls.test.tsx +70 -0
- package/src/__tests__/recorderMediaInsert.test.ts +86 -0
- package/src/__tests__/templateAnnotationRoundTrip.test.ts +18 -0
- package/src/__tests__/templatePickerMetadata.test.ts +32 -0
- package/src/__tests__/timelineSource.test.ts +134 -0
- package/src/__tests__/tiptapBridge.test.ts +92 -0
- package/src/__tests__/tiptapBridgeConformance.test.ts +47 -0
- package/src/__tests__/tooltip.test.tsx +72 -0
- package/src/__tests__/transitionCatalog.test.ts +64 -0
- package/src/__tests__/useBlockNavigator.test.tsx +67 -0
- package/src/__tests__/useJsonEditorTokens.test.ts +59 -0
- package/src/__tests__/useMediaRecorder.test.ts +41 -0
- package/src/__tests__/useTimelineClock.test.ts +21 -0
- package/src/blockProperties.ts +88 -0
- package/src/blockRange.ts +132 -0
- package/src/buildPreviewDoc.ts +98 -9
- package/src/codeContext/CodeContextSectionView.tsx +124 -0
- package/src/codeContext/CodeContextZoneManager.ts +149 -0
- package/src/codeContext/CodeContextZones.tsx +121 -0
- package/src/codeContext/diffContextSections.ts +38 -0
- package/src/codeContext/types.ts +75 -0
- package/src/customTemplates/AddBin.tsx +126 -0
- package/src/customTemplates/CustomLayoutManager.tsx +233 -0
- package/src/customTemplates/CustomTemplateContext.tsx +182 -0
- package/src/customTemplates/LayerToolbar.tsx +580 -0
- package/src/customTemplates/ShapeGlyph.tsx +47 -0
- package/src/customTemplates/TemplateDesigner.tsx +430 -0
- package/src/customTemplates/__tests__/library.test.ts +88 -0
- package/src/customTemplates/__tests__/normalizePositions.test.ts +109 -0
- package/src/customTemplates/__tests__/shapeDefs.test.ts +49 -0
- package/src/customTemplates/__tests__/useMemoryLayerAdapter.test.ts +95 -0
- package/src/customTemplates/designer.css +673 -0
- package/src/customTemplates/index.ts +31 -0
- package/src/customTemplates/library.ts +97 -0
- package/src/customTemplates/normalizePositions.ts +75 -0
- package/src/customTemplates/shapeDefs.ts +131 -0
- package/src/customTemplates/thumbnail.tsx +63 -0
- package/src/customTemplates/tokenDefs.ts +60 -0
- package/src/customTemplates/useDocCustomTemplates.ts +52 -0
- package/src/customTemplates/useMemoryLayerAdapter.ts +123 -0
- package/src/customThemes/CustomThemeContext.tsx +179 -0
- package/src/customThemes/CustomThemeDialog.tsx +286 -0
- package/src/customThemes/__tests__/CustomThemeContext.test.tsx +64 -0
- package/src/customThemes/__tests__/CustomThemeDialog.test.tsx +47 -0
- package/src/customThemes/__tests__/customThemeLibrary.test.ts +51 -0
- package/src/customThemes/customThemeLibrary.ts +97 -0
- package/src/customThemes/index.ts +31 -0
- package/src/customThemes/themeControls.tsx +229 -0
- package/src/customThemes/themeDraft.ts +272 -0
- package/src/customThemes/useDocCustomThemes.ts +49 -0
- package/src/diagram/DiagramCanvas.tsx +240 -0
- package/src/diagram/DiagramExtension.ts +209 -0
- package/src/diagram/DiagramMaximizedOverlay.tsx +46 -0
- package/src/diagram/DiagramWidget.tsx +270 -0
- package/src/diagram/diagramCommands.ts +604 -0
- package/src/diagram/diagramConstants.ts +17 -0
- package/src/diagram/useDiagramData.ts +126 -0
- package/src/embeddedMedia.ts +78 -0
- package/src/frontmatter.ts +29 -0
- package/src/headingTransition.ts +231 -0
- package/src/imageEditor/CanvasSurface.tsx +383 -88
- package/src/imageEditor/PropertiesPanel.tsx +47 -1
- package/src/imageEditor/Toolbar.tsx +229 -16
- package/src/imageEditor/createShapeLayer.ts +280 -0
- package/src/imageEditor/icons.tsx +34 -114
- package/src/imageEditor/image-editor.css +54 -5
- package/src/imageEditor/state.ts +23 -3
- package/src/index.ts +109 -1
- package/src/jsonEditor/useJsonEditorTokens.ts +13 -43
- package/src/recorder/RecorderModal.tsx +120 -53
- package/src/recorder/RecorderPanel.tsx +2 -26
- package/src/recorder/hooks/useMediaRecorder.ts +17 -11
- package/src/recorder/insertMediaBlock.ts +30 -0
- package/src/resolveBlockVisual.ts +33 -0
- package/src/scene/Scene.tsx +540 -0
- package/src/scene/SceneBlockExtension.ts +198 -0
- package/src/scene/SceneBlockToolbar.tsx +201 -0
- package/src/scene/SceneBlockWidget.tsx +434 -0
- package/src/scene/ScenePropsBar.tsx +85 -0
- package/src/scene/SceneSelection.tsx +107 -0
- package/src/scene/SceneViewport.tsx +102 -0
- package/src/scene/ShapePalette.tsx +181 -0
- package/src/scene/__tests__/DiagramAdapter.test.ts +56 -0
- package/src/scene/__tests__/bezierEdit.test.ts +85 -0
- package/src/scene/__tests__/blockLayers.test.ts +57 -0
- package/src/scene/__tests__/shapeLayers.test.ts +106 -0
- package/src/scene/__tests__/useSceneHitTest.test.ts +90 -0
- package/src/scene/__tests__/useScenePanZoom.test.ts +103 -0
- package/src/scene/adapters/DiagramAdapter.ts +168 -0
- package/src/scene/adapters/DrawingAdapter.ts +415 -0
- package/src/scene/adapters/LayoutAdapter.ts +310 -0
- package/src/scene/adapters/blockLayers.ts +159 -0
- package/src/scene/commands/SceneCommand.ts +70 -0
- package/src/scene/commands/drawingCommands.ts +318 -0
- package/src/scene/commands/layoutCommands.ts +301 -0
- package/src/scene/hooks/useSceneHitTest.ts +105 -0
- package/src/scene/hooks/useScenePanZoom.ts +147 -0
- package/src/scene/hooks/useSceneSelection.ts +62 -0
- package/src/scene/index.ts +95 -0
- package/src/scene/layers/DiagramEdges.tsx +127 -0
- package/src/scene/layers/edgeGeometry.ts +77 -0
- package/src/scene/layers/nodeCard.tsx +145 -0
- package/src/scene/layers/renderLayer.tsx +70 -0
- package/src/scene/layers/shapeLayers.ts +201 -0
- package/src/scene/paths/bezierEdit.ts +208 -0
- package/src/scene/scene.css +649 -0
- package/src/scene/text/SceneTextOverlay.tsx +161 -0
- package/src/scene/text/sceneTextChannel.ts +40 -0
- package/src/scene/text/sceneTextConfig.ts +27 -0
- package/src/scene/text/sceneTiptap.ts +36 -0
- package/src/scene/text/useSceneTextEditing.ts +39 -0
- package/src/scene/tools/ConnectTool.ts +111 -0
- package/src/scene/tools/DrawingConnectTool.ts +161 -0
- package/src/scene/tools/PathTool.ts +158 -0
- package/src/scene/tools/PlaceTool.ts +47 -0
- package/src/scene/tools/SceneTool.ts +75 -0
- package/src/scene/tools/SelectTool.ts +284 -0
- package/src/scene/tools/ShapeTool.ts +144 -0
- package/src/scene/tools/TextTool.ts +72 -0
- package/src/scene/tools/TokenTool.ts +95 -0
- package/src/scene/tools/createDrawShapeTool.ts +82 -0
- package/src/styles/code-context.css +155 -0
- package/src/styles/diagram.css +183 -0
- package/src/styles/editor.css +1708 -150
- package/src/styles/folder-view.css +210 -0
- package/src/styles/image-edit-affordance.css +2 -2
- package/src/styles/index.css +5 -0
- package/src/timelineSource.ts +244 -0
- package/src/tiptapBridge.ts +115 -34
- package/src/tooltipPlacement.ts +13 -0
- package/src/transitionCatalog.ts +159 -0
- package/src/types/monaco-shims.d.ts +10 -0
- package/src/useBlockNavigator.ts +153 -0
- package/src/useMonacoLoader.ts +23 -1
- package/src/useTimelineClock.ts +76 -0
- package/src/utils/dropUtils.ts +1 -1
package/src/Toolbar.tsx
CHANGED
|
@@ -10,17 +10,30 @@
|
|
|
10
10
|
import type { ReactNode } from 'react';
|
|
11
11
|
import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react';
|
|
12
12
|
import type { Editor as TiptapEditor } from '@tiptap/core';
|
|
13
|
+
import { TextSelection } from '@tiptap/pm/state';
|
|
14
|
+
import { Fragment } from '@tiptap/pm/model';
|
|
13
15
|
import type { IRange } from 'monaco-editor';
|
|
14
16
|
import { useEditorContext, type EditorView } from './EditorContext';
|
|
15
17
|
import { VersionHistoryPanel } from './VersionHistoryPanel';
|
|
16
18
|
import { RecorderEntry } from './RecorderEntry';
|
|
17
19
|
import { ViewMenuPanel } from './ViewMenuPanel';
|
|
18
20
|
import { TemplatePicker, TEMPLATE_NAMES } from './TemplatePicker';
|
|
21
|
+
import { TransitionPicker } from './TransitionPicker';
|
|
22
|
+
import {
|
|
23
|
+
readHeadingLineTransition,
|
|
24
|
+
setHeadingLineTransition,
|
|
25
|
+
readBlockAttrsTransition,
|
|
26
|
+
setBlockAttrsTransition,
|
|
27
|
+
EMPTY_TRANSITION,
|
|
28
|
+
type TransitionFields,
|
|
29
|
+
} from './headingTransition';
|
|
19
30
|
import { profileBlockContents, recommendTemplatesForBlock } from '@bendyline/squisq/recommend';
|
|
20
31
|
import { findBlockSliceAtLine, findBlockSliceByHeadingIndex } from './blockSlice';
|
|
21
32
|
import { LinkDialog } from './LinkDialog';
|
|
22
33
|
import { DocumentSettingsDialog } from './DocumentSettingsDialog';
|
|
23
34
|
import { EmojiPicker, EMOJI_PICKER_WIDTH, EMOJI_PICKER_MAX_HEIGHT } from './EmojiPicker';
|
|
35
|
+
import { CustomLayoutManager } from './customTemplates/CustomLayoutManager';
|
|
36
|
+
import { Icon } from './Icon';
|
|
24
37
|
import type { PickerEntry } from './emojiData';
|
|
25
38
|
import { createPortal } from 'react-dom';
|
|
26
39
|
|
|
@@ -39,6 +52,10 @@ export interface ToolbarProps {
|
|
|
39
52
|
onToggleFiles?: () => void;
|
|
40
53
|
/** Content rendered at the left edge of the toolbar, before the view tabs. */
|
|
41
54
|
slotLeft?: ReactNode;
|
|
55
|
+
/** Content rendered immediately after the view tabs, on the left side of the
|
|
56
|
+
* toolbar (before the formatting controls). Used for the preview mode
|
|
57
|
+
* switch in Play view. */
|
|
58
|
+
slotAfterTabs?: ReactNode;
|
|
42
59
|
/** Content rendered after the formatting controls (in the middle area). */
|
|
43
60
|
slotAfterActions?: ReactNode;
|
|
44
61
|
/** Content rendered at the rightmost end of the toolbar, after all other elements. */
|
|
@@ -54,11 +71,12 @@ export interface ToolbarProps {
|
|
|
54
71
|
interface ToolbarButton {
|
|
55
72
|
id: string;
|
|
56
73
|
label: string;
|
|
74
|
+
/** Text glyph shown when the button has no Font Awesome icon (headings). */
|
|
57
75
|
icon: string;
|
|
58
76
|
title: string;
|
|
59
77
|
group: 'format' | 'lists' | 'structure' | 'insert' | 'media';
|
|
60
|
-
/**
|
|
61
|
-
|
|
78
|
+
/** Font Awesome class string (e.g. `"fa-solid fa-bold"`); omitted for text buttons. */
|
|
79
|
+
faIcon?: string;
|
|
62
80
|
}
|
|
63
81
|
|
|
64
82
|
const BUTTONS: ToolbarButton[] = [
|
|
@@ -69,7 +87,7 @@ const BUTTONS: ToolbarButton[] = [
|
|
|
69
87
|
icon: 'B',
|
|
70
88
|
title: 'Bold (Ctrl+B)',
|
|
71
89
|
group: 'format',
|
|
72
|
-
|
|
90
|
+
faIcon: 'fa-solid fa-bold',
|
|
73
91
|
},
|
|
74
92
|
{
|
|
75
93
|
id: 'italic',
|
|
@@ -77,7 +95,7 @@ const BUTTONS: ToolbarButton[] = [
|
|
|
77
95
|
icon: 'I',
|
|
78
96
|
title: 'Italic (Ctrl+I)',
|
|
79
97
|
group: 'format',
|
|
80
|
-
|
|
98
|
+
faIcon: 'fa-solid fa-italic',
|
|
81
99
|
},
|
|
82
100
|
{
|
|
83
101
|
id: 'strikethrough',
|
|
@@ -85,15 +103,30 @@ const BUTTONS: ToolbarButton[] = [
|
|
|
85
103
|
icon: 'S',
|
|
86
104
|
title: 'Strikethrough',
|
|
87
105
|
group: 'format',
|
|
88
|
-
|
|
106
|
+
faIcon: 'fa-solid fa-strikethrough',
|
|
89
107
|
},
|
|
90
108
|
|
|
91
109
|
// Lists group — sits between format and structure so bullets/numbers
|
|
92
110
|
// are adjacent to the inline formatters people reach for together.
|
|
93
|
-
{
|
|
94
|
-
|
|
111
|
+
{
|
|
112
|
+
id: 'ul',
|
|
113
|
+
label: '•',
|
|
114
|
+
icon: '•',
|
|
115
|
+
title: 'Bullet list',
|
|
116
|
+
group: 'lists',
|
|
117
|
+
faIcon: 'fa-solid fa-list-ul',
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: 'ol',
|
|
121
|
+
label: '1.',
|
|
122
|
+
icon: '1.',
|
|
123
|
+
title: 'Numbered list',
|
|
124
|
+
group: 'lists',
|
|
125
|
+
faIcon: 'fa-solid fa-list-ol',
|
|
126
|
+
},
|
|
95
127
|
|
|
96
|
-
// Structure group
|
|
128
|
+
// Structure group — headings keep their text labels; Font Awesome Free
|
|
129
|
+
// has no numbered (H1–H6) heading glyphs, and the numerals are clearer.
|
|
97
130
|
{ id: 'h1', label: 'H1', icon: 'H1', title: 'Heading 1', group: 'structure' },
|
|
98
131
|
{ id: 'h2', label: 'H2', icon: 'H2', title: 'Heading 2', group: 'structure' },
|
|
99
132
|
{ id: 'h3', label: 'H3', icon: 'H3', title: 'Heading 3', group: 'structure' },
|
|
@@ -102,119 +135,105 @@ const BUTTONS: ToolbarButton[] = [
|
|
|
102
135
|
{ id: 'h6', label: 'H6', icon: 'H6', title: 'Heading 6', group: 'structure' },
|
|
103
136
|
|
|
104
137
|
// Insert group — block-level inserts (quote, code blocks, rules)
|
|
105
|
-
{
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
138
|
+
{
|
|
139
|
+
id: 'quote',
|
|
140
|
+
label: '❝',
|
|
141
|
+
icon: '❝',
|
|
142
|
+
title: 'Blockquote',
|
|
143
|
+
group: 'insert',
|
|
144
|
+
faIcon: 'fa-solid fa-quote-left',
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: 'codeblock',
|
|
148
|
+
label: '{ }',
|
|
149
|
+
icon: '{ }',
|
|
150
|
+
title: 'Code block',
|
|
151
|
+
group: 'insert',
|
|
152
|
+
faIcon: 'fa-solid fa-file-code',
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
id: 'code',
|
|
156
|
+
label: '</>',
|
|
157
|
+
icon: '</>',
|
|
158
|
+
title: 'Inline code',
|
|
159
|
+
group: 'insert',
|
|
160
|
+
faIcon: 'fa-solid fa-code',
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
id: 'hr',
|
|
164
|
+
label: '—',
|
|
165
|
+
icon: '—',
|
|
166
|
+
title: 'Horizontal rule',
|
|
167
|
+
group: 'insert',
|
|
168
|
+
faIcon: 'fa-solid fa-minus',
|
|
169
|
+
},
|
|
109
170
|
|
|
110
171
|
// Media group — links, tables, images, emoji
|
|
111
|
-
{
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
172
|
+
{
|
|
173
|
+
id: 'link',
|
|
174
|
+
label: '🔗',
|
|
175
|
+
icon: '🔗',
|
|
176
|
+
title: 'Insert link',
|
|
177
|
+
group: 'media',
|
|
178
|
+
faIcon: 'fa-solid fa-link',
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
id: 'table',
|
|
182
|
+
label: 'table',
|
|
183
|
+
icon: '',
|
|
184
|
+
title: 'Insert table',
|
|
185
|
+
group: 'media',
|
|
186
|
+
faIcon: 'fa-solid fa-table',
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
id: 'diagram',
|
|
190
|
+
label: 'diagram',
|
|
191
|
+
icon: '',
|
|
192
|
+
title: 'Insert diagram',
|
|
193
|
+
group: 'media',
|
|
194
|
+
faIcon: 'fa-solid fa-diagram-project',
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
id: 'drawing',
|
|
198
|
+
label: 'drawing',
|
|
199
|
+
icon: '',
|
|
200
|
+
title: 'Insert drawing',
|
|
201
|
+
group: 'media',
|
|
202
|
+
faIcon: 'fa-solid fa-pen-nib',
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
id: 'layout',
|
|
206
|
+
label: 'layout',
|
|
207
|
+
icon: '',
|
|
208
|
+
title: 'Insert layout',
|
|
209
|
+
group: 'media',
|
|
210
|
+
faIcon: 'fa-solid fa-object-group',
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
id: 'image',
|
|
214
|
+
label: '🖼',
|
|
215
|
+
icon: '🖼',
|
|
216
|
+
title: 'Insert image',
|
|
217
|
+
group: 'media',
|
|
218
|
+
faIcon: 'fa-solid fa-image',
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
id: 'emoji',
|
|
222
|
+
label: '😊',
|
|
223
|
+
icon: '😊',
|
|
224
|
+
title: 'Insert emoji',
|
|
225
|
+
group: 'media',
|
|
226
|
+
faIcon: 'fa-solid fa-face-smile',
|
|
227
|
+
},
|
|
115
228
|
];
|
|
116
229
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
fill="none"
|
|
125
|
-
stroke="currentColor"
|
|
126
|
-
strokeWidth="1.4"
|
|
127
|
-
strokeLinecap="round"
|
|
128
|
-
>
|
|
129
|
-
<rect x="1" y="1" width="12" height="12" rx="1" />
|
|
130
|
-
<line x1="1" y1="5" x2="13" y2="5" />
|
|
131
|
-
<line x1="1" y1="9" x2="13" y2="9" />
|
|
132
|
-
<line x1="5" y1="1" x2="5" y2="13" />
|
|
133
|
-
<line x1="9" y1="1" x2="9" y2="13" />
|
|
134
|
-
</svg>
|
|
135
|
-
);
|
|
136
|
-
|
|
137
|
-
const LINK_ICON = (
|
|
138
|
-
<svg
|
|
139
|
-
width="14"
|
|
140
|
-
height="14"
|
|
141
|
-
viewBox="0 0 14 14"
|
|
142
|
-
fill="none"
|
|
143
|
-
stroke="currentColor"
|
|
144
|
-
strokeWidth="1.4"
|
|
145
|
-
strokeLinecap="round"
|
|
146
|
-
strokeLinejoin="round"
|
|
147
|
-
>
|
|
148
|
-
<path d="M5.75 8.25 L8.25 5.75" />
|
|
149
|
-
<path d="M6.5 3.75 L8 2.25 a2.5 2.5 0 0 1 3.54 3.54 L10 7.25" />
|
|
150
|
-
<path d="M7.5 10.25 L6 11.75 a2.5 2.5 0 0 1 -3.54 -3.54 L4 6.75" />
|
|
151
|
-
</svg>
|
|
152
|
-
);
|
|
153
|
-
|
|
154
|
-
const IMAGE_ICON = (
|
|
155
|
-
<svg
|
|
156
|
-
width="14"
|
|
157
|
-
height="14"
|
|
158
|
-
viewBox="0 0 14 14"
|
|
159
|
-
fill="none"
|
|
160
|
-
stroke="currentColor"
|
|
161
|
-
strokeWidth="1.4"
|
|
162
|
-
strokeLinecap="round"
|
|
163
|
-
strokeLinejoin="round"
|
|
164
|
-
>
|
|
165
|
-
<rect x="1.5" y="2.5" width="11" height="9" rx="1" />
|
|
166
|
-
<circle cx="5" cy="5.5" r="0.9" />
|
|
167
|
-
<path d="M2 10 L5.5 7 L8 9 L10 7.5 L12.5 10" />
|
|
168
|
-
</svg>
|
|
169
|
-
);
|
|
170
|
-
|
|
171
|
-
const PAPERCLIP_ICON = (
|
|
172
|
-
<svg
|
|
173
|
-
width="14"
|
|
174
|
-
height="14"
|
|
175
|
-
viewBox="0 0 14 14"
|
|
176
|
-
fill="none"
|
|
177
|
-
stroke="currentColor"
|
|
178
|
-
strokeWidth="1.4"
|
|
179
|
-
strokeLinecap="round"
|
|
180
|
-
strokeLinejoin="round"
|
|
181
|
-
>
|
|
182
|
-
<path d="M11 4 L5.5 9.5 a1.75 1.75 0 0 0 2.5 2.5 L12.5 7.5 a3 3 0 0 0 -4.25 -4.25 L3 8.5 a4.25 4.25 0 0 0 6 6 L13 10.5" />
|
|
183
|
-
</svg>
|
|
184
|
-
);
|
|
185
|
-
|
|
186
|
-
const EMOJI_ICON = (
|
|
187
|
-
<svg
|
|
188
|
-
width="14"
|
|
189
|
-
height="14"
|
|
190
|
-
viewBox="0 0 14 14"
|
|
191
|
-
fill="none"
|
|
192
|
-
stroke="currentColor"
|
|
193
|
-
strokeWidth="1.4"
|
|
194
|
-
strokeLinecap="round"
|
|
195
|
-
strokeLinejoin="round"
|
|
196
|
-
>
|
|
197
|
-
<circle cx="7" cy="7" r="5.25" />
|
|
198
|
-
<circle cx="5.25" cy="5.75" r="0.6" fill="currentColor" stroke="none" />
|
|
199
|
-
<circle cx="8.75" cy="5.75" r="0.6" fill="currentColor" stroke="none" />
|
|
200
|
-
<path d="M4.75 8.5 a2.5 2.5 0 0 0 4.5 0" />
|
|
201
|
-
</svg>
|
|
202
|
-
);
|
|
203
|
-
|
|
204
|
-
/** Returns an SVG element when the button id maps to one, otherwise null. */
|
|
205
|
-
function buttonIconSvg(id: string): React.ReactNode | null {
|
|
206
|
-
switch (id) {
|
|
207
|
-
case 'table':
|
|
208
|
-
return TABLE_ICON;
|
|
209
|
-
case 'link':
|
|
210
|
-
return LINK_ICON;
|
|
211
|
-
case 'image':
|
|
212
|
-
return IMAGE_ICON;
|
|
213
|
-
case 'emoji':
|
|
214
|
-
return EMOJI_ICON;
|
|
215
|
-
default:
|
|
216
|
-
return null;
|
|
217
|
-
}
|
|
230
|
+
const FIRST_MEDIA_INDEX = BUTTONS.findIndex((b) => b.group === 'media');
|
|
231
|
+
const MEDIA_BUTTONS = BUTTONS.filter((b) => b.group === 'media');
|
|
232
|
+
const INSERT_MENU_WIDTH = 200;
|
|
233
|
+
|
|
234
|
+
/** Renders a button's icon: a Font Awesome glyph when set, else the text label. */
|
|
235
|
+
function buttonIcon(btn: ToolbarButton): ReactNode {
|
|
236
|
+
return btn.faIcon ? <Icon icon={btn.faIcon} /> : btn.icon;
|
|
218
237
|
}
|
|
219
238
|
|
|
220
239
|
// ─── Tiptap active-state map ────────────────────────────
|
|
@@ -256,6 +275,96 @@ function isTiptapActive(editor: TiptapEditor, id: string): boolean {
|
|
|
256
275
|
}
|
|
257
276
|
}
|
|
258
277
|
|
|
278
|
+
// ─── Scene-block inserts (diagram / drawing / layout) ───
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* A freshly-inserted layout starts with one centered text layer so the
|
|
282
|
+
* canvas isn't a blank surface. Each layer is a readable child sub-block
|
|
283
|
+
* (`### {#id} {[type …]}`); a text layer's content is its markdown body.
|
|
284
|
+
* Coordinates are absolute within the 1920×1080 scene viewport
|
|
285
|
+
* (`SceneBlockWidget`). See `scene/commands/layoutCommands.ts`.
|
|
286
|
+
*/
|
|
287
|
+
const LAYOUT_STARTER_TEXT_PARAMS =
|
|
288
|
+
'x=360 y=380 width=1200 height=320 fontSize=64 fontWeight=bold align=center valign=middle color="#1e293b"';
|
|
289
|
+
/** Multi-line markdown for a new layout (raw / code views). */
|
|
290
|
+
const LAYOUT_STARTER_MARKDOWN = `\n## Layout {[layout]}\n\n### {#text-1} {[text ${LAYOUT_STARTER_TEXT_PARAMS}]}\n\nLayout\n`;
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Insert a block-level heading carrying a Scene template (`diagram` /
|
|
294
|
+
* `drawing` / `layout`) at the top level, after the block the caret sits
|
|
295
|
+
* in. Going through a command (rather than `insertContent` at the caret)
|
|
296
|
+
* keeps the heading from being coerced into inline text when the caret is
|
|
297
|
+
* nested inside a list item or other block. The matching extension
|
|
298
|
+
* (DiagramExtension / SceneBlockExtension) then mounts the editable canvas
|
|
299
|
+
* right below it.
|
|
300
|
+
*/
|
|
301
|
+
function insertTemplateHeading(
|
|
302
|
+
editor: TiptapEditor,
|
|
303
|
+
opts: { template: string; text: string; level?: number; blockAttrs?: string | null },
|
|
304
|
+
): void {
|
|
305
|
+
editor
|
|
306
|
+
.chain()
|
|
307
|
+
.focus()
|
|
308
|
+
.command(({ tr, state, dispatch }) => {
|
|
309
|
+
const headingType = state.schema.nodes.heading;
|
|
310
|
+
if (!headingType) return false;
|
|
311
|
+
const heading = headingType.create(
|
|
312
|
+
{
|
|
313
|
+
level: opts.level ?? 2,
|
|
314
|
+
dataTemplate: opts.template,
|
|
315
|
+
dataBlockAttrs: opts.blockAttrs ?? null,
|
|
316
|
+
},
|
|
317
|
+
state.schema.text(opts.text),
|
|
318
|
+
);
|
|
319
|
+
const { $from } = state.selection;
|
|
320
|
+
const insertPos = $from.depth > 0 ? $from.after(1) : state.doc.content.size;
|
|
321
|
+
if (dispatch) {
|
|
322
|
+
tr.insert(insertPos, heading);
|
|
323
|
+
// Drop the caret into the new heading's text so it can be renamed.
|
|
324
|
+
tr.setSelection(TextSelection.create(tr.doc, insertPos + 1));
|
|
325
|
+
}
|
|
326
|
+
return true;
|
|
327
|
+
})
|
|
328
|
+
.run();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Insert a `{[layout]}` block seeded with one text layer. The layer is a
|
|
333
|
+
* child sub-block heading (`### {#text-1} {[text …]}`) whose markdown body
|
|
334
|
+
* ("Layout") is the text content — the readable counterpart to the old
|
|
335
|
+
* base64 `layers=` blob. The parent + child + body go in as one fragment so
|
|
336
|
+
* the layout widget mounts with the seed already present.
|
|
337
|
+
*/
|
|
338
|
+
function insertLayoutBlock(editor: TiptapEditor): void {
|
|
339
|
+
editor
|
|
340
|
+
.chain()
|
|
341
|
+
.focus()
|
|
342
|
+
.command(({ tr, state, dispatch }) => {
|
|
343
|
+
const headingType = state.schema.nodes.heading;
|
|
344
|
+
const paragraphType = state.schema.nodes.paragraph;
|
|
345
|
+
if (!headingType || !paragraphType) return false;
|
|
346
|
+
const parent = headingType.create(
|
|
347
|
+
{ level: 2, dataTemplate: 'layout' },
|
|
348
|
+
state.schema.text('Layout'),
|
|
349
|
+
);
|
|
350
|
+
const child = headingType.create({
|
|
351
|
+
level: 3,
|
|
352
|
+
dataTemplate: 'text',
|
|
353
|
+
dataTemplateParams: LAYOUT_STARTER_TEXT_PARAMS,
|
|
354
|
+
dataBlockAttrs: '#text-1',
|
|
355
|
+
});
|
|
356
|
+
const body = paragraphType.create(null, state.schema.text('Layout'));
|
|
357
|
+
const { $from } = state.selection;
|
|
358
|
+
const insertPos = $from.depth > 0 ? $from.after(1) : state.doc.content.size;
|
|
359
|
+
if (dispatch) {
|
|
360
|
+
tr.insert(insertPos, Fragment.fromArray([parent, child, body]));
|
|
361
|
+
tr.setSelection(TextSelection.create(tr.doc, insertPos + 1));
|
|
362
|
+
}
|
|
363
|
+
return true;
|
|
364
|
+
})
|
|
365
|
+
.run();
|
|
366
|
+
}
|
|
367
|
+
|
|
259
368
|
/**
|
|
260
369
|
* Formatting toolbar.
|
|
261
370
|
* - WYSIWYG: calls Tiptap chain commands (toggleBold, etc.)
|
|
@@ -266,6 +375,7 @@ export function Toolbar({
|
|
|
266
375
|
showFiles,
|
|
267
376
|
onToggleFiles,
|
|
268
377
|
slotLeft,
|
|
378
|
+
slotAfterTabs,
|
|
269
379
|
slotAfterActions,
|
|
270
380
|
slotRight,
|
|
271
381
|
showPlayTab = true,
|
|
@@ -277,13 +387,19 @@ export function Toolbar({
|
|
|
277
387
|
setMarkdownSource,
|
|
278
388
|
tiptapEditor,
|
|
279
389
|
monacoEditor,
|
|
390
|
+
activeSceneText,
|
|
280
391
|
mediaProvider,
|
|
281
392
|
editorMode,
|
|
282
393
|
versioning,
|
|
283
394
|
allowRecording,
|
|
284
395
|
documentLinkProvider,
|
|
285
|
-
|
|
396
|
+
colorScheme,
|
|
286
397
|
} = useEditorContext();
|
|
398
|
+
// When a canvas textbox is being edited, its Tiptap instance takes over
|
|
399
|
+
// the formatting buttons; otherwise they drive the document editor. The
|
|
400
|
+
// `level` gates which buttons apply (inline labels vs. rich textboxes).
|
|
401
|
+
const formattingEditor = activeSceneText?.editor ?? tiptapEditor;
|
|
402
|
+
const sceneTextLevel = activeSceneText?.level ?? null;
|
|
287
403
|
const isCodeMode = editorMode === 'code';
|
|
288
404
|
// In code mode only the raw view is meaningful; the WYSIWYG and Preview
|
|
289
405
|
// surfaces aren't mounted, so hide their tabs.
|
|
@@ -347,17 +463,30 @@ export function Toolbar({
|
|
|
347
463
|
|
|
348
464
|
const closeEmojiPicker = useCallback(() => setEmojiPickerAnchor(null), []);
|
|
349
465
|
|
|
350
|
-
//
|
|
351
|
-
|
|
352
|
-
|
|
466
|
+
// Insert menu — toolbar-anchored dropdown that replaces the individual media-group
|
|
467
|
+
// buttons with a single "+" button. Portaled out to avoid overflow:hidden clipping.
|
|
468
|
+
const insertMenuButtonRef = useRef<HTMLButtonElement | null>(null);
|
|
469
|
+
const insertMenuRef = useRef<HTMLDivElement | null>(null);
|
|
470
|
+
const [insertMenuAnchor, setInsertMenuAnchor] = useState<{ top: number; left: number } | null>(
|
|
471
|
+
null,
|
|
353
472
|
);
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
473
|
+
|
|
474
|
+
const openInsertMenu = useCallback((trigger?: HTMLElement | null) => {
|
|
475
|
+
const btn = trigger ?? insertMenuButtonRef.current;
|
|
476
|
+
if (!btn) return;
|
|
477
|
+
const rect = btn.getBoundingClientRect();
|
|
478
|
+
const gap = 4;
|
|
479
|
+
const margin = 8;
|
|
480
|
+
const vw = window.innerWidth;
|
|
481
|
+
let left = rect.left;
|
|
482
|
+
if (left + INSERT_MENU_WIDTH + margin > vw) {
|
|
483
|
+
left = Math.max(margin, rect.right - INSERT_MENU_WIDTH);
|
|
484
|
+
}
|
|
485
|
+
setInsertMenuAnchor({ top: rect.bottom + gap, left });
|
|
359
486
|
}, []);
|
|
360
487
|
|
|
488
|
+
const closeInsertMenu = useCallback(() => setInsertMenuAnchor(null), []);
|
|
489
|
+
|
|
361
490
|
// ── Overflow detection ────────────────────────────────
|
|
362
491
|
const actionsRef = useRef<HTMLDivElement>(null);
|
|
363
492
|
const [measuredOverflowIndex, setMeasuredOverflowIndex] = useState<number | null>(null);
|
|
@@ -367,11 +496,12 @@ export function Toolbar({
|
|
|
367
496
|
// Document settings (frontmatter) dialog
|
|
368
497
|
const [showDocSettings, setShowDocSettings] = useState(false);
|
|
369
498
|
|
|
370
|
-
//
|
|
371
|
-
const
|
|
499
|
+
// Custom layout manager dialog (list of doc/library layouts + designer)
|
|
500
|
+
const [showLayoutManager, setShowLayoutManager] = useState(false);
|
|
501
|
+
|
|
502
|
+
const overflowIndex = measuredOverflowIndex;
|
|
372
503
|
|
|
373
504
|
useEffect(() => {
|
|
374
|
-
if (isNarrow) return; // Skip measurement on narrow — everything overflows
|
|
375
505
|
const container = actionsRef.current;
|
|
376
506
|
if (!container) return;
|
|
377
507
|
|
|
@@ -381,13 +511,18 @@ export function Toolbar({
|
|
|
381
511
|
':scope > .squisq-toolbar-group > .squisq-toolbar-button',
|
|
382
512
|
);
|
|
383
513
|
let firstHidden: number | null = null;
|
|
384
|
-
|
|
514
|
+
let btnIndex = 0;
|
|
515
|
+
children.forEach((child) => {
|
|
385
516
|
if (firstHidden !== null) return;
|
|
517
|
+
// data-btn-count lets a single DOM button represent multiple BUTTONS entries
|
|
518
|
+
// (e.g. the Insert dropdown button represents the entire media group).
|
|
519
|
+
const btnCount = Number(child.dataset.btnCount ?? 1);
|
|
386
520
|
const rect = child.getBoundingClientRect();
|
|
387
521
|
// A button is hidden if its right edge extends past the container
|
|
388
522
|
if (rect.right > containerRight + 2) {
|
|
389
|
-
firstHidden =
|
|
523
|
+
firstHidden = btnIndex;
|
|
390
524
|
}
|
|
525
|
+
btnIndex += btnCount;
|
|
391
526
|
});
|
|
392
527
|
setMeasuredOverflowIndex(firstHidden);
|
|
393
528
|
};
|
|
@@ -396,7 +531,7 @@ export function Toolbar({
|
|
|
396
531
|
ro.observe(container);
|
|
397
532
|
measure();
|
|
398
533
|
return () => ro.disconnect();
|
|
399
|
-
}, [activeView
|
|
534
|
+
}, [activeView]);
|
|
400
535
|
|
|
401
536
|
// Close overflow menu on outside click
|
|
402
537
|
useEffect(() => {
|
|
@@ -410,6 +545,18 @@ export function Toolbar({
|
|
|
410
545
|
return () => document.removeEventListener('mousedown', handleClick);
|
|
411
546
|
}, [showOverflow]);
|
|
412
547
|
|
|
548
|
+
// Close insert menu on outside click
|
|
549
|
+
useEffect(() => {
|
|
550
|
+
if (!insertMenuAnchor) return;
|
|
551
|
+
const handleClick = (e: MouseEvent) => {
|
|
552
|
+
if (insertMenuButtonRef.current?.contains(e.target as Node)) return;
|
|
553
|
+
if (insertMenuRef.current?.contains(e.target as Node)) return;
|
|
554
|
+
closeInsertMenu();
|
|
555
|
+
};
|
|
556
|
+
document.addEventListener('mousedown', handleClick);
|
|
557
|
+
return () => document.removeEventListener('mousedown', handleClick);
|
|
558
|
+
}, [insertMenuAnchor, closeInsertMenu]);
|
|
559
|
+
|
|
413
560
|
// Open-up vs open-down: the overflow menu is anchored to its trigger with
|
|
414
561
|
// `top: 100%` by default. When the toolbar lives near the bottom of a
|
|
415
562
|
// small container (e.g. a chat composer), a downward menu gets clipped.
|
|
@@ -438,16 +585,17 @@ export function Toolbar({
|
|
|
438
585
|
// Force re-render when Tiptap selection or formatting state changes
|
|
439
586
|
const [, forceUpdate] = useReducer((c: number) => c + 1, 0);
|
|
440
587
|
useEffect(() => {
|
|
441
|
-
if (!
|
|
442
|
-
|
|
588
|
+
if (!formattingEditor) return;
|
|
589
|
+
formattingEditor.on('transaction', forceUpdate);
|
|
443
590
|
return () => {
|
|
444
|
-
|
|
591
|
+
formattingEditor.off('transaction', forceUpdate);
|
|
445
592
|
};
|
|
446
|
-
}, [
|
|
593
|
+
}, [formattingEditor]);
|
|
447
594
|
|
|
448
595
|
// ── Tiptap handler ─────────────────────────────────────
|
|
449
596
|
const handleTiptap = useCallback(
|
|
450
597
|
(id: string) => {
|
|
598
|
+
const tiptapEditor = formattingEditor;
|
|
451
599
|
if (!tiptapEditor) return;
|
|
452
600
|
const chain = tiptapEditor.chain().focus();
|
|
453
601
|
switch (id) {
|
|
@@ -525,9 +673,24 @@ export function Toolbar({
|
|
|
525
673
|
case 'table':
|
|
526
674
|
tiptapEditor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run();
|
|
527
675
|
break;
|
|
676
|
+
case 'diagram':
|
|
677
|
+
// Empty diagram is usable via its "+ Add first node" affordance;
|
|
678
|
+
// sub-headings under this block become nodes.
|
|
679
|
+
insertTemplateHeading(tiptapEditor, { template: 'diagram', text: 'Diagram' });
|
|
680
|
+
break;
|
|
681
|
+
case 'drawing':
|
|
682
|
+
// Empty drawing is usable via the canvas's "Shapes ▾" palette;
|
|
683
|
+
// each shape persists as a child heading.
|
|
684
|
+
insertTemplateHeading(tiptapEditor, { template: 'drawing', text: 'Drawing' });
|
|
685
|
+
break;
|
|
686
|
+
case 'layout':
|
|
687
|
+
// Seed with a starter text layer (a child sub-block) so the canvas
|
|
688
|
+
// isn't blank; further layers add via the toolbar Text/Box buttons.
|
|
689
|
+
insertLayoutBlock(tiptapEditor);
|
|
690
|
+
break;
|
|
528
691
|
}
|
|
529
692
|
},
|
|
530
|
-
[
|
|
693
|
+
[formattingEditor],
|
|
531
694
|
);
|
|
532
695
|
|
|
533
696
|
// ── Raw markdown handler ───────────────────────────────
|
|
@@ -664,19 +827,37 @@ export function Toolbar({
|
|
|
664
827
|
newCursorOffset = 3; // after \n|
|
|
665
828
|
break;
|
|
666
829
|
}
|
|
830
|
+
case 'diagram': {
|
|
831
|
+
// A diagram is just a heading with the `{[diagram]}` template
|
|
832
|
+
// annotation; the WYSIWYG view renders its editable canvas.
|
|
833
|
+
replacement = '\n## Diagram {[diagram]}\n';
|
|
834
|
+
newCursorOffset = replacement.length;
|
|
835
|
+
break;
|
|
836
|
+
}
|
|
837
|
+
case 'drawing': {
|
|
838
|
+
replacement = '\n## Drawing {[drawing]}\n';
|
|
839
|
+
newCursorOffset = replacement.length;
|
|
840
|
+
break;
|
|
841
|
+
}
|
|
842
|
+
case 'layout': {
|
|
843
|
+
// Seed a starter text layer (a child sub-block) so it isn't blank.
|
|
844
|
+
replacement = LAYOUT_STARTER_MARKDOWN;
|
|
845
|
+
newCursorOffset = replacement.length;
|
|
846
|
+
break;
|
|
847
|
+
}
|
|
667
848
|
}
|
|
668
849
|
|
|
669
850
|
// Apply the edit via Monaco's executeEdits for proper undo support
|
|
670
851
|
const range = selection;
|
|
671
852
|
monacoEditor.executeEdits('toolbar', [{ range, text: replacement }]);
|
|
672
853
|
|
|
673
|
-
// If no selection,
|
|
854
|
+
// If no selection, move the cursor to the command's preferred edit point.
|
|
674
855
|
if (!hasSelection && newCursorOffset > 0) {
|
|
675
856
|
const startPos = model.getPositionAt(
|
|
676
857
|
model.getOffsetAt(range.getStartPosition()) + newCursorOffset,
|
|
677
858
|
);
|
|
678
|
-
// Just place cursor after the prefix
|
|
679
859
|
monacoEditor.setPosition(startPos);
|
|
860
|
+
monacoEditor.revealPositionInCenterIfOutsideViewport(startPos);
|
|
680
861
|
}
|
|
681
862
|
|
|
682
863
|
monacoEditor.focus();
|
|
@@ -727,6 +908,15 @@ export function Toolbar({
|
|
|
727
908
|
insertion =
|
|
728
909
|
'\n| Header 1 | Header 2 | Header 3 |\n| --- | --- | --- |\n| Cell | Cell | Cell |\n| Cell | Cell | Cell |\n';
|
|
729
910
|
break;
|
|
911
|
+
case 'diagram':
|
|
912
|
+
insertion = '\n## Diagram {[diagram]}\n';
|
|
913
|
+
break;
|
|
914
|
+
case 'drawing':
|
|
915
|
+
insertion = '\n## Drawing {[drawing]}\n';
|
|
916
|
+
break;
|
|
917
|
+
case 'layout':
|
|
918
|
+
insertion = LAYOUT_STARTER_MARKDOWN;
|
|
919
|
+
break;
|
|
730
920
|
}
|
|
731
921
|
if (insertion) {
|
|
732
922
|
setMarkdownSource(markdownSource + insertion);
|
|
@@ -773,7 +963,9 @@ export function Toolbar({
|
|
|
773
963
|
else openEmojiPicker();
|
|
774
964
|
return;
|
|
775
965
|
}
|
|
776
|
-
|
|
966
|
+
// A focused canvas textbox takes the formatting (handleTiptap targets
|
|
967
|
+
// `formattingEditor`); otherwise route to the document editor.
|
|
968
|
+
if (activeSceneText || (activeView === 'wysiwyg' && tiptapEditor)) {
|
|
777
969
|
handleTiptap(id);
|
|
778
970
|
} else {
|
|
779
971
|
handleRaw(id);
|
|
@@ -782,6 +974,7 @@ export function Toolbar({
|
|
|
782
974
|
[
|
|
783
975
|
activeView,
|
|
784
976
|
tiptapEditor,
|
|
977
|
+
activeSceneText,
|
|
785
978
|
handleTiptap,
|
|
786
979
|
handleRaw,
|
|
787
980
|
emojiPickerAnchor,
|
|
@@ -868,17 +1061,20 @@ export function Toolbar({
|
|
|
868
1061
|
const trimmedUrl = url.trim();
|
|
869
1062
|
const trimmedText = text.trim();
|
|
870
1063
|
|
|
871
|
-
|
|
1064
|
+
// Target the focused canvas textbox editor when one is active, else
|
|
1065
|
+
// the document editor (the dialog was opened from the same target).
|
|
1066
|
+
const wysiwygTarget = formattingEditor;
|
|
1067
|
+
if (linkDialog.target === 'wysiwyg' && wysiwygTarget) {
|
|
872
1068
|
if (!trimmedUrl) {
|
|
873
1069
|
// Empty URL on update = unlink. On insert with no URL, do nothing.
|
|
874
1070
|
if (linkDialog.mode === 'update') {
|
|
875
|
-
|
|
1071
|
+
wysiwygTarget.chain().focus().unsetLink().run();
|
|
876
1072
|
}
|
|
877
1073
|
setLinkDialog(null);
|
|
878
1074
|
return;
|
|
879
1075
|
}
|
|
880
1076
|
const visibleText = trimmedText || trimmedUrl;
|
|
881
|
-
const chain =
|
|
1077
|
+
const chain = wysiwygTarget.chain().focus();
|
|
882
1078
|
// Insert (or replace selection) with text carrying a link mark. When
|
|
883
1079
|
// updating an existing link, the selection was extended to the full
|
|
884
1080
|
// mark range earlier, so this replaces the entire `[text](url)`.
|
|
@@ -927,13 +1123,37 @@ export function Toolbar({
|
|
|
927
1123
|
|
|
928
1124
|
setLinkDialog(null);
|
|
929
1125
|
},
|
|
930
|
-
[linkDialog,
|
|
1126
|
+
[linkDialog, formattingEditor, monacoEditor],
|
|
931
1127
|
);
|
|
932
1128
|
|
|
933
|
-
const groups = ['format', 'lists', 'structure', 'insert'
|
|
1129
|
+
const groups = ['format', 'lists', 'structure', 'insert'] as const;
|
|
934
1130
|
const isWysiwyg = activeView === 'wysiwyg' && tiptapEditor;
|
|
935
1131
|
const isPreview = activeView === 'preview';
|
|
936
1132
|
|
|
1133
|
+
// Formatting buttons reflect/drive the focused canvas textbox when one is
|
|
1134
|
+
// active, else the document's WYSIWYG editor.
|
|
1135
|
+
const formatActive = !!activeSceneText || isWysiwyg;
|
|
1136
|
+
// Inline-level textboxes (diagram/drawing labels) only support inline
|
|
1137
|
+
// marks — grey out block formatters so the toolbar reflects what applies.
|
|
1138
|
+
const INLINE_SCENE_BUTTONS = new Set([
|
|
1139
|
+
'bold',
|
|
1140
|
+
'italic',
|
|
1141
|
+
'strikethrough',
|
|
1142
|
+
'code',
|
|
1143
|
+
'link',
|
|
1144
|
+
'emoji',
|
|
1145
|
+
]);
|
|
1146
|
+
// `block`-level textboxes (layout) add lists/blockquote/code block on top of
|
|
1147
|
+
// the inline marks — but NOT headings (their content is a sub-block body, so
|
|
1148
|
+
// a heading there would re-parse as a sibling sub-block).
|
|
1149
|
+
const BLOCK_SCENE_BUTTONS = new Set([...INLINE_SCENE_BUTTONS, 'ul', 'ol', 'quote', 'codeblock']);
|
|
1150
|
+
const buttonAllowed = (id: string): boolean => {
|
|
1151
|
+
if (!sceneTextLevel) return true;
|
|
1152
|
+
if (sceneTextLevel === 'rich') return true;
|
|
1153
|
+
if (sceneTextLevel === 'block') return BLOCK_SCENE_BUTTONS.has(id);
|
|
1154
|
+
return INLINE_SCENE_BUTTONS.has(id);
|
|
1155
|
+
};
|
|
1156
|
+
|
|
937
1157
|
// ── Progressive heading disclosure ───────────────────
|
|
938
1158
|
// H1\u2013H3 are always visible. H4 appears once the document already
|
|
939
1159
|
// contains an H3, H5 once it contains an H4, and H6 once it contains
|
|
@@ -962,6 +1182,9 @@ export function Toolbar({
|
|
|
962
1182
|
if (!m) return true;
|
|
963
1183
|
return Number(m[1]) <= visibleHeadingMax;
|
|
964
1184
|
};
|
|
1185
|
+
const hasVisibleMediaButtons = MEDIA_BUTTONS.some((b) => isButtonVisible(b.id));
|
|
1186
|
+
const showInsertInOverflow =
|
|
1187
|
+
overflowIndex !== null && overflowIndex <= FIRST_MEDIA_INDEX && hasVisibleMediaButtons;
|
|
965
1188
|
|
|
966
1189
|
// Detect whether cursor is inside a table (WYSIWYG mode only)
|
|
967
1190
|
const isInTable = isWysiwyg ? tiptapEditor.isActive('table') : false;
|
|
@@ -980,10 +1203,12 @@ export function Toolbar({
|
|
|
980
1203
|
const isRawView = activeView === 'raw';
|
|
981
1204
|
const [rawTemplate, setRawTemplate] = useState<string | null>(null);
|
|
982
1205
|
const [rawHeadingLine, setRawHeadingLine] = useState<number | null>(null);
|
|
1206
|
+
const [rawTransition, setRawTransition] = useState<TransitionFields>(EMPTY_TRANSITION);
|
|
983
1207
|
useEffect(() => {
|
|
984
1208
|
if (!isRawView || !monacoEditor) {
|
|
985
1209
|
setRawTemplate(null);
|
|
986
1210
|
setRawHeadingLine(null);
|
|
1211
|
+
setRawTransition(EMPTY_TRANSITION);
|
|
987
1212
|
return;
|
|
988
1213
|
}
|
|
989
1214
|
const recompute = () => {
|
|
@@ -992,6 +1217,7 @@ export function Toolbar({
|
|
|
992
1217
|
if (!model || !pos) {
|
|
993
1218
|
setRawTemplate(null);
|
|
994
1219
|
setRawHeadingLine(null);
|
|
1220
|
+
setRawTransition(EMPTY_TRANSITION);
|
|
995
1221
|
return;
|
|
996
1222
|
}
|
|
997
1223
|
const line = model.getLineContent(pos.lineNumber);
|
|
@@ -999,9 +1225,11 @@ export function Toolbar({
|
|
|
999
1225
|
if (!headingMatch) {
|
|
1000
1226
|
setRawTemplate(null);
|
|
1001
1227
|
setRawHeadingLine(null);
|
|
1228
|
+
setRawTransition(EMPTY_TRANSITION);
|
|
1002
1229
|
return;
|
|
1003
1230
|
}
|
|
1004
1231
|
setRawHeadingLine(pos.lineNumber);
|
|
1232
|
+
setRawTransition(readHeadingLineTransition(line));
|
|
1005
1233
|
const annotMatch = headingMatch[1].match(/\s*\{\[([^\]]+)\]\}[\s\]}]*$/);
|
|
1006
1234
|
if (annotMatch) {
|
|
1007
1235
|
// First whitespace-delimited token is the template name; the rest are params.
|
|
@@ -1115,6 +1343,52 @@ export function Toolbar({
|
|
|
1115
1343
|
}
|
|
1116
1344
|
};
|
|
1117
1345
|
|
|
1346
|
+
// ── Current block transition ──────────────────────────
|
|
1347
|
+
// Non-null exactly when a block (heading) is active — same condition as
|
|
1348
|
+
// the template picker. Read from the Pandoc `{…}` block (canonical), with
|
|
1349
|
+
// the `{[…]}` params as a fallback for hand-typed transitions.
|
|
1350
|
+
let currentTransition: TransitionFields | null = null;
|
|
1351
|
+
if (isWysiwyg && wysiwygTemplate !== null) {
|
|
1352
|
+
const attrs = tiptapEditor.getAttributes('heading') as {
|
|
1353
|
+
dataBlockAttrs?: string | null;
|
|
1354
|
+
dataTemplateParams?: string | null;
|
|
1355
|
+
};
|
|
1356
|
+
currentTransition = readBlockAttrsTransition(attrs.dataBlockAttrs, attrs.dataTemplateParams);
|
|
1357
|
+
} else if (isRawView && rawTemplate !== null) {
|
|
1358
|
+
currentTransition = rawTransition;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
const handleTransitionPick = (next: TransitionFields) => {
|
|
1362
|
+
// Raw (Monaco) — rewrite the heading line, writing into the `{…}` block.
|
|
1363
|
+
if (isRawView && monacoEditor) {
|
|
1364
|
+
const model = monacoEditor.getModel();
|
|
1365
|
+
const pos = monacoEditor.getPosition();
|
|
1366
|
+
if (!model || !pos) return;
|
|
1367
|
+
const lineNumber = pos.lineNumber;
|
|
1368
|
+
const lineText = model.getLineContent(lineNumber);
|
|
1369
|
+
const newLine = setHeadingLineTransition(lineText, next);
|
|
1370
|
+
if (newLine === lineText) return;
|
|
1371
|
+
monacoEditor.executeEdits('toolbar-transition-pick', [
|
|
1372
|
+
{
|
|
1373
|
+
range: {
|
|
1374
|
+
startLineNumber: lineNumber,
|
|
1375
|
+
startColumn: 1,
|
|
1376
|
+
endLineNumber: lineNumber,
|
|
1377
|
+
endColumn: lineText.length + 1,
|
|
1378
|
+
},
|
|
1379
|
+
text: newLine,
|
|
1380
|
+
},
|
|
1381
|
+
]);
|
|
1382
|
+
monacoEditor.focus();
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
// WYSIWYG — rewrite the heading node's `dataBlockAttrs` (Pandoc inner).
|
|
1386
|
+
if (!tiptapEditor) return;
|
|
1387
|
+
const attrs = tiptapEditor.getAttributes('heading') as { dataBlockAttrs?: string | null };
|
|
1388
|
+
const inner = setBlockAttrsTransition(attrs.dataBlockAttrs, next);
|
|
1389
|
+
tiptapEditor.chain().focus().updateAttributes('heading', { dataBlockAttrs: inner }).run();
|
|
1390
|
+
};
|
|
1391
|
+
|
|
1118
1392
|
return (
|
|
1119
1393
|
<div
|
|
1120
1394
|
className={`squisq-toolbar ${className || ''}`}
|
|
@@ -1167,8 +1441,10 @@ export function Toolbar({
|
|
|
1167
1441
|
))}
|
|
1168
1442
|
</div>
|
|
1169
1443
|
)}
|
|
1170
|
-
{/*
|
|
1171
|
-
{
|
|
1444
|
+
{/* After-tabs slot — left side, before formatting controls (preview mode switch) */}
|
|
1445
|
+
{slotAfterTabs}
|
|
1446
|
+
{/* Formatting buttons — hidden in preview mode and code mode */}
|
|
1447
|
+
{!isPreview && !isCodeMode && (
|
|
1172
1448
|
<div className="squisq-toolbar-actions" ref={actionsRef}>
|
|
1173
1449
|
{groups.map((group, gi) => (
|
|
1174
1450
|
<div key={group} className="squisq-toolbar-group">
|
|
@@ -1177,10 +1453,10 @@ export function Toolbar({
|
|
|
1177
1453
|
const active =
|
|
1178
1454
|
btn.id === 'emoji'
|
|
1179
1455
|
? emojiPickerAnchor !== null
|
|
1180
|
-
:
|
|
1181
|
-
? isTiptapActive(
|
|
1456
|
+
: formatActive && formattingEditor
|
|
1457
|
+
? isTiptapActive(formattingEditor, btn.id)
|
|
1182
1458
|
: false;
|
|
1183
|
-
const disabled = btn.id === 'image' && !mediaProvider;
|
|
1459
|
+
const disabled = (btn.id === 'image' && !mediaProvider) || !buttonAllowed(btn.id);
|
|
1184
1460
|
return (
|
|
1185
1461
|
<button
|
|
1186
1462
|
key={btn.id}
|
|
@@ -1191,15 +1467,31 @@ export function Toolbar({
|
|
|
1191
1467
|
aria-label={btn.title}
|
|
1192
1468
|
aria-pressed={active}
|
|
1193
1469
|
disabled={disabled}
|
|
1194
|
-
style={btn.iconStyle}
|
|
1195
1470
|
>
|
|
1196
|
-
{
|
|
1471
|
+
{buttonIcon(btn)}
|
|
1197
1472
|
</button>
|
|
1198
1473
|
);
|
|
1199
1474
|
})}
|
|
1200
1475
|
</div>
|
|
1201
1476
|
))}
|
|
1202
1477
|
|
|
1478
|
+
{/* Insert menu button — collapses all media-group actions into a single dropdown */}
|
|
1479
|
+
<div className="squisq-toolbar-group">
|
|
1480
|
+
<div className="squisq-toolbar-separator" />
|
|
1481
|
+
<button
|
|
1482
|
+
ref={insertMenuButtonRef}
|
|
1483
|
+
className={`squisq-toolbar-button${insertMenuAnchor ? ' squisq-toolbar-button--active' : ''}`}
|
|
1484
|
+
data-btn-count={String(MEDIA_BUTTONS.length)}
|
|
1485
|
+
data-tooltip="Insert..."
|
|
1486
|
+
onClick={() => (insertMenuAnchor ? closeInsertMenu() : openInsertMenu())}
|
|
1487
|
+
aria-label="Insert"
|
|
1488
|
+
aria-expanded={insertMenuAnchor !== null}
|
|
1489
|
+
aria-haspopup="menu"
|
|
1490
|
+
>
|
|
1491
|
+
<Icon icon="fa-solid fa-plus" />
|
|
1492
|
+
</button>
|
|
1493
|
+
</div>
|
|
1494
|
+
|
|
1203
1495
|
{/* Template picker — visible when the cursor is in a heading.
|
|
1204
1496
|
In WYSIWYG, reads from the heading node's `dataTemplate`; in
|
|
1205
1497
|
Markdown view, parses the `{[...]}` suffix on the cursor's line. */}
|
|
@@ -1216,6 +1508,18 @@ export function Toolbar({
|
|
|
1216
1508
|
</>
|
|
1217
1509
|
)}
|
|
1218
1510
|
|
|
1511
|
+
{/* Transition picker — visible alongside the template picker when a
|
|
1512
|
+
block (heading) is active. Writes `transition=` into the block's
|
|
1513
|
+
Pandoc `{…}` attribute block (the canonical home for transitions). */}
|
|
1514
|
+
{currentTransition !== null && (
|
|
1515
|
+
<>
|
|
1516
|
+
<div className="squisq-toolbar-separator" />
|
|
1517
|
+
<div className="squisq-toolbar-group squisq-transition-picker-group">
|
|
1518
|
+
<TransitionPicker value={currentTransition} onChange={handleTransitionPick} />
|
|
1519
|
+
</div>
|
|
1520
|
+
</>
|
|
1521
|
+
)}
|
|
1522
|
+
|
|
1219
1523
|
{/* Table controls — visible when cursor is in a table (WYSIWYG) */}
|
|
1220
1524
|
{isInTable && (
|
|
1221
1525
|
<>
|
|
@@ -1392,14 +1696,17 @@ export function Toolbar({
|
|
|
1392
1696
|
>
|
|
1393
1697
|
{BUTTONS.slice(overflowIndex)
|
|
1394
1698
|
.filter((b) => isButtonVisible(b.id))
|
|
1699
|
+
// Media buttons are represented by the synthetic Insert dropdown
|
|
1700
|
+
// in both the visible toolbar and the overflow menu.
|
|
1701
|
+
.filter((b) => b.group !== 'media')
|
|
1395
1702
|
.map((btn) => {
|
|
1396
1703
|
const active =
|
|
1397
1704
|
btn.id === 'emoji'
|
|
1398
1705
|
? emojiPickerAnchor !== null
|
|
1399
|
-
:
|
|
1400
|
-
? isTiptapActive(
|
|
1706
|
+
: formatActive && formattingEditor
|
|
1707
|
+
? isTiptapActive(formattingEditor, btn.id)
|
|
1401
1708
|
: false;
|
|
1402
|
-
const disabled = btn.id === 'image' && !mediaProvider;
|
|
1709
|
+
const disabled = (btn.id === 'image' && !mediaProvider) || !buttonAllowed(btn.id);
|
|
1403
1710
|
return (
|
|
1404
1711
|
<button
|
|
1405
1712
|
key={btn.id}
|
|
@@ -1414,16 +1721,33 @@ export function Toolbar({
|
|
|
1414
1721
|
}}
|
|
1415
1722
|
disabled={disabled}
|
|
1416
1723
|
>
|
|
1417
|
-
{
|
|
1418
|
-
<span className="squisq-toolbar-overflow-icon" style={btn.iconStyle}>
|
|
1419
|
-
{btn.icon}
|
|
1420
|
-
</span>
|
|
1421
|
-
)}
|
|
1724
|
+
<span className="squisq-toolbar-overflow-icon">{buttonIcon(btn)}</span>
|
|
1422
1725
|
<span>{btn.title}</span>
|
|
1423
1726
|
</button>
|
|
1424
1727
|
);
|
|
1425
1728
|
})}
|
|
1426
1729
|
|
|
1730
|
+
{showInsertInOverflow && (
|
|
1731
|
+
<button
|
|
1732
|
+
className={`squisq-toolbar-overflow-item${insertMenuAnchor ? ' squisq-toolbar-overflow-item--active' : ''}`}
|
|
1733
|
+
onClick={(event) => {
|
|
1734
|
+
if (insertMenuAnchor) {
|
|
1735
|
+
closeInsertMenu();
|
|
1736
|
+
} else {
|
|
1737
|
+
openInsertMenu(event.currentTarget);
|
|
1738
|
+
}
|
|
1739
|
+
setShowOverflow(false);
|
|
1740
|
+
}}
|
|
1741
|
+
aria-haspopup="menu"
|
|
1742
|
+
aria-expanded={insertMenuAnchor !== null}
|
|
1743
|
+
>
|
|
1744
|
+
<span className="squisq-toolbar-overflow-icon">
|
|
1745
|
+
<Icon icon="fa-solid fa-plus" />
|
|
1746
|
+
</span>
|
|
1747
|
+
<span>Insert...</span>
|
|
1748
|
+
</button>
|
|
1749
|
+
)}
|
|
1750
|
+
|
|
1427
1751
|
{/* Contextual: template picker in overflow */}
|
|
1428
1752
|
{currentTemplate !== null && (
|
|
1429
1753
|
<div className="squisq-toolbar-overflow-item squisq-toolbar-overflow-template">
|
|
@@ -1496,9 +1820,12 @@ export function Toolbar({
|
|
|
1496
1820
|
|
|
1497
1821
|
{/* After-actions slot — after formatting controls */}
|
|
1498
1822
|
{slotAfterActions}
|
|
1499
|
-
{/* Spacer —
|
|
1500
|
-
|
|
1501
|
-
|
|
1823
|
+
{/* Spacer — pushes right-side items to the end when the flex:1 actions
|
|
1824
|
+
container isn't rendered. In preview mode PreviewToolbarControls
|
|
1825
|
+
supplies its own flex:1 filler (and measures that leftover width to
|
|
1826
|
+
decide whether to collapse), so a second spacer here would split the
|
|
1827
|
+
slack and make the controls collapse too early. */}
|
|
1828
|
+
{isCodeMode && <div style={{ flex: 1 }} />}
|
|
1502
1829
|
{/* Version history — renders only when the host enabled versioning
|
|
1503
1830
|
and a container is wired up. The component owns its own button
|
|
1504
1831
|
and popover; we just give it a slot in the toolbar. */}
|
|
@@ -1516,21 +1843,18 @@ export function Toolbar({
|
|
|
1516
1843
|
data-tooltip="Document settings"
|
|
1517
1844
|
aria-label="Document settings"
|
|
1518
1845
|
>
|
|
1519
|
-
<
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
strokeLinecap="round"
|
|
1532
|
-
/>
|
|
1533
|
-
</svg>
|
|
1846
|
+
<Icon icon="fa-solid fa-file-lines" />
|
|
1847
|
+
</button>
|
|
1848
|
+
)}
|
|
1849
|
+
{!isCodeMode && (
|
|
1850
|
+
<button
|
|
1851
|
+
type="button"
|
|
1852
|
+
className={`squisq-toolbar-button${showLayoutManager ? ' squisq-toolbar-button--active' : ''}`}
|
|
1853
|
+
onClick={() => setShowLayoutManager(true)}
|
|
1854
|
+
data-tooltip="Custom layouts"
|
|
1855
|
+
aria-label="Custom layouts"
|
|
1856
|
+
>
|
|
1857
|
+
<Icon icon="fa-solid fa-shapes" />
|
|
1534
1858
|
</button>
|
|
1535
1859
|
)}
|
|
1536
1860
|
{!isCodeMode && <ViewMenuPanel />}
|
|
@@ -1543,7 +1867,7 @@ export function Toolbar({
|
|
|
1543
1867
|
aria-pressed={showFiles}
|
|
1544
1868
|
aria-label="Toggle Files panel"
|
|
1545
1869
|
>
|
|
1546
|
-
|
|
1870
|
+
<Icon icon="fa-solid fa-paperclip" />
|
|
1547
1871
|
</button>
|
|
1548
1872
|
)}
|
|
1549
1873
|
{/* Right slot — rightmost end of toolbar */}
|
|
@@ -1561,6 +1885,9 @@ export function Toolbar({
|
|
|
1561
1885
|
/>
|
|
1562
1886
|
)}
|
|
1563
1887
|
|
|
1888
|
+
{/* Custom layout manager — full-window list + designer dialog. */}
|
|
1889
|
+
{showLayoutManager && <CustomLayoutManager onClose={() => setShowLayoutManager(false)} />}
|
|
1890
|
+
|
|
1564
1891
|
{/* Link insert/edit dialog — shared by WYSIWYG and Raw views. */}
|
|
1565
1892
|
{linkDialog && (
|
|
1566
1893
|
<LinkDialog
|
|
@@ -1573,6 +1900,43 @@ export function Toolbar({
|
|
|
1573
1900
|
/>
|
|
1574
1901
|
)}
|
|
1575
1902
|
|
|
1903
|
+
{/* Insert menu — portaled to the document body so the overflow:hidden
|
|
1904
|
+
actions row doesn't clip it. Position computed from trigger rect. */}
|
|
1905
|
+
{insertMenuAnchor &&
|
|
1906
|
+
createPortal(
|
|
1907
|
+
<div
|
|
1908
|
+
ref={insertMenuRef}
|
|
1909
|
+
className="squisq-insert-menu"
|
|
1910
|
+
data-theme={colorScheme}
|
|
1911
|
+
style={{ position: 'fixed', top: insertMenuAnchor.top, left: insertMenuAnchor.left }}
|
|
1912
|
+
role="menu"
|
|
1913
|
+
>
|
|
1914
|
+
<div className="squisq-insert-menu-header">Insert</div>
|
|
1915
|
+
{MEDIA_BUTTONS.filter((b) => isButtonVisible(b.id)).map((btn) => {
|
|
1916
|
+
const disabled = (btn.id === 'image' && !mediaProvider) || !buttonAllowed(btn.id);
|
|
1917
|
+
const stripped = btn.title.replace(/^Insert\s+/i, '');
|
|
1918
|
+
const label = stripped.charAt(0).toUpperCase() + stripped.slice(1);
|
|
1919
|
+
return (
|
|
1920
|
+
<button
|
|
1921
|
+
key={btn.id}
|
|
1922
|
+
ref={btn.id === 'emoji' ? emojiButtonRef : undefined}
|
|
1923
|
+
className="squisq-toolbar-overflow-item"
|
|
1924
|
+
disabled={disabled}
|
|
1925
|
+
onClick={() => {
|
|
1926
|
+
handleAction(btn.id);
|
|
1927
|
+
if (btn.id !== 'emoji') closeInsertMenu();
|
|
1928
|
+
}}
|
|
1929
|
+
role="menuitem"
|
|
1930
|
+
>
|
|
1931
|
+
<span className="squisq-toolbar-overflow-icon">{buttonIcon(btn)}</span>
|
|
1932
|
+
<span>{label}</span>
|
|
1933
|
+
</button>
|
|
1934
|
+
);
|
|
1935
|
+
})}
|
|
1936
|
+
</div>,
|
|
1937
|
+
document.body,
|
|
1938
|
+
)}
|
|
1939
|
+
|
|
1576
1940
|
{/* Emoji picker — portaled to the document body so the toolbar's
|
|
1577
1941
|
overflow:hidden actions row doesn't clip the popover. Position
|
|
1578
1942
|
is computed from the trigger button's screen rect at open. */}
|
|
@@ -1583,7 +1947,7 @@ export function Toolbar({
|
|
|
1583
1947
|
onSelect={handleEmojiSelect}
|
|
1584
1948
|
onClose={closeEmojiPicker}
|
|
1585
1949
|
anchorRef={emojiButtonRef as React.RefObject<HTMLElement>}
|
|
1586
|
-
theme={
|
|
1950
|
+
theme={colorScheme === 'dark' ? 'dark' : 'light'}
|
|
1587
1951
|
style={{
|
|
1588
1952
|
position: 'fixed',
|
|
1589
1953
|
top: emojiPickerAnchor.top,
|