@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/PreviewPanel.tsx
CHANGED
|
@@ -8,8 +8,11 @@
|
|
|
8
8
|
* in sync.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { useState, useEffect } from 'react';
|
|
11
|
+
import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
|
12
|
+
import type { ReactNode } from 'react';
|
|
13
|
+
import { createPortal } from 'react-dom';
|
|
12
14
|
import { DocPlayer, LinearDocView, useMediaProvider } from '@bendyline/squisq-react';
|
|
15
|
+
import type { AudioController, PlaybackState } from '@bendyline/squisq-react';
|
|
13
16
|
import type { Doc } from '@bendyline/squisq/schemas';
|
|
14
17
|
import { applyTransform } from '@bendyline/squisq/transform';
|
|
15
18
|
import { resolveAudioMapping } from '@bendyline/squisq/doc';
|
|
@@ -17,7 +20,10 @@ import type { ContentContainer } from '@bendyline/squisq/storage';
|
|
|
17
20
|
import { useEditorContext } from './EditorContext';
|
|
18
21
|
import { usePreviewSettings } from './PreviewControls';
|
|
19
22
|
import { buildPreviewDoc } from './buildPreviewDoc';
|
|
23
|
+
import { buildDocumentPreviewMarkdown } from './buildDocumentPreviewMarkdown';
|
|
20
24
|
import { PlainHtmlPreview } from './PlainHtmlPreview';
|
|
25
|
+
import { TeleprompterView } from './teleprompter/TeleprompterView';
|
|
26
|
+
import { usePresentationModeOptional } from './presentation/PresentationMode';
|
|
21
27
|
|
|
22
28
|
export interface PreviewPanelProps {
|
|
23
29
|
/** Base path for resolving media URLs in DocPlayer */
|
|
@@ -40,8 +46,19 @@ export interface PreviewPanelProps {
|
|
|
40
46
|
* are rendered in the main toolbar via PreviewToolbarControls.
|
|
41
47
|
*/
|
|
42
48
|
export function PreviewPanel({ basePath = '/', className, workspaceContainer }: PreviewPanelProps) {
|
|
43
|
-
const {
|
|
49
|
+
const {
|
|
50
|
+
doc,
|
|
51
|
+
parseError,
|
|
52
|
+
isParsing,
|
|
53
|
+
markdownSource,
|
|
54
|
+
mediaRevision,
|
|
55
|
+
setMarkdownSource,
|
|
56
|
+
bumpMediaRevision,
|
|
57
|
+
allowRecording,
|
|
58
|
+
colorScheme,
|
|
59
|
+
} = useEditorContext();
|
|
44
60
|
const mediaProvider = useMediaProvider();
|
|
61
|
+
const presentation = usePresentationModeOptional();
|
|
45
62
|
const {
|
|
46
63
|
activeViewport,
|
|
47
64
|
activeDisplayMode,
|
|
@@ -49,7 +66,15 @@ export function PreviewPanel({ basePath = '/', className, workspaceContainer }:
|
|
|
49
66
|
activeTransformStyle,
|
|
50
67
|
activeCaptionStyle,
|
|
51
68
|
activeCaptionsEnabled,
|
|
69
|
+
activeCoverSlide,
|
|
52
70
|
} = usePreviewSettings();
|
|
71
|
+
const mainSurfaceRef = useRef<HTMLDivElement>(null);
|
|
72
|
+
const popupSurfaceRef = useRef<HTMLDivElement>(null);
|
|
73
|
+
const [playbackState, setPlaybackState] = useState<PlaybackState | null>(null);
|
|
74
|
+
|
|
75
|
+
const handlePlaybackStateChange = useCallback((next: PlaybackState) => {
|
|
76
|
+
setPlaybackState(next);
|
|
77
|
+
}, []);
|
|
53
78
|
|
|
54
79
|
// Build the player-ready Doc whenever the parsed doc changes.
|
|
55
80
|
// Transform runs on the ORIGINAL doc (which has block.contents with
|
|
@@ -59,40 +84,174 @@ export function PreviewPanel({ basePath = '/', className, workspaceContainer }:
|
|
|
59
84
|
// Audio mapping is async (reads container files), so we use a two-phase
|
|
60
85
|
// approach: first build the base doc synchronously, then resolve audio
|
|
61
86
|
// in an effect and update the state.
|
|
62
|
-
const [
|
|
87
|
+
const [previewProjection, setPreviewProjection] = useState<{
|
|
88
|
+
/** Transformed content model shared by Page and Document. */
|
|
89
|
+
contentDoc: Doc;
|
|
90
|
+
/** Flattened/timed player model shared by Slideshow and Video. */
|
|
91
|
+
playerDoc: Doc;
|
|
92
|
+
} | null>(null);
|
|
63
93
|
|
|
64
94
|
useEffect(() => {
|
|
65
95
|
if (!doc || !doc.blocks.length) {
|
|
66
|
-
|
|
96
|
+
setPreviewProjection(null);
|
|
67
97
|
return;
|
|
68
98
|
}
|
|
69
99
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
100
|
+
// Audio resolution runs BEFORE the transform: a document-anchored
|
|
101
|
+
// narration take re-times the SOURCE blocks, and the transform's
|
|
102
|
+
// provenance (`sourceStartTime` in seconds) is derived from those
|
|
103
|
+
// times — so summarized slides land where the words are spoken.
|
|
104
|
+
const build = (sourceDoc: Doc) => {
|
|
105
|
+
const contentDoc = activeTransformStyle
|
|
106
|
+
? applyTransform(sourceDoc, activeTransformStyle).doc
|
|
107
|
+
: sourceDoc;
|
|
108
|
+
return { contentDoc, playerDoc: buildPreviewDoc(contentDoc) };
|
|
109
|
+
};
|
|
75
110
|
|
|
76
|
-
// If we have a workspace container, try to resolve audio mapping before building preview
|
|
77
111
|
if (workspaceContainer) {
|
|
78
112
|
let cancelled = false;
|
|
79
|
-
resolveAudioMapping(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
113
|
+
resolveAudioMapping(doc, workspaceContainer).then(
|
|
114
|
+
(audioDoc) => {
|
|
115
|
+
if (!cancelled) setPreviewProjection(build(audioDoc));
|
|
116
|
+
},
|
|
117
|
+
() => {
|
|
118
|
+
// The synchronous preview below remains valid when optional audio
|
|
119
|
+
// discovery fails; consume the rejection rather than leaking it.
|
|
120
|
+
},
|
|
121
|
+
);
|
|
84
122
|
// Set an immediate preview without audio while mapping resolves
|
|
85
|
-
|
|
123
|
+
setPreviewProjection(build(doc));
|
|
86
124
|
return () => {
|
|
87
125
|
cancelled = true;
|
|
88
126
|
};
|
|
89
127
|
}
|
|
90
128
|
|
|
91
|
-
|
|
129
|
+
setPreviewProjection(build(doc));
|
|
92
130
|
}, [doc, activeTransformStyle, workspaceContainer]);
|
|
93
131
|
|
|
94
|
-
|
|
95
|
-
|
|
132
|
+
const previewDoc = previewProjection?.playerDoc ?? null;
|
|
133
|
+
const contentDoc = previewProjection?.contentDoc ?? null;
|
|
134
|
+
|
|
135
|
+
// The popup owns no clock, audio, or transport. It follows the primary
|
|
136
|
+
// player's published state while all actions remain inert, keeping the
|
|
137
|
+
// opener the single source of truth.
|
|
138
|
+
const followerAudioController = useMemo<AudioController | null>(() => {
|
|
139
|
+
if (!playbackState || !previewDoc) return null;
|
|
140
|
+
const noOp = () => Promise.resolve();
|
|
141
|
+
return {
|
|
142
|
+
currentTime: playbackState.currentTime,
|
|
143
|
+
isPlaying: playbackState.isPlaying,
|
|
144
|
+
currentSegment: playbackState.currentSegmentIndex,
|
|
145
|
+
totalDuration: playbackState.totalDuration,
|
|
146
|
+
isEnded:
|
|
147
|
+
playbackState.totalDuration > 0 && playbackState.currentTime >= playbackState.totalDuration,
|
|
148
|
+
isReady: true,
|
|
149
|
+
isAvailable: true,
|
|
150
|
+
play: noOp,
|
|
151
|
+
pause: noOp,
|
|
152
|
+
toggle: noOp,
|
|
153
|
+
seekTo: noOp,
|
|
154
|
+
skipToSegment: noOp,
|
|
155
|
+
restart: noOp,
|
|
156
|
+
};
|
|
157
|
+
}, [playbackState, previewDoc]);
|
|
158
|
+
|
|
159
|
+
const documentMarkdown = useMemo(
|
|
160
|
+
() =>
|
|
161
|
+
activeTransformStyle && contentDoc
|
|
162
|
+
? buildDocumentPreviewMarkdown(contentDoc)
|
|
163
|
+
: markdownSource,
|
|
164
|
+
[activeTransformStyle, contentDoc, markdownSource],
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
// The public DisplayMode values predate the current labels: raw `page`
|
|
168
|
+
// is the plain Document preview, while raw `linear` is the styled Page view.
|
|
169
|
+
const isDocumentMode = activeDisplayMode === 'page';
|
|
170
|
+
const isPageMode = activeDisplayMode === 'linear';
|
|
171
|
+
const isNarrateMode = activeDisplayMode === 'narrate';
|
|
172
|
+
|
|
173
|
+
useEffect(() => {
|
|
174
|
+
if (presentation?.activeTarget === 'window') return;
|
|
175
|
+
setPlaybackState(null);
|
|
176
|
+
}, [presentation?.activeTarget]);
|
|
177
|
+
|
|
178
|
+
// Page and Document have no playback clock, so mirror their normalized
|
|
179
|
+
// scroll position instead. The popup remains pointer-inert: the editor view
|
|
180
|
+
// is the controller and the audience window follows it.
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
if (presentation?.activeTarget !== 'window' || !presentation.popupRoot) return;
|
|
183
|
+
const mainRoot = mainSurfaceRef.current;
|
|
184
|
+
const followerRoot = popupSurfaceRef.current;
|
|
185
|
+
if (!mainRoot || !followerRoot) return;
|
|
186
|
+
|
|
187
|
+
if (isPageMode) {
|
|
188
|
+
const source = mainRoot.querySelector<HTMLElement>('.squisq-linear');
|
|
189
|
+
const follower = followerRoot.querySelector<HTMLElement>('.squisq-linear');
|
|
190
|
+
if (!source || !follower) return;
|
|
191
|
+
const sync = () => {
|
|
192
|
+
const sourceRange = Math.max(1, source.scrollHeight - source.clientHeight);
|
|
193
|
+
const followerRange = Math.max(0, follower.scrollHeight - follower.clientHeight);
|
|
194
|
+
follower.scrollTop = (source.scrollTop / sourceRange) * followerRange;
|
|
195
|
+
};
|
|
196
|
+
source.addEventListener('scroll', sync, { passive: true });
|
|
197
|
+
sync();
|
|
198
|
+
return () => source.removeEventListener('scroll', sync);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (isDocumentMode) {
|
|
202
|
+
const sourceFrame = mainRoot.querySelector<HTMLIFrameElement>('iframe');
|
|
203
|
+
const followerFrame = followerRoot.querySelector<HTMLIFrameElement>('iframe');
|
|
204
|
+
if (!sourceFrame || !followerFrame) return;
|
|
205
|
+
let detachScroll: (() => void) | null = null;
|
|
206
|
+
const attach = () => {
|
|
207
|
+
detachScroll?.();
|
|
208
|
+
try {
|
|
209
|
+
const sourceWindow = sourceFrame.contentWindow;
|
|
210
|
+
const followerWindow = followerFrame.contentWindow;
|
|
211
|
+
if (!sourceWindow || !followerWindow) return;
|
|
212
|
+
const sync = () => {
|
|
213
|
+
const sourceDoc = sourceWindow.document.documentElement;
|
|
214
|
+
const followerDoc = followerWindow.document.documentElement;
|
|
215
|
+
const sourceRange = Math.max(1, sourceDoc.scrollHeight - sourceWindow.innerHeight);
|
|
216
|
+
const followerRange = Math.max(
|
|
217
|
+
0,
|
|
218
|
+
followerDoc.scrollHeight - followerWindow.innerHeight,
|
|
219
|
+
);
|
|
220
|
+
followerWindow.scrollTo(0, (sourceWindow.scrollY / sourceRange) * followerRange);
|
|
221
|
+
};
|
|
222
|
+
sourceWindow.addEventListener('scroll', sync, { passive: true });
|
|
223
|
+
detachScroll = () => sourceWindow.removeEventListener('scroll', sync);
|
|
224
|
+
sync();
|
|
225
|
+
} catch {
|
|
226
|
+
// The srcDoc frames are same-origin, but a host may tighten their
|
|
227
|
+
// sandbox. In that case the duplicate content remains useful even
|
|
228
|
+
// though scroll mirroring is unavailable.
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
sourceFrame.addEventListener('load', attach);
|
|
232
|
+
followerFrame.addEventListener('load', attach);
|
|
233
|
+
attach();
|
|
234
|
+
return () => {
|
|
235
|
+
sourceFrame.removeEventListener('load', attach);
|
|
236
|
+
followerFrame.removeEventListener('load', attach);
|
|
237
|
+
detachScroll?.();
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
}, [
|
|
241
|
+
contentDoc,
|
|
242
|
+
documentMarkdown,
|
|
243
|
+
isDocumentMode,
|
|
244
|
+
isPageMode,
|
|
245
|
+
presentation?.activeTarget,
|
|
246
|
+
presentation?.popupRoot,
|
|
247
|
+
]);
|
|
248
|
+
|
|
249
|
+
// Status overlays for non-ready states. Narrate is exempt: the
|
|
250
|
+
// teleprompter holds live state (mic capture, an in-flight recording,
|
|
251
|
+
// pacing position) that an unmount would destroy — e.g. saving a take
|
|
252
|
+
// rewrites the markdown, which triggers exactly this reparse. It keeps
|
|
253
|
+
// rendering the last-good doc until the new parse lands.
|
|
254
|
+
if (isParsing && !isNarrateMode) {
|
|
96
255
|
return (
|
|
97
256
|
<div className={`squisq-preview-status ${className || ''}`} data-testid="preview-panel">
|
|
98
257
|
<p>Parsing…</p>
|
|
@@ -100,7 +259,7 @@ export function PreviewPanel({ basePath = '/', className, workspaceContainer }:
|
|
|
100
259
|
);
|
|
101
260
|
}
|
|
102
261
|
|
|
103
|
-
if (parseError) {
|
|
262
|
+
if (parseError && !isNarrateMode) {
|
|
104
263
|
return (
|
|
105
264
|
<div className={`squisq-preview-status ${className || ''}`} data-testid="preview-panel">
|
|
106
265
|
<h3>Parse Error</h3>
|
|
@@ -109,15 +268,11 @@ export function PreviewPanel({ basePath = '/', className, workspaceContainer }:
|
|
|
109
268
|
);
|
|
110
269
|
}
|
|
111
270
|
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
// Document mode renders directly from markdown — it doesn't depend on the
|
|
118
|
-
// parsed Doc tree or the player preview build, so let it fall through even
|
|
119
|
-
// when those aren't ready yet.
|
|
120
|
-
if (!previewDoc && !isDocumentMode) {
|
|
271
|
+
// Document mode renders a text-first markdown projection, and Narrate
|
|
272
|
+
// builds its script from the parsed source doc (showing its own empty
|
|
273
|
+
// state). Neither depends on the player projection, so let them fall
|
|
274
|
+
// through even when that isn't ready yet.
|
|
275
|
+
if (!previewDoc && !isDocumentMode && !isNarrateMode) {
|
|
121
276
|
return (
|
|
122
277
|
<div className={`squisq-preview-status ${className || ''}`} data-testid="preview-panel">
|
|
123
278
|
<p>No content to preview. Start typing in the editor.</p>
|
|
@@ -125,62 +280,143 @@ export function PreviewPanel({ basePath = '/', className, workspaceContainer }:
|
|
|
125
280
|
);
|
|
126
281
|
}
|
|
127
282
|
|
|
128
|
-
const fillsContainer = isDocumentMode || isPageMode ? 'stretch' : 'center';
|
|
283
|
+
const fillsContainer = isDocumentMode || isPageMode || isNarrateMode ? 'stretch' : 'center';
|
|
284
|
+
const audienceWindowOpen = presentation?.activeTarget === 'window';
|
|
285
|
+
|
|
286
|
+
const renderSurface = (audience: boolean): ReactNode => {
|
|
287
|
+
if (isDocumentMode) {
|
|
288
|
+
return (
|
|
289
|
+
<PlainHtmlPreview
|
|
290
|
+
markdown={documentMarkdown}
|
|
291
|
+
title={(contentDoc?.frontmatter?.title as string | undefined) ?? undefined}
|
|
292
|
+
mediaProvider={mediaProvider}
|
|
293
|
+
mediaRevision={mediaRevision}
|
|
294
|
+
theme={activeTheme}
|
|
295
|
+
globalKeyboardShortcuts={!audience}
|
|
296
|
+
/>
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (isNarrateMode) {
|
|
301
|
+
if (audience) return null;
|
|
302
|
+
// Narrate keeps one controller/microphone session in the main view and
|
|
303
|
+
// portals only its display surface to the audience window.
|
|
304
|
+
return (
|
|
305
|
+
<TeleprompterView
|
|
306
|
+
doc={doc}
|
|
307
|
+
theme={activeTheme}
|
|
308
|
+
workspaceContainer={workspaceContainer ?? null}
|
|
309
|
+
basePath={basePath}
|
|
310
|
+
presentationTarget={
|
|
311
|
+
presentation?.activeTarget === 'window' ? presentation.popupRoot : null
|
|
312
|
+
}
|
|
313
|
+
recording={
|
|
314
|
+
allowRecording && mediaProvider
|
|
315
|
+
? {
|
|
316
|
+
mediaProvider,
|
|
317
|
+
container: workspaceContainer ?? null,
|
|
318
|
+
markdownSource,
|
|
319
|
+
setMarkdownSource,
|
|
320
|
+
bumpMediaRevision,
|
|
321
|
+
}
|
|
322
|
+
: null
|
|
323
|
+
}
|
|
324
|
+
/>
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (isPageMode) {
|
|
329
|
+
return (
|
|
330
|
+
<LinearDocView
|
|
331
|
+
doc={contentDoc ?? doc!}
|
|
332
|
+
basePath={basePath}
|
|
333
|
+
viewport={activeViewport}
|
|
334
|
+
theme={activeTheme}
|
|
335
|
+
globalKeyboardShortcuts={!audience}
|
|
336
|
+
/>
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (audience && !followerAudioController) {
|
|
341
|
+
return <div className="squisq-presentation-connecting">Connecting presentation...</div>;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const audienceCaptionMode = playbackState?.captionMode;
|
|
345
|
+
return (
|
|
346
|
+
<DocPlayer
|
|
347
|
+
// A style change replaces the slide sequence. Remounting clears state
|
|
348
|
+
// belonging to the previous transformed deck in both render targets.
|
|
349
|
+
key={`${audience ? 'audience' : 'primary'}-${activeTransformStyle || 'none'}`}
|
|
350
|
+
doc={previewDoc!}
|
|
351
|
+
basePath={basePath}
|
|
352
|
+
showControls={!audience}
|
|
353
|
+
muted
|
|
354
|
+
forceViewport={activeViewport}
|
|
355
|
+
displayMode={activeDisplayMode}
|
|
356
|
+
theme={activeTheme}
|
|
357
|
+
captionStyle={audienceCaptionMode === 'social' ? 'social' : activeCaptionStyle}
|
|
358
|
+
captionsEnabled={
|
|
359
|
+
audience && audienceCaptionMode ? audienceCaptionMode !== 'off' : activeCaptionsEnabled
|
|
360
|
+
}
|
|
361
|
+
showCoverSlide={activeCoverSlide}
|
|
362
|
+
coverVisible={audience ? playbackState?.isCoverVisible : undefined}
|
|
363
|
+
audioController={audience ? followerAudioController! : undefined}
|
|
364
|
+
enableSwipe={!audience}
|
|
365
|
+
globalKeyboardShortcuts={!audience}
|
|
366
|
+
onPlaybackStateChange={
|
|
367
|
+
!audience && audienceWindowOpen ? handlePlaybackStateChange : undefined
|
|
368
|
+
}
|
|
369
|
+
/>
|
|
370
|
+
);
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const surfaceStyle = {
|
|
374
|
+
flex: 1,
|
|
375
|
+
display: 'flex',
|
|
376
|
+
alignItems: fillsContainer,
|
|
377
|
+
justifyContent: 'center',
|
|
378
|
+
overflow: 'hidden',
|
|
379
|
+
minHeight: 0,
|
|
380
|
+
} as const;
|
|
129
381
|
|
|
130
382
|
return (
|
|
131
|
-
|
|
132
|
-
className={`squisq-preview-container ${className || ''}`}
|
|
133
|
-
data-testid="preview-panel"
|
|
134
|
-
style={{
|
|
135
|
-
width: '100%',
|
|
136
|
-
height: '100%',
|
|
137
|
-
display: 'flex',
|
|
138
|
-
flexDirection: 'column',
|
|
139
|
-
overflow: 'hidden',
|
|
140
|
-
background: 'var(--squisq-bg, #f5f5f5)',
|
|
141
|
-
}}
|
|
142
|
-
>
|
|
143
|
-
{/* Player / Document / Page view */}
|
|
383
|
+
<>
|
|
144
384
|
<div
|
|
145
|
-
className=
|
|
385
|
+
className={`squisq-preview-container ${className || ''}`}
|
|
386
|
+
data-testid="preview-panel"
|
|
146
387
|
style={{
|
|
147
|
-
|
|
388
|
+
width: '100%',
|
|
389
|
+
height: '100%',
|
|
148
390
|
display: 'flex',
|
|
149
|
-
|
|
150
|
-
justifyContent: 'center',
|
|
391
|
+
flexDirection: 'column',
|
|
151
392
|
overflow: 'hidden',
|
|
152
|
-
|
|
393
|
+
background: 'var(--squisq-bg, #f5f5f5)',
|
|
153
394
|
}}
|
|
154
395
|
>
|
|
155
|
-
{
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
title={(doc?.frontmatter?.title as string | undefined) ?? undefined}
|
|
159
|
-
mediaProvider={mediaProvider}
|
|
160
|
-
mediaRevision={mediaRevision}
|
|
161
|
-
theme={activeTheme}
|
|
162
|
-
/>
|
|
163
|
-
) : isPageMode ? (
|
|
164
|
-
<LinearDocView
|
|
165
|
-
doc={doc!}
|
|
166
|
-
basePath={basePath}
|
|
167
|
-
viewport={activeViewport}
|
|
168
|
-
theme={activeTheme}
|
|
169
|
-
/>
|
|
170
|
-
) : (
|
|
171
|
-
<DocPlayer
|
|
172
|
-
doc={previewDoc!}
|
|
173
|
-
basePath={basePath}
|
|
174
|
-
showControls
|
|
175
|
-
muted
|
|
176
|
-
forceViewport={activeViewport}
|
|
177
|
-
displayMode={activeDisplayMode}
|
|
178
|
-
theme={activeTheme}
|
|
179
|
-
captionStyle={activeCaptionStyle}
|
|
180
|
-
captionsEnabled={activeCaptionsEnabled}
|
|
181
|
-
/>
|
|
182
|
-
)}
|
|
396
|
+
<div ref={mainSurfaceRef} className="squisq-preview-player" style={surfaceStyle}>
|
|
397
|
+
{renderSurface(false)}
|
|
398
|
+
</div>
|
|
183
399
|
</div>
|
|
184
|
-
|
|
400
|
+
{audienceWindowOpen && presentation?.popupRoot && !isNarrateMode
|
|
401
|
+
? createPortal(
|
|
402
|
+
<div
|
|
403
|
+
className="squisq-editor-shell squisq-presentation-window"
|
|
404
|
+
data-theme={colorScheme}
|
|
405
|
+
>
|
|
406
|
+
<div className="squisq-preview-container squisq-presentation-window-preview">
|
|
407
|
+
<div
|
|
408
|
+
ref={popupSurfaceRef}
|
|
409
|
+
className="squisq-preview-player squisq-presentation-window-surface"
|
|
410
|
+
style={surfaceStyle}
|
|
411
|
+
aria-label="Audience presentation"
|
|
412
|
+
>
|
|
413
|
+
{renderSurface(true)}
|
|
414
|
+
</div>
|
|
415
|
+
</div>
|
|
416
|
+
</div>,
|
|
417
|
+
presentation.popupRoot,
|
|
418
|
+
)
|
|
419
|
+
: null}
|
|
420
|
+
</>
|
|
185
421
|
);
|
|
186
422
|
}
|
package/src/RawEditor.tsx
CHANGED
|
@@ -14,6 +14,7 @@ import { getAvailableTemplates } from '@bendyline/squisq/doc';
|
|
|
14
14
|
import { suggestIcons, resolveIcon, iconGlyph } from '@bendyline/squisq/icons';
|
|
15
15
|
import { BLOCK_META_KEY_DESCRIPTORS, tokenizeAttrTokens } from '@bendyline/squisq/markdown';
|
|
16
16
|
import { SQUISQ_MEDIA_MIME, parseSquisqMediaPayload } from './mediaDragMime';
|
|
17
|
+
import { canHandleSquisqMediaDrop, ownsMonacoModel } from './rawEditorIsolation';
|
|
17
18
|
import { useMonacoLoader } from './useMonacoLoader';
|
|
18
19
|
|
|
19
20
|
// Monaco is loaded lazily through `useMonacoLoader` (see the hook for the
|
|
@@ -108,9 +109,13 @@ export function RawEditor({
|
|
|
108
109
|
const keyDisposable = useRef<monaco.IDisposable | null>(null);
|
|
109
110
|
// Ref so the keydown handler always sees the latest callback.
|
|
110
111
|
const submitOnEnterRef = useRef(submitOnEnter);
|
|
112
|
+
const readOnlyRef = useRef(readOnly);
|
|
111
113
|
useEffect(() => {
|
|
112
114
|
submitOnEnterRef.current = submitOnEnter;
|
|
113
115
|
}, [submitOnEnter]);
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
readOnlyRef.current = readOnly;
|
|
118
|
+
}, [readOnly]);
|
|
114
119
|
// Ref so the completion provider — registered once at mount — always
|
|
115
120
|
// sees the latest mentionProvider without needing to unregister.
|
|
116
121
|
const mentionProviderRef = useRef(mentionProvider);
|
|
@@ -194,6 +199,7 @@ export function RawEditor({
|
|
|
194
199
|
completionDisposable.current = monaco.languages.registerCompletionItemProvider('markdown', {
|
|
195
200
|
triggerCharacters: ['['],
|
|
196
201
|
provideCompletionItems(model: monaco.editor.ITextModel, position: monaco.Position) {
|
|
202
|
+
if (!ownsMonacoModel(editor, model)) return { suggestions: [] };
|
|
197
203
|
const lineContent = model.getLineContent(position.lineNumber);
|
|
198
204
|
|
|
199
205
|
// Only trigger inside a heading line that has {[ before the cursor
|
|
@@ -248,6 +254,7 @@ export function RawEditor({
|
|
|
248
254
|
{
|
|
249
255
|
triggerCharacters: ['@'],
|
|
250
256
|
async provideCompletionItems(model, position) {
|
|
257
|
+
if (!ownsMonacoModel(editor, model)) return { suggestions: [] };
|
|
251
258
|
const provider = mentionProviderRef.current;
|
|
252
259
|
if (!provider) return { suggestions: [] };
|
|
253
260
|
const lineContent = model.getLineContent(position.lineNumber);
|
|
@@ -306,6 +313,7 @@ export function RawEditor({
|
|
|
306
313
|
{
|
|
307
314
|
triggerCharacters: ['['],
|
|
308
315
|
provideCompletionItems(model, position) {
|
|
316
|
+
if (!ownsMonacoModel(editor, model)) return { suggestions: [] };
|
|
309
317
|
const lineContent = model.getLineContent(position.lineNumber);
|
|
310
318
|
const textBeforeCursor = lineContent.substring(0, position.column - 1);
|
|
311
319
|
const textAfterCursor = lineContent.substring(position.column - 1);
|
|
@@ -388,6 +396,7 @@ export function RawEditor({
|
|
|
388
396
|
{
|
|
389
397
|
triggerCharacters: ['=', ' '],
|
|
390
398
|
provideCompletionItems(model, position) {
|
|
399
|
+
if (!ownsMonacoModel(editor, model)) return { suggestions: [] };
|
|
391
400
|
const lineContent = model.getLineContent(position.lineNumber);
|
|
392
401
|
// Block-meta attributes only mean something on a heading's
|
|
393
402
|
// template annotation — same gate as the template provider.
|
|
@@ -498,12 +507,14 @@ export function RawEditor({
|
|
|
498
507
|
const domNode = editor.getDomNode();
|
|
499
508
|
if (domNode) {
|
|
500
509
|
const onDragOver = (e: DragEvent) => {
|
|
501
|
-
|
|
510
|
+
const dt = e.dataTransfer;
|
|
511
|
+
if (canHandleSquisqMediaDrop(readOnlyRef.current, dt)) {
|
|
502
512
|
e.preventDefault();
|
|
503
|
-
|
|
513
|
+
dt!.dropEffect = 'copy';
|
|
504
514
|
}
|
|
505
515
|
};
|
|
506
516
|
const onDrop = (e: DragEvent) => {
|
|
517
|
+
if (!canHandleSquisqMediaDrop(readOnlyRef.current, e.dataTransfer)) return;
|
|
507
518
|
const dt = e.dataTransfer;
|
|
508
519
|
if (!dt) return;
|
|
509
520
|
const raw = dt.getData(SQUISQ_MEDIA_MIME);
|
package/src/StatusBar.tsx
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { useMemo } from 'react';
|
|
8
|
+
import { countBlocks } from '@bendyline/squisq/doc';
|
|
8
9
|
import { useEditorContext } from './EditorContext';
|
|
9
10
|
|
|
10
11
|
export interface StatusBarProps {
|
|
@@ -23,7 +24,7 @@ export function StatusBar({ className }: StatusBarProps) {
|
|
|
23
24
|
const chars = markdownSource.length;
|
|
24
25
|
const words = markdownSource.trim() ? markdownSource.trim().split(/\s+/).length : 0;
|
|
25
26
|
const lines = markdownSource.split('\n').length;
|
|
26
|
-
const blocks = doc
|
|
27
|
+
const blocks = doc ? countBlocks(doc.blocks) : 0;
|
|
27
28
|
return { chars, words, lines, blocks };
|
|
28
29
|
}, [markdownSource, doc]);
|
|
29
30
|
|
|
@@ -32,7 +33,9 @@ export function StatusBar({ className }: StatusBarProps) {
|
|
|
32
33
|
<span className="squisq-status-item">{stats.words} words</span>
|
|
33
34
|
<span className="squisq-status-item">{stats.chars} chars</span>
|
|
34
35
|
<span className="squisq-status-item">{stats.lines} lines</span>
|
|
35
|
-
<span className="squisq-status-item">
|
|
36
|
+
<span className="squisq-status-item">
|
|
37
|
+
{stats.blocks} {stats.blocks === 1 ? 'block' : 'blocks'}
|
|
38
|
+
</span>
|
|
36
39
|
<span className="squisq-status-spacer" />
|
|
37
40
|
{isParsing && <span className="squisq-status-item squisq-status-parsing">Parsing…</span>}
|
|
38
41
|
{parseError && (
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TemplateAnnotation — Tiptap Heading Extension
|
|
3
3
|
*
|
|
4
|
-
* Extends Tiptap's built-in Heading node to support `data-template
|
|
5
|
-
* `data-template-params` HTML attributes. These
|
|
6
|
-
*
|
|
4
|
+
* Extends Tiptap's built-in Heading node to support `data-template`,
|
|
5
|
+
* `data-template-params`, and `data-template-empty` HTML attributes. These
|
|
6
|
+
* attributes store the authored template annotation without exposing its raw
|
|
7
|
+
* `{[…]}` syntax in the Write view.
|
|
7
8
|
*
|
|
8
9
|
* When present, the heading renders a visible badge (styled CSS chip)
|
|
9
10
|
* showing the template name, e.g. `[chart]`.
|
|
@@ -41,6 +42,14 @@ export const HeadingWithTemplate = Heading.extend({
|
|
|
41
42
|
return { 'data-template-params': attributes.dataTemplateParams };
|
|
42
43
|
},
|
|
43
44
|
},
|
|
45
|
+
dataTemplateEmpty: {
|
|
46
|
+
default: false,
|
|
47
|
+
parseHTML: (element: HTMLElement) => element.hasAttribute('data-template-empty'),
|
|
48
|
+
renderHTML: (attributes: Record<string, unknown>) => {
|
|
49
|
+
if (!attributes.dataTemplateEmpty) return {};
|
|
50
|
+
return { 'data-template-empty': 'true' };
|
|
51
|
+
},
|
|
52
|
+
},
|
|
44
53
|
dataBlockAttrs: {
|
|
45
54
|
default: null,
|
|
46
55
|
parseHTML: (element: HTMLElement) => element.getAttribute('data-block-attrs') || null,
|
|
@@ -63,10 +72,10 @@ export const HeadingWithTemplate = Heading.extend({
|
|
|
63
72
|
(HTMLAttributes['data-template-params'] as string | undefined) ?? null,
|
|
64
73
|
);
|
|
65
74
|
|
|
66
|
-
// Render heading with a trailing badge span. The badge
|
|
67
|
-
// content —
|
|
68
|
-
//
|
|
69
|
-
//
|
|
75
|
+
// Render heading with a trailing badge span. The badge and its inner core
|
|
76
|
+
// have no real text content — labels/braces are painted via CSS so the
|
|
77
|
+
// template name never becomes part of serialized heading text (which
|
|
78
|
+
// would leak into markdown on round-trip).
|
|
70
79
|
//
|
|
71
80
|
// When no template is set we still render a subtle "empty" badge so
|
|
72
81
|
// authors have a visible affordance for opening the template picker
|
|
@@ -86,10 +95,17 @@ export const HeadingWithTemplate = Heading.extend({
|
|
|
86
95
|
role: 'button',
|
|
87
96
|
tabindex: '0',
|
|
88
97
|
'aria-haspopup': 'listbox',
|
|
89
|
-
title: 'Change block
|
|
98
|
+
title: 'Change block type',
|
|
90
99
|
'data-template': templateName,
|
|
91
|
-
'data-template-label': templateLabel(templateName),
|
|
92
100
|
},
|
|
101
|
+
[
|
|
102
|
+
'span',
|
|
103
|
+
{
|
|
104
|
+
class: 'squisq-template-badge-core',
|
|
105
|
+
'data-template-label': templateLabel(templateName),
|
|
106
|
+
'aria-hidden': 'true',
|
|
107
|
+
},
|
|
108
|
+
],
|
|
93
109
|
],
|
|
94
110
|
propsBadgeSpec(propsSummary),
|
|
95
111
|
];
|
|
@@ -107,8 +123,16 @@ export const HeadingWithTemplate = Heading.extend({
|
|
|
107
123
|
role: 'button',
|
|
108
124
|
tabindex: '0',
|
|
109
125
|
'aria-haspopup': 'listbox',
|
|
110
|
-
title: 'Choose block
|
|
126
|
+
title: 'Choose block type',
|
|
111
127
|
},
|
|
128
|
+
[
|
|
129
|
+
'span',
|
|
130
|
+
{
|
|
131
|
+
class: 'squisq-template-badge-core',
|
|
132
|
+
'data-template-label': 'Block',
|
|
133
|
+
'aria-hidden': 'true',
|
|
134
|
+
},
|
|
135
|
+
],
|
|
112
136
|
],
|
|
113
137
|
propsBadgeSpec(propsSummary),
|
|
114
138
|
];
|