@bendyline/squisq-editor-react 1.6.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -3
- package/dist/index.d.ts +1304 -208
- package/dist/index.js +22838 -14494
- package/dist/index.js.map +1 -1
- package/dist/monaco.d.ts +1 -0
- package/dist/monaco.js +4 -0
- package/dist/monaco.js.map +1 -0
- package/dist/styles/index.css +2384 -408
- package/package.json +9 -5
- package/src/BlockPropertiesPopover.tsx +48 -17
- package/src/DocumentSettingsDialog.tsx +13 -21
- package/src/EditorContext.tsx +199 -42
- package/src/EditorShell.tsx +403 -270
- package/src/ImageEditor.tsx +22 -1
- package/src/InlinePreviewGutter.tsx +16 -8
- package/src/MediaBin.tsx +276 -40
- package/src/OutlinePanel.tsx +202 -7
- package/src/PlainHtmlPreview.tsx +40 -1
- package/src/PreviewControls.tsx +466 -93
- package/src/PreviewPanel.tsx +313 -77
- package/src/RawEditor.tsx +13 -2
- package/src/StatusBar.tsx +5 -2
- package/src/TemplateAnnotation.ts +34 -10
- package/src/TemplateContentPreview.tsx +56 -0
- package/src/TemplatePicker.tsx +355 -134
- package/src/ThemeCustomizerPanel.tsx +30 -15
- package/src/ThemePicker.tsx +10 -5
- package/src/TimelineBlockPreview.tsx +1 -1
- package/src/Toolbar.tsx +719 -238
- package/src/TransitionPicker.tsx +44 -5
- package/src/ViewMenuPanel.tsx +12 -14
- package/src/ViewSwitcher.tsx +4 -4
- package/src/WysiwygEditor.tsx +167 -92
- package/src/__tests__/blockPropertiesPopoverTheme.test.tsx +33 -0
- package/src/__tests__/blockTagActivity.test.ts +183 -0
- package/src/__tests__/buildDocumentPreviewMarkdown.test.ts +75 -0
- package/src/__tests__/buildPreviewDocContent.test.ts +48 -0
- package/src/__tests__/buildPreviewDocTransition.test.ts +22 -3
- package/src/__tests__/canvasSurfaceTextEditing.test.tsx +60 -0
- package/src/__tests__/customLayoutManagerTheme.test.tsx +34 -0
- package/src/__tests__/documentSettingsDialog.test.tsx +29 -1
- package/src/__tests__/editorScrollHandoff.test.tsx +102 -0
- package/src/__tests__/editorScrollSync.test.ts +65 -0
- package/src/__tests__/editorShellProps.test.tsx +390 -1
- package/src/__tests__/emojiPicker.test.tsx +2 -42
- package/src/__tests__/headingTransition.test.ts +53 -15
- package/src/__tests__/imageEditAffordance.test.tsx +5 -1
- package/src/__tests__/imageEditModalContract.test.tsx +119 -0
- package/src/__tests__/imageEditorShell.test.tsx +38 -1
- package/src/__tests__/jsonEditor.test.tsx +56 -1
- package/src/__tests__/layersPanel.test.tsx +75 -5
- package/src/__tests__/majorApiTypes.test.ts +26 -0
- package/src/__tests__/mediaBinDrop.test.tsx +90 -0
- package/src/__tests__/mediaReferences.test.ts +82 -0
- package/src/__tests__/narrationSave.test.ts +148 -0
- package/src/__tests__/outlinePanel.test.tsx +93 -3
- package/src/__tests__/outlineSource.test.ts +217 -0
- package/src/__tests__/plainHtmlPreview.test.tsx +17 -1
- package/src/__tests__/presentationMode.test.tsx +290 -0
- package/src/__tests__/previewControls.test.tsx +312 -6
- package/src/__tests__/previewPanelAudioFailure.test.tsx +46 -0
- package/src/__tests__/previewPanelPresentation.test.tsx +126 -0
- package/src/__tests__/previewPanelTransforms.test.tsx +117 -0
- package/src/__tests__/rawEditorModelIsolation.test.ts +19 -0
- package/src/__tests__/resolveBlockVisual.test.ts +43 -0
- package/src/__tests__/statusBar.test.tsx +27 -0
- package/src/__tests__/teleprompter.test.tsx +190 -0
- package/src/__tests__/templateAnnotationRoundTrip.test.ts +49 -2
- package/src/__tests__/templateContentPreview.test.ts +101 -0
- package/src/__tests__/templatePickerPortal.test.tsx +62 -0
- package/src/__tests__/tiptapBridge.test.ts +70 -0
- package/src/__tests__/tiptapCodeBlockRoundTrip.test.ts +109 -0
- package/src/__tests__/transformStyleId.test.ts +13 -0
- package/src/__tests__/useImageEditor.test.tsx +67 -0
- package/src/__tests__/useMediaRecorder.test.ts +75 -0
- package/src/__tests__/viewMenuPanel.test.tsx +130 -0
- package/src/__tests__/wysiwygImageUpload.test.ts +69 -0
- package/src/__tests__/wysiwygTemplateBadgeFocus.test.tsx +56 -0
- package/src/asciiDiagram/AsciiDiagramExtension.ts +343 -0
- package/src/asciiDiagram/AsciiDiagramWidget.tsx +224 -0
- package/src/asciiDiagram/RepairableDiagramExtension.ts +200 -0
- package/src/asciiDiagram/__tests__/AsciiDiagramExtension.test.ts +145 -0
- package/src/asciiDiagram/__tests__/RepairableDiagramExtension.test.ts +159 -0
- package/src/asciiDiagram/__tests__/asciiDiagramCommands.test.ts +227 -0
- package/src/asciiDiagram/__tests__/asciiDiagramOps.test.ts +114 -0
- package/src/asciiDiagram/__tests__/asciiPaste.test.ts +43 -0
- package/src/asciiDiagram/asciiDiagramCommands.ts +148 -0
- package/src/asciiDiagram/asciiDiagramData.ts +116 -0
- package/src/asciiDiagram/asciiDiagramOps.ts +219 -0
- package/src/asciiDiagram/asciiPaste.ts +19 -0
- package/src/blockTagActivity.ts +233 -0
- package/src/buildDocumentPreviewMarkdown.ts +168 -0
- package/src/buildPreviewDoc.ts +19 -12
- package/src/customTemplates/CustomLayoutManager.tsx +3 -2
- package/src/customTemplates/CustomTemplateContext.tsx +6 -0
- package/src/customTemplates/TemplateDesigner.tsx +7 -4
- package/src/customTemplates/__tests__/library.test.ts +8 -0
- package/src/customTemplates/designer.css +158 -99
- package/src/customTemplates/library.ts +16 -3
- package/src/customThemes/CustomThemeContext.tsx +6 -0
- package/src/customThemes/CustomThemeDialog.tsx +35 -5
- package/src/customThemes/ImportThemeSection.tsx +178 -0
- package/src/customThemes/__tests__/customThemeLibrary.test.ts +9 -0
- package/src/customThemes/__tests__/importThemeSection.test.tsx +192 -0
- package/src/customThemes/customThemeLibrary.ts +7 -4
- package/src/customThemes/index.ts +7 -1
- package/src/customThemes/themeDraft.ts +23 -7
- package/src/diagram/DiagramCanvas.tsx +9 -4
- package/src/diagram/types.ts +35 -0
- package/src/editorScrollSync.ts +73 -0
- package/src/emojiData.ts +3 -23
- package/src/headingTransition.ts +85 -25
- package/src/imageEditor/CanvasSurface.tsx +29 -23
- package/src/imageEditor/LayersPanel.tsx +78 -3
- package/src/imageEditor/Toolbar.tsx +5 -1
- package/src/imageEditor/icons.tsx +4 -0
- package/src/imageEditor/image-editor.css +75 -0
- package/src/imageEditor/layers/SelectionHandles.tsx +1 -1
- package/src/imageEditor/useImageEditor.ts +71 -14
- package/src/index.ts +193 -20
- package/src/jsonEditor/JsonEditorContext.tsx +12 -6
- package/src/jsonEditor/RenderNode.tsx +24 -3
- package/src/jsonEditor/editors.tsx +86 -19
- package/src/mediaEntries.ts +12 -0
- package/src/mediaReferences.ts +299 -0
- package/src/monaco.ts +35 -0
- package/src/monacoWorkers.ts +89 -0
- package/src/outlineSource.ts +171 -0
- package/src/presentation/PresentationMode.tsx +596 -0
- package/src/rawEditorIsolation.ts +18 -0
- package/src/recorder/hooks/useMediaRecorder.ts +97 -53
- package/src/resolveBlockVisual.ts +10 -4
- package/src/scene/Scene.tsx +101 -25
- package/src/scene/SceneBlockExtension.ts +17 -10
- package/src/scene/SceneBlockWidget.tsx +11 -4
- package/src/scene/SceneSelection.tsx +19 -15
- package/src/scene/SceneSideToolbar.tsx +89 -0
- package/src/scene/SceneViewport.tsx +7 -3
- package/src/scene/ShapePalette.tsx +17 -2
- package/src/scene/__tests__/DiagramAdapter.test.ts +86 -0
- package/src/scene/__tests__/SceneBlockExtension.test.ts +33 -0
- package/src/scene/__tests__/sceneIsolation.test.tsx +93 -0
- package/src/scene/adapters/DiagramAdapter.ts +12 -101
- package/src/scene/commands/SceneCommand.ts +4 -1
- package/src/scene/index.ts +1 -6
- package/src/scene/layers/DiagramEdges.tsx +24 -9
- package/src/scene/layers/edgeGeometry.ts +31 -4
- package/src/scene/layers/nodeCard.tsx +27 -8
- package/src/scene/scene.css +145 -2
- package/src/scene/text/SceneTextOverlay.tsx +5 -5
- package/src/scene/text/sceneTextChannel.ts +26 -20
- package/src/scene/text/sceneTextConfig.ts +4 -0
- package/src/scene/tools/ConnectTool.ts +126 -28
- package/src/scene/tools/DrawingConnectTool.ts +86 -16
- package/src/scene/tools/SceneTool.ts +10 -0
- package/src/scene/tools/SelectTool.ts +34 -18
- package/src/scene/tools/ShapeTool.ts +2 -3
- package/src/styles/ascii-diagram.css +79 -0
- package/src/styles/ascii-timeline.css +499 -0
- package/src/styles/editor.css +1566 -219
- package/src/styles/index.css +3 -0
- package/src/styles/tree-view.css +139 -0
- package/src/teleprompter/TeleprompterControls.tsx +218 -0
- package/src/teleprompter/TeleprompterSelfView.tsx +22 -0
- package/src/teleprompter/TeleprompterSurface.tsx +267 -0
- package/src/teleprompter/TeleprompterView.tsx +338 -0
- package/src/teleprompter/canvasRenderer.ts +161 -0
- package/src/teleprompter/floatingWindow.ts +352 -0
- package/src/teleprompter/index.ts +61 -0
- package/src/teleprompter/pcmWorklet.ts +62 -0
- package/src/teleprompter/recording/insertPreamble.ts +80 -0
- package/src/teleprompter/recording/narrationSave.ts +134 -0
- package/src/teleprompter/recording/useNarrationRecorder.ts +367 -0
- package/src/teleprompter/scrollModel.ts +85 -0
- package/src/teleprompter/teleprompterTheme.ts +303 -0
- package/src/teleprompter/types.ts +38 -0
- package/src/teleprompter/useFloatingWindow.ts +74 -0
- package/src/teleprompter/useMicAnalysis.ts +211 -0
- package/src/teleprompter/useTeleprompter.ts +421 -0
- package/src/templateContentPreviewResolver.ts +360 -0
- package/src/timeline/TimelineEditorWidget.tsx +702 -0
- package/src/timeline/TimelineViewExtension.ts +252 -0
- package/src/timeline/__tests__/TimelineEditorWidget.test.tsx +355 -0
- package/src/timeline/__tests__/TimelineViewExtension.test.ts +163 -0
- package/src/timeline/__tests__/timelineCommands.test.ts +327 -0
- package/src/timeline/__tests__/timelineOps.test.ts +222 -0
- package/src/timeline/__tests__/timelinePaste.test.ts +34 -0
- package/src/timeline/timelineCommands.ts +367 -0
- package/src/timeline/timelineData.ts +54 -0
- package/src/timeline/timelineOps.ts +277 -0
- package/src/timeline/timelinePaste.ts +8 -0
- package/src/tiptapBridge.ts +84 -49
- package/src/transformStyleId.ts +17 -0
- package/src/treeview/TreeOutlineWidget.tsx +240 -0
- package/src/treeview/TreeViewExtension.ts +250 -0
- package/src/treeview/__tests__/TreeViewExtension.test.ts +122 -0
- package/src/treeview/__tests__/treeOps.test.ts +125 -0
- package/src/treeview/__tests__/treePaste.test.ts +40 -0
- package/src/treeview/__tests__/treeViewCommands.test.ts +163 -0
- package/src/treeview/treeOps.ts +172 -0
- package/src/treeview/treePaste.ts +13 -0
- package/src/treeview/treeViewCommands.ts +94 -0
- package/src/treeview/treeViewData.ts +41 -0
- package/src/useMonacoLoader.ts +11 -34
- package/src/wysiwygImageUpload.ts +113 -0
- package/src/diagram/DiagramExtension.ts +0 -209
- package/src/diagram/DiagramWidget.tsx +0 -270
- package/src/diagram/useDiagramData.ts +0 -126
package/src/Toolbar.tsx
CHANGED
|
@@ -9,21 +9,35 @@
|
|
|
9
9
|
|
|
10
10
|
import type { ReactNode } from 'react';
|
|
11
11
|
import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react';
|
|
12
|
-
import type { Editor as TiptapEditor } from '@tiptap/core';
|
|
12
|
+
import type { Editor as TiptapEditor, JSONContent } from '@tiptap/core';
|
|
13
13
|
import { TextSelection } from '@tiptap/pm/state';
|
|
14
14
|
import { Fragment } from '@tiptap/pm/model';
|
|
15
15
|
import type { IRange } from 'monaco-editor';
|
|
16
|
+
import type { Block } from '@bendyline/squisq/schemas';
|
|
17
|
+
import { VIEWPORT_PRESETS } from '@bendyline/squisq/schemas';
|
|
18
|
+
import { DEFAULT_THEME, flattenBlocks } from '@bendyline/squisq/doc';
|
|
19
|
+
import {
|
|
20
|
+
KNOWN_BLOCK_META_KEYS,
|
|
21
|
+
matchTrailingTemplateAnnotation,
|
|
22
|
+
splitKeyValueToken,
|
|
23
|
+
tokenizeAttrTokens,
|
|
24
|
+
} from '@bendyline/squisq/markdown';
|
|
16
25
|
import { useEditorContext, type EditorView } from './EditorContext';
|
|
17
26
|
import { VersionHistoryPanel } from './VersionHistoryPanel';
|
|
18
27
|
import { RecorderEntry } from './RecorderEntry';
|
|
19
28
|
import { ViewMenuPanel } from './ViewMenuPanel';
|
|
20
|
-
import {
|
|
21
|
-
|
|
29
|
+
import {
|
|
30
|
+
TemplatePicker,
|
|
31
|
+
TEMPLATE_NAMES,
|
|
32
|
+
TEMPLATE_GALLERY_PORTAL_SELECTOR,
|
|
33
|
+
templateLabel,
|
|
34
|
+
} from './TemplatePicker';
|
|
35
|
+
import { TransitionPicker, TRANSITION_FLYOUT_PORTAL_ID } from './TransitionPicker';
|
|
22
36
|
import {
|
|
23
37
|
readHeadingLineTransition,
|
|
24
38
|
setHeadingLineTransition,
|
|
25
39
|
readBlockAttrsTransition,
|
|
26
|
-
|
|
40
|
+
setHeadingAttrsTransition,
|
|
27
41
|
EMPTY_TRANSITION,
|
|
28
42
|
type TransitionFields,
|
|
29
43
|
} from './headingTransition';
|
|
@@ -36,25 +50,31 @@ import { CustomLayoutManager } from './customTemplates/CustomLayoutManager';
|
|
|
36
50
|
import { Icon } from './Icon';
|
|
37
51
|
import type { PickerEntry } from './emojiData';
|
|
38
52
|
import { createPortal } from 'react-dom';
|
|
53
|
+
import { PreviewModeMenu, displayModeLabel, usePreviewSettingsOptional } from './PreviewControls';
|
|
54
|
+
import { filterVisibleMediaEntries } from './mediaEntries';
|
|
39
55
|
|
|
40
56
|
const VIEWS: { id: EditorView; label: string; shortLabel?: string; shortcut: string }[] = [
|
|
41
|
-
{ id: 'wysiwyg', label: '
|
|
42
|
-
{ id: 'raw', label: '
|
|
43
|
-
{ id: 'preview', label: '
|
|
57
|
+
{ id: 'wysiwyg', label: 'Write', shortcut: '⌘1' },
|
|
58
|
+
{ id: 'raw', label: 'Source', shortcut: '⌘2' },
|
|
59
|
+
{ id: 'preview', label: 'Use', shortcut: '⌘3' },
|
|
44
60
|
];
|
|
45
61
|
|
|
62
|
+
const BLOCK_META_KEYS = new Set<string>(Object.keys(KNOWN_BLOCK_META_KEYS));
|
|
63
|
+
|
|
46
64
|
export interface ToolbarProps {
|
|
47
65
|
/** Additional class name */
|
|
48
66
|
className?: string;
|
|
49
67
|
/** Whether the Files panel is currently shown */
|
|
50
68
|
showFiles?: boolean;
|
|
69
|
+
/** Number of files currently available through the MediaProvider. */
|
|
70
|
+
fileCount?: number;
|
|
51
71
|
/** Toggle the Files panel. When provided, a "Files" button appears in the toolbar. */
|
|
52
72
|
onToggleFiles?: () => void;
|
|
53
73
|
/** Content rendered at the left edge of the toolbar, before the view tabs. */
|
|
54
74
|
slotLeft?: ReactNode;
|
|
55
75
|
/** Content rendered immediately after the view tabs, on the left side of the
|
|
56
76
|
* toolbar (before the formatting controls). Used for the preview mode
|
|
57
|
-
* switch in
|
|
77
|
+
* switch in Use view. */
|
|
58
78
|
slotAfterTabs?: ReactNode;
|
|
59
79
|
/** Content rendered after the formatting controls (in the middle area). */
|
|
60
80
|
slotAfterActions?: ReactNode;
|
|
@@ -185,6 +205,14 @@ const BUTTONS: ToolbarButton[] = [
|
|
|
185
205
|
group: 'media',
|
|
186
206
|
faIcon: 'fa-solid fa-table',
|
|
187
207
|
},
|
|
208
|
+
{
|
|
209
|
+
id: 'tasklist',
|
|
210
|
+
label: 'tasks',
|
|
211
|
+
icon: '',
|
|
212
|
+
title: 'Insert Task List',
|
|
213
|
+
group: 'media',
|
|
214
|
+
faIcon: 'fa-solid fa-list-check',
|
|
215
|
+
},
|
|
188
216
|
{
|
|
189
217
|
id: 'diagram',
|
|
190
218
|
label: 'diagram',
|
|
@@ -193,6 +221,22 @@ const BUTTONS: ToolbarButton[] = [
|
|
|
193
221
|
group: 'media',
|
|
194
222
|
faIcon: 'fa-solid fa-diagram-project',
|
|
195
223
|
},
|
|
224
|
+
{
|
|
225
|
+
id: 'tree',
|
|
226
|
+
label: 'tree',
|
|
227
|
+
icon: '',
|
|
228
|
+
title: 'Insert tree',
|
|
229
|
+
group: 'media',
|
|
230
|
+
faIcon: 'fa-solid fa-folder-tree',
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
id: 'timeline',
|
|
234
|
+
label: 'timeline',
|
|
235
|
+
icon: '',
|
|
236
|
+
title: 'Insert timeline',
|
|
237
|
+
group: 'media',
|
|
238
|
+
faIcon: 'fa-solid fa-timeline',
|
|
239
|
+
},
|
|
196
240
|
{
|
|
197
241
|
id: 'drawing',
|
|
198
242
|
label: 'drawing',
|
|
@@ -230,12 +274,51 @@ const BUTTONS: ToolbarButton[] = [
|
|
|
230
274
|
const FIRST_MEDIA_INDEX = BUTTONS.findIndex((b) => b.group === 'media');
|
|
231
275
|
const MEDIA_BUTTONS = BUTTONS.filter((b) => b.group === 'media');
|
|
232
276
|
const INSERT_MENU_WIDTH = 200;
|
|
277
|
+
const TASK_LIST_ITEMS = ['Task 1', 'Task 2', 'Task 3'] as const;
|
|
278
|
+
const TASK_LIST_MARKDOWN = TASK_LIST_ITEMS.map((item) => `- [ ] ${item}`).join('\n');
|
|
279
|
+
|
|
280
|
+
// BUTTONS position per id — stamped on each rendered button as
|
|
281
|
+
// data-btn-index so the overflow measurement can map a clipped DOM button
|
|
282
|
+
// back to its BUTTONS entry. Walking a counter over the DOM instead would
|
|
283
|
+
// drift whenever some entries render no button of their own (hidden H5/H6
|
|
284
|
+
// heading levels, the media group collapsed behind the Insert dropdown).
|
|
285
|
+
const BUTTON_INDEX_BY_ID = new Map(BUTTONS.map((b, i) => [b.id, i]));
|
|
233
286
|
|
|
234
287
|
/** Renders a button's icon: a Font Awesome glyph when set, else the text label. */
|
|
235
288
|
function buttonIcon(btn: ToolbarButton): ReactNode {
|
|
236
289
|
return btn.faIcon ? <Icon icon={btn.faIcon} /> : btn.icon;
|
|
237
290
|
}
|
|
238
291
|
|
|
292
|
+
function fileCountLabel(count: number): string {
|
|
293
|
+
return `${count} ${count === 1 ? 'file' : 'files'}`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function fileCountBadge(count: number): string {
|
|
297
|
+
return count > 99 ? '99+' : String(count);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function taskListContent(): JSONContent {
|
|
301
|
+
return {
|
|
302
|
+
type: 'taskList',
|
|
303
|
+
content: TASK_LIST_ITEMS.map((item) => ({
|
|
304
|
+
type: 'taskItem',
|
|
305
|
+
attrs: { checked: false },
|
|
306
|
+
content: [
|
|
307
|
+
{
|
|
308
|
+
type: 'paragraph',
|
|
309
|
+
content: [{ type: 'text', text: item }],
|
|
310
|
+
},
|
|
311
|
+
],
|
|
312
|
+
})),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function insertTaskList(editor: TiptapEditor): void {
|
|
317
|
+
const supportsTaskList = !!editor.schema.nodes.taskList && !!editor.schema.nodes.taskItem;
|
|
318
|
+
const content = supportsTaskList ? taskListContent() : TASK_LIST_MARKDOWN;
|
|
319
|
+
editor.chain().focus().insertContent(content).run();
|
|
320
|
+
}
|
|
321
|
+
|
|
239
322
|
// ─── Tiptap active-state map ────────────────────────────
|
|
240
323
|
|
|
241
324
|
/** Returns true if the given button id is currently active in Tiptap */
|
|
@@ -289,6 +372,79 @@ const LAYOUT_STARTER_TEXT_PARAMS =
|
|
|
289
372
|
/** Multi-line markdown for a new layout (raw / code views). */
|
|
290
373
|
const LAYOUT_STARTER_MARKDOWN = `\n## Layout {[layout]}\n\n### {#text-1} {[text ${LAYOUT_STARTER_TEXT_PARAMS}]}\n\nLayout\n`;
|
|
291
374
|
|
|
375
|
+
/**
|
|
376
|
+
* Starter art for a new diagram — diagrams are authored as ASCII-art code
|
|
377
|
+
* fences (the fence is the source of truth; `AsciiDiagramExtension` mounts
|
|
378
|
+
* the interactive canvas over it). This exact art is the codec's own
|
|
379
|
+
* canonical rendering of a two-node flow, so it re-parses and re-renders
|
|
380
|
+
* byte-stably.
|
|
381
|
+
*/
|
|
382
|
+
const DIAGRAM_STARTER_ART = [
|
|
383
|
+
'┌─────────┐',
|
|
384
|
+
'│ Start │',
|
|
385
|
+
'└────┬────┘',
|
|
386
|
+
' │',
|
|
387
|
+
' ▼',
|
|
388
|
+
'┌────┴────┐',
|
|
389
|
+
'│ Next │',
|
|
390
|
+
'└─────────┘',
|
|
391
|
+
].join('\n');
|
|
392
|
+
/**
|
|
393
|
+
* Fenced form for raw / code views — tagged with the explicit `diagram`
|
|
394
|
+
* language so the block's identity survives markdown ↔ Tiptap round-trips
|
|
395
|
+
* (the language class round-trips; fence meta does not).
|
|
396
|
+
*/
|
|
397
|
+
const DIAGRAM_STARTER_MARKDOWN = '\n```diagram\n' + DIAGRAM_STARTER_ART + '\n```\n';
|
|
398
|
+
|
|
399
|
+
/** Insert a starter ASCII-diagram code fence after the current top-level block. */
|
|
400
|
+
function insertAsciiDiagramBlock(editor: TiptapEditor): void {
|
|
401
|
+
insertFenceBlock(editor, DIAGRAM_STARTER_ART, 'diagram');
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Starter art for a new file tree — an ASCII tree fence (the fence is the
|
|
406
|
+
* source of truth; `TreeViewExtension` mounts the interactive outline).
|
|
407
|
+
*/
|
|
408
|
+
const TREE_STARTER_ART = ['src/', '├── index.ts', '└── utils/', ' └── helpers.ts'].join('\n');
|
|
409
|
+
const TREE_STARTER_MARKDOWN = '\n```tree\n' + TREE_STARTER_ART + '\n```\n';
|
|
410
|
+
|
|
411
|
+
/** Insert a starter ASCII tree code fence after the current top-level block. */
|
|
412
|
+
function insertTreeBlock(editor: TiptapEditor): void {
|
|
413
|
+
insertFenceBlock(editor, TREE_STARTER_ART, 'tree');
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** Starter multi-event rail for the authored timeline view. */
|
|
417
|
+
const TIMELINE_STARTER_ART =
|
|
418
|
+
'Milestones: ● Start {#start} ─────● Review {#review} ─────● Ship {#ship} ───►';
|
|
419
|
+
const TIMELINE_STARTER_MARKDOWN = '\n```timeline\n' + TIMELINE_STARTER_ART + '\n```\n';
|
|
420
|
+
|
|
421
|
+
/** Insert a starter ASCII timeline code fence after the current top-level block. */
|
|
422
|
+
function insertTimelineBlock(editor: TiptapEditor): void {
|
|
423
|
+
insertFenceBlock(editor, TIMELINE_STARTER_ART, 'timeline');
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Insert a code fence after the current top-level block. `lang` tags the
|
|
428
|
+
* fence's `language` attribute — pass the explicit authored-view tag so its
|
|
429
|
+
* identity round-trips through markdown and Tiptap.
|
|
430
|
+
*/
|
|
431
|
+
function insertFenceBlock(editor: TiptapEditor, art: string, lang?: string): void {
|
|
432
|
+
editor
|
|
433
|
+
.chain()
|
|
434
|
+
.focus()
|
|
435
|
+
.command(({ tr, state, dispatch }) => {
|
|
436
|
+
const codeBlockType = state.schema.nodes.codeBlock;
|
|
437
|
+
if (!codeBlockType) return false;
|
|
438
|
+
const attrs = lang ? { language: lang } : null;
|
|
439
|
+
const block = codeBlockType.create(attrs, state.schema.text(art));
|
|
440
|
+
const { $from } = state.selection;
|
|
441
|
+
const insertPos = $from.depth > 0 ? $from.after(1) : state.doc.content.size;
|
|
442
|
+
if (dispatch) tr.insert(insertPos, block);
|
|
443
|
+
return true;
|
|
444
|
+
})
|
|
445
|
+
.run();
|
|
446
|
+
}
|
|
447
|
+
|
|
292
448
|
/**
|
|
293
449
|
* Insert a block-level heading carrying a Scene template (`diagram` /
|
|
294
450
|
* `drawing` / `layout`) at the top level, after the block the caret sits
|
|
@@ -365,6 +521,44 @@ function insertLayoutBlock(editor: TiptapEditor): void {
|
|
|
365
521
|
.run();
|
|
366
522
|
}
|
|
367
523
|
|
|
524
|
+
function findBlockBySourceLine(blocks: readonly Block[], lineNumber: number): Block | null {
|
|
525
|
+
for (const block of blocks) {
|
|
526
|
+
const pos = block.sourceHeading?.position;
|
|
527
|
+
if (!pos) continue;
|
|
528
|
+
if (pos.start.line <= lineNumber && pos.end.line >= lineNumber) return block;
|
|
529
|
+
}
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function splitTemplateAnnotationParams(text: string): { text: string; params: string[] } {
|
|
534
|
+
const match = matchTrailingTemplateAnnotation(text);
|
|
535
|
+
if (!match) return { text: text.trimEnd(), params: [] };
|
|
536
|
+
const tokens = tokenizeAttrTokens(match.inner.trim());
|
|
537
|
+
const firstIsParam = tokens.length > 0 && tokens[0].indexOf('=') > 0;
|
|
538
|
+
return {
|
|
539
|
+
text: text.slice(0, match.index).trimEnd(),
|
|
540
|
+
params: keepBlockMetaParamTokens(tokens.slice(firstIsParam ? 0 : 1)),
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function buildTemplateAnnotation(template: string, params: string[]): string | null {
|
|
545
|
+
const parts = template ? [template, ...params] : params;
|
|
546
|
+
return parts.length > 0 ? `{[${parts.join(' ')}]}` : null;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function keepBlockMetaParamTokens(tokens: readonly string[]): string[] {
|
|
550
|
+
return tokens.filter((token) => {
|
|
551
|
+
const kv = splitKeyValueToken(token);
|
|
552
|
+
return kv ? BLOCK_META_KEYS.has(kv.key) : false;
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function keepBlockMetaParamString(inner: string | null | undefined): string | null {
|
|
557
|
+
if (!inner) return null;
|
|
558
|
+
const params = keepBlockMetaParamTokens(tokenizeAttrTokens(inner));
|
|
559
|
+
return params.length > 0 ? params.join(' ') : null;
|
|
560
|
+
}
|
|
561
|
+
|
|
368
562
|
/**
|
|
369
563
|
* Formatting toolbar.
|
|
370
564
|
* - WYSIWYG: calls Tiptap chain commands (toggleBold, etc.)
|
|
@@ -373,6 +567,7 @@ function insertLayoutBlock(editor: TiptapEditor): void {
|
|
|
373
567
|
export function Toolbar({
|
|
374
568
|
className,
|
|
375
569
|
showFiles,
|
|
570
|
+
fileCount,
|
|
376
571
|
onToggleFiles,
|
|
377
572
|
slotLeft,
|
|
378
573
|
slotAfterTabs,
|
|
@@ -384,17 +579,24 @@ export function Toolbar({
|
|
|
384
579
|
activeView,
|
|
385
580
|
setActiveView,
|
|
386
581
|
markdownSource,
|
|
582
|
+
doc,
|
|
387
583
|
setMarkdownSource,
|
|
388
584
|
tiptapEditor,
|
|
389
585
|
monacoEditor,
|
|
390
586
|
activeSceneText,
|
|
391
587
|
mediaProvider,
|
|
392
588
|
editorMode,
|
|
589
|
+
layoutMode,
|
|
590
|
+
activeBlockStartLine,
|
|
393
591
|
versioning,
|
|
394
592
|
allowRecording,
|
|
395
593
|
documentLinkProvider,
|
|
396
594
|
colorScheme,
|
|
595
|
+
mediaRevision,
|
|
596
|
+
bumpMediaRevision,
|
|
397
597
|
} = useEditorContext();
|
|
598
|
+
const previewSettings = usePreviewSettingsOptional();
|
|
599
|
+
const [useModeMenuRequest, requestUseModeMenu] = useReducer((count: number) => count + 1, 0);
|
|
398
600
|
// When a canvas textbox is being edited, its Tiptap instance takes over
|
|
399
601
|
// the formatting buttons; otherwise they drive the document editor. The
|
|
400
602
|
// `level` gates which buttons apply (inline labels vs. rich textboxes).
|
|
@@ -409,6 +611,30 @@ export function Toolbar({
|
|
|
409
611
|
return true;
|
|
410
612
|
});
|
|
411
613
|
const showViewTabs = visibleViews.length > 1;
|
|
614
|
+
const [scannedFileCount, setScannedFileCount] = useState(0);
|
|
615
|
+
const resolvedFileCount = fileCount ?? scannedFileCount;
|
|
616
|
+
|
|
617
|
+
useEffect(() => {
|
|
618
|
+
if (fileCount !== undefined) return;
|
|
619
|
+
if (!mediaProvider) {
|
|
620
|
+
setScannedFileCount(0);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
let cancelled = false;
|
|
625
|
+
mediaProvider.listMedia().then(
|
|
626
|
+
(entries) => {
|
|
627
|
+
if (!cancelled) setScannedFileCount(filterVisibleMediaEntries(entries).length);
|
|
628
|
+
},
|
|
629
|
+
() => {
|
|
630
|
+
if (!cancelled) setScannedFileCount(0);
|
|
631
|
+
},
|
|
632
|
+
);
|
|
633
|
+
|
|
634
|
+
return () => {
|
|
635
|
+
cancelled = true;
|
|
636
|
+
};
|
|
637
|
+
}, [fileCount, mediaProvider, mediaRevision, showFiles]);
|
|
412
638
|
|
|
413
639
|
// Hidden file input for image picker
|
|
414
640
|
const imageInputRef = useRef<HTMLInputElement>(null);
|
|
@@ -490,6 +716,11 @@ export function Toolbar({
|
|
|
490
716
|
// ── Overflow detection ────────────────────────────────
|
|
491
717
|
const actionsRef = useRef<HTMLDivElement>(null);
|
|
492
718
|
const [measuredOverflowIndex, setMeasuredOverflowIndex] = useState<number | null>(null);
|
|
719
|
+
// '|'-joined data-contextual ids of the contextual groups (template /
|
|
720
|
+
// transition pickers, table controls) whose right edge doesn't fit in the
|
|
721
|
+
// actions row. Stored as a string so identical measurements bail out of
|
|
722
|
+
// re-rendering.
|
|
723
|
+
const [clippedContextualKey, setClippedContextualKey] = useState('');
|
|
493
724
|
const [showOverflow, setShowOverflow] = useState(false);
|
|
494
725
|
const overflowRef = useRef<HTMLDivElement>(null);
|
|
495
726
|
|
|
@@ -500,6 +731,7 @@ export function Toolbar({
|
|
|
500
731
|
const [showLayoutManager, setShowLayoutManager] = useState(false);
|
|
501
732
|
|
|
502
733
|
const overflowIndex = measuredOverflowIndex;
|
|
734
|
+
const clippedContextual = new Set(clippedContextualKey ? clippedContextualKey.split('|') : []);
|
|
503
735
|
|
|
504
736
|
useEffect(() => {
|
|
505
737
|
const container = actionsRef.current;
|
|
@@ -507,39 +739,79 @@ export function Toolbar({
|
|
|
507
739
|
|
|
508
740
|
const measure = () => {
|
|
509
741
|
const containerRight = container.getBoundingClientRect().right;
|
|
742
|
+
// Contextual groups aren't BUTTONS entries, so they must not shift the
|
|
743
|
+
// btnIndex mapping below — measure them separately.
|
|
510
744
|
const children = container.querySelectorAll<HTMLElement>(
|
|
511
|
-
':scope > .squisq-toolbar-group > .squisq-toolbar-button',
|
|
745
|
+
':scope > .squisq-toolbar-group:not(.squisq-toolbar-contextual) > .squisq-toolbar-button',
|
|
512
746
|
);
|
|
513
747
|
let firstHidden: number | null = null;
|
|
514
|
-
let btnIndex = 0;
|
|
515
748
|
children.forEach((child) => {
|
|
516
749
|
if (firstHidden !== null) return;
|
|
517
|
-
//
|
|
518
|
-
//
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
firstHidden = btnIndex;
|
|
750
|
+
// A button is hidden if its right edge extends past the container.
|
|
751
|
+
// Its data-btn-index carries its BUTTONS position (see
|
|
752
|
+
// BUTTON_INDEX_BY_ID for why the DOM can't be walked positionally).
|
|
753
|
+
if (child.getBoundingClientRect().right > containerRight + 2) {
|
|
754
|
+
const index = Number(child.dataset.btnIndex);
|
|
755
|
+
firstHidden = Number.isNaN(index) ? null : index;
|
|
524
756
|
}
|
|
525
|
-
btnIndex += btnCount;
|
|
526
757
|
});
|
|
527
758
|
setMeasuredOverflowIndex(firstHidden);
|
|
759
|
+
// Contextual groups (template/transition pickers, table controls) that
|
|
760
|
+
// don't fit move into the overflow menu instead of painting cropped.
|
|
761
|
+
const clipped: string[] = [];
|
|
762
|
+
container
|
|
763
|
+
.querySelectorAll<HTMLElement>(':scope > .squisq-toolbar-contextual')
|
|
764
|
+
.forEach((group) => {
|
|
765
|
+
if (group.getBoundingClientRect().right > containerRight + 2) {
|
|
766
|
+
clipped.push(group.dataset.contextual ?? '');
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
setClippedContextualKey(clipped.join('|'));
|
|
528
770
|
};
|
|
529
771
|
|
|
530
|
-
|
|
531
|
-
|
|
772
|
+
// Observe the row and every group in it: contextual groups mount/unmount
|
|
773
|
+
// with the cursor context and change width with their value, all without
|
|
774
|
+
// resizing the flex:1 container itself — a container-only observer would
|
|
775
|
+
// go stale.
|
|
776
|
+
const ro = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(measure);
|
|
777
|
+
const observeAll = () => {
|
|
778
|
+
ro?.disconnect();
|
|
779
|
+
ro?.observe(container);
|
|
780
|
+
container
|
|
781
|
+
.querySelectorAll<HTMLElement>(':scope > .squisq-toolbar-group')
|
|
782
|
+
.forEach((group) => ro?.observe(group));
|
|
783
|
+
};
|
|
784
|
+
const mo = new MutationObserver(() => {
|
|
785
|
+
observeAll();
|
|
786
|
+
measure();
|
|
787
|
+
});
|
|
788
|
+
mo.observe(container, { childList: true });
|
|
789
|
+
if (!ro) window.addEventListener('resize', measure);
|
|
790
|
+
observeAll();
|
|
532
791
|
measure();
|
|
533
|
-
return () =>
|
|
792
|
+
return () => {
|
|
793
|
+
ro?.disconnect();
|
|
794
|
+
if (!ro) window.removeEventListener('resize', measure);
|
|
795
|
+
mo.disconnect();
|
|
796
|
+
};
|
|
534
797
|
}, [activeView]);
|
|
535
798
|
|
|
536
|
-
// Close overflow menu on outside click
|
|
799
|
+
// Close overflow menu on outside click. Clicks inside the template
|
|
800
|
+
// gallery / transition flyout portals count as inside: those popovers are
|
|
801
|
+
// hosted by pickers living in this menu, and closing the menu on their
|
|
802
|
+
// mousedown would unmount them before the click completes.
|
|
537
803
|
useEffect(() => {
|
|
538
804
|
if (!showOverflow) return;
|
|
539
805
|
const handleClick = (e: MouseEvent) => {
|
|
540
|
-
|
|
541
|
-
|
|
806
|
+
const target = e.target as Node;
|
|
807
|
+
if (overflowRef.current?.contains(target)) return;
|
|
808
|
+
if (
|
|
809
|
+
target instanceof Element &&
|
|
810
|
+
target.closest(`${TEMPLATE_GALLERY_PORTAL_SELECTOR}, #${TRANSITION_FLYOUT_PORTAL_ID}`)
|
|
811
|
+
) {
|
|
812
|
+
return;
|
|
542
813
|
}
|
|
814
|
+
setShowOverflow(false);
|
|
543
815
|
};
|
|
544
816
|
document.addEventListener('mousedown', handleClick);
|
|
545
817
|
return () => document.removeEventListener('mousedown', handleClick);
|
|
@@ -673,10 +945,23 @@ export function Toolbar({
|
|
|
673
945
|
case 'table':
|
|
674
946
|
tiptapEditor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run();
|
|
675
947
|
break;
|
|
948
|
+
case 'tasklist':
|
|
949
|
+
insertTaskList(tiptapEditor);
|
|
950
|
+
break;
|
|
676
951
|
case 'diagram':
|
|
677
|
-
//
|
|
678
|
-
//
|
|
679
|
-
|
|
952
|
+
// Diagrams are ASCII-art code fences; the AsciiDiagramExtension
|
|
953
|
+
// mounts the interactive canvas over the inserted starter.
|
|
954
|
+
insertAsciiDiagramBlock(tiptapEditor);
|
|
955
|
+
break;
|
|
956
|
+
case 'tree':
|
|
957
|
+
// File trees are ASCII tree fences; TreeViewExtension mounts the
|
|
958
|
+
// interactive outline over the inserted starter.
|
|
959
|
+
insertTreeBlock(tiptapEditor);
|
|
960
|
+
break;
|
|
961
|
+
case 'timeline':
|
|
962
|
+
// The authored fence is immediately replaced by the WYSIWYG rail
|
|
963
|
+
// canvas; Preview uses the same events as vector layers.
|
|
964
|
+
insertTimelineBlock(tiptapEditor);
|
|
680
965
|
break;
|
|
681
966
|
case 'drawing':
|
|
682
967
|
// Empty drawing is usable via the canvas's "Shapes ▾" palette;
|
|
@@ -827,10 +1112,25 @@ export function Toolbar({
|
|
|
827
1112
|
newCursorOffset = 3; // after \n|
|
|
828
1113
|
break;
|
|
829
1114
|
}
|
|
1115
|
+
case 'tasklist': {
|
|
1116
|
+
replacement = `\n${TASK_LIST_MARKDOWN}\n`;
|
|
1117
|
+
newCursorOffset = replacement.length;
|
|
1118
|
+
break;
|
|
1119
|
+
}
|
|
830
1120
|
case 'diagram': {
|
|
831
|
-
// A diagram is
|
|
832
|
-
//
|
|
833
|
-
replacement =
|
|
1121
|
+
// A diagram is an ASCII-art code fence; the WYSIWYG view
|
|
1122
|
+
// renders its editable canvas, preview renders it as a slide.
|
|
1123
|
+
replacement = DIAGRAM_STARTER_MARKDOWN;
|
|
1124
|
+
newCursorOffset = replacement.length;
|
|
1125
|
+
break;
|
|
1126
|
+
}
|
|
1127
|
+
case 'tree': {
|
|
1128
|
+
replacement = TREE_STARTER_MARKDOWN;
|
|
1129
|
+
newCursorOffset = replacement.length;
|
|
1130
|
+
break;
|
|
1131
|
+
}
|
|
1132
|
+
case 'timeline': {
|
|
1133
|
+
replacement = TIMELINE_STARTER_MARKDOWN;
|
|
834
1134
|
newCursorOffset = replacement.length;
|
|
835
1135
|
break;
|
|
836
1136
|
}
|
|
@@ -908,8 +1208,17 @@ export function Toolbar({
|
|
|
908
1208
|
insertion =
|
|
909
1209
|
'\n| Header 1 | Header 2 | Header 3 |\n| --- | --- | --- |\n| Cell | Cell | Cell |\n| Cell | Cell | Cell |\n';
|
|
910
1210
|
break;
|
|
1211
|
+
case 'tasklist':
|
|
1212
|
+
insertion = `\n${TASK_LIST_MARKDOWN}\n`;
|
|
1213
|
+
break;
|
|
911
1214
|
case 'diagram':
|
|
912
|
-
insertion =
|
|
1215
|
+
insertion = DIAGRAM_STARTER_MARKDOWN;
|
|
1216
|
+
break;
|
|
1217
|
+
case 'tree':
|
|
1218
|
+
insertion = TREE_STARTER_MARKDOWN;
|
|
1219
|
+
break;
|
|
1220
|
+
case 'timeline':
|
|
1221
|
+
insertion = TIMELINE_STARTER_MARKDOWN;
|
|
913
1222
|
break;
|
|
914
1223
|
case 'drawing':
|
|
915
1224
|
insertion = '\n## Drawing {[drawing]}\n';
|
|
@@ -932,6 +1241,7 @@ export function Toolbar({
|
|
|
932
1241
|
if (!mediaProvider) return;
|
|
933
1242
|
const buffer = await file.arrayBuffer();
|
|
934
1243
|
const relativePath = await mediaProvider.addMedia(file.name, buffer, file.type);
|
|
1244
|
+
bumpMediaRevision();
|
|
935
1245
|
const altText = file.name.replace(/\.[^.]+$/, '').replace(/[-_]/g, ' ');
|
|
936
1246
|
|
|
937
1247
|
if (activeView === 'wysiwyg' && tiptapEditor) {
|
|
@@ -948,7 +1258,15 @@ export function Toolbar({
|
|
|
948
1258
|
setMarkdownSource(markdownSource + `\n\n`);
|
|
949
1259
|
}
|
|
950
1260
|
},
|
|
951
|
-
[
|
|
1261
|
+
[
|
|
1262
|
+
mediaProvider,
|
|
1263
|
+
activeView,
|
|
1264
|
+
tiptapEditor,
|
|
1265
|
+
monacoEditor,
|
|
1266
|
+
markdownSource,
|
|
1267
|
+
setMarkdownSource,
|
|
1268
|
+
bumpMediaRevision,
|
|
1269
|
+
],
|
|
952
1270
|
);
|
|
953
1271
|
|
|
954
1272
|
const handleAction = useCallback(
|
|
@@ -1230,11 +1548,11 @@ export function Toolbar({
|
|
|
1230
1548
|
}
|
|
1231
1549
|
setRawHeadingLine(pos.lineNumber);
|
|
1232
1550
|
setRawTransition(readHeadingLineTransition(line));
|
|
1233
|
-
const annotMatch = headingMatch[1]
|
|
1551
|
+
const annotMatch = matchTrailingTemplateAnnotation(headingMatch[1]);
|
|
1234
1552
|
if (annotMatch) {
|
|
1235
|
-
|
|
1236
|
-
const
|
|
1237
|
-
setRawTemplate(
|
|
1553
|
+
const tokens = tokenizeAttrTokens(annotMatch.inner.trim());
|
|
1554
|
+
const firstIsParam = tokens.length > 0 && tokens[0].indexOf('=') > 0;
|
|
1555
|
+
setRawTemplate(firstIsParam ? '' : (tokens[0] ?? ''));
|
|
1238
1556
|
} else {
|
|
1239
1557
|
setRawTemplate('');
|
|
1240
1558
|
}
|
|
@@ -1302,6 +1620,57 @@ export function Toolbar({
|
|
|
1302
1620
|
return recommendTemplatesForBlock(profile, TEMPLATE_NAMES).recommended;
|
|
1303
1621
|
}, [currentTemplate, isRawView, isWysiwyg, rawHeadingLine, wysiwygHeadingIndex, markdownSource]);
|
|
1304
1622
|
|
|
1623
|
+
const templatePreviewBlock = useMemo(() => {
|
|
1624
|
+
if (!doc || currentTemplate === null) return null;
|
|
1625
|
+
const flat = flattenBlocks(doc.blocks);
|
|
1626
|
+
if (flat.length === 0) return null;
|
|
1627
|
+
|
|
1628
|
+
if (isRawView && rawHeadingLine !== null) {
|
|
1629
|
+
return findBlockBySourceLine(flat, rawHeadingLine);
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
if (isWysiwyg) {
|
|
1633
|
+
if (layoutMode !== 'document' && activeBlockStartLine !== null) {
|
|
1634
|
+
return findBlockBySourceLine(flat, activeBlockStartLine);
|
|
1635
|
+
}
|
|
1636
|
+
if (wysiwygHeadingIndex !== null) {
|
|
1637
|
+
return flat[wysiwygHeadingIndex] ?? null;
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
return null;
|
|
1642
|
+
}, [
|
|
1643
|
+
activeBlockStartLine,
|
|
1644
|
+
currentTemplate,
|
|
1645
|
+
doc,
|
|
1646
|
+
isRawView,
|
|
1647
|
+
isWysiwyg,
|
|
1648
|
+
layoutMode,
|
|
1649
|
+
rawHeadingLine,
|
|
1650
|
+
wysiwygHeadingIndex,
|
|
1651
|
+
]);
|
|
1652
|
+
|
|
1653
|
+
const templatePreviewSource = useMemo(
|
|
1654
|
+
() =>
|
|
1655
|
+
templatePreviewBlock
|
|
1656
|
+
? {
|
|
1657
|
+
block: templatePreviewBlock,
|
|
1658
|
+
theme: previewSettings?.activeTheme ?? DEFAULT_THEME,
|
|
1659
|
+
viewport: previewSettings?.activeViewport ?? VIEWPORT_PRESETS.landscape,
|
|
1660
|
+
basePath: '/',
|
|
1661
|
+
mediaProvider,
|
|
1662
|
+
customTemplates: doc?.customTemplates,
|
|
1663
|
+
}
|
|
1664
|
+
: undefined,
|
|
1665
|
+
[
|
|
1666
|
+
mediaProvider,
|
|
1667
|
+
doc?.customTemplates,
|
|
1668
|
+
previewSettings?.activeTheme,
|
|
1669
|
+
previewSettings?.activeViewport,
|
|
1670
|
+
templatePreviewBlock,
|
|
1671
|
+
],
|
|
1672
|
+
);
|
|
1673
|
+
|
|
1305
1674
|
const handleTemplatePick = (value: string) => {
|
|
1306
1675
|
// Raw (Monaco) — rewrite the heading line's annotation suffix in place.
|
|
1307
1676
|
if (isRawView && monacoEditor) {
|
|
@@ -1313,9 +1682,9 @@ export function Toolbar({
|
|
|
1313
1682
|
const headingMatch = lineText.match(/^(#{1,6}\s+)(.+)$/);
|
|
1314
1683
|
if (!headingMatch) return;
|
|
1315
1684
|
const prefix = headingMatch[1];
|
|
1316
|
-
|
|
1317
|
-
const
|
|
1318
|
-
const newLine =
|
|
1685
|
+
const { text: bareText, params } = splitTemplateAnnotationParams(headingMatch[2]);
|
|
1686
|
+
const annotation = buildTemplateAnnotation(value, params);
|
|
1687
|
+
const newLine = annotation ? `${prefix}${bareText} ${annotation}` : `${prefix}${bareText}`;
|
|
1319
1688
|
monacoEditor.executeEdits('toolbar-template-pick', [
|
|
1320
1689
|
{
|
|
1321
1690
|
range: {
|
|
@@ -1332,14 +1701,22 @@ export function Toolbar({
|
|
|
1332
1701
|
}
|
|
1333
1702
|
// WYSIWYG — update the heading node attributes.
|
|
1334
1703
|
if (!tiptapEditor) return;
|
|
1704
|
+
const attrs = tiptapEditor.getAttributes('heading') as {
|
|
1705
|
+
dataTemplateParams?: string | null;
|
|
1706
|
+
};
|
|
1707
|
+
const dataTemplateParams = keepBlockMetaParamString(attrs.dataTemplateParams);
|
|
1335
1708
|
if (value === '') {
|
|
1336
1709
|
tiptapEditor
|
|
1337
1710
|
.chain()
|
|
1338
1711
|
.focus()
|
|
1339
|
-
.updateAttributes('heading', { dataTemplate: null, dataTemplateParams
|
|
1712
|
+
.updateAttributes('heading', { dataTemplate: null, dataTemplateParams })
|
|
1340
1713
|
.run();
|
|
1341
1714
|
} else {
|
|
1342
|
-
tiptapEditor
|
|
1715
|
+
tiptapEditor
|
|
1716
|
+
.chain()
|
|
1717
|
+
.focus()
|
|
1718
|
+
.updateAttributes('heading', { dataTemplate: value, dataTemplateParams })
|
|
1719
|
+
.run();
|
|
1343
1720
|
}
|
|
1344
1721
|
};
|
|
1345
1722
|
|
|
@@ -1382,13 +1759,36 @@ export function Toolbar({
|
|
|
1382
1759
|
monacoEditor.focus();
|
|
1383
1760
|
return;
|
|
1384
1761
|
}
|
|
1385
|
-
// WYSIWYG —
|
|
1762
|
+
// WYSIWYG — write transitions into `{[…]}` params and migrate legacy
|
|
1763
|
+
// Pandoc transition keys out of `dataBlockAttrs`.
|
|
1386
1764
|
if (!tiptapEditor) return;
|
|
1387
|
-
const attrs = tiptapEditor.getAttributes('heading') as {
|
|
1388
|
-
|
|
1389
|
-
|
|
1765
|
+
const attrs = tiptapEditor.getAttributes('heading') as {
|
|
1766
|
+
dataBlockAttrs?: string | null;
|
|
1767
|
+
dataTemplateParams?: string | null;
|
|
1768
|
+
};
|
|
1769
|
+
const updated = setHeadingAttrsTransition(attrs.dataBlockAttrs, attrs.dataTemplateParams, next);
|
|
1770
|
+
tiptapEditor
|
|
1771
|
+
.chain()
|
|
1772
|
+
.focus()
|
|
1773
|
+
.updateAttributes('heading', {
|
|
1774
|
+
dataBlockAttrs: updated.blockAttrsInner,
|
|
1775
|
+
dataTemplateParams: updated.templateParams,
|
|
1776
|
+
})
|
|
1777
|
+
.run();
|
|
1390
1778
|
};
|
|
1391
1779
|
|
|
1780
|
+
// ── Per-block controls in the overflow menu ────────────
|
|
1781
|
+
// The template + transition pickers configure the active heading block, so
|
|
1782
|
+
// when either is pushed into the ··· menu we group them under a labeled
|
|
1783
|
+
// "<name> Block:" heading. `currentTemplate`/`currentTransition` are non-null
|
|
1784
|
+
// together (both gated on a heading being active); an empty template string
|
|
1785
|
+
// is a heading with no visual template, labeled "Heading".
|
|
1786
|
+
const showTemplateInOverflow = currentTemplate !== null && clippedContextual.has('template');
|
|
1787
|
+
const showTransitionInOverflow =
|
|
1788
|
+
currentTransition !== null && clippedContextual.has('transition');
|
|
1789
|
+
const showBlockSectionInOverflow = showTemplateInOverflow || showTransitionInOverflow;
|
|
1790
|
+
const overflowBlockLabel = currentTemplate ? templateLabel(currentTemplate) : 'Heading';
|
|
1791
|
+
|
|
1392
1792
|
return (
|
|
1393
1793
|
<div
|
|
1394
1794
|
className={`squisq-toolbar ${className || ''}`}
|
|
@@ -1413,35 +1813,65 @@ export function Toolbar({
|
|
|
1413
1813
|
{/* View tabs — hidden when only one view is available (e.g. code mode). */}
|
|
1414
1814
|
{showViewTabs && (
|
|
1415
1815
|
<div className="squisq-toolbar-view-tabs" role="tablist" aria-label="Editor view">
|
|
1416
|
-
{visibleViews.map((view) =>
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1816
|
+
{visibleViews.map((view) => {
|
|
1817
|
+
const viewLabel =
|
|
1818
|
+
view.id === 'preview' && previewSettings
|
|
1819
|
+
? displayModeLabel(previewSettings.activeDisplayMode)
|
|
1820
|
+
: view.label;
|
|
1821
|
+
const tab = (
|
|
1822
|
+
<button
|
|
1823
|
+
role="tab"
|
|
1824
|
+
data-view={view.id}
|
|
1825
|
+
aria-selected={activeView === view.id}
|
|
1826
|
+
className={`squisq-toolbar-view-tab${activeView === view.id ? ' squisq-toolbar-view-tab--active' : ''}`}
|
|
1827
|
+
onClick={() => {
|
|
1828
|
+
if (view.id === 'preview' && activeView === 'preview' && previewSettings) {
|
|
1829
|
+
requestUseModeMenu();
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
setActiveView(view.id);
|
|
1833
|
+
}}
|
|
1834
|
+
data-tooltip={`${viewLabel} (${view.shortcut})`}
|
|
1429
1835
|
>
|
|
1430
|
-
{view.label}
|
|
1431
|
-
</span>
|
|
1432
|
-
{view.shortLabel && view.shortLabel !== view.label && (
|
|
1433
1836
|
<span
|
|
1434
|
-
className="squisq-toolbar-view-tab-label squisq-toolbar-view-tab-label--
|
|
1435
|
-
data-label={
|
|
1837
|
+
className="squisq-toolbar-view-tab-label squisq-toolbar-view-tab-label--long"
|
|
1838
|
+
data-label={viewLabel}
|
|
1436
1839
|
>
|
|
1437
|
-
{
|
|
1840
|
+
{viewLabel}
|
|
1438
1841
|
</span>
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1842
|
+
{view.shortLabel && view.shortLabel !== view.label && (
|
|
1843
|
+
<span
|
|
1844
|
+
className="squisq-toolbar-view-tab-label squisq-toolbar-view-tab-label--short"
|
|
1845
|
+
data-label={view.shortLabel}
|
|
1846
|
+
>
|
|
1847
|
+
{view.shortLabel}
|
|
1848
|
+
</span>
|
|
1849
|
+
)}
|
|
1850
|
+
</button>
|
|
1851
|
+
);
|
|
1852
|
+
|
|
1853
|
+
if (view.id !== 'preview' || !previewSettings) {
|
|
1854
|
+
return (
|
|
1855
|
+
<div key={view.id} className="squisq-toolbar-view-tab-wrap" role="presentation">
|
|
1856
|
+
{tab}
|
|
1857
|
+
</div>
|
|
1858
|
+
);
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
return (
|
|
1862
|
+
<div
|
|
1863
|
+
key={view.id}
|
|
1864
|
+
className={`squisq-toolbar-view-tab-wrap squisq-toolbar-use-tab${activeView === 'preview' ? ' squisq-toolbar-use-tab--active' : ''}`}
|
|
1865
|
+
role="presentation"
|
|
1866
|
+
>
|
|
1867
|
+
{tab}
|
|
1868
|
+
<PreviewModeMenu openRequest={useModeMenuRequest} />
|
|
1869
|
+
</div>
|
|
1870
|
+
);
|
|
1871
|
+
})}
|
|
1442
1872
|
</div>
|
|
1443
1873
|
)}
|
|
1444
|
-
{/* After-tabs slot — left side, before formatting
|
|
1874
|
+
{/* After-tabs slot — left side, before formatting or preview controls. */}
|
|
1445
1875
|
{slotAfterTabs}
|
|
1446
1876
|
{/* Formatting buttons — hidden in preview mode and code mode */}
|
|
1447
1877
|
{!isPreview && !isCodeMode && (
|
|
@@ -1462,6 +1892,7 @@ export function Toolbar({
|
|
|
1462
1892
|
key={btn.id}
|
|
1463
1893
|
ref={btn.id === 'emoji' ? emojiButtonRef : undefined}
|
|
1464
1894
|
className={`squisq-toolbar-button${active ? ' squisq-toolbar-button--active' : ''}`}
|
|
1895
|
+
data-btn-index={BUTTON_INDEX_BY_ID.get(btn.id)}
|
|
1465
1896
|
data-tooltip={disabled ? 'Insert image (requires media provider)' : btn.title}
|
|
1466
1897
|
onClick={() => handleAction(btn.id)}
|
|
1467
1898
|
aria-label={btn.title}
|
|
@@ -1481,7 +1912,7 @@ export function Toolbar({
|
|
|
1481
1912
|
<button
|
|
1482
1913
|
ref={insertMenuButtonRef}
|
|
1483
1914
|
className={`squisq-toolbar-button${insertMenuAnchor ? ' squisq-toolbar-button--active' : ''}`}
|
|
1484
|
-
data-btn-
|
|
1915
|
+
data-btn-index={FIRST_MEDIA_INDEX}
|
|
1485
1916
|
data-tooltip="Insert..."
|
|
1486
1917
|
onClick={() => (insertMenuAnchor ? closeInsertMenu() : openInsertMenu())}
|
|
1487
1918
|
aria-label="Insert"
|
|
@@ -1494,192 +1925,207 @@ export function Toolbar({
|
|
|
1494
1925
|
|
|
1495
1926
|
{/* Template picker — visible when the cursor is in a heading.
|
|
1496
1927
|
In WYSIWYG, reads from the heading node's `dataTemplate`; in
|
|
1497
|
-
Markdown view, parses the `{[...]}` suffix on the cursor's line.
|
|
1928
|
+
Markdown view, parses the `{[...]}` suffix on the cursor's line.
|
|
1929
|
+
The `squisq-toolbar-contextual` class + data-contextual id opt
|
|
1930
|
+
this group (and the two below) into the clipping measurement:
|
|
1931
|
+
when it doesn't fit it hides here and moves into the ··· menu. */}
|
|
1498
1932
|
{currentTemplate !== null && (
|
|
1499
|
-
|
|
1933
|
+
<div
|
|
1934
|
+
className={`squisq-toolbar-group squisq-toolbar-contextual squisq-template-picker${clippedContextual.has('template') ? ' squisq-toolbar-contextual--clipped' : ''}`}
|
|
1935
|
+
data-contextual="template"
|
|
1936
|
+
>
|
|
1500
1937
|
<div className="squisq-toolbar-separator" />
|
|
1501
|
-
<
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1938
|
+
<TemplatePicker
|
|
1939
|
+
value={currentTemplate}
|
|
1940
|
+
onChange={handleTemplatePick}
|
|
1941
|
+
colorScheme={colorScheme}
|
|
1942
|
+
recommended={recommendedTemplates}
|
|
1943
|
+
previewSource={templatePreviewSource}
|
|
1944
|
+
/>
|
|
1945
|
+
</div>
|
|
1509
1946
|
)}
|
|
1510
1947
|
|
|
1511
1948
|
{/* Transition picker — visible alongside the template picker when a
|
|
1512
1949
|
block (heading) is active. Writes `transition=` into the block's
|
|
1513
1950
|
Pandoc `{…}` attribute block (the canonical home for transitions). */}
|
|
1514
1951
|
{currentTransition !== null && (
|
|
1515
|
-
|
|
1952
|
+
<div
|
|
1953
|
+
className={`squisq-toolbar-group squisq-toolbar-contextual squisq-transition-picker-group${clippedContextual.has('transition') ? ' squisq-toolbar-contextual--clipped' : ''}`}
|
|
1954
|
+
data-contextual="transition"
|
|
1955
|
+
>
|
|
1516
1956
|
<div className="squisq-toolbar-separator" />
|
|
1517
|
-
<
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1957
|
+
<TransitionPicker
|
|
1958
|
+
value={currentTransition}
|
|
1959
|
+
onChange={handleTransitionPick}
|
|
1960
|
+
colorScheme={colorScheme}
|
|
1961
|
+
accentColor={previewSettings?.activeTheme.colors.primary}
|
|
1962
|
+
/>
|
|
1963
|
+
</div>
|
|
1521
1964
|
)}
|
|
1522
1965
|
|
|
1523
1966
|
{/* Table controls — visible when cursor is in a table (WYSIWYG) */}
|
|
1524
1967
|
{isInTable && (
|
|
1525
|
-
|
|
1968
|
+
<div
|
|
1969
|
+
className={`squisq-toolbar-group squisq-toolbar-contextual squisq-table-controls${clippedContextual.has('table') ? ' squisq-toolbar-contextual--clipped' : ''}`}
|
|
1970
|
+
data-contextual="table"
|
|
1971
|
+
>
|
|
1526
1972
|
<div className="squisq-toolbar-separator" />
|
|
1527
|
-
<
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1973
|
+
<span className="squisq-table-controls-label">Table:</span>
|
|
1974
|
+
<button
|
|
1975
|
+
className="squisq-toolbar-button"
|
|
1976
|
+
data-tooltip="Add column before"
|
|
1977
|
+
onClick={() => tiptapEditor!.chain().focus().addColumnBefore().run()}
|
|
1978
|
+
aria-label="Add column before"
|
|
1979
|
+
>
|
|
1980
|
+
<svg
|
|
1981
|
+
width="16"
|
|
1982
|
+
height="16"
|
|
1983
|
+
viewBox="0 0 16 16"
|
|
1984
|
+
fill="none"
|
|
1985
|
+
stroke="currentColor"
|
|
1986
|
+
strokeWidth="1.5"
|
|
1987
|
+
strokeLinecap="round"
|
|
1534
1988
|
>
|
|
1535
|
-
<
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1989
|
+
<rect x="7" y="2" width="8" height="12" rx="1" />
|
|
1990
|
+
<line x1="11" y1="2" x2="11" y2="14" />
|
|
1991
|
+
<line x1="1" y1="8" x2="4.5" y2="8" />
|
|
1992
|
+
<line x1="2.75" y1="6.25" x2="2.75" y2="9.75" />
|
|
1993
|
+
</svg>
|
|
1994
|
+
</button>
|
|
1995
|
+
<button
|
|
1996
|
+
className="squisq-toolbar-button"
|
|
1997
|
+
data-tooltip="Add column after"
|
|
1998
|
+
onClick={() => tiptapEditor!.chain().focus().addColumnAfter().run()}
|
|
1999
|
+
aria-label="Add column after"
|
|
2000
|
+
>
|
|
2001
|
+
<svg
|
|
2002
|
+
width="16"
|
|
2003
|
+
height="16"
|
|
2004
|
+
viewBox="0 0 16 16"
|
|
2005
|
+
fill="none"
|
|
2006
|
+
stroke="currentColor"
|
|
2007
|
+
strokeWidth="1.5"
|
|
2008
|
+
strokeLinecap="round"
|
|
1555
2009
|
>
|
|
1556
|
-
<
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
2010
|
+
<rect x="1" y="2" width="8" height="12" rx="1" />
|
|
2011
|
+
<line x1="5" y1="2" x2="5" y2="14" />
|
|
2012
|
+
<line x1="11.5" y1="8" x2="15" y2="8" />
|
|
2013
|
+
<line x1="13.25" y1="6.25" x2="13.25" y2="9.75" />
|
|
2014
|
+
</svg>
|
|
2015
|
+
</button>
|
|
2016
|
+
<button
|
|
2017
|
+
className="squisq-toolbar-button"
|
|
2018
|
+
data-tooltip="Delete column"
|
|
2019
|
+
onClick={() => tiptapEditor!.chain().focus().deleteColumn().run()}
|
|
2020
|
+
aria-label="Delete column"
|
|
2021
|
+
>
|
|
2022
|
+
<svg
|
|
2023
|
+
width="16"
|
|
2024
|
+
height="16"
|
|
2025
|
+
viewBox="0 0 16 16"
|
|
2026
|
+
fill="none"
|
|
2027
|
+
stroke="currentColor"
|
|
2028
|
+
strokeWidth="1.5"
|
|
2029
|
+
strokeLinecap="round"
|
|
1576
2030
|
>
|
|
1577
|
-
<
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
2031
|
+
<rect x="4" y="1" width="8" height="14" rx="1" />
|
|
2032
|
+
<line x1="6" y1="5.5" x2="10" y2="10.5" />
|
|
2033
|
+
<line x1="10" y1="5.5" x2="6" y2="10.5" />
|
|
2034
|
+
</svg>
|
|
2035
|
+
</button>
|
|
2036
|
+
<button
|
|
2037
|
+
className="squisq-toolbar-button"
|
|
2038
|
+
data-tooltip="Add row above"
|
|
2039
|
+
onClick={() => tiptapEditor!.chain().focus().addRowBefore().run()}
|
|
2040
|
+
aria-label="Add row above"
|
|
2041
|
+
>
|
|
2042
|
+
<svg
|
|
2043
|
+
width="16"
|
|
2044
|
+
height="16"
|
|
2045
|
+
viewBox="0 0 16 16"
|
|
2046
|
+
fill="none"
|
|
2047
|
+
stroke="currentColor"
|
|
2048
|
+
strokeWidth="1.5"
|
|
2049
|
+
strokeLinecap="round"
|
|
1596
2050
|
>
|
|
1597
|
-
<
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
2051
|
+
<rect x="2" y="6" width="12" height="9" rx="1" />
|
|
2052
|
+
<line x1="2" y1="10.5" x2="14" y2="10.5" />
|
|
2053
|
+
<line x1="8" y1="1" x2="8" y2="4.5" />
|
|
2054
|
+
<line x1="6.25" y1="2.75" x2="9.75" y2="2.75" />
|
|
2055
|
+
</svg>
|
|
2056
|
+
</button>
|
|
2057
|
+
<button
|
|
2058
|
+
className="squisq-toolbar-button"
|
|
2059
|
+
data-tooltip="Add row below"
|
|
2060
|
+
onClick={() => tiptapEditor!.chain().focus().addRowAfter().run()}
|
|
2061
|
+
aria-label="Add row below"
|
|
2062
|
+
>
|
|
2063
|
+
<svg
|
|
2064
|
+
width="16"
|
|
2065
|
+
height="16"
|
|
2066
|
+
viewBox="0 0 16 16"
|
|
2067
|
+
fill="none"
|
|
2068
|
+
stroke="currentColor"
|
|
2069
|
+
strokeWidth="1.5"
|
|
2070
|
+
strokeLinecap="round"
|
|
1617
2071
|
>
|
|
1618
|
-
<
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
2072
|
+
<rect x="2" y="1" width="12" height="9" rx="1" />
|
|
2073
|
+
<line x1="2" y1="5.5" x2="14" y2="5.5" />
|
|
2074
|
+
<line x1="8" y1="11.5" x2="8" y2="15" />
|
|
2075
|
+
<line x1="6.25" y1="13.25" x2="9.75" y2="13.25" />
|
|
2076
|
+
</svg>
|
|
2077
|
+
</button>
|
|
2078
|
+
<button
|
|
2079
|
+
className="squisq-toolbar-button"
|
|
2080
|
+
data-tooltip="Delete row"
|
|
2081
|
+
onClick={() => tiptapEditor!.chain().focus().deleteRow().run()}
|
|
2082
|
+
aria-label="Delete row"
|
|
2083
|
+
>
|
|
2084
|
+
<svg
|
|
2085
|
+
width="16"
|
|
2086
|
+
height="16"
|
|
2087
|
+
viewBox="0 0 16 16"
|
|
2088
|
+
fill="none"
|
|
2089
|
+
stroke="currentColor"
|
|
2090
|
+
strokeWidth="1.5"
|
|
2091
|
+
strokeLinecap="round"
|
|
1638
2092
|
>
|
|
1639
|
-
<
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
2093
|
+
<rect x="1" y="4" width="14" height="8" rx="1" />
|
|
2094
|
+
<line x1="5.5" y1="6" x2="10.5" y2="10" />
|
|
2095
|
+
<line x1="10.5" y1="6" x2="5.5" y2="10" />
|
|
2096
|
+
</svg>
|
|
2097
|
+
</button>
|
|
2098
|
+
<button
|
|
2099
|
+
className="squisq-toolbar-button squisq-toolbar-button--danger"
|
|
2100
|
+
data-tooltip="Delete table"
|
|
2101
|
+
onClick={() => tiptapEditor!.chain().focus().deleteTable().run()}
|
|
2102
|
+
aria-label="Delete table"
|
|
2103
|
+
>
|
|
2104
|
+
<svg
|
|
2105
|
+
width="16"
|
|
2106
|
+
height="16"
|
|
2107
|
+
viewBox="0 0 16 16"
|
|
2108
|
+
fill="none"
|
|
2109
|
+
stroke="currentColor"
|
|
2110
|
+
strokeWidth="1.5"
|
|
2111
|
+
strokeLinecap="round"
|
|
1658
2112
|
>
|
|
1659
|
-
<
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
>
|
|
1668
|
-
<rect x="1" y="1" width="14" height="14" rx="1" />
|
|
1669
|
-
<line x1="1" y1="5.5" x2="15" y2="5.5" />
|
|
1670
|
-
<line x1="5.5" y1="1" x2="5.5" y2="15" />
|
|
1671
|
-
<line x1="4.5" y1="4.5" x2="11.5" y2="11.5" strokeWidth="2" />
|
|
1672
|
-
<line x1="11.5" y1="4.5" x2="4.5" y2="11.5" strokeWidth="2" />
|
|
1673
|
-
</svg>
|
|
1674
|
-
</button>
|
|
1675
|
-
</div>
|
|
1676
|
-
</>
|
|
2113
|
+
<rect x="1" y="1" width="14" height="14" rx="1" />
|
|
2114
|
+
<line x1="1" y1="5.5" x2="15" y2="5.5" />
|
|
2115
|
+
<line x1="5.5" y1="1" x2="5.5" y2="15" />
|
|
2116
|
+
<line x1="4.5" y1="4.5" x2="11.5" y2="11.5" strokeWidth="2" />
|
|
2117
|
+
<line x1="11.5" y1="4.5" x2="4.5" y2="11.5" strokeWidth="2" />
|
|
2118
|
+
</svg>
|
|
2119
|
+
</button>
|
|
2120
|
+
</div>
|
|
1677
2121
|
)}
|
|
1678
2122
|
</div>
|
|
1679
2123
|
)}
|
|
1680
2124
|
|
|
1681
|
-
{/* Overflow menu — outside the overflow:hidden actions container
|
|
1682
|
-
|
|
2125
|
+
{/* Overflow menu — outside the overflow:hidden actions container.
|
|
2126
|
+
Also appears when a contextual group (template/transition picker,
|
|
2127
|
+
table controls) is clipped, even if every plain button fits. */}
|
|
2128
|
+
{!isPreview && !isCodeMode && (overflowIndex !== null || clippedContextual.size > 0) && (
|
|
1683
2129
|
<div className="squisq-toolbar-overflow" ref={overflowRef}>
|
|
1684
2130
|
<button
|
|
1685
2131
|
className={`squisq-toolbar-button squisq-toolbar-overflow-trigger${showOverflow ? ' squisq-toolbar-button--active' : ''}`}
|
|
@@ -1694,7 +2140,7 @@ export function Toolbar({
|
|
|
1694
2140
|
<div
|
|
1695
2141
|
className={`squisq-toolbar-overflow-menu squisq-toolbar-overflow-menu--${overflowPlacement}`}
|
|
1696
2142
|
>
|
|
1697
|
-
{BUTTONS.slice(overflowIndex)
|
|
2143
|
+
{BUTTONS.slice(overflowIndex ?? BUTTONS.length)
|
|
1698
2144
|
.filter((b) => isButtonVisible(b.id))
|
|
1699
2145
|
// Media buttons are represented by the synthetic Insert dropdown
|
|
1700
2146
|
// in both the visible toolbar and the overflow menu.
|
|
@@ -1748,23 +2194,53 @@ export function Toolbar({
|
|
|
1748
2194
|
</button>
|
|
1749
2195
|
)}
|
|
1750
2196
|
|
|
1751
|
-
{/*
|
|
1752
|
-
|
|
2197
|
+
{/* Per-block section — the template + transition pickers move
|
|
2198
|
+
here when clipped, under a divider and a "<name> Block:"
|
|
2199
|
+
heading that names the block they configure. */}
|
|
2200
|
+
{showBlockSectionInOverflow && (
|
|
2201
|
+
<>
|
|
2202
|
+
<div
|
|
2203
|
+
className="squisq-toolbar-separator"
|
|
2204
|
+
style={{ margin: '4px 0', width: '100%', height: 1 }}
|
|
2205
|
+
/>
|
|
2206
|
+
<div className="squisq-toolbar-overflow-heading">{overflowBlockLabel} Block:</div>
|
|
2207
|
+
</>
|
|
2208
|
+
)}
|
|
2209
|
+
|
|
2210
|
+
{/* Contextual: template picker in overflow — only when the
|
|
2211
|
+
inline one is clipped, so the control lives in exactly one
|
|
2212
|
+
visible place at a time. */}
|
|
2213
|
+
{showTemplateInOverflow && (
|
|
1753
2214
|
<div className="squisq-toolbar-overflow-item squisq-toolbar-overflow-template">
|
|
1754
|
-
<span>Template:</span>
|
|
1755
2215
|
<TemplatePicker
|
|
1756
|
-
value={currentTemplate}
|
|
2216
|
+
value={currentTemplate!}
|
|
1757
2217
|
onChange={(v) => {
|
|
1758
2218
|
handleTemplatePick(v);
|
|
1759
2219
|
setShowOverflow(false);
|
|
1760
2220
|
}}
|
|
2221
|
+
colorScheme={colorScheme}
|
|
1761
2222
|
recommended={recommendedTemplates}
|
|
2223
|
+
previewSource={templatePreviewSource}
|
|
2224
|
+
/>
|
|
2225
|
+
</div>
|
|
2226
|
+
)}
|
|
2227
|
+
|
|
2228
|
+
{/* Contextual: transition picker in overflow — mirrors the
|
|
2229
|
+
template picker above. The menu stays open on change since
|
|
2230
|
+
the flyout carries follow-up controls (direction/duration). */}
|
|
2231
|
+
{showTransitionInOverflow && (
|
|
2232
|
+
<div className="squisq-toolbar-overflow-item squisq-toolbar-overflow-template">
|
|
2233
|
+
<TransitionPicker
|
|
2234
|
+
value={currentTransition!}
|
|
2235
|
+
onChange={handleTransitionPick}
|
|
2236
|
+
colorScheme={colorScheme}
|
|
2237
|
+
accentColor={previewSettings?.activeTheme.colors.primary}
|
|
1762
2238
|
/>
|
|
1763
2239
|
</div>
|
|
1764
2240
|
)}
|
|
1765
2241
|
|
|
1766
2242
|
{/* Contextual: table controls in overflow */}
|
|
1767
|
-
{isInTable && (
|
|
2243
|
+
{isInTable && clippedContextual.has('table') && (
|
|
1768
2244
|
<>
|
|
1769
2245
|
<div
|
|
1770
2246
|
className="squisq-toolbar-separator"
|
|
@@ -1863,11 +2339,16 @@ export function Toolbar({
|
|
|
1863
2339
|
<button
|
|
1864
2340
|
className={`squisq-toolbar-button squisq-toolbar-files-toggle${showFiles ? ' squisq-toolbar-button--active' : ''}`}
|
|
1865
2341
|
onClick={onToggleFiles}
|
|
1866
|
-
data-tooltip={showFiles ? 'Hide
|
|
2342
|
+
data-tooltip={`${showFiles ? 'Hide' : 'Show'} Files panel${resolvedFileCount > 0 ? ` (${fileCountLabel(resolvedFileCount)})` : ''}`}
|
|
1867
2343
|
aria-pressed={showFiles}
|
|
1868
|
-
aria-label=
|
|
2344
|
+
aria-label={`Toggle Files panel${resolvedFileCount > 0 ? `, ${fileCountLabel(resolvedFileCount)}` : ''}`}
|
|
1869
2345
|
>
|
|
1870
2346
|
<Icon icon="fa-solid fa-paperclip" />
|
|
2347
|
+
{resolvedFileCount > 0 && (
|
|
2348
|
+
<span className="squisq-toolbar-files-badge" aria-hidden="true">
|
|
2349
|
+
{fileCountBadge(resolvedFileCount)}
|
|
2350
|
+
</span>
|
|
2351
|
+
)}
|
|
1871
2352
|
</button>
|
|
1872
2353
|
)}
|
|
1873
2354
|
{/* Right slot — rightmost end of toolbar */}
|