@bendyline/squisq-editor-react 2.0.1 → 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.
Files changed (70) hide show
  1. package/README.md +10 -2
  2. package/dist/index.d.ts +505 -15
  3. package/dist/index.js +6035 -2283
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles/index.css +870 -40
  6. package/package.json +6 -5
  7. package/src/EditorContext.tsx +27 -0
  8. package/src/EditorShell.tsx +24 -0
  9. package/src/PreviewControls.tsx +20 -5
  10. package/src/PreviewPanel.tsx +5 -1
  11. package/src/RawEditor.tsx +12 -0
  12. package/src/RecorderEntry.tsx +3 -0
  13. package/src/Toolbar.tsx +315 -17
  14. package/src/WysiwygEditor.tsx +25 -11
  15. package/src/__tests__/buildPreviewDocContent.test.ts +17 -0
  16. package/src/__tests__/editorShellProps.test.tsx +65 -0
  17. package/src/__tests__/findMode.test.tsx +104 -0
  18. package/src/__tests__/findModel.test.ts +55 -0
  19. package/src/__tests__/markdownCodeFence.test.ts +45 -0
  20. package/src/__tests__/mediaReferences.test.ts +15 -0
  21. package/src/__tests__/previewControls.test.tsx +31 -0
  22. package/src/__tests__/tiptapBridge.test.ts +9 -0
  23. package/src/__tests__/toolbarSelectionConversion.test.tsx +26 -0
  24. package/src/__tests__/writeCanvasSettings.test.ts +15 -0
  25. package/src/asciiDiagram/__tests__/AsciiDiagramExtension.test.ts +20 -1
  26. package/src/buildPreviewDoc.ts +9 -7
  27. package/src/codeSnippet/CodeSnippetExtension.ts +205 -0
  28. package/src/codeSnippet/CodeSnippetWidget.tsx +109 -0
  29. package/src/codeSnippet/__tests__/CodeSnippetExtension.test.ts +95 -0
  30. package/src/codeSnippet/__tests__/codeSnippetLanguages.test.ts +50 -0
  31. package/src/codeSnippet/codeSnippetCommands.ts +21 -0
  32. package/src/codeSnippet/codeSnippetData.ts +43 -0
  33. package/src/codeSnippet/codeSnippetLanguages.ts +216 -0
  34. package/src/diagram/DiagramCanvas.tsx +1 -0
  35. package/src/find/FindHighlightExtension.ts +81 -0
  36. package/src/find/FindToolbar.tsx +314 -0
  37. package/src/find/findModel.ts +77 -0
  38. package/src/index.ts +89 -0
  39. package/src/markdownCodeFence.ts +72 -0
  40. package/src/mediaReferences.ts +5 -3
  41. package/src/mermaid/MermaidDiagramCanvas.tsx +693 -0
  42. package/src/mermaid/MermaidDiagramExtension.ts +243 -0
  43. package/src/mermaid/MermaidDiagramTypeThumbnail.tsx +184 -0
  44. package/src/mermaid/MermaidDiagramWidget.tsx +653 -0
  45. package/src/mermaid/MermaidShapePalette.tsx +245 -0
  46. package/src/mermaid/__tests__/MermaidDiagramExtension.test.ts +361 -0
  47. package/src/mermaid/__tests__/mermaidDiagramTypes.test.ts +35 -0
  48. package/src/mermaid/__tests__/mermaidRenderer.test.ts +49 -0
  49. package/src/mermaid/__tests__/mermaidSourceOps.test.ts +213 -0
  50. package/src/mermaid/__tests__/mermaidSyntax.test.ts +71 -0
  51. package/src/mermaid/mermaidCommands.ts +34 -0
  52. package/src/mermaid/mermaidData.ts +31 -0
  53. package/src/mermaid/mermaidDiagramTypes.ts +325 -0
  54. package/src/mermaid/mermaidModel.ts +31 -0
  55. package/src/mermaid/mermaidRenderer.ts +181 -0
  56. package/src/mermaid/mermaidShapes.ts +113 -0
  57. package/src/mermaid/mermaidSourceOps.ts +454 -0
  58. package/src/scene/Scene.tsx +46 -15
  59. package/src/scene/SceneBlockToolbar.tsx +29 -14
  60. package/src/scene/SceneViewControls.tsx +43 -0
  61. package/src/scene/__tests__/fitScale.test.ts +19 -0
  62. package/src/scene/__tests__/useScenePanZoom.test.ts +28 -1
  63. package/src/scene/fitScale.ts +17 -0
  64. package/src/scene/hooks/useScenePanZoom.ts +11 -4
  65. package/src/scene/scene.css +85 -3
  66. package/src/styles/code-snippet.css +76 -0
  67. package/src/styles/editor.css +322 -2
  68. package/src/styles/index.css +2 -0
  69. package/src/styles/mermaid-diagram.css +487 -0
  70. 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.0.1",
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.0.1",
60
- "@bendyline/squisq-formats": "2.0.1",
61
- "@bendyline/squisq-react": "2.0.1",
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",
@@ -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,
@@ -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,
@@ -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;
@@ -616,6 +616,20 @@ const SUMMARIZE_TOOLTIP =
616
616
  type ControlKey = 'format' | 'theme' | 'transform' | 'captions' | 'cover';
617
617
  const CONTROL_KEYS: ControlKey[] = ['format', 'theme', 'transform', 'captions', 'cover'];
618
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
+
619
633
  const PREVIEW_POPOVER_GAP = 4;
620
634
  const PREVIEW_POPOVER_MARGIN = 8;
621
635
  const PREVIEW_POPOVER_FALLBACK_WIDTH = 220;
@@ -666,6 +680,7 @@ const selectStyle: React.CSSProperties = {
666
680
  */
667
681
  export function PreviewToolbarControls() {
668
682
  const s = usePreviewSettings();
683
+ const controlKeys = controlKeysForMode(s.activeDisplayMode);
669
684
  const [visibleCount, setVisibleCount] = useState(CONTROL_KEYS.length);
670
685
  const [popoverOpen, setPopoverOpen] = useState(false);
671
686
  // `rootRef` (flex:1) always spans the toolbar's leftover width, so its
@@ -732,7 +747,7 @@ export function PreviewToolbarControls() {
732
747
  ro.observe(probe);
733
748
  measure();
734
749
  return () => ro.disconnect();
735
- }, []);
750
+ }, [controlKeys.length]);
736
751
 
737
752
  // Close popover on outside click
738
753
  useEffect(() => {
@@ -900,9 +915,9 @@ export function PreviewToolbarControls() {
900
915
  }
901
916
  };
902
917
 
903
- const hasOverflow = visibleCount < CONTROL_KEYS.length;
904
- const visibleKeys = CONTROL_KEYS.slice(0, visibleCount);
905
- const overflowKeys = CONTROL_KEYS.slice(visibleCount);
918
+ const hasOverflow = visibleCount < controlKeys.length;
919
+ const visibleKeys = controlKeys.slice(0, visibleCount);
920
+ const overflowKeys = controlKeys.slice(visibleCount);
906
921
 
907
922
  useEffect(() => {
908
923
  if (!hasOverflow && popoverOpen) closePopover();
@@ -920,7 +935,7 @@ export function PreviewToolbarControls() {
920
935
  inline/overflow split. Absolutely positioned so it never affects
921
936
  layout. */}
922
937
  <div className="squisq-preview-controls-probe" ref={probeRef} aria-hidden="true">
923
- {CONTROL_KEYS.map((key) => renderControl(key, false))}
938
+ {controlKeys.map((key) => renderControl(key, false))}
924
939
  </div>
925
940
 
926
941
  {visibleKeys.length > 0 && (
@@ -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;
@@ -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