@bendyline/squisq-editor-react 1.5.3 → 1.6.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/dist/index.d.ts +757 -20
- package/dist/index.js +16875 -6883
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/BlockCardView.tsx +121 -0
- package/src/BlockPreviewPanel.tsx +69 -0
- package/src/BlockPropertiesPopover.tsx +191 -0
- package/src/EditorContext.tsx +143 -0
- package/src/EditorShell.tsx +200 -120
- package/src/FolderView.tsx +131 -0
- package/src/Icon.tsx +26 -0
- package/src/ImageEditor.tsx +69 -22
- package/src/OutlinePanel.tsx +38 -3
- package/src/PlainHtmlPreview.tsx +30 -3
- package/src/PreviewControls.tsx +180 -8
- package/src/RawEditor.tsx +169 -13
- package/src/RecorderEntry.tsx +9 -16
- package/src/TemplateAnnotation.ts +44 -0
- package/src/TemplatePicker.tsx +329 -54
- package/src/ThemeCustomizerPanel.tsx +30 -336
- package/src/ThemePicker.tsx +112 -3
- package/src/TimelineBlockPreview.tsx +37 -0
- package/src/TimelineTrack.tsx +671 -0
- package/src/Toolbar.tsx +528 -174
- package/src/Tooltip.tsx +22 -4
- package/src/TransitionPicker.tsx +351 -0
- package/src/VersionHistoryPanel.tsx +2 -14
- package/src/ViewMenuPanel.tsx +17 -14
- package/src/WysiwygEditor.tsx +161 -65
- package/src/__tests__/blockProperties.test.ts +92 -0
- package/src/__tests__/blockRange.test.ts +105 -0
- package/src/__tests__/buildPreviewDocTransition.test.ts +73 -0
- package/src/__tests__/createShapeLayer.test.ts +46 -0
- package/src/__tests__/drawingShapeRoundTrip.test.ts +49 -0
- package/src/__tests__/embeddedMedia.test.ts +48 -0
- package/src/__tests__/headingTransition.test.ts +138 -0
- package/src/__tests__/layoutChildRoundTrip.test.ts +71 -0
- package/src/__tests__/plainHtmlPreview.test.tsx +10 -8
- package/src/__tests__/recorderMediaInsert.test.ts +86 -0
- package/src/__tests__/templateAnnotationRoundTrip.test.ts +18 -0
- package/src/__tests__/templatePickerMetadata.test.ts +32 -0
- package/src/__tests__/timelineSource.test.ts +134 -0
- package/src/__tests__/tiptapBridge.test.ts +92 -0
- package/src/__tests__/tiptapBridgeConformance.test.ts +47 -0
- package/src/__tests__/tooltip.test.tsx +72 -0
- package/src/__tests__/transitionCatalog.test.ts +64 -0
- package/src/__tests__/useBlockNavigator.test.tsx +67 -0
- package/src/__tests__/useMediaRecorder.test.ts +24 -0
- package/src/__tests__/useTimelineClock.test.ts +21 -0
- package/src/blockProperties.ts +88 -0
- package/src/blockRange.ts +132 -0
- package/src/buildPreviewDoc.ts +98 -9
- package/src/customTemplates/AddBin.tsx +126 -0
- package/src/customTemplates/CustomLayoutManager.tsx +233 -0
- package/src/customTemplates/CustomTemplateContext.tsx +182 -0
- package/src/customTemplates/LayerToolbar.tsx +580 -0
- package/src/customTemplates/ShapeGlyph.tsx +47 -0
- package/src/customTemplates/TemplateDesigner.tsx +430 -0
- package/src/customTemplates/__tests__/library.test.ts +88 -0
- package/src/customTemplates/__tests__/normalizePositions.test.ts +109 -0
- package/src/customTemplates/__tests__/shapeDefs.test.ts +49 -0
- package/src/customTemplates/__tests__/useMemoryLayerAdapter.test.ts +95 -0
- package/src/customTemplates/designer.css +673 -0
- package/src/customTemplates/index.ts +31 -0
- package/src/customTemplates/library.ts +97 -0
- package/src/customTemplates/normalizePositions.ts +75 -0
- package/src/customTemplates/shapeDefs.ts +131 -0
- package/src/customTemplates/thumbnail.tsx +63 -0
- package/src/customTemplates/tokenDefs.ts +60 -0
- package/src/customTemplates/useDocCustomTemplates.ts +52 -0
- package/src/customTemplates/useMemoryLayerAdapter.ts +123 -0
- package/src/customThemes/CustomThemeContext.tsx +179 -0
- package/src/customThemes/CustomThemeDialog.tsx +286 -0
- package/src/customThemes/__tests__/CustomThemeContext.test.tsx +64 -0
- package/src/customThemes/__tests__/CustomThemeDialog.test.tsx +47 -0
- package/src/customThemes/__tests__/customThemeLibrary.test.ts +51 -0
- package/src/customThemes/customThemeLibrary.ts +97 -0
- package/src/customThemes/index.ts +31 -0
- package/src/customThemes/themeControls.tsx +229 -0
- package/src/customThemes/themeDraft.ts +272 -0
- package/src/customThemes/useDocCustomThemes.ts +49 -0
- package/src/diagram/DiagramCanvas.tsx +240 -0
- package/src/diagram/DiagramExtension.ts +209 -0
- package/src/diagram/DiagramMaximizedOverlay.tsx +46 -0
- package/src/diagram/DiagramWidget.tsx +270 -0
- package/src/diagram/diagramCommands.ts +604 -0
- package/src/diagram/diagramConstants.ts +17 -0
- package/src/diagram/useDiagramData.ts +126 -0
- package/src/embeddedMedia.ts +78 -0
- package/src/frontmatter.ts +29 -0
- package/src/headingTransition.ts +231 -0
- package/src/imageEditor/CanvasSurface.tsx +383 -88
- package/src/imageEditor/PropertiesPanel.tsx +47 -1
- package/src/imageEditor/Toolbar.tsx +229 -16
- package/src/imageEditor/createShapeLayer.ts +280 -0
- package/src/imageEditor/icons.tsx +34 -114
- package/src/imageEditor/image-editor.css +54 -5
- package/src/imageEditor/state.ts +23 -3
- package/src/index.ts +77 -0
- package/src/recorder/RecorderModal.tsx +120 -53
- package/src/recorder/RecorderPanel.tsx +2 -26
- package/src/recorder/hooks/useMediaRecorder.ts +8 -1
- package/src/recorder/insertMediaBlock.ts +30 -0
- package/src/resolveBlockVisual.ts +33 -0
- package/src/scene/Scene.tsx +540 -0
- package/src/scene/SceneBlockExtension.ts +198 -0
- package/src/scene/SceneBlockToolbar.tsx +201 -0
- package/src/scene/SceneBlockWidget.tsx +434 -0
- package/src/scene/ScenePropsBar.tsx +85 -0
- package/src/scene/SceneSelection.tsx +107 -0
- package/src/scene/SceneViewport.tsx +102 -0
- package/src/scene/ShapePalette.tsx +181 -0
- package/src/scene/__tests__/DiagramAdapter.test.ts +56 -0
- package/src/scene/__tests__/bezierEdit.test.ts +85 -0
- package/src/scene/__tests__/blockLayers.test.ts +57 -0
- package/src/scene/__tests__/shapeLayers.test.ts +106 -0
- package/src/scene/__tests__/useSceneHitTest.test.ts +90 -0
- package/src/scene/__tests__/useScenePanZoom.test.ts +103 -0
- package/src/scene/adapters/DiagramAdapter.ts +168 -0
- package/src/scene/adapters/DrawingAdapter.ts +415 -0
- package/src/scene/adapters/LayoutAdapter.ts +310 -0
- package/src/scene/adapters/blockLayers.ts +159 -0
- package/src/scene/commands/SceneCommand.ts +70 -0
- package/src/scene/commands/drawingCommands.ts +318 -0
- package/src/scene/commands/layoutCommands.ts +301 -0
- package/src/scene/hooks/useSceneHitTest.ts +105 -0
- package/src/scene/hooks/useScenePanZoom.ts +147 -0
- package/src/scene/hooks/useSceneSelection.ts +62 -0
- package/src/scene/index.ts +95 -0
- package/src/scene/layers/DiagramEdges.tsx +127 -0
- package/src/scene/layers/edgeGeometry.ts +77 -0
- package/src/scene/layers/nodeCard.tsx +145 -0
- package/src/scene/layers/renderLayer.tsx +70 -0
- package/src/scene/layers/shapeLayers.ts +201 -0
- package/src/scene/paths/bezierEdit.ts +208 -0
- package/src/scene/scene.css +649 -0
- package/src/scene/text/SceneTextOverlay.tsx +161 -0
- package/src/scene/text/sceneTextChannel.ts +40 -0
- package/src/scene/text/sceneTextConfig.ts +27 -0
- package/src/scene/text/sceneTiptap.ts +36 -0
- package/src/scene/text/useSceneTextEditing.ts +39 -0
- package/src/scene/tools/ConnectTool.ts +111 -0
- package/src/scene/tools/DrawingConnectTool.ts +161 -0
- package/src/scene/tools/PathTool.ts +158 -0
- package/src/scene/tools/PlaceTool.ts +47 -0
- package/src/scene/tools/SceneTool.ts +75 -0
- package/src/scene/tools/SelectTool.ts +284 -0
- package/src/scene/tools/ShapeTool.ts +144 -0
- package/src/scene/tools/TextTool.ts +72 -0
- package/src/scene/tools/TokenTool.ts +95 -0
- package/src/scene/tools/createDrawShapeTool.ts +82 -0
- package/src/styles/diagram.css +183 -0
- package/src/styles/editor.css +1615 -203
- package/src/styles/folder-view.css +210 -0
- package/src/styles/image-edit-affordance.css +2 -2
- package/src/styles/index.css +4 -0
- package/src/timelineSource.ts +244 -0
- package/src/tiptapBridge.ts +115 -34
- package/src/tooltipPlacement.ts +13 -0
- package/src/transitionCatalog.ts +159 -0
- package/src/types/monaco-shims.d.ts +10 -0
- package/src/useBlockNavigator.ts +153 -0
- package/src/useMonacoLoader.ts +23 -1
- package/src/useTimelineClock.ts +76 -0
- package/src/utils/dropUtils.ts +1 -1
package/src/RawEditor.tsx
CHANGED
|
@@ -12,6 +12,7 @@ import type * as monaco from 'monaco-editor';
|
|
|
12
12
|
import { useEditorContext } from './EditorContext';
|
|
13
13
|
import { getAvailableTemplates } from '@bendyline/squisq/doc';
|
|
14
14
|
import { suggestIcons, resolveIcon, iconGlyph } from '@bendyline/squisq/icons';
|
|
15
|
+
import { BLOCK_META_KEY_DESCRIPTORS, tokenizeAttrTokens } from '@bendyline/squisq/markdown';
|
|
15
16
|
import { SQUISQ_MEDIA_MIME, parseSquisqMediaPayload } from './mediaDragMime';
|
|
16
17
|
import { useMonacoLoader } from './useMonacoLoader';
|
|
17
18
|
|
|
@@ -30,7 +31,12 @@ import { useMonacoLoader } from './useMonacoLoader';
|
|
|
30
31
|
// resolve: { alias: [{ find: /^monaco-editor$/, replacement: './monaco-slim.ts' }] }
|
|
31
32
|
//
|
|
32
33
|
// Where monaco-slim.ts re-exports 'monaco-editor/esm/vs/editor/editor.api'
|
|
33
|
-
// plus only the language contributions needed
|
|
34
|
+
// plus only the language contributions actually needed
|
|
35
|
+
// (`basic-languages/monaco.contribution` for the broad TM grammars,
|
|
36
|
+
// and any of `language/{css,html,json,typescript}/monaco.contribution`
|
|
37
|
+
// for the rich language services). Skipping the language contributions
|
|
38
|
+
// entirely means `defaultLanguage` becomes inert — no tokenizer
|
|
39
|
+
// registered, so every file renders as plain foreground text.
|
|
34
40
|
|
|
35
41
|
// Squisq Monaco themes: same syntax highlighting as vs / vs-dark, but with
|
|
36
42
|
// Monaco's internal gutter (line numbers + folding margin) and overview
|
|
@@ -82,7 +88,7 @@ export function RawEditor({
|
|
|
82
88
|
submitOnEnter,
|
|
83
89
|
readOnly = false,
|
|
84
90
|
}: RawEditorProps) {
|
|
85
|
-
const {
|
|
91
|
+
const { editorSource, setEditorSource, setMonacoEditor, language, mentionProvider, doc } =
|
|
86
92
|
useEditorContext();
|
|
87
93
|
const { monaco: monacoNs, ready: monacoReady } = useMonacoLoader();
|
|
88
94
|
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
|
|
@@ -90,6 +96,7 @@ export function RawEditor({
|
|
|
90
96
|
const completionDisposable = useRef<monaco.IDisposable | null>(null);
|
|
91
97
|
const mentionCompletionDisposable = useRef<monaco.IDisposable | null>(null);
|
|
92
98
|
const iconCompletionDisposable = useRef<monaco.IDisposable | null>(null);
|
|
99
|
+
const attrCompletionDisposable = useRef<monaco.IDisposable | null>(null);
|
|
93
100
|
const iconGlyphDecorations = useRef<monaco.editor.IEditorDecorationsCollection | null>(null);
|
|
94
101
|
const dropCleanupRef = useRef<(() => void) | null>(null);
|
|
95
102
|
const keyDisposable = useRef<monaco.IDisposable | null>(null);
|
|
@@ -105,6 +112,35 @@ export function RawEditor({
|
|
|
105
112
|
mentionProviderRef.current = mentionProvider;
|
|
106
113
|
}, [mentionProvider]);
|
|
107
114
|
|
|
115
|
+
// Template completion list: built-in registry names plus any custom
|
|
116
|
+
// templates inlined in the active doc's frontmatter (`doc.customTemplates`
|
|
117
|
+
// — the same set that actually renders via `{[name]}`). Held in a ref
|
|
118
|
+
// (like mentionProvider above) so the once-registered provider always
|
|
119
|
+
// reads the latest set — custom templates created after mount must show
|
|
120
|
+
// up without re-mounting Monaco.
|
|
121
|
+
const docCustomTemplates = doc?.customTemplates;
|
|
122
|
+
const templateEntriesRef = useRef<{ name: string; detail: string }[] | null>(null);
|
|
123
|
+
if (templateEntriesRef.current === null) {
|
|
124
|
+
templateEntriesRef.current = getAvailableTemplates().map((name) => ({
|
|
125
|
+
name,
|
|
126
|
+
detail: 'Block template',
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
const builtIns = getAvailableTemplates().map((name) => ({
|
|
131
|
+
name,
|
|
132
|
+
detail: 'Block template',
|
|
133
|
+
}));
|
|
134
|
+
const seen = new Set(builtIns.map((b) => b.name));
|
|
135
|
+
const custom = (docCustomTemplates ?? [])
|
|
136
|
+
.filter((t) => !seen.has(t.name))
|
|
137
|
+
.map((t) => ({
|
|
138
|
+
name: t.name,
|
|
139
|
+
detail: t.label ? `Custom — ${t.label}` : 'Custom template',
|
|
140
|
+
}));
|
|
141
|
+
templateEntriesRef.current = [...builtIns, ...custom];
|
|
142
|
+
}, [docCustomTemplates]);
|
|
143
|
+
|
|
108
144
|
const handleBeforeMount: BeforeMount = useCallback((monaco) => {
|
|
109
145
|
monaco.editor.defineTheme('squisq-light', {
|
|
110
146
|
base: 'vs',
|
|
@@ -143,11 +179,12 @@ export function RawEditor({
|
|
|
143
179
|
mentionCompletionDisposable.current = null;
|
|
144
180
|
iconCompletionDisposable.current?.dispose();
|
|
145
181
|
iconCompletionDisposable.current = null;
|
|
182
|
+
attrCompletionDisposable.current?.dispose();
|
|
183
|
+
attrCompletionDisposable.current = null;
|
|
146
184
|
|
|
147
185
|
// Register the `{[template]}` completion provider only for markdown
|
|
148
186
|
// files — it's meaningless for TypeScript, JSON, Python, etc.
|
|
149
187
|
if (language === 'markdown') {
|
|
150
|
-
const templates = getAvailableTemplates();
|
|
151
188
|
completionDisposable.current = monaco.languages.registerCompletionItemProvider('markdown', {
|
|
152
189
|
triggerCharacters: ['['],
|
|
153
190
|
provideCompletionItems(model: monaco.editor.ITextModel, position: monaco.Position) {
|
|
@@ -161,6 +198,12 @@ export function RawEditor({
|
|
|
161
198
|
const bracketIdx = textBeforeCursor.lastIndexOf('{[');
|
|
162
199
|
if (bracketIdx === -1) return { suggestions: [] };
|
|
163
200
|
|
|
201
|
+
// Template names are the FIRST token after `{[`. Once a space
|
|
202
|
+
// follows the name we're in the `key=value` attribute region —
|
|
203
|
+
// the attribute provider handles that, so bail here to avoid
|
|
204
|
+
// offering template names mid-attribute.
|
|
205
|
+
if (/\s/.test(textBeforeCursor.slice(bracketIdx + 2))) return { suggestions: [] };
|
|
206
|
+
|
|
164
207
|
// When Monaco's bracket auto-pair has already produced the
|
|
165
208
|
// closing `]}` we just leave it in place and skip the
|
|
166
209
|
// suffix — otherwise accepting `sectionHeader` on
|
|
@@ -176,13 +219,13 @@ export function RawEditor({
|
|
|
176
219
|
position.column,
|
|
177
220
|
);
|
|
178
221
|
|
|
179
|
-
const suggestions =
|
|
222
|
+
const suggestions = (templateEntriesRef.current ?? []).map(({ name, detail }) => ({
|
|
180
223
|
label: name,
|
|
181
224
|
filterText: name,
|
|
182
225
|
kind: monaco.languages.CompletionItemKind.Value,
|
|
183
226
|
insertText: name + suffix,
|
|
184
227
|
range,
|
|
185
|
-
detail
|
|
228
|
+
detail,
|
|
186
229
|
sortText: name,
|
|
187
230
|
}));
|
|
188
231
|
|
|
@@ -324,6 +367,110 @@ export function RawEditor({
|
|
|
324
367
|
},
|
|
325
368
|
},
|
|
326
369
|
);
|
|
370
|
+
|
|
371
|
+
// `{[name key=value]}` block-meta attribute completion. Fires on
|
|
372
|
+
// heading lines once the cursor is past the template name (the
|
|
373
|
+
// first token), where attributes like `transition=` apply to the
|
|
374
|
+
// block. Two modes:
|
|
375
|
+
// - bare token → suggest attribute KEYS (insert `key=`)
|
|
376
|
+
// - after `key=` → suggest that key's VALUES, when it has a
|
|
377
|
+
// closed set (e.g. transition / transitionDirection)
|
|
378
|
+
// The descriptor list comes from core so keys and value enums stay
|
|
379
|
+
// in lockstep with what `coerceAnnotationValues` actually accepts.
|
|
380
|
+
attrCompletionDisposable.current = monaco.languages.registerCompletionItemProvider(
|
|
381
|
+
'markdown',
|
|
382
|
+
{
|
|
383
|
+
triggerCharacters: ['=', ' '],
|
|
384
|
+
provideCompletionItems(model, position) {
|
|
385
|
+
const lineContent = model.getLineContent(position.lineNumber);
|
|
386
|
+
// Block-meta attributes only mean something on a heading's
|
|
387
|
+
// template annotation — same gate as the template provider.
|
|
388
|
+
if (!/^#{1,6}\s/.test(lineContent)) return { suggestions: [] };
|
|
389
|
+
|
|
390
|
+
const textBeforeCursor = lineContent.substring(0, position.column - 1);
|
|
391
|
+
const bracketIdx = textBeforeCursor.lastIndexOf('{[');
|
|
392
|
+
if (bracketIdx === -1) return { suggestions: [] };
|
|
393
|
+
|
|
394
|
+
// Bail if a `]` already closed the annotation before the
|
|
395
|
+
// cursor — we'd be past it, not inside the attribute list.
|
|
396
|
+
const innerBefore = textBeforeCursor.slice(bracketIdx + 2);
|
|
397
|
+
if (innerBefore.includes(']')) return { suggestions: [] };
|
|
398
|
+
// The first token is the template name; attributes only begin
|
|
399
|
+
// after a whitespace separator. No space yet → still the name.
|
|
400
|
+
if (!/\s/.test(innerBefore)) return { suggestions: [] };
|
|
401
|
+
|
|
402
|
+
// Current token = run of non-whitespace ending at the cursor.
|
|
403
|
+
const lastWsMatch = innerBefore.match(/\s(\S*)$/);
|
|
404
|
+
const currentToken = lastWsMatch ? lastWsMatch[1] : '';
|
|
405
|
+
const tokenStartCol = position.column - currentToken.length;
|
|
406
|
+
|
|
407
|
+
const eqIdx = currentToken.indexOf('=');
|
|
408
|
+
if (eqIdx >= 0) {
|
|
409
|
+
// ── Value mode: `key=<partial>` ──
|
|
410
|
+
const key = currentToken.slice(0, eqIdx);
|
|
411
|
+
const descriptor = BLOCK_META_KEY_DESCRIPTORS.find((d) => d.key === key);
|
|
412
|
+
if (!descriptor?.values) return { suggestions: [] };
|
|
413
|
+
const range = new monaco.Range(
|
|
414
|
+
position.lineNumber,
|
|
415
|
+
tokenStartCol + eqIdx + 1, // after `=`
|
|
416
|
+
position.lineNumber,
|
|
417
|
+
position.column,
|
|
418
|
+
);
|
|
419
|
+
return {
|
|
420
|
+
suggestions: descriptor.values.map((value, i) => ({
|
|
421
|
+
label: value,
|
|
422
|
+
filterText: value,
|
|
423
|
+
kind: monaco.languages.CompletionItemKind.EnumMember,
|
|
424
|
+
insertText: value,
|
|
425
|
+
range,
|
|
426
|
+
detail: descriptor.description,
|
|
427
|
+
sortText: String(i).padStart(4, '0'),
|
|
428
|
+
})),
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ── Key mode: suggest attribute names not already present ──
|
|
433
|
+
// Collect keys already set anywhere in the annotation so we
|
|
434
|
+
// don't re-offer them; keep the key under the cursor eligible
|
|
435
|
+
// so re-editing an existing `key=` still suggests it.
|
|
436
|
+
const afterBracket = lineContent.slice(bracketIdx + 2);
|
|
437
|
+
const closeIdx = afterBracket.indexOf(']}');
|
|
438
|
+
const fullInner = closeIdx === -1 ? afterBracket : afterBracket.slice(0, closeIdx);
|
|
439
|
+
const present = new Set(
|
|
440
|
+
tokenizeAttrTokens(fullInner)
|
|
441
|
+
.map((t) => {
|
|
442
|
+
const i = t.indexOf('=');
|
|
443
|
+
return i > 0 ? t.slice(0, i) : null;
|
|
444
|
+
})
|
|
445
|
+
.filter((k): k is string => k != null),
|
|
446
|
+
);
|
|
447
|
+
present.delete(currentToken);
|
|
448
|
+
|
|
449
|
+
const range = new monaco.Range(
|
|
450
|
+
position.lineNumber,
|
|
451
|
+
tokenStartCol,
|
|
452
|
+
position.lineNumber,
|
|
453
|
+
position.column,
|
|
454
|
+
);
|
|
455
|
+
const suggestions = BLOCK_META_KEY_DESCRIPTORS.filter((d) => !present.has(d.key)).map(
|
|
456
|
+
(d, i) => ({
|
|
457
|
+
label: d.key,
|
|
458
|
+
filterText: d.key,
|
|
459
|
+
kind: monaco.languages.CompletionItemKind.Property,
|
|
460
|
+
insertText: `${d.key}=`,
|
|
461
|
+
range,
|
|
462
|
+
detail: d.values ? d.description : `${d.description} — ${d.valueHint}`,
|
|
463
|
+
sortText: String(i).padStart(4, '0'),
|
|
464
|
+
// Closed-set keys chain straight into their value list.
|
|
465
|
+
...(d.values
|
|
466
|
+
? { command: { id: 'editor.action.triggerSuggest', title: '' } }
|
|
467
|
+
: {}),
|
|
468
|
+
}),
|
|
469
|
+
);
|
|
470
|
+
return { suggestions };
|
|
471
|
+
},
|
|
472
|
+
},
|
|
473
|
+
);
|
|
327
474
|
}
|
|
328
475
|
|
|
329
476
|
// Chat-composer mode: intercept Enter before Monaco inserts a newline.
|
|
@@ -401,6 +548,8 @@ export function RawEditor({
|
|
|
401
548
|
mentionCompletionDisposable.current = null;
|
|
402
549
|
iconCompletionDisposable.current?.dispose();
|
|
403
550
|
iconCompletionDisposable.current = null;
|
|
551
|
+
attrCompletionDisposable.current?.dispose();
|
|
552
|
+
attrCompletionDisposable.current = null;
|
|
404
553
|
iconGlyphDecorations.current?.clear();
|
|
405
554
|
iconGlyphDecorations.current = null;
|
|
406
555
|
dropCleanupRef.current?.();
|
|
@@ -414,24 +563,31 @@ export function RawEditor({
|
|
|
414
563
|
(value) => {
|
|
415
564
|
if (isExternalUpdate.current) return;
|
|
416
565
|
if (value !== undefined) {
|
|
417
|
-
|
|
566
|
+
setEditorSource(value);
|
|
418
567
|
}
|
|
419
568
|
},
|
|
420
|
-
[
|
|
569
|
+
[setEditorSource],
|
|
421
570
|
);
|
|
422
571
|
|
|
423
|
-
// When external changes happen (e.g. from WYSIWYG
|
|
572
|
+
// When external changes happen (e.g. from WYSIWYG, or navigating to another
|
|
573
|
+
// block in block-at-a-time mode), update Monaco. In block mode `editorSource`
|
|
574
|
+
// is the active block's slice, so this swaps Monaco's content on navigation.
|
|
424
575
|
useEffect(() => {
|
|
425
576
|
const editor = editorRef.current;
|
|
426
577
|
if (editor) {
|
|
427
578
|
const currentValue = editor.getValue();
|
|
428
|
-
|
|
579
|
+
// Ignore trailing-whitespace-only differences from block-mode splice
|
|
580
|
+
// normalization — calling setValue for those resets the Monaco cursor.
|
|
581
|
+
if (
|
|
582
|
+
currentValue !== editorSource &&
|
|
583
|
+
currentValue.replace(/\s+$/, '') !== editorSource.replace(/\s+$/, '')
|
|
584
|
+
) {
|
|
429
585
|
isExternalUpdate.current = true;
|
|
430
|
-
editor.setValue(
|
|
586
|
+
editor.setValue(editorSource);
|
|
431
587
|
isExternalUpdate.current = false;
|
|
432
588
|
}
|
|
433
589
|
}
|
|
434
|
-
}, [
|
|
590
|
+
}, [editorSource]);
|
|
435
591
|
|
|
436
592
|
// ── Inline FontAwesome glyph decorations ────────────
|
|
437
593
|
// Walk the markdown source on every change, find each resolvable
|
|
@@ -482,7 +638,7 @@ export function RawEditor({
|
|
|
482
638
|
} else {
|
|
483
639
|
iconGlyphDecorations.current.set(decorations);
|
|
484
640
|
}
|
|
485
|
-
}, [
|
|
641
|
+
}, [editorSource, language, monacoNs]);
|
|
486
642
|
|
|
487
643
|
const effectiveTheme = SQUISQ_THEMES[theme] ?? theme;
|
|
488
644
|
|
|
@@ -516,7 +672,7 @@ export function RawEditor({
|
|
|
516
672
|
<div className={className} style={{ width: '100%', height: '100%' }} data-testid="raw-editor">
|
|
517
673
|
<Editor
|
|
518
674
|
defaultLanguage={language}
|
|
519
|
-
value={
|
|
675
|
+
value={editorSource}
|
|
520
676
|
theme={effectiveTheme}
|
|
521
677
|
beforeMount={handleBeforeMount}
|
|
522
678
|
onMount={handleMount}
|
package/src/RecorderEntry.tsx
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
import { useCallback } from 'react';
|
|
25
25
|
import { RecorderPanel } from './recorder/RecorderPanel.js';
|
|
26
26
|
import type { RecorderSaveResult } from './recorder/RecorderModal.js';
|
|
27
|
+
import { insertMediaBlock } from './recorder/insertMediaBlock.js';
|
|
27
28
|
import { useEditorContext } from './EditorContext';
|
|
28
29
|
|
|
29
30
|
/**
|
|
@@ -98,14 +99,10 @@ export function RecorderEntry() {
|
|
|
98
99
|
}
|
|
99
100
|
const audioTag = `<audio src="${result.relativePath}" controls></audio>`;
|
|
100
101
|
if (activeView === 'wysiwyg' && tiptapEditor) {
|
|
101
|
-
tiptapEditor
|
|
102
|
-
|
|
103
|
-
.
|
|
104
|
-
|
|
105
|
-
type: 'audio',
|
|
106
|
-
attrs: { src: result.relativePath, controls: true },
|
|
107
|
-
})
|
|
108
|
-
.run();
|
|
102
|
+
insertMediaBlock(tiptapEditor, {
|
|
103
|
+
type: 'audio',
|
|
104
|
+
attrs: { src: result.relativePath, controls: true },
|
|
105
|
+
});
|
|
109
106
|
return;
|
|
110
107
|
}
|
|
111
108
|
if (activeView === 'raw' && monacoEditor) {
|
|
@@ -124,14 +121,10 @@ export function RecorderEntry() {
|
|
|
124
121
|
// aspect ratio is preserved regardless of source dimensions.
|
|
125
122
|
const videoTag = `<video src="${result.relativePath}" controls width="480"></video>`;
|
|
126
123
|
if (activeView === 'wysiwyg' && tiptapEditor) {
|
|
127
|
-
tiptapEditor
|
|
128
|
-
|
|
129
|
-
.
|
|
130
|
-
|
|
131
|
-
type: 'video',
|
|
132
|
-
attrs: { src: result.relativePath, controls: true, width: 480 },
|
|
133
|
-
})
|
|
134
|
-
.run();
|
|
124
|
+
insertMediaBlock(tiptapEditor, {
|
|
125
|
+
type: 'video',
|
|
126
|
+
attrs: { src: result.relativePath, controls: true, width: 480 },
|
|
127
|
+
});
|
|
135
128
|
return;
|
|
136
129
|
}
|
|
137
130
|
if (activeView === 'raw' && monacoEditor) {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import Heading from '@tiptap/extension-heading';
|
|
17
17
|
import { templateLabel } from './TemplatePicker';
|
|
18
|
+
import { summarizeBlockProps } from './blockProperties';
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* HeadingWithTemplate — drop-in replacement for Tiptap's Heading that
|
|
@@ -40,6 +41,14 @@ export const HeadingWithTemplate = Heading.extend({
|
|
|
40
41
|
return { 'data-template-params': attributes.dataTemplateParams };
|
|
41
42
|
},
|
|
42
43
|
},
|
|
44
|
+
dataBlockAttrs: {
|
|
45
|
+
default: null,
|
|
46
|
+
parseHTML: (element: HTMLElement) => element.getAttribute('data-block-attrs') || null,
|
|
47
|
+
renderHTML: (attributes: Record<string, unknown>) => {
|
|
48
|
+
if (!attributes.dataBlockAttrs) return {};
|
|
49
|
+
return { 'data-block-attrs': attributes.dataBlockAttrs };
|
|
50
|
+
},
|
|
51
|
+
},
|
|
43
52
|
};
|
|
44
53
|
},
|
|
45
54
|
|
|
@@ -47,6 +56,12 @@ export const HeadingWithTemplate = Heading.extend({
|
|
|
47
56
|
const level = node.attrs.level;
|
|
48
57
|
const tag = `h${level}`;
|
|
49
58
|
const templateName = HTMLAttributes['data-template'];
|
|
59
|
+
// Concise summary of authored block properties (transition / timing),
|
|
60
|
+
// painted on the props badge so the canvas shows what's set at a glance.
|
|
61
|
+
const propsSummary = summarizeBlockProps(
|
|
62
|
+
(HTMLAttributes['data-block-attrs'] as string | undefined) ?? null,
|
|
63
|
+
(HTMLAttributes['data-template-params'] as string | undefined) ?? null,
|
|
64
|
+
);
|
|
50
65
|
|
|
51
66
|
// Render heading with a trailing badge span. The badge has no text
|
|
52
67
|
// content — its label is painted via CSS `content: attr(data-template-label)`
|
|
@@ -76,6 +91,7 @@ export const HeadingWithTemplate = Heading.extend({
|
|
|
76
91
|
'data-template-label': templateLabel(templateName),
|
|
77
92
|
},
|
|
78
93
|
],
|
|
94
|
+
propsBadgeSpec(propsSummary),
|
|
79
95
|
];
|
|
80
96
|
}
|
|
81
97
|
|
|
@@ -94,6 +110,34 @@ export const HeadingWithTemplate = Heading.extend({
|
|
|
94
110
|
title: 'Choose block template',
|
|
95
111
|
},
|
|
96
112
|
],
|
|
113
|
+
propsBadgeSpec(propsSummary),
|
|
97
114
|
];
|
|
98
115
|
},
|
|
99
116
|
});
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The block-properties chip rendered beside the template badge — the on-canvas
|
|
120
|
+
* entry point to the {@link BlockPropertiesPopover} (transition, timing, …).
|
|
121
|
+
*
|
|
122
|
+
* Like the template badge, this is an EMPTY inert span: the sliders glyph and
|
|
123
|
+
* the optional `summary` text are painted via CSS (`::before` / `::after` with
|
|
124
|
+
* `content: attr(data-props-summary)`, using the Font Awesome font the editor
|
|
125
|
+
* already loads). Keeping it childless is deliberate — `tiptapToMarkdown`
|
|
126
|
+
* serializes the rendered heading HTML, so any real child node here would risk
|
|
127
|
+
* leaking into the markdown; a `data-*` attribute is stripped with the span.
|
|
128
|
+
* Clicks are delegated in `WysiwygEditor`.
|
|
129
|
+
*/
|
|
130
|
+
function propsBadgeSpec(summary: string): [string, Record<string, string>] {
|
|
131
|
+
const attrs: Record<string, string> = {
|
|
132
|
+
class: 'squisq-props-badge',
|
|
133
|
+
contenteditable: 'false',
|
|
134
|
+
role: 'button',
|
|
135
|
+
tabindex: '0',
|
|
136
|
+
'aria-haspopup': 'dialog',
|
|
137
|
+
title: summary ? `Block properties — ${summary}` : 'Block properties',
|
|
138
|
+
};
|
|
139
|
+
// Only set the attribute when there's something to show, so the CSS
|
|
140
|
+
// `[data-props-summary]` selector cleanly distinguishes "has properties".
|
|
141
|
+
if (summary) attrs['data-props-summary'] = summary;
|
|
142
|
+
return ['span', attrs];
|
|
143
|
+
}
|