@bendyline/squisq-editor-react 1.5.3 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -10
- package/dist/index.d.ts +1114 -52
- package/dist/index.js +18446 -8050
- package/dist/index.js.map +1 -1
- package/dist/styles/fa-brands-400-AHOAZHCU.woff2 +0 -0
- package/dist/styles/fa-regular-400-VRZYIBIZ.woff2 +0 -0
- package/dist/styles/fa-solid-900-MDEYK55F.woff2 +0 -0
- package/dist/styles/fa-v4compatibility-ETEVP6IB.woff2 +0 -0
- package/dist/styles/index.css +13867 -0
- package/package.json +15 -7
- package/src/BlockCardView.tsx +121 -0
- package/src/BlockPreviewPanel.tsx +69 -0
- package/src/BlockPropertiesPopover.tsx +191 -0
- package/src/EditorContext.tsx +165 -16
- package/src/EditorShell.tsx +265 -144
- package/src/FolderView.tsx +131 -0
- package/src/Icon.tsx +26 -0
- package/src/ImageEditor.tsx +69 -22
- package/src/OutlinePanel.tsx +64 -7
- package/src/PlainHtmlPreview.tsx +30 -3
- package/src/PreviewControls.tsx +491 -122
- package/src/PreviewPanel.tsx +15 -9
- package/src/RawEditor.tsx +179 -17
- package/src/RecorderEntry.tsx +9 -16
- package/src/TemplateAnnotation.ts +44 -0
- package/src/TemplatePicker.tsx +329 -54
- package/src/ThemeCustomizerPanel.tsx +30 -336
- package/src/ThemePicker.tsx +112 -3
- package/src/TimelineBlockPreview.tsx +37 -0
- package/src/TimelineTrack.tsx +671 -0
- package/src/Toolbar.tsx +544 -180
- package/src/Tooltip.tsx +22 -4
- package/src/TransitionPicker.tsx +351 -0
- package/src/VersionHistoryPanel.tsx +4 -16
- package/src/ViewMenuPanel.tsx +17 -14
- package/src/WysiwygEditor.tsx +161 -65
- package/src/__tests__/blockProperties.test.ts +92 -0
- package/src/__tests__/blockRange.test.ts +105 -0
- package/src/__tests__/buildPreviewDocTransition.test.ts +73 -0
- package/src/__tests__/codeContextSectionView.test.tsx +95 -0
- package/src/__tests__/codeContextZoneManager.test.ts +127 -0
- package/src/__tests__/createShapeLayer.test.ts +46 -0
- package/src/__tests__/diffContextSections.test.ts +39 -0
- package/src/__tests__/drawingShapeRoundTrip.test.ts +49 -0
- package/src/__tests__/editorShellCodeContext.test.tsx +86 -0
- package/src/__tests__/editorShellProps.test.tsx +96 -0
- package/src/__tests__/embeddedMedia.test.ts +48 -0
- package/src/__tests__/headingTransition.test.ts +138 -0
- package/src/__tests__/layoutChildRoundTrip.test.ts +71 -0
- package/src/__tests__/plainHtmlPreview.test.tsx +10 -8
- package/src/__tests__/previewControls.test.tsx +70 -0
- package/src/__tests__/recorderMediaInsert.test.ts +86 -0
- package/src/__tests__/templateAnnotationRoundTrip.test.ts +18 -0
- package/src/__tests__/templatePickerMetadata.test.ts +32 -0
- package/src/__tests__/timelineSource.test.ts +134 -0
- package/src/__tests__/tiptapBridge.test.ts +92 -0
- package/src/__tests__/tiptapBridgeConformance.test.ts +47 -0
- package/src/__tests__/tooltip.test.tsx +72 -0
- package/src/__tests__/transitionCatalog.test.ts +64 -0
- package/src/__tests__/useBlockNavigator.test.tsx +67 -0
- package/src/__tests__/useJsonEditorTokens.test.ts +59 -0
- package/src/__tests__/useMediaRecorder.test.ts +41 -0
- package/src/__tests__/useTimelineClock.test.ts +21 -0
- package/src/blockProperties.ts +88 -0
- package/src/blockRange.ts +132 -0
- package/src/buildPreviewDoc.ts +98 -9
- package/src/codeContext/CodeContextSectionView.tsx +124 -0
- package/src/codeContext/CodeContextZoneManager.ts +149 -0
- package/src/codeContext/CodeContextZones.tsx +121 -0
- package/src/codeContext/diffContextSections.ts +38 -0
- package/src/codeContext/types.ts +75 -0
- package/src/customTemplates/AddBin.tsx +126 -0
- package/src/customTemplates/CustomLayoutManager.tsx +233 -0
- package/src/customTemplates/CustomTemplateContext.tsx +182 -0
- package/src/customTemplates/LayerToolbar.tsx +580 -0
- package/src/customTemplates/ShapeGlyph.tsx +47 -0
- package/src/customTemplates/TemplateDesigner.tsx +430 -0
- package/src/customTemplates/__tests__/library.test.ts +88 -0
- package/src/customTemplates/__tests__/normalizePositions.test.ts +109 -0
- package/src/customTemplates/__tests__/shapeDefs.test.ts +49 -0
- package/src/customTemplates/__tests__/useMemoryLayerAdapter.test.ts +95 -0
- package/src/customTemplates/designer.css +673 -0
- package/src/customTemplates/index.ts +31 -0
- package/src/customTemplates/library.ts +97 -0
- package/src/customTemplates/normalizePositions.ts +75 -0
- package/src/customTemplates/shapeDefs.ts +131 -0
- package/src/customTemplates/thumbnail.tsx +63 -0
- package/src/customTemplates/tokenDefs.ts +60 -0
- package/src/customTemplates/useDocCustomTemplates.ts +52 -0
- package/src/customTemplates/useMemoryLayerAdapter.ts +123 -0
- package/src/customThemes/CustomThemeContext.tsx +179 -0
- package/src/customThemes/CustomThemeDialog.tsx +286 -0
- package/src/customThemes/__tests__/CustomThemeContext.test.tsx +64 -0
- package/src/customThemes/__tests__/CustomThemeDialog.test.tsx +47 -0
- package/src/customThemes/__tests__/customThemeLibrary.test.ts +51 -0
- package/src/customThemes/customThemeLibrary.ts +97 -0
- package/src/customThemes/index.ts +31 -0
- package/src/customThemes/themeControls.tsx +229 -0
- package/src/customThemes/themeDraft.ts +272 -0
- package/src/customThemes/useDocCustomThemes.ts +49 -0
- package/src/diagram/DiagramCanvas.tsx +240 -0
- package/src/diagram/DiagramExtension.ts +209 -0
- package/src/diagram/DiagramMaximizedOverlay.tsx +46 -0
- package/src/diagram/DiagramWidget.tsx +270 -0
- package/src/diagram/diagramCommands.ts +604 -0
- package/src/diagram/diagramConstants.ts +17 -0
- package/src/diagram/useDiagramData.ts +126 -0
- package/src/embeddedMedia.ts +78 -0
- package/src/frontmatter.ts +29 -0
- package/src/headingTransition.ts +231 -0
- package/src/imageEditor/CanvasSurface.tsx +383 -88
- package/src/imageEditor/PropertiesPanel.tsx +47 -1
- package/src/imageEditor/Toolbar.tsx +229 -16
- package/src/imageEditor/createShapeLayer.ts +280 -0
- package/src/imageEditor/icons.tsx +34 -114
- package/src/imageEditor/image-editor.css +54 -5
- package/src/imageEditor/state.ts +23 -3
- package/src/index.ts +109 -1
- package/src/jsonEditor/useJsonEditorTokens.ts +13 -43
- package/src/recorder/RecorderModal.tsx +120 -53
- package/src/recorder/RecorderPanel.tsx +2 -26
- package/src/recorder/hooks/useMediaRecorder.ts +17 -11
- package/src/recorder/insertMediaBlock.ts +30 -0
- package/src/resolveBlockVisual.ts +33 -0
- package/src/scene/Scene.tsx +540 -0
- package/src/scene/SceneBlockExtension.ts +198 -0
- package/src/scene/SceneBlockToolbar.tsx +201 -0
- package/src/scene/SceneBlockWidget.tsx +434 -0
- package/src/scene/ScenePropsBar.tsx +85 -0
- package/src/scene/SceneSelection.tsx +107 -0
- package/src/scene/SceneViewport.tsx +102 -0
- package/src/scene/ShapePalette.tsx +181 -0
- package/src/scene/__tests__/DiagramAdapter.test.ts +56 -0
- package/src/scene/__tests__/bezierEdit.test.ts +85 -0
- package/src/scene/__tests__/blockLayers.test.ts +57 -0
- package/src/scene/__tests__/shapeLayers.test.ts +106 -0
- package/src/scene/__tests__/useSceneHitTest.test.ts +90 -0
- package/src/scene/__tests__/useScenePanZoom.test.ts +103 -0
- package/src/scene/adapters/DiagramAdapter.ts +168 -0
- package/src/scene/adapters/DrawingAdapter.ts +415 -0
- package/src/scene/adapters/LayoutAdapter.ts +310 -0
- package/src/scene/adapters/blockLayers.ts +159 -0
- package/src/scene/commands/SceneCommand.ts +70 -0
- package/src/scene/commands/drawingCommands.ts +318 -0
- package/src/scene/commands/layoutCommands.ts +301 -0
- package/src/scene/hooks/useSceneHitTest.ts +105 -0
- package/src/scene/hooks/useScenePanZoom.ts +147 -0
- package/src/scene/hooks/useSceneSelection.ts +62 -0
- package/src/scene/index.ts +95 -0
- package/src/scene/layers/DiagramEdges.tsx +127 -0
- package/src/scene/layers/edgeGeometry.ts +77 -0
- package/src/scene/layers/nodeCard.tsx +145 -0
- package/src/scene/layers/renderLayer.tsx +70 -0
- package/src/scene/layers/shapeLayers.ts +201 -0
- package/src/scene/paths/bezierEdit.ts +208 -0
- package/src/scene/scene.css +649 -0
- package/src/scene/text/SceneTextOverlay.tsx +161 -0
- package/src/scene/text/sceneTextChannel.ts +40 -0
- package/src/scene/text/sceneTextConfig.ts +27 -0
- package/src/scene/text/sceneTiptap.ts +36 -0
- package/src/scene/text/useSceneTextEditing.ts +39 -0
- package/src/scene/tools/ConnectTool.ts +111 -0
- package/src/scene/tools/DrawingConnectTool.ts +161 -0
- package/src/scene/tools/PathTool.ts +158 -0
- package/src/scene/tools/PlaceTool.ts +47 -0
- package/src/scene/tools/SceneTool.ts +75 -0
- package/src/scene/tools/SelectTool.ts +284 -0
- package/src/scene/tools/ShapeTool.ts +144 -0
- package/src/scene/tools/TextTool.ts +72 -0
- package/src/scene/tools/TokenTool.ts +95 -0
- package/src/scene/tools/createDrawShapeTool.ts +82 -0
- package/src/styles/code-context.css +155 -0
- package/src/styles/diagram.css +183 -0
- package/src/styles/editor.css +1708 -150
- package/src/styles/folder-view.css +210 -0
- package/src/styles/image-edit-affordance.css +2 -2
- package/src/styles/index.css +5 -0
- package/src/timelineSource.ts +244 -0
- package/src/tiptapBridge.ts +115 -34
- package/src/tooltipPlacement.ts +13 -0
- package/src/transitionCatalog.ts +159 -0
- package/src/types/monaco-shims.d.ts +10 -0
- package/src/useBlockNavigator.ts +153 -0
- package/src/useMonacoLoader.ts +23 -1
- package/src/useTimelineClock.ts +76 -0
- package/src/utils/dropUtils.ts +1 -1
package/src/PreviewPanel.tsx
CHANGED
|
@@ -48,6 +48,7 @@ export function PreviewPanel({ basePath = '/', className, workspaceContainer }:
|
|
|
48
48
|
activeTheme,
|
|
49
49
|
activeTransformStyle,
|
|
50
50
|
activeCaptionStyle,
|
|
51
|
+
activeCaptionsEnabled,
|
|
51
52
|
} = usePreviewSettings();
|
|
52
53
|
|
|
53
54
|
// Build the player-ready Doc whenever the parsed doc changes.
|
|
@@ -108,10 +109,15 @@ export function PreviewPanel({ basePath = '/', className, workspaceContainer }:
|
|
|
108
109
|
);
|
|
109
110
|
}
|
|
110
111
|
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
// The public DisplayMode values predate the current labels: raw `page`
|
|
113
|
+
// is the plain Document preview, while raw `linear` is the styled Page view.
|
|
114
|
+
const isDocumentMode = activeDisplayMode === 'page';
|
|
115
|
+
const isPageMode = activeDisplayMode === 'linear';
|
|
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) {
|
|
115
121
|
return (
|
|
116
122
|
<div className={`squisq-preview-status ${className || ''}`} data-testid="preview-panel">
|
|
117
123
|
<p>No content to preview. Start typing in the editor.</p>
|
|
@@ -119,8 +125,7 @@ export function PreviewPanel({ basePath = '/', className, workspaceContainer }:
|
|
|
119
125
|
);
|
|
120
126
|
}
|
|
121
127
|
|
|
122
|
-
const fillsContainer =
|
|
123
|
-
activeDisplayMode === 'linear' || activeDisplayMode === 'page' ? 'stretch' : 'center';
|
|
128
|
+
const fillsContainer = isDocumentMode || isPageMode ? 'stretch' : 'center';
|
|
124
129
|
|
|
125
130
|
return (
|
|
126
131
|
<div
|
|
@@ -147,7 +152,7 @@ export function PreviewPanel({ basePath = '/', className, workspaceContainer }:
|
|
|
147
152
|
minHeight: 0,
|
|
148
153
|
}}
|
|
149
154
|
>
|
|
150
|
-
{
|
|
155
|
+
{isDocumentMode ? (
|
|
151
156
|
<PlainHtmlPreview
|
|
152
157
|
markdown={markdownSource}
|
|
153
158
|
title={(doc?.frontmatter?.title as string | undefined) ?? undefined}
|
|
@@ -155,7 +160,7 @@ export function PreviewPanel({ basePath = '/', className, workspaceContainer }:
|
|
|
155
160
|
mediaRevision={mediaRevision}
|
|
156
161
|
theme={activeTheme}
|
|
157
162
|
/>
|
|
158
|
-
) :
|
|
163
|
+
) : isPageMode ? (
|
|
159
164
|
<LinearDocView
|
|
160
165
|
doc={doc!}
|
|
161
166
|
basePath={basePath}
|
|
@@ -164,7 +169,7 @@ export function PreviewPanel({ basePath = '/', className, workspaceContainer }:
|
|
|
164
169
|
/>
|
|
165
170
|
) : (
|
|
166
171
|
<DocPlayer
|
|
167
|
-
|
|
172
|
+
doc={previewDoc!}
|
|
168
173
|
basePath={basePath}
|
|
169
174
|
showControls
|
|
170
175
|
muted
|
|
@@ -172,6 +177,7 @@ export function PreviewPanel({ basePath = '/', className, workspaceContainer }:
|
|
|
172
177
|
displayMode={activeDisplayMode}
|
|
173
178
|
theme={activeTheme}
|
|
174
179
|
captionStyle={activeCaptionStyle}
|
|
180
|
+
captionsEnabled={activeCaptionsEnabled}
|
|
175
181
|
/>
|
|
176
182
|
)}
|
|
177
183
|
</div>
|
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
|
|
@@ -50,8 +56,14 @@ const SQUISQ_THEMES: Record<string, string> = {
|
|
|
50
56
|
};
|
|
51
57
|
|
|
52
58
|
export interface RawEditorProps {
|
|
53
|
-
/**
|
|
54
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Monaco editor theme name (default: `'vs'`). Accepts Monaco's built-in
|
|
61
|
+
* theme ids (`'vs'`, `'vs-dark'`, `'hc-black'`) — which are transparently
|
|
62
|
+
* mapped to the Squisq-tinted variants — or any custom theme registered
|
|
63
|
+
* via `monaco.editor.defineTheme`. This is the *code editor* color
|
|
64
|
+
* theme, distinct from the shell's light/dark `colorScheme`.
|
|
65
|
+
*/
|
|
66
|
+
monacoTheme?: string;
|
|
55
67
|
/** Show minimap (default: false) */
|
|
56
68
|
minimap?: boolean;
|
|
57
69
|
/** Font size in pixels (default: 14) */
|
|
@@ -74,7 +86,7 @@ export interface RawEditorProps {
|
|
|
74
86
|
* Binds to the shared EditorContext for source synchronization.
|
|
75
87
|
*/
|
|
76
88
|
export function RawEditor({
|
|
77
|
-
|
|
89
|
+
monacoTheme = 'vs',
|
|
78
90
|
minimap = false,
|
|
79
91
|
fontSize = 14,
|
|
80
92
|
wordWrap = 'on',
|
|
@@ -82,7 +94,7 @@ export function RawEditor({
|
|
|
82
94
|
submitOnEnter,
|
|
83
95
|
readOnly = false,
|
|
84
96
|
}: RawEditorProps) {
|
|
85
|
-
const {
|
|
97
|
+
const { editorSource, setEditorSource, setMonacoEditor, language, mentionProvider, doc } =
|
|
86
98
|
useEditorContext();
|
|
87
99
|
const { monaco: monacoNs, ready: monacoReady } = useMonacoLoader();
|
|
88
100
|
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
|
|
@@ -90,6 +102,7 @@ export function RawEditor({
|
|
|
90
102
|
const completionDisposable = useRef<monaco.IDisposable | null>(null);
|
|
91
103
|
const mentionCompletionDisposable = useRef<monaco.IDisposable | null>(null);
|
|
92
104
|
const iconCompletionDisposable = useRef<monaco.IDisposable | null>(null);
|
|
105
|
+
const attrCompletionDisposable = useRef<monaco.IDisposable | null>(null);
|
|
93
106
|
const iconGlyphDecorations = useRef<monaco.editor.IEditorDecorationsCollection | null>(null);
|
|
94
107
|
const dropCleanupRef = useRef<(() => void) | null>(null);
|
|
95
108
|
const keyDisposable = useRef<monaco.IDisposable | null>(null);
|
|
@@ -105,6 +118,35 @@ export function RawEditor({
|
|
|
105
118
|
mentionProviderRef.current = mentionProvider;
|
|
106
119
|
}, [mentionProvider]);
|
|
107
120
|
|
|
121
|
+
// Template completion list: built-in registry names plus any custom
|
|
122
|
+
// templates inlined in the active doc's frontmatter (`doc.customTemplates`
|
|
123
|
+
// — the same set that actually renders via `{[name]}`). Held in a ref
|
|
124
|
+
// (like mentionProvider above) so the once-registered provider always
|
|
125
|
+
// reads the latest set — custom templates created after mount must show
|
|
126
|
+
// up without re-mounting Monaco.
|
|
127
|
+
const docCustomTemplates = doc?.customTemplates;
|
|
128
|
+
const templateEntriesRef = useRef<{ name: string; detail: string }[] | null>(null);
|
|
129
|
+
if (templateEntriesRef.current === null) {
|
|
130
|
+
templateEntriesRef.current = getAvailableTemplates().map((name) => ({
|
|
131
|
+
name,
|
|
132
|
+
detail: 'Block template',
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
const builtIns = getAvailableTemplates().map((name) => ({
|
|
137
|
+
name,
|
|
138
|
+
detail: 'Block template',
|
|
139
|
+
}));
|
|
140
|
+
const seen = new Set(builtIns.map((b) => b.name));
|
|
141
|
+
const custom = (docCustomTemplates ?? [])
|
|
142
|
+
.filter((t) => !seen.has(t.name))
|
|
143
|
+
.map((t) => ({
|
|
144
|
+
name: t.name,
|
|
145
|
+
detail: t.label ? `Custom — ${t.label}` : 'Custom template',
|
|
146
|
+
}));
|
|
147
|
+
templateEntriesRef.current = [...builtIns, ...custom];
|
|
148
|
+
}, [docCustomTemplates]);
|
|
149
|
+
|
|
108
150
|
const handleBeforeMount: BeforeMount = useCallback((monaco) => {
|
|
109
151
|
monaco.editor.defineTheme('squisq-light', {
|
|
110
152
|
base: 'vs',
|
|
@@ -143,11 +185,12 @@ export function RawEditor({
|
|
|
143
185
|
mentionCompletionDisposable.current = null;
|
|
144
186
|
iconCompletionDisposable.current?.dispose();
|
|
145
187
|
iconCompletionDisposable.current = null;
|
|
188
|
+
attrCompletionDisposable.current?.dispose();
|
|
189
|
+
attrCompletionDisposable.current = null;
|
|
146
190
|
|
|
147
191
|
// Register the `{[template]}` completion provider only for markdown
|
|
148
192
|
// files — it's meaningless for TypeScript, JSON, Python, etc.
|
|
149
193
|
if (language === 'markdown') {
|
|
150
|
-
const templates = getAvailableTemplates();
|
|
151
194
|
completionDisposable.current = monaco.languages.registerCompletionItemProvider('markdown', {
|
|
152
195
|
triggerCharacters: ['['],
|
|
153
196
|
provideCompletionItems(model: monaco.editor.ITextModel, position: monaco.Position) {
|
|
@@ -161,6 +204,12 @@ export function RawEditor({
|
|
|
161
204
|
const bracketIdx = textBeforeCursor.lastIndexOf('{[');
|
|
162
205
|
if (bracketIdx === -1) return { suggestions: [] };
|
|
163
206
|
|
|
207
|
+
// Template names are the FIRST token after `{[`. Once a space
|
|
208
|
+
// follows the name we're in the `key=value` attribute region —
|
|
209
|
+
// the attribute provider handles that, so bail here to avoid
|
|
210
|
+
// offering template names mid-attribute.
|
|
211
|
+
if (/\s/.test(textBeforeCursor.slice(bracketIdx + 2))) return { suggestions: [] };
|
|
212
|
+
|
|
164
213
|
// When Monaco's bracket auto-pair has already produced the
|
|
165
214
|
// closing `]}` we just leave it in place and skip the
|
|
166
215
|
// suffix — otherwise accepting `sectionHeader` on
|
|
@@ -176,13 +225,13 @@ export function RawEditor({
|
|
|
176
225
|
position.column,
|
|
177
226
|
);
|
|
178
227
|
|
|
179
|
-
const suggestions =
|
|
228
|
+
const suggestions = (templateEntriesRef.current ?? []).map(({ name, detail }) => ({
|
|
180
229
|
label: name,
|
|
181
230
|
filterText: name,
|
|
182
231
|
kind: monaco.languages.CompletionItemKind.Value,
|
|
183
232
|
insertText: name + suffix,
|
|
184
233
|
range,
|
|
185
|
-
detail
|
|
234
|
+
detail,
|
|
186
235
|
sortText: name,
|
|
187
236
|
}));
|
|
188
237
|
|
|
@@ -324,6 +373,110 @@ export function RawEditor({
|
|
|
324
373
|
},
|
|
325
374
|
},
|
|
326
375
|
);
|
|
376
|
+
|
|
377
|
+
// `{[name key=value]}` block-meta attribute completion. Fires on
|
|
378
|
+
// heading lines once the cursor is past the template name (the
|
|
379
|
+
// first token), where attributes like `transition=` apply to the
|
|
380
|
+
// block. Two modes:
|
|
381
|
+
// - bare token → suggest attribute KEYS (insert `key=`)
|
|
382
|
+
// - after `key=` → suggest that key's VALUES, when it has a
|
|
383
|
+
// closed set (e.g. transition / transitionDirection)
|
|
384
|
+
// The descriptor list comes from core so keys and value enums stay
|
|
385
|
+
// in lockstep with what `coerceAnnotationValues` actually accepts.
|
|
386
|
+
attrCompletionDisposable.current = monaco.languages.registerCompletionItemProvider(
|
|
387
|
+
'markdown',
|
|
388
|
+
{
|
|
389
|
+
triggerCharacters: ['=', ' '],
|
|
390
|
+
provideCompletionItems(model, position) {
|
|
391
|
+
const lineContent = model.getLineContent(position.lineNumber);
|
|
392
|
+
// Block-meta attributes only mean something on a heading's
|
|
393
|
+
// template annotation — same gate as the template provider.
|
|
394
|
+
if (!/^#{1,6}\s/.test(lineContent)) return { suggestions: [] };
|
|
395
|
+
|
|
396
|
+
const textBeforeCursor = lineContent.substring(0, position.column - 1);
|
|
397
|
+
const bracketIdx = textBeforeCursor.lastIndexOf('{[');
|
|
398
|
+
if (bracketIdx === -1) return { suggestions: [] };
|
|
399
|
+
|
|
400
|
+
// Bail if a `]` already closed the annotation before the
|
|
401
|
+
// cursor — we'd be past it, not inside the attribute list.
|
|
402
|
+
const innerBefore = textBeforeCursor.slice(bracketIdx + 2);
|
|
403
|
+
if (innerBefore.includes(']')) return { suggestions: [] };
|
|
404
|
+
// The first token is the template name; attributes only begin
|
|
405
|
+
// after a whitespace separator. No space yet → still the name.
|
|
406
|
+
if (!/\s/.test(innerBefore)) return { suggestions: [] };
|
|
407
|
+
|
|
408
|
+
// Current token = run of non-whitespace ending at the cursor.
|
|
409
|
+
const lastWsMatch = innerBefore.match(/\s(\S*)$/);
|
|
410
|
+
const currentToken = lastWsMatch ? lastWsMatch[1] : '';
|
|
411
|
+
const tokenStartCol = position.column - currentToken.length;
|
|
412
|
+
|
|
413
|
+
const eqIdx = currentToken.indexOf('=');
|
|
414
|
+
if (eqIdx >= 0) {
|
|
415
|
+
// ── Value mode: `key=<partial>` ──
|
|
416
|
+
const key = currentToken.slice(0, eqIdx);
|
|
417
|
+
const descriptor = BLOCK_META_KEY_DESCRIPTORS.find((d) => d.key === key);
|
|
418
|
+
if (!descriptor?.values) return { suggestions: [] };
|
|
419
|
+
const range = new monaco.Range(
|
|
420
|
+
position.lineNumber,
|
|
421
|
+
tokenStartCol + eqIdx + 1, // after `=`
|
|
422
|
+
position.lineNumber,
|
|
423
|
+
position.column,
|
|
424
|
+
);
|
|
425
|
+
return {
|
|
426
|
+
suggestions: descriptor.values.map((value, i) => ({
|
|
427
|
+
label: value,
|
|
428
|
+
filterText: value,
|
|
429
|
+
kind: monaco.languages.CompletionItemKind.EnumMember,
|
|
430
|
+
insertText: value,
|
|
431
|
+
range,
|
|
432
|
+
detail: descriptor.description,
|
|
433
|
+
sortText: String(i).padStart(4, '0'),
|
|
434
|
+
})),
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// ── Key mode: suggest attribute names not already present ──
|
|
439
|
+
// Collect keys already set anywhere in the annotation so we
|
|
440
|
+
// don't re-offer them; keep the key under the cursor eligible
|
|
441
|
+
// so re-editing an existing `key=` still suggests it.
|
|
442
|
+
const afterBracket = lineContent.slice(bracketIdx + 2);
|
|
443
|
+
const closeIdx = afterBracket.indexOf(']}');
|
|
444
|
+
const fullInner = closeIdx === -1 ? afterBracket : afterBracket.slice(0, closeIdx);
|
|
445
|
+
const present = new Set(
|
|
446
|
+
tokenizeAttrTokens(fullInner)
|
|
447
|
+
.map((t) => {
|
|
448
|
+
const i = t.indexOf('=');
|
|
449
|
+
return i > 0 ? t.slice(0, i) : null;
|
|
450
|
+
})
|
|
451
|
+
.filter((k): k is string => k != null),
|
|
452
|
+
);
|
|
453
|
+
present.delete(currentToken);
|
|
454
|
+
|
|
455
|
+
const range = new monaco.Range(
|
|
456
|
+
position.lineNumber,
|
|
457
|
+
tokenStartCol,
|
|
458
|
+
position.lineNumber,
|
|
459
|
+
position.column,
|
|
460
|
+
);
|
|
461
|
+
const suggestions = BLOCK_META_KEY_DESCRIPTORS.filter((d) => !present.has(d.key)).map(
|
|
462
|
+
(d, i) => ({
|
|
463
|
+
label: d.key,
|
|
464
|
+
filterText: d.key,
|
|
465
|
+
kind: monaco.languages.CompletionItemKind.Property,
|
|
466
|
+
insertText: `${d.key}=`,
|
|
467
|
+
range,
|
|
468
|
+
detail: d.values ? d.description : `${d.description} — ${d.valueHint}`,
|
|
469
|
+
sortText: String(i).padStart(4, '0'),
|
|
470
|
+
// Closed-set keys chain straight into their value list.
|
|
471
|
+
...(d.values
|
|
472
|
+
? { command: { id: 'editor.action.triggerSuggest', title: '' } }
|
|
473
|
+
: {}),
|
|
474
|
+
}),
|
|
475
|
+
);
|
|
476
|
+
return { suggestions };
|
|
477
|
+
},
|
|
478
|
+
},
|
|
479
|
+
);
|
|
327
480
|
}
|
|
328
481
|
|
|
329
482
|
// Chat-composer mode: intercept Enter before Monaco inserts a newline.
|
|
@@ -401,6 +554,8 @@ export function RawEditor({
|
|
|
401
554
|
mentionCompletionDisposable.current = null;
|
|
402
555
|
iconCompletionDisposable.current?.dispose();
|
|
403
556
|
iconCompletionDisposable.current = null;
|
|
557
|
+
attrCompletionDisposable.current?.dispose();
|
|
558
|
+
attrCompletionDisposable.current = null;
|
|
404
559
|
iconGlyphDecorations.current?.clear();
|
|
405
560
|
iconGlyphDecorations.current = null;
|
|
406
561
|
dropCleanupRef.current?.();
|
|
@@ -414,24 +569,31 @@ export function RawEditor({
|
|
|
414
569
|
(value) => {
|
|
415
570
|
if (isExternalUpdate.current) return;
|
|
416
571
|
if (value !== undefined) {
|
|
417
|
-
|
|
572
|
+
setEditorSource(value);
|
|
418
573
|
}
|
|
419
574
|
},
|
|
420
|
-
[
|
|
575
|
+
[setEditorSource],
|
|
421
576
|
);
|
|
422
577
|
|
|
423
|
-
// When external changes happen (e.g. from WYSIWYG
|
|
578
|
+
// When external changes happen (e.g. from WYSIWYG, or navigating to another
|
|
579
|
+
// block in block-at-a-time mode), update Monaco. In block mode `editorSource`
|
|
580
|
+
// is the active block's slice, so this swaps Monaco's content on navigation.
|
|
424
581
|
useEffect(() => {
|
|
425
582
|
const editor = editorRef.current;
|
|
426
583
|
if (editor) {
|
|
427
584
|
const currentValue = editor.getValue();
|
|
428
|
-
|
|
585
|
+
// Ignore trailing-whitespace-only differences from block-mode splice
|
|
586
|
+
// normalization — calling setValue for those resets the Monaco cursor.
|
|
587
|
+
if (
|
|
588
|
+
currentValue !== editorSource &&
|
|
589
|
+
currentValue.replace(/\s+$/, '') !== editorSource.replace(/\s+$/, '')
|
|
590
|
+
) {
|
|
429
591
|
isExternalUpdate.current = true;
|
|
430
|
-
editor.setValue(
|
|
592
|
+
editor.setValue(editorSource);
|
|
431
593
|
isExternalUpdate.current = false;
|
|
432
594
|
}
|
|
433
595
|
}
|
|
434
|
-
}, [
|
|
596
|
+
}, [editorSource]);
|
|
435
597
|
|
|
436
598
|
// ── Inline FontAwesome glyph decorations ────────────
|
|
437
599
|
// Walk the markdown source on every change, find each resolvable
|
|
@@ -482,9 +644,9 @@ export function RawEditor({
|
|
|
482
644
|
} else {
|
|
483
645
|
iconGlyphDecorations.current.set(decorations);
|
|
484
646
|
}
|
|
485
|
-
}, [
|
|
647
|
+
}, [editorSource, language, monacoNs]);
|
|
486
648
|
|
|
487
|
-
const effectiveTheme = SQUISQ_THEMES[
|
|
649
|
+
const effectiveTheme = SQUISQ_THEMES[monacoTheme] ?? monacoTheme;
|
|
488
650
|
|
|
489
651
|
// Wait for the lazy monaco namespace + `loader.config()` to settle
|
|
490
652
|
// before mounting `<Editor>`. Without this gate, the @monaco-editor/
|
|
@@ -516,7 +678,7 @@ export function RawEditor({
|
|
|
516
678
|
<div className={className} style={{ width: '100%', height: '100%' }} data-testid="raw-editor">
|
|
517
679
|
<Editor
|
|
518
680
|
defaultLanguage={language}
|
|
519
|
-
value={
|
|
681
|
+
value={editorSource}
|
|
520
682
|
theme={effectiveTheme}
|
|
521
683
|
beforeMount={handleBeforeMount}
|
|
522
684
|
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
|
+
}
|