@bendyline/squisq-editor-react 2.0.0 → 2.1.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 +10 -2
- package/dist/index.d.ts +579 -47
- package/dist/index.js +7477 -2943
- package/dist/index.js.map +1 -1
- package/dist/styles/index.css +1008 -38
- package/package.json +6 -5
- package/src/DocumentSettingsDialog.tsx +32 -18
- package/src/EditorContext.tsx +27 -0
- package/src/EditorShell.tsx +25 -1
- package/src/PreviewControls.tsx +77 -29
- package/src/PreviewPanel.tsx +5 -1
- package/src/RawEditor.tsx +12 -0
- package/src/RecorderEntry.tsx +5 -0
- package/src/Toolbar.tsx +479 -36
- package/src/WysiwygEditor.tsx +25 -11
- package/src/__tests__/buildPreviewDocContent.test.ts +17 -0
- package/src/__tests__/codeContextSectionView.test.tsx +8 -6
- package/src/__tests__/documentSettingsDialog.test.tsx +22 -0
- package/src/__tests__/editorShellProps.test.tsx +65 -0
- package/src/__tests__/findMode.test.tsx +104 -0
- package/src/__tests__/findModel.test.ts +55 -0
- package/src/__tests__/markdownCodeFence.test.ts +45 -0
- package/src/__tests__/mediaAttachmentFlow.test.ts +2 -2
- package/src/__tests__/mediaReferences.test.ts +15 -0
- package/src/__tests__/previewControls.test.tsx +195 -0
- package/src/__tests__/recorderTheme.test.tsx +42 -0
- package/src/__tests__/selectionConversions.test.ts +80 -0
- package/src/__tests__/tiptapBridge.test.ts +57 -7
- package/src/__tests__/tiptapImageRoundTrip.test.ts +1 -1
- package/src/__tests__/toolbarSelectionConversion.test.tsx +190 -0
- package/src/__tests__/writeCanvasSettings.test.ts +15 -0
- package/src/asciiDiagram/AsciiDiagramWidget.tsx +34 -4
- package/src/asciiDiagram/__tests__/AsciiDiagramExtension.test.ts +20 -1
- package/src/asciiDiagram/__tests__/asciiDiagramCommands.test.ts +58 -1
- package/src/asciiDiagram/asciiDiagramCommands.ts +36 -10
- package/src/asciiDiagram/asciiDiagramData.ts +33 -0
- package/src/asciiDiagram/asciiDiagramOps.ts +19 -0
- package/src/buildPreviewDoc.ts +9 -7
- package/src/codeContext/types.ts +1 -1
- package/src/codeSnippet/CodeSnippetExtension.ts +205 -0
- package/src/codeSnippet/CodeSnippetWidget.tsx +109 -0
- package/src/codeSnippet/__tests__/CodeSnippetExtension.test.ts +95 -0
- package/src/codeSnippet/__tests__/codeSnippetLanguages.test.ts +50 -0
- package/src/codeSnippet/codeSnippetCommands.ts +21 -0
- package/src/codeSnippet/codeSnippetData.ts +43 -0
- package/src/codeSnippet/codeSnippetLanguages.ts +216 -0
- package/src/customTemplates/__tests__/useMemoryLayerAdapter.test.ts +13 -0
- package/src/customTemplates/useMemoryLayerAdapter.ts +7 -1
- package/src/diagram/DiagramCanvas.tsx +17 -2
- package/src/find/FindHighlightExtension.ts +81 -0
- package/src/find/FindToolbar.tsx +314 -0
- package/src/find/findModel.ts +77 -0
- package/src/frontmatterSettings.ts +23 -0
- package/src/index.ts +96 -1
- package/src/markdownCodeFence.ts +72 -0
- package/src/mediaReferences.ts +5 -3
- package/src/mermaid/MermaidDiagramCanvas.tsx +693 -0
- package/src/mermaid/MermaidDiagramExtension.ts +243 -0
- package/src/mermaid/MermaidDiagramTypeThumbnail.tsx +184 -0
- package/src/mermaid/MermaidDiagramWidget.tsx +653 -0
- package/src/mermaid/MermaidShapePalette.tsx +245 -0
- package/src/mermaid/__tests__/MermaidDiagramExtension.test.ts +361 -0
- package/src/mermaid/__tests__/mermaidDiagramTypes.test.ts +35 -0
- package/src/mermaid/__tests__/mermaidRenderer.test.ts +49 -0
- package/src/mermaid/__tests__/mermaidSourceOps.test.ts +213 -0
- package/src/mermaid/__tests__/mermaidSyntax.test.ts +71 -0
- package/src/mermaid/mermaidCommands.ts +34 -0
- package/src/mermaid/mermaidData.ts +31 -0
- package/src/mermaid/mermaidDiagramTypes.ts +325 -0
- package/src/mermaid/mermaidModel.ts +31 -0
- package/src/mermaid/mermaidRenderer.ts +181 -0
- package/src/mermaid/mermaidShapes.ts +113 -0
- package/src/mermaid/mermaidSourceOps.ts +454 -0
- package/src/recorder/RecorderButton.tsx +9 -1
- package/src/recorder/RecorderModal.tsx +84 -41
- package/src/recorder/RecorderPanel.tsx +9 -1
- package/src/scene/Scene.tsx +46 -15
- package/src/scene/SceneBlockToolbar.tsx +29 -14
- package/src/scene/SceneViewControls.tsx +43 -0
- package/src/scene/__tests__/fitScale.test.ts +19 -0
- package/src/scene/__tests__/sceneIsolation.test.tsx +27 -1
- package/src/scene/__tests__/useScenePanZoom.test.ts +28 -1
- package/src/scene/adapters/DrawingAdapter.ts +6 -1
- package/src/scene/adapters/LayoutAdapter.ts +3 -0
- package/src/scene/commands/SceneCommand.ts +13 -2
- package/src/scene/fitScale.ts +17 -0
- package/src/scene/hooks/useScenePanZoom.ts +11 -4
- package/src/scene/scene.css +85 -3
- package/src/scene/tools/SelectTool.ts +5 -5
- package/src/selectionConversions.ts +155 -0
- package/src/styles/ascii-timeline.css +101 -4
- package/src/styles/code-snippet.css +76 -0
- package/src/styles/editor.css +352 -2
- package/src/styles/index.css +2 -0
- package/src/styles/mermaid-diagram.css +487 -0
- package/src/styles/tree-view.css +51 -1
- package/src/timeline/TimelineEditorWidget.tsx +200 -41
- package/src/timeline/__tests__/TimelineEditorWidget.test.tsx +61 -2
- package/src/timeline/__tests__/timelineCommands.test.ts +32 -0
- package/src/timeline/__tests__/timelineOps.test.ts +55 -0
- package/src/timeline/timelineCommands.ts +54 -0
- package/src/timeline/timelineOps.ts +107 -3
- package/src/tiptapBridge.ts +23 -5
- package/src/treeview/TreeOutlineWidget.tsx +153 -3
- package/src/treeview/__tests__/TreeOutlineWidget.test.tsx +156 -0
- package/src/treeview/__tests__/treeOps.test.ts +52 -0
- package/src/treeview/__tests__/treeViewCommands.test.ts +16 -0
- package/src/treeview/treeOps.ts +59 -0
- package/src/treeview/treeViewCommands.ts +5 -0
- package/src/writeCanvasSettings.ts +30 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bendyline/squisq-editor-react",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "React editor shell with raw/WYSIWYG/preview modes for Squisq documents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Bendyline",
|
|
@@ -56,9 +56,9 @@
|
|
|
56
56
|
}
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@bendyline/squisq": "2.
|
|
60
|
-
"@bendyline/squisq-formats": "2.
|
|
61
|
-
"@bendyline/squisq-react": "2.
|
|
59
|
+
"@bendyline/squisq": "2.1.0",
|
|
60
|
+
"@bendyline/squisq-formats": "2.1.0",
|
|
61
|
+
"@bendyline/squisq-react": "2.1.0",
|
|
62
62
|
"@fortawesome/fontawesome-free": "7.2.0",
|
|
63
63
|
"@monaco-editor/react": "4.7.0",
|
|
64
64
|
"@tiptap/extension-image": "2.27.2",
|
|
@@ -74,7 +74,8 @@
|
|
|
74
74
|
"@tiptap/pm": "2.27.2",
|
|
75
75
|
"@tiptap/react": "2.27.2",
|
|
76
76
|
"@tiptap/starter-kit": "2.27.2",
|
|
77
|
-
"@tiptap/suggestion": "2.27.2"
|
|
77
|
+
"@tiptap/suggestion": "2.27.2",
|
|
78
|
+
"mermaid": "11.16.0"
|
|
78
79
|
},
|
|
79
80
|
"devDependencies": {
|
|
80
81
|
"@types/react": "18.3.28",
|
|
@@ -25,17 +25,14 @@ import {
|
|
|
25
25
|
import { getTransformStyleSummaries } from '@bendyline/squisq/transform';
|
|
26
26
|
import { ThemePicker } from './ThemePicker';
|
|
27
27
|
import { resolvePersistedTransformStyleId } from './transformStyleId';
|
|
28
|
+
import {
|
|
29
|
+
FRONTMATTER_SETTING_DEFAULTS,
|
|
30
|
+
FRONTMATTER_SETTING_KEYS,
|
|
31
|
+
omitFrontmatterDefault,
|
|
32
|
+
} from './frontmatterSettings';
|
|
28
33
|
|
|
29
34
|
// ── Frontmatter key constants ─────────────────────────────────────
|
|
30
35
|
|
|
31
|
-
/** Mirror of `FM_KEYS` in PreviewControls.tsx. Canonical names are the
|
|
32
|
-
* ones we write; legacy aliases are still read so older docs work. */
|
|
33
|
-
const FM = {
|
|
34
|
-
theme: { canonical: 'squisq-theme', legacy: ['themeId', 'theme'] },
|
|
35
|
-
transform: { canonical: 'squisq-transform', legacy: 'transform-style' },
|
|
36
|
-
captions: { canonical: 'squisq-captions', legacy: 'caption-style' },
|
|
37
|
-
} as const;
|
|
38
|
-
|
|
39
36
|
function readFm(
|
|
40
37
|
fm: Record<string, unknown> | undefined,
|
|
41
38
|
canonical: string,
|
|
@@ -80,9 +77,17 @@ export function DocumentSettingsDialog({
|
|
|
80
77
|
const currentTheme = readFrontmatterThemeId(frontmatter) ?? '';
|
|
81
78
|
const currentTransform =
|
|
82
79
|
resolvePersistedTransformStyleId(
|
|
83
|
-
readFm(
|
|
80
|
+
readFm(
|
|
81
|
+
frontmatter,
|
|
82
|
+
FRONTMATTER_SETTING_KEYS.transform.canonical,
|
|
83
|
+
FRONTMATTER_SETTING_KEYS.transform.legacy,
|
|
84
|
+
),
|
|
84
85
|
) ?? '';
|
|
85
|
-
const currentCaptions = readFm(
|
|
86
|
+
const currentCaptions = readFm(
|
|
87
|
+
frontmatter,
|
|
88
|
+
FRONTMATTER_SETTING_KEYS.captions.canonical,
|
|
89
|
+
FRONTMATTER_SETTING_KEYS.captions.legacy,
|
|
90
|
+
);
|
|
86
91
|
|
|
87
92
|
const [title, setTitle] = useState(currentTitle);
|
|
88
93
|
const [theme, setTheme] = useState(currentTheme);
|
|
@@ -132,20 +137,29 @@ export function DocumentSettingsDialog({
|
|
|
132
137
|
|
|
133
138
|
const updates: Record<string, string | null> = {
|
|
134
139
|
title: nextTitle,
|
|
135
|
-
[
|
|
140
|
+
[FRONTMATTER_SETTING_KEYS.theme.canonical]: omitFrontmatterDefault(
|
|
141
|
+
theme || FRONTMATTER_SETTING_DEFAULTS.theme,
|
|
142
|
+
FRONTMATTER_SETTING_DEFAULTS.theme,
|
|
143
|
+
),
|
|
136
144
|
// Saving settings canonicalizes all older theme spellings.
|
|
137
|
-
[
|
|
138
|
-
[
|
|
139
|
-
[
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
145
|
+
[FRONTMATTER_SETTING_KEYS.theme.legacy[0]]: null,
|
|
146
|
+
[FRONTMATTER_SETTING_KEYS.theme.legacy[1]]: null,
|
|
147
|
+
[FRONTMATTER_SETTING_KEYS.transform.canonical]: omitFrontmatterDefault(
|
|
148
|
+
transform,
|
|
149
|
+
FRONTMATTER_SETTING_DEFAULTS.transform,
|
|
150
|
+
),
|
|
151
|
+
[FRONTMATTER_SETTING_KEYS.transform.legacy]: null,
|
|
152
|
+
[FRONTMATTER_SETTING_KEYS.captions.canonical]: omitFrontmatterDefault(
|
|
153
|
+
captions || FRONTMATTER_SETTING_DEFAULTS.captions,
|
|
154
|
+
FRONTMATTER_SETTING_DEFAULTS.captions,
|
|
155
|
+
),
|
|
156
|
+
[FRONTMATTER_SETTING_KEYS.captions.legacy]: null,
|
|
143
157
|
};
|
|
144
158
|
|
|
145
159
|
const nextSource = setFrontmatterValues(markdownSource, updates);
|
|
146
160
|
onSave(nextSource);
|
|
147
161
|
},
|
|
148
|
-
[title, theme, transform, captions, inferredTitle, markdownSource,
|
|
162
|
+
[title, theme, transform, captions, inferredTitle, markdownSource, onSave],
|
|
149
163
|
);
|
|
150
164
|
|
|
151
165
|
return (
|
package/src/EditorContext.tsx
CHANGED
|
@@ -153,6 +153,8 @@ export interface EditorState {
|
|
|
153
153
|
colorScheme: EditorColorScheme;
|
|
154
154
|
/** Operating mode — 'markdown' for the full shell, 'code' for Monaco-only. */
|
|
155
155
|
editorMode: EditorMode;
|
|
156
|
+
/** Whether the host-triggered Find toolbar is active. */
|
|
157
|
+
findMode: boolean;
|
|
156
158
|
/** Monaco language ID for the Raw editor. */
|
|
157
159
|
language: string;
|
|
158
160
|
/**
|
|
@@ -263,6 +265,8 @@ export interface EditorActions {
|
|
|
263
265
|
setMarkdownDoc: (doc: MarkdownDocument) => void;
|
|
264
266
|
/** Switch the active view */
|
|
265
267
|
setActiveView: (view: EditorView) => void;
|
|
268
|
+
/** Enter or leave the host-triggered Find toolbar mode. */
|
|
269
|
+
setFindMode: (active: boolean) => void;
|
|
266
270
|
/** Register / unregister the Tiptap editor instance (called by WysiwygEditor) */
|
|
267
271
|
setTiptapEditor: (editor: TiptapEditor | null) => void;
|
|
268
272
|
/** Register / unregister the Monaco editor instance (called by RawEditor) */
|
|
@@ -462,6 +466,13 @@ export interface EditorProviderProps {
|
|
|
462
466
|
fileName?: string;
|
|
463
467
|
/** Explicit Monaco language ID — wins over the fileName-derived one. */
|
|
464
468
|
language?: string;
|
|
469
|
+
/**
|
|
470
|
+
* Controlled Find-mode state. No trigger button is rendered by default;
|
|
471
|
+
* hosts can set this prop or call `setFindMode` from editor context.
|
|
472
|
+
*/
|
|
473
|
+
findMode?: boolean;
|
|
474
|
+
/** Notified when Find mode requests a state change (for example, its X button). */
|
|
475
|
+
onFindModeChange?: (active: boolean) => void;
|
|
465
476
|
/**
|
|
466
477
|
* Initial visibility of the inline preview gutter. Defaults to false.
|
|
467
478
|
* The toolbar's View menu can toggle it at runtime.
|
|
@@ -577,6 +588,8 @@ export function EditorProvider({
|
|
|
577
588
|
allowNarrate = true,
|
|
578
589
|
fileName,
|
|
579
590
|
language,
|
|
591
|
+
findMode: controlledFindMode,
|
|
592
|
+
onFindModeChange,
|
|
580
593
|
inlinePreview = false,
|
|
581
594
|
showStatusBar = true,
|
|
582
595
|
outline = false,
|
|
@@ -618,6 +631,16 @@ export function EditorProvider({
|
|
|
618
631
|
const [activeView, setActiveViewRaw] = useState<EditorView>(
|
|
619
632
|
editorMode === 'markdown' ? initialView : 'raw',
|
|
620
633
|
);
|
|
634
|
+
const [uncontrolledFindMode, setUncontrolledFindMode] = useState(false);
|
|
635
|
+
const findMode = editorMode !== 'image' && (controlledFindMode ?? uncontrolledFindMode);
|
|
636
|
+
const setFindMode = useCallback(
|
|
637
|
+
(active: boolean) => {
|
|
638
|
+
if (editorMode === 'image') return;
|
|
639
|
+
if (controlledFindMode === undefined) setUncontrolledFindMode(active);
|
|
640
|
+
onFindModeChange?.(active);
|
|
641
|
+
},
|
|
642
|
+
[controlledFindMode, editorMode, onFindModeChange],
|
|
643
|
+
);
|
|
621
644
|
const activeViewRef = useRef(activeView);
|
|
622
645
|
activeViewRef.current = activeView;
|
|
623
646
|
const tiptapEditorRef = useRef<TiptapEditor | null>(null);
|
|
@@ -1128,6 +1151,7 @@ export function EditorProvider({
|
|
|
1128
1151
|
isParsing,
|
|
1129
1152
|
colorScheme,
|
|
1130
1153
|
editorMode,
|
|
1154
|
+
findMode,
|
|
1131
1155
|
language: resolvedLanguage,
|
|
1132
1156
|
inlinePreviewVisible,
|
|
1133
1157
|
statusBarVisible,
|
|
@@ -1165,6 +1189,7 @@ export function EditorProvider({
|
|
|
1165
1189
|
addBlock,
|
|
1166
1190
|
setMarkdownDoc,
|
|
1167
1191
|
setActiveView,
|
|
1192
|
+
setFindMode,
|
|
1168
1193
|
setTiptapEditor,
|
|
1169
1194
|
setMonacoEditor,
|
|
1170
1195
|
setColorScheme,
|
|
@@ -1189,6 +1214,7 @@ export function EditorProvider({
|
|
|
1189
1214
|
isParsing,
|
|
1190
1215
|
colorScheme,
|
|
1191
1216
|
editorMode,
|
|
1217
|
+
findMode,
|
|
1192
1218
|
resolvedLanguage,
|
|
1193
1219
|
inlinePreviewVisible,
|
|
1194
1220
|
statusBarVisible,
|
|
@@ -1222,6 +1248,7 @@ export function EditorProvider({
|
|
|
1222
1248
|
addBlock,
|
|
1223
1249
|
setMarkdownDoc,
|
|
1224
1250
|
setActiveView,
|
|
1251
|
+
setFindMode,
|
|
1225
1252
|
setTiptapEditor,
|
|
1226
1253
|
setMonacoEditor,
|
|
1227
1254
|
setColorScheme,
|
package/src/EditorShell.tsx
CHANGED
|
@@ -65,6 +65,7 @@ import {
|
|
|
65
65
|
import type { PrunePolicy, SaveVersionResult } from '@bendyline/squisq/versions';
|
|
66
66
|
import type { CSSProperties, KeyboardEvent as ReactKeyboardEvent, ReactNode } from 'react';
|
|
67
67
|
import { MediaContext } from '@bendyline/squisq-react';
|
|
68
|
+
import { writeCanvasSettingsStyle, type WriteCanvasSettings } from './writeCanvasSettings';
|
|
68
69
|
|
|
69
70
|
export type { EditorColorScheme } from './EditorContext';
|
|
70
71
|
|
|
@@ -210,6 +211,12 @@ export interface EditorShellProps {
|
|
|
210
211
|
* toolbar, tabs, and status bar keep their normal sizing.
|
|
211
212
|
*/
|
|
212
213
|
thinMargins?: boolean;
|
|
214
|
+
/**
|
|
215
|
+
* Host-controlled typography for the markdown Write canvas. `textSize` is
|
|
216
|
+
* measured in CSS pixels and `lineSpacing` is a unitless line-height
|
|
217
|
+
* multiplier. This is presentation-only and does not modify the markdown.
|
|
218
|
+
*/
|
|
219
|
+
writeCanvasSettings?: WriteCanvasSettings;
|
|
213
220
|
/**
|
|
214
221
|
* Render the bottom status bar (word / character / line / block counts
|
|
215
222
|
* and parse-state indicator). Defaults to `true`. Set to `false` in
|
|
@@ -243,6 +250,14 @@ export interface EditorShellProps {
|
|
|
243
250
|
* switches the shell into code mode.
|
|
244
251
|
*/
|
|
245
252
|
language?: string;
|
|
253
|
+
/**
|
|
254
|
+
* Controlled Find-mode state. Find has no built-in trigger button; hosts
|
|
255
|
+
* opt in by setting this prop (or by calling `setFindMode` from a toolbar
|
|
256
|
+
* slot rendered inside the editor context).
|
|
257
|
+
*/
|
|
258
|
+
findMode?: boolean;
|
|
259
|
+
/** Called when Find mode requests a state change, including its X button. */
|
|
260
|
+
onFindModeChange?: (active: boolean) => void;
|
|
246
261
|
/**
|
|
247
262
|
* Optional async provider for `@`-mention suggestions. When supplied,
|
|
248
263
|
* typing `@` inside the editor opens a popover of candidates; selecting
|
|
@@ -431,10 +446,13 @@ export function EditorShell({
|
|
|
431
446
|
fullWidth = false,
|
|
432
447
|
uxFont,
|
|
433
448
|
thinMargins = false,
|
|
449
|
+
writeCanvasSettings,
|
|
434
450
|
showStatusBar = true,
|
|
435
451
|
imageDisplayMode = 'inline',
|
|
436
452
|
fileName,
|
|
437
453
|
language,
|
|
454
|
+
findMode,
|
|
455
|
+
onFindModeChange,
|
|
438
456
|
mentionProvider,
|
|
439
457
|
documentLinkProvider,
|
|
440
458
|
allowRecording = true,
|
|
@@ -498,6 +516,8 @@ export function EditorShell({
|
|
|
498
516
|
allowNarrate={allowNarrate}
|
|
499
517
|
fileName={fileName}
|
|
500
518
|
language={language}
|
|
519
|
+
findMode={findMode}
|
|
520
|
+
onFindModeChange={onFindModeChange}
|
|
501
521
|
inlinePreview={inlinePreview}
|
|
502
522
|
showStatusBar={showStatusBar}
|
|
503
523
|
outline={outline}
|
|
@@ -527,6 +547,7 @@ export function EditorShell({
|
|
|
527
547
|
fullWidth={fullWidth}
|
|
528
548
|
uxFont={uxFont}
|
|
529
549
|
thinMargins={thinMargins}
|
|
550
|
+
writeCanvasSettings={writeCanvasSettings}
|
|
530
551
|
readOnly={readOnly}
|
|
531
552
|
imageSrc={imageSrc}
|
|
532
553
|
imageAlt={imageAlt}
|
|
@@ -564,6 +585,7 @@ interface EditorShellInnerProps {
|
|
|
564
585
|
fullWidth: boolean;
|
|
565
586
|
uxFont?: string;
|
|
566
587
|
thinMargins: boolean;
|
|
588
|
+
writeCanvasSettings?: WriteCanvasSettings;
|
|
567
589
|
readOnly: boolean;
|
|
568
590
|
imageSrc?: string;
|
|
569
591
|
imageAlt?: string;
|
|
@@ -597,6 +619,7 @@ function EditorShellInner({
|
|
|
597
619
|
fullWidth,
|
|
598
620
|
uxFont,
|
|
599
621
|
thinMargins,
|
|
622
|
+
writeCanvasSettings,
|
|
600
623
|
readOnly,
|
|
601
624
|
imageSrc,
|
|
602
625
|
imageAlt,
|
|
@@ -716,7 +739,7 @@ function EditorShellInner({
|
|
|
716
739
|
*
|
|
717
740
|
* Without this, upload-via-MediaBin and upload-via-drop both
|
|
718
741
|
* added the file to the bin and nowhere else — the composer sent
|
|
719
|
-
* an empty body and the downstream
|
|
742
|
+
* an empty body and the downstream consumer reported "nothing came
|
|
720
743
|
* through."
|
|
721
744
|
*/
|
|
722
745
|
const insertMediaRef = useCallback(
|
|
@@ -862,6 +885,7 @@ function EditorShellInner({
|
|
|
862
885
|
// tabs, status bar) consume `--squisq-ux-font` as their
|
|
863
886
|
// `font-family`, falling back to the system stack when unset.
|
|
864
887
|
...(uxFont ? ({ '--squisq-ux-font': uxFont } as CSSProperties) : {}),
|
|
888
|
+
...writeCanvasSettingsStyle(writeCanvasSettings),
|
|
865
889
|
// Exposed so the toolbar's view-tabs section AND the outline pane read
|
|
866
890
|
// one shared width, lining up the view-tabs' right-edge separator with
|
|
867
891
|
// the outline's right edge. A fixed `outlineWidth` pins it to px;
|
package/src/PreviewControls.tsx
CHANGED
|
@@ -47,6 +47,11 @@ import {
|
|
|
47
47
|
} from './customThemes';
|
|
48
48
|
import { Icon } from './Icon';
|
|
49
49
|
import { resolvePersistedTransformStyleId } from './transformStyleId';
|
|
50
|
+
import {
|
|
51
|
+
FRONTMATTER_SETTING_DEFAULTS,
|
|
52
|
+
FRONTMATTER_SETTING_KEYS,
|
|
53
|
+
omitFrontmatterDefault,
|
|
54
|
+
} from './frontmatterSettings';
|
|
50
55
|
|
|
51
56
|
// ── Context ──────────────────────────────────────────────────────
|
|
52
57
|
|
|
@@ -198,16 +203,6 @@ export interface PreviewSettingsProviderProps {
|
|
|
198
203
|
themeOverride?: Theme | null;
|
|
199
204
|
}
|
|
200
205
|
|
|
201
|
-
/** Frontmatter keys we read/write for preview settings. The squisq-prefixed
|
|
202
|
-
* keys are the canonical names; the legacy keys are still read so existing
|
|
203
|
-
* documents keep working. Persistence (writes) uses only the squisq names. */
|
|
204
|
-
const FM_KEYS = {
|
|
205
|
-
theme: { canonical: 'squisq-theme', legacy: ['themeId', 'theme'] as const },
|
|
206
|
-
transform: { canonical: 'squisq-transform', legacy: 'transform-style' as const },
|
|
207
|
-
captions: { canonical: 'squisq-captions', legacy: 'caption-style' as const },
|
|
208
|
-
coverSlide: { canonical: 'squisq-cover-slide', legacy: 'cover-slide' as const },
|
|
209
|
-
} as const;
|
|
210
|
-
|
|
211
206
|
function readFrontmatterKey(
|
|
212
207
|
fm: Record<string, unknown> | undefined,
|
|
213
208
|
canonical: string,
|
|
@@ -226,7 +221,7 @@ export function PreviewSettingsProvider({
|
|
|
226
221
|
const { markdownSource, setMarkdownSource, allowNarrate } = useEditorContext();
|
|
227
222
|
|
|
228
223
|
const persistFrontmatter = useCallback(
|
|
229
|
-
(updates: Record<string, string | null>) => {
|
|
224
|
+
(updates: Record<string, string | number | boolean | null | undefined>) => {
|
|
230
225
|
const next = setFrontmatterValues(markdownSource, updates);
|
|
231
226
|
if (next !== markdownSource) {
|
|
232
227
|
setMarkdownSource(next);
|
|
@@ -271,7 +266,7 @@ export function PreviewSettingsProvider({
|
|
|
271
266
|
);
|
|
272
267
|
const [selectedThemeId, setSelectedThemeId] = useState<string | null>(null);
|
|
273
268
|
useEffect(() => setSelectedThemeId(null), [fmTheme]);
|
|
274
|
-
const resolvedThemeId = selectedThemeId ?? fmTheme ??
|
|
269
|
+
const resolvedThemeId = selectedThemeId ?? fmTheme ?? FRONTMATTER_SETTING_DEFAULTS.theme;
|
|
275
270
|
// Doc themes precede browser-library themes in `allThemes`; choosing a
|
|
276
271
|
// library-only entry copies it into the document below for portable export.
|
|
277
272
|
const resolvedTheme = useMemo(
|
|
@@ -300,9 +295,12 @@ export function PreviewSettingsProvider({
|
|
|
300
295
|
const selectedCustom = customThemes.find((theme) => theme.id === id);
|
|
301
296
|
const alreadyDocScoped = docThemes.some((theme) => theme.id === id);
|
|
302
297
|
const updates: Record<string, string | null> = {
|
|
303
|
-
[
|
|
304
|
-
|
|
305
|
-
|
|
298
|
+
[FRONTMATTER_SETTING_KEYS.theme.canonical]: omitFrontmatterDefault(
|
|
299
|
+
id,
|
|
300
|
+
FRONTMATTER_SETTING_DEFAULTS.theme,
|
|
301
|
+
),
|
|
302
|
+
[FRONTMATTER_SETTING_KEYS.theme.legacy[0]]: null,
|
|
303
|
+
[FRONTMATTER_SETTING_KEYS.theme.legacy[1]]: null,
|
|
306
304
|
};
|
|
307
305
|
if (selectedCustom && !alreadyDocScoped) {
|
|
308
306
|
updates[FRONTMATTER_CUSTOM_THEMES_KEY] =
|
|
@@ -337,7 +335,7 @@ export function PreviewSettingsProvider({
|
|
|
337
335
|
idx >= 0 ? docThemes.map((t, i) => (i === idx ? theme : t)) : [...docThemes, theme];
|
|
338
336
|
const updates: Record<string, string | null> = {
|
|
339
337
|
[FRONTMATTER_CUSTOM_THEMES_KEY]: writeCustomThemesToFrontmatter(nextThemes) ?? null,
|
|
340
|
-
[
|
|
338
|
+
[FRONTMATTER_SETTING_KEYS.theme.canonical]: theme.id,
|
|
341
339
|
};
|
|
342
340
|
if (extras?.templates && extras.templates.length > 0) {
|
|
343
341
|
const existing = doc?.customTemplates ?? [];
|
|
@@ -367,7 +365,11 @@ export function PreviewSettingsProvider({
|
|
|
367
365
|
const fmTransform = useMemo(
|
|
368
366
|
() =>
|
|
369
367
|
resolvePersistedTransformStyleId(
|
|
370
|
-
readFrontmatterKey(
|
|
368
|
+
readFrontmatterKey(
|
|
369
|
+
frontmatter,
|
|
370
|
+
FRONTMATTER_SETTING_KEYS.transform.canonical,
|
|
371
|
+
FRONTMATTER_SETTING_KEYS.transform.legacy,
|
|
372
|
+
),
|
|
371
373
|
),
|
|
372
374
|
[frontmatter],
|
|
373
375
|
);
|
|
@@ -379,7 +381,13 @@ export function PreviewSettingsProvider({
|
|
|
379
381
|
setSelectedTransformStyle(id);
|
|
380
382
|
if (id !== null) {
|
|
381
383
|
// Empty string = "None" — remove the key rather than writing a blank value.
|
|
382
|
-
persistFrontmatter({
|
|
384
|
+
persistFrontmatter({
|
|
385
|
+
[FRONTMATTER_SETTING_KEYS.transform.canonical]: omitFrontmatterDefault(
|
|
386
|
+
id,
|
|
387
|
+
FRONTMATTER_SETTING_DEFAULTS.transform,
|
|
388
|
+
),
|
|
389
|
+
[FRONTMATTER_SETTING_KEYS.transform.legacy]: null,
|
|
390
|
+
});
|
|
383
391
|
}
|
|
384
392
|
},
|
|
385
393
|
[persistFrontmatter],
|
|
@@ -391,19 +399,30 @@ export function PreviewSettingsProvider({
|
|
|
391
399
|
const fmCaptionMode = useMemo(
|
|
392
400
|
() =>
|
|
393
401
|
resolveFrontmatterCaptionMode(
|
|
394
|
-
readFrontmatterKey(
|
|
402
|
+
readFrontmatterKey(
|
|
403
|
+
frontmatter,
|
|
404
|
+
FRONTMATTER_SETTING_KEYS.captions.canonical,
|
|
405
|
+
FRONTMATTER_SETTING_KEYS.captions.legacy,
|
|
406
|
+
),
|
|
395
407
|
),
|
|
396
408
|
[frontmatter],
|
|
397
409
|
);
|
|
398
410
|
const [selectedCaptionMode, setSelectedCaptionMode] = useState<CaptionMode | null>(null);
|
|
399
411
|
useEffect(() => setSelectedCaptionMode(null), [fmCaptionMode]);
|
|
400
|
-
const activeCaptionMode =
|
|
412
|
+
const activeCaptionMode =
|
|
413
|
+
selectedCaptionMode ?? fmCaptionMode ?? FRONTMATTER_SETTING_DEFAULTS.captions;
|
|
401
414
|
const activeCaptionsEnabled = activeCaptionMode !== 'off';
|
|
402
415
|
const activeCaptionStyle: CaptionStyle = activeCaptionMode === 'social' ? 'social' : 'standard';
|
|
403
416
|
const handleSetCaptionMode = useCallback(
|
|
404
417
|
(mode: CaptionMode) => {
|
|
405
418
|
setSelectedCaptionMode(mode);
|
|
406
|
-
persistFrontmatter({
|
|
419
|
+
persistFrontmatter({
|
|
420
|
+
[FRONTMATTER_SETTING_KEYS.captions.canonical]: omitFrontmatterDefault(
|
|
421
|
+
mode,
|
|
422
|
+
FRONTMATTER_SETTING_DEFAULTS.captions,
|
|
423
|
+
),
|
|
424
|
+
[FRONTMATTER_SETTING_KEYS.captions.legacy]: null,
|
|
425
|
+
});
|
|
407
426
|
},
|
|
408
427
|
[persistFrontmatter],
|
|
409
428
|
);
|
|
@@ -413,17 +432,31 @@ export function PreviewSettingsProvider({
|
|
|
413
432
|
const fmCoverSlide = useMemo(
|
|
414
433
|
() =>
|
|
415
434
|
resolveFrontmatterBoolean(
|
|
416
|
-
readFrontmatterKey(
|
|
435
|
+
readFrontmatterKey(
|
|
436
|
+
frontmatter,
|
|
437
|
+
FRONTMATTER_SETTING_KEYS.coverSlide.canonical,
|
|
438
|
+
FRONTMATTER_SETTING_KEYS.coverSlide.legacy,
|
|
439
|
+
),
|
|
417
440
|
),
|
|
418
441
|
[frontmatter],
|
|
419
442
|
);
|
|
420
443
|
const [selectedCoverSlide, setSelectedCoverSlide] = useState<boolean | null>(null);
|
|
421
444
|
useEffect(() => setSelectedCoverSlide(null), [fmCoverSlide]);
|
|
422
|
-
const activeCoverSlide =
|
|
445
|
+
const activeCoverSlide =
|
|
446
|
+
selectedCoverSlide ?? fmCoverSlide ?? FRONTMATTER_SETTING_DEFAULTS.coverSlide;
|
|
423
447
|
const handleSetCoverSlideEnabled = useCallback(
|
|
424
448
|
(enabled: boolean) => {
|
|
425
449
|
setSelectedCoverSlide(enabled);
|
|
426
|
-
persistFrontmatter({
|
|
450
|
+
persistFrontmatter({
|
|
451
|
+
// The cover is enabled by default, so only persist the non-default
|
|
452
|
+
// state. Pass the boolean through so YAML writes `false`, not
|
|
453
|
+
// the string `"false"`.
|
|
454
|
+
[FRONTMATTER_SETTING_KEYS.coverSlide.canonical]: omitFrontmatterDefault(
|
|
455
|
+
enabled,
|
|
456
|
+
FRONTMATTER_SETTING_DEFAULTS.coverSlide,
|
|
457
|
+
),
|
|
458
|
+
[FRONTMATTER_SETTING_KEYS.coverSlide.legacy]: null,
|
|
459
|
+
});
|
|
427
460
|
},
|
|
428
461
|
[persistFrontmatter],
|
|
429
462
|
);
|
|
@@ -583,6 +616,20 @@ const SUMMARIZE_TOOLTIP =
|
|
|
583
616
|
type ControlKey = 'format' | 'theme' | 'transform' | 'captions' | 'cover';
|
|
584
617
|
const CONTROL_KEYS: ControlKey[] = ['format', 'theme', 'transform', 'captions', 'cover'];
|
|
585
618
|
|
|
619
|
+
/**
|
|
620
|
+
* Controls that apply to the active display mode. Page (`linear`) is a
|
|
621
|
+
* variable-height HTML rendition: the aspect-ratio format switch only
|
|
622
|
+
* affects embedded canvas sections (which default sensibly) and captions
|
|
623
|
+
* never applied, so both are hidden there. Cover, Theme, and Summarize
|
|
624
|
+
* remain live in every mode.
|
|
625
|
+
*/
|
|
626
|
+
function controlKeysForMode(displayMode: string): ControlKey[] {
|
|
627
|
+
if (displayMode === 'linear') {
|
|
628
|
+
return CONTROL_KEYS.filter((key) => key !== 'format' && key !== 'captions');
|
|
629
|
+
}
|
|
630
|
+
return CONTROL_KEYS;
|
|
631
|
+
}
|
|
632
|
+
|
|
586
633
|
const PREVIEW_POPOVER_GAP = 4;
|
|
587
634
|
const PREVIEW_POPOVER_MARGIN = 8;
|
|
588
635
|
const PREVIEW_POPOVER_FALLBACK_WIDTH = 220;
|
|
@@ -633,6 +680,7 @@ const selectStyle: React.CSSProperties = {
|
|
|
633
680
|
*/
|
|
634
681
|
export function PreviewToolbarControls() {
|
|
635
682
|
const s = usePreviewSettings();
|
|
683
|
+
const controlKeys = controlKeysForMode(s.activeDisplayMode);
|
|
636
684
|
const [visibleCount, setVisibleCount] = useState(CONTROL_KEYS.length);
|
|
637
685
|
const [popoverOpen, setPopoverOpen] = useState(false);
|
|
638
686
|
// `rootRef` (flex:1) always spans the toolbar's leftover width, so its
|
|
@@ -699,7 +747,7 @@ export function PreviewToolbarControls() {
|
|
|
699
747
|
ro.observe(probe);
|
|
700
748
|
measure();
|
|
701
749
|
return () => ro.disconnect();
|
|
702
|
-
}, []);
|
|
750
|
+
}, [controlKeys.length]);
|
|
703
751
|
|
|
704
752
|
// Close popover on outside click
|
|
705
753
|
useEffect(() => {
|
|
@@ -867,9 +915,9 @@ export function PreviewToolbarControls() {
|
|
|
867
915
|
}
|
|
868
916
|
};
|
|
869
917
|
|
|
870
|
-
const hasOverflow = visibleCount <
|
|
871
|
-
const visibleKeys =
|
|
872
|
-
const overflowKeys =
|
|
918
|
+
const hasOverflow = visibleCount < controlKeys.length;
|
|
919
|
+
const visibleKeys = controlKeys.slice(0, visibleCount);
|
|
920
|
+
const overflowKeys = controlKeys.slice(visibleCount);
|
|
873
921
|
|
|
874
922
|
useEffect(() => {
|
|
875
923
|
if (!hasOverflow && popoverOpen) closePopover();
|
|
@@ -887,7 +935,7 @@ export function PreviewToolbarControls() {
|
|
|
887
935
|
inline/overflow split. Absolutely positioned so it never affects
|
|
888
936
|
layout. */}
|
|
889
937
|
<div className="squisq-preview-controls-probe" ref={probeRef} aria-hidden="true">
|
|
890
|
-
{
|
|
938
|
+
{controlKeys.map((key) => renderControl(key, false))}
|
|
891
939
|
</div>
|
|
892
940
|
|
|
893
941
|
{visibleKeys.length > 0 && (
|
package/src/PreviewPanel.tsx
CHANGED
|
@@ -14,7 +14,7 @@ import { createPortal } from 'react-dom';
|
|
|
14
14
|
import { DocPlayer, LinearDocView, useMediaProvider } from '@bendyline/squisq-react';
|
|
15
15
|
import type { AudioController, PlaybackState } from '@bendyline/squisq-react';
|
|
16
16
|
import type { Doc } from '@bendyline/squisq/schemas';
|
|
17
|
-
import { applyTransform } from '@bendyline/squisq/transform';
|
|
17
|
+
import { applyTransform, resolveTransformStyle } from '@bendyline/squisq/transform';
|
|
18
18
|
import { resolveAudioMapping } from '@bendyline/squisq/doc';
|
|
19
19
|
import type { ContentContainer } from '@bendyline/squisq/storage';
|
|
20
20
|
import { useEditorContext } from './EditorContext';
|
|
@@ -333,6 +333,10 @@ export function PreviewPanel({ basePath = '/', className, workspaceContainer }:
|
|
|
333
333
|
viewport={activeViewport}
|
|
334
334
|
theme={activeTheme}
|
|
335
335
|
globalKeyboardShortcuts={!audience}
|
|
336
|
+
showCover={activeCoverSlide}
|
|
337
|
+
transformPage={
|
|
338
|
+
activeTransformStyle ? resolveTransformStyle(activeTransformStyle).page : undefined
|
|
339
|
+
}
|
|
336
340
|
/>
|
|
337
341
|
);
|
|
338
342
|
}
|
package/src/RawEditor.tsx
CHANGED
|
@@ -16,6 +16,7 @@ import { BLOCK_META_KEY_DESCRIPTORS, tokenizeAttrTokens } from '@bendyline/squis
|
|
|
16
16
|
import { SQUISQ_MEDIA_MIME, parseSquisqMediaPayload } from './mediaDragMime';
|
|
17
17
|
import { canHandleSquisqMediaDrop, ownsMonacoModel } from './rawEditorIsolation';
|
|
18
18
|
import { useMonacoLoader } from './useMonacoLoader';
|
|
19
|
+
import { isMarkdownFencedCodeLine, markdownFencedCodeLineMask } from './markdownCodeFence';
|
|
19
20
|
|
|
20
21
|
// Monaco is loaded lazily through `useMonacoLoader` (see the hook for the
|
|
21
22
|
// rationale). The type-only `import type * as monaco from 'monaco-editor'`
|
|
@@ -200,6 +201,9 @@ export function RawEditor({
|
|
|
200
201
|
triggerCharacters: ['['],
|
|
201
202
|
provideCompletionItems(model: monaco.editor.ITextModel, position: monaco.Position) {
|
|
202
203
|
if (!ownsMonacoModel(editor, model)) return { suggestions: [] };
|
|
204
|
+
if (isMarkdownFencedCodeLine(model.getValue(), position.lineNumber)) {
|
|
205
|
+
return { suggestions: [] };
|
|
206
|
+
}
|
|
203
207
|
const lineContent = model.getLineContent(position.lineNumber);
|
|
204
208
|
|
|
205
209
|
// Only trigger inside a heading line that has {[ before the cursor
|
|
@@ -314,6 +318,9 @@ export function RawEditor({
|
|
|
314
318
|
triggerCharacters: ['['],
|
|
315
319
|
provideCompletionItems(model, position) {
|
|
316
320
|
if (!ownsMonacoModel(editor, model)) return { suggestions: [] };
|
|
321
|
+
if (isMarkdownFencedCodeLine(model.getValue(), position.lineNumber)) {
|
|
322
|
+
return { suggestions: [] };
|
|
323
|
+
}
|
|
317
324
|
const lineContent = model.getLineContent(position.lineNumber);
|
|
318
325
|
const textBeforeCursor = lineContent.substring(0, position.column - 1);
|
|
319
326
|
const textAfterCursor = lineContent.substring(position.column - 1);
|
|
@@ -397,6 +404,9 @@ export function RawEditor({
|
|
|
397
404
|
triggerCharacters: ['=', ' '],
|
|
398
405
|
provideCompletionItems(model, position) {
|
|
399
406
|
if (!ownsMonacoModel(editor, model)) return { suggestions: [] };
|
|
407
|
+
if (isMarkdownFencedCodeLine(model.getValue(), position.lineNumber)) {
|
|
408
|
+
return { suggestions: [] };
|
|
409
|
+
}
|
|
400
410
|
const lineContent = model.getLineContent(position.lineNumber);
|
|
401
411
|
// Block-meta attributes only mean something on a heading's
|
|
402
412
|
// template annotation — same gate as the template provider.
|
|
@@ -624,8 +634,10 @@ export function RawEditor({
|
|
|
624
634
|
|
|
625
635
|
const decorations: monaco.editor.IModelDeltaDecoration[] = [];
|
|
626
636
|
const lines = model.getLineCount();
|
|
637
|
+
const fencedLines = markdownFencedCodeLineMask(model.getValue());
|
|
627
638
|
const re = /\{\[([a-zA-Z0-9_:-]+)\]\}/g;
|
|
628
639
|
for (let line = 1; line <= lines; line++) {
|
|
640
|
+
if (fencedLines[line - 1]) continue;
|
|
629
641
|
const text = model.getLineContent(line);
|
|
630
642
|
re.lastIndex = 0;
|
|
631
643
|
let match: RegExpExecArray | null;
|
package/src/RecorderEntry.tsx
CHANGED
|
@@ -26,6 +26,7 @@ import { RecorderPanel } from './recorder/RecorderPanel.js';
|
|
|
26
26
|
import type { RecorderSaveResult } from './recorder/RecorderModal.js';
|
|
27
27
|
import { insertMediaBlock } from './recorder/insertMediaBlock.js';
|
|
28
28
|
import { useEditorContext } from './EditorContext';
|
|
29
|
+
import { markdownFencedCodeLineMask } from './markdownCodeFence';
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* Insert a narration annotation `{[audio=filename]}` onto the heading
|
|
@@ -42,8 +43,10 @@ function annotateMonacoHeading(
|
|
|
42
43
|
if (!model) return false;
|
|
43
44
|
const position = editor.getPosition();
|
|
44
45
|
if (!position) return false;
|
|
46
|
+
const fencedLines = markdownFencedCodeLineMask(model.getValue());
|
|
45
47
|
// Walk upward from the cursor to find the nearest heading line.
|
|
46
48
|
for (let line = position.lineNumber; line >= 1; line--) {
|
|
49
|
+
if (fencedLines[line - 1]) continue;
|
|
47
50
|
const text = model.getLineContent(line);
|
|
48
51
|
if (!/^#{1,6}\s/.test(text)) continue;
|
|
49
52
|
// Skip if the heading already has an audio annotation — don't
|
|
@@ -81,6 +84,7 @@ export function RecorderEntry() {
|
|
|
81
84
|
bumpMediaRevision,
|
|
82
85
|
markdownSource,
|
|
83
86
|
setMarkdownSource,
|
|
87
|
+
colorScheme,
|
|
84
88
|
} = useEditorContext();
|
|
85
89
|
|
|
86
90
|
const handleSave = useCallback(
|
|
@@ -150,6 +154,7 @@ export function RecorderEntry() {
|
|
|
150
154
|
<RecorderPanel
|
|
151
155
|
mediaProvider={mediaProvider}
|
|
152
156
|
container={workspaceContainer}
|
|
157
|
+
colorScheme={colorScheme}
|
|
153
158
|
onSave={handleSave}
|
|
154
159
|
className="squisq-toolbar-button"
|
|
155
160
|
/>
|