@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/src/Toolbar.tsx
CHANGED
|
@@ -52,6 +52,27 @@ import type { PickerEntry } from './emojiData';
|
|
|
52
52
|
import { createPortal } from 'react-dom';
|
|
53
53
|
import { PreviewModeMenu, displayModeLabel, usePreviewSettingsOptional } from './PreviewControls';
|
|
54
54
|
import { filterVisibleMediaEntries } from './mediaEntries';
|
|
55
|
+
import {
|
|
56
|
+
selectionToTable,
|
|
57
|
+
selectionToTableMarkdown,
|
|
58
|
+
selectionToTaskItems,
|
|
59
|
+
selectionToTaskListMarkdown,
|
|
60
|
+
type SelectionTaskItem,
|
|
61
|
+
} from './selectionConversions';
|
|
62
|
+
import { isMarkdownFencedCodeLine, markdownFencedCodeLineMask } from './markdownCodeFence';
|
|
63
|
+
import {
|
|
64
|
+
CODE_SNIPPET_LANGUAGES,
|
|
65
|
+
codeSnippetMarkdown,
|
|
66
|
+
type CodeSnippetLanguage,
|
|
67
|
+
} from './codeSnippet/codeSnippetLanguages';
|
|
68
|
+
import {
|
|
69
|
+
DEFAULT_MERMAID_DIAGRAM_TYPE,
|
|
70
|
+
MERMAID_DIAGRAM_TYPES,
|
|
71
|
+
mermaidDiagramMarkdown,
|
|
72
|
+
type MermaidDiagramType,
|
|
73
|
+
} from './mermaid/mermaidDiagramTypes';
|
|
74
|
+
import { MermaidDiagramTypeThumbnail } from './mermaid/MermaidDiagramTypeThumbnail';
|
|
75
|
+
import { FindToolbar } from './find/FindToolbar';
|
|
55
76
|
|
|
56
77
|
const VIEWS: { id: EditorView; label: string; shortLabel?: string; shortcut: string }[] = [
|
|
57
78
|
{ id: 'wysiwyg', label: 'Write', shortcut: '⌘1' },
|
|
@@ -221,6 +242,14 @@ const BUTTONS: ToolbarButton[] = [
|
|
|
221
242
|
group: 'media',
|
|
222
243
|
faIcon: 'fa-solid fa-diagram-project',
|
|
223
244
|
},
|
|
245
|
+
{
|
|
246
|
+
id: 'complexdiagram',
|
|
247
|
+
label: 'complex diagram',
|
|
248
|
+
icon: '',
|
|
249
|
+
title: 'Insert Complex Diagram (Mermaid)',
|
|
250
|
+
group: 'media',
|
|
251
|
+
faIcon: 'fa-solid fa-code-branch',
|
|
252
|
+
},
|
|
224
253
|
{
|
|
225
254
|
id: 'tree',
|
|
226
255
|
label: 'tree',
|
|
@@ -273,7 +302,10 @@ const BUTTONS: ToolbarButton[] = [
|
|
|
273
302
|
|
|
274
303
|
const FIRST_MEDIA_INDEX = BUTTONS.findIndex((b) => b.group === 'media');
|
|
275
304
|
const MEDIA_BUTTONS = BUTTONS.filter((b) => b.group === 'media');
|
|
305
|
+
const CONVERT_BUTTONS = MEDIA_BUTTONS.filter((b) => b.id === 'table' || b.id === 'tasklist');
|
|
276
306
|
const INSERT_MENU_WIDTH = 200;
|
|
307
|
+
const CODE_SNIPPET_MENU_WIDTH = 220;
|
|
308
|
+
const MERMAID_TYPE_MENU_WIDTH = 520;
|
|
277
309
|
const TASK_LIST_ITEMS = ['Task 1', 'Task 2', 'Task 3'] as const;
|
|
278
310
|
const TASK_LIST_MARKDOWN = TASK_LIST_ITEMS.map((item) => `- [ ] ${item}`).join('\n');
|
|
279
311
|
|
|
@@ -297,18 +329,35 @@ function fileCountBadge(count: number): string {
|
|
|
297
329
|
return count > 99 ? '99+' : String(count);
|
|
298
330
|
}
|
|
299
331
|
|
|
300
|
-
function
|
|
332
|
+
function paragraphContent(text: string): JSONContent {
|
|
333
|
+
return text ? { type: 'paragraph', content: [{ type: 'text', text }] } : { type: 'paragraph' };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function tableContent(rows: string[][]): JSONContent {
|
|
337
|
+
return {
|
|
338
|
+
type: 'table',
|
|
339
|
+
content: rows.map((row, rowIndex) => ({
|
|
340
|
+
type: 'tableRow',
|
|
341
|
+
content: row.map((cell) => ({
|
|
342
|
+
type: rowIndex === 0 ? 'tableHeader' : 'tableCell',
|
|
343
|
+
content: [paragraphContent(cell)],
|
|
344
|
+
})),
|
|
345
|
+
})),
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function taskListContent(
|
|
350
|
+
items: readonly SelectionTaskItem[] = TASK_LIST_ITEMS.map((text) => ({
|
|
351
|
+
checked: false,
|
|
352
|
+
text,
|
|
353
|
+
})),
|
|
354
|
+
): JSONContent {
|
|
301
355
|
return {
|
|
302
356
|
type: 'taskList',
|
|
303
|
-
content:
|
|
357
|
+
content: items.map((item) => ({
|
|
304
358
|
type: 'taskItem',
|
|
305
|
-
attrs: { checked:
|
|
306
|
-
content: [
|
|
307
|
-
{
|
|
308
|
-
type: 'paragraph',
|
|
309
|
-
content: [{ type: 'text', text: item }],
|
|
310
|
-
},
|
|
311
|
-
],
|
|
359
|
+
attrs: { checked: item.checked },
|
|
360
|
+
content: [paragraphContent(item.text)],
|
|
312
361
|
})),
|
|
313
362
|
};
|
|
314
363
|
}
|
|
@@ -319,6 +368,26 @@ function insertTaskList(editor: TiptapEditor): void {
|
|
|
319
368
|
editor.chain().focus().insertContent(content).run();
|
|
320
369
|
}
|
|
321
370
|
|
|
371
|
+
/**
|
|
372
|
+
* Expand a full-line Tiptap text selection to the surrounding top-level node
|
|
373
|
+
* boundaries. Inserting a list/table at an in-paragraph range makes
|
|
374
|
+
* ProseMirror preserve the emptied paragraph before or after the new block.
|
|
375
|
+
* Partial-line selections deliberately retain their exact range.
|
|
376
|
+
*/
|
|
377
|
+
function blockConversionRange(editor: TiptapEditor): { from: number; to: number } {
|
|
378
|
+
const { from, to, $from, $to } = editor.state.selection;
|
|
379
|
+
return {
|
|
380
|
+
from:
|
|
381
|
+
$from.depth === 1 && $from.parent.isTextblock && $from.parentOffset === 0
|
|
382
|
+
? $from.before(1)
|
|
383
|
+
: from,
|
|
384
|
+
to:
|
|
385
|
+
$to.depth === 1 && $to.parent.isTextblock && $to.parentOffset === $to.parent.content.size
|
|
386
|
+
? $to.after(1)
|
|
387
|
+
: to,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
322
391
|
// ─── Tiptap active-state map ────────────────────────────
|
|
323
392
|
|
|
324
393
|
/** Returns true if the given button id is currently active in Tiptap */
|
|
@@ -377,17 +446,23 @@ const LAYOUT_STARTER_MARKDOWN = `\n## Layout {[layout]}\n\n### {#text-1} {[text
|
|
|
377
446
|
* fences (the fence is the source of truth; `AsciiDiagramExtension` mounts
|
|
378
447
|
* the interactive canvas over it). This exact art is the codec's own
|
|
379
448
|
* canonical rendering of a two-node flow, so it re-parses and re-renders
|
|
380
|
-
* byte-stably.
|
|
449
|
+
* byte-stably. The leading rows/columns are deliberate: ASCII has no
|
|
450
|
+
* separate canvas-origin metadata, so this persisted gutter gives the first
|
|
451
|
+
* node real grid space to move north/west. Without it, content-fit makes the
|
|
452
|
+
* node look centered while its authored coordinate is still (0, 0), and a
|
|
453
|
+
* drag into the apparent margin clamps straight back to that corner.
|
|
381
454
|
*/
|
|
382
455
|
const DIAGRAM_STARTER_ART = [
|
|
383
|
-
'
|
|
384
|
-
'
|
|
385
|
-
'
|
|
386
|
-
'
|
|
387
|
-
'
|
|
388
|
-
'
|
|
389
|
-
'
|
|
390
|
-
'
|
|
456
|
+
'',
|
|
457
|
+
'',
|
|
458
|
+
' ┌─────────┐',
|
|
459
|
+
' │ Start │',
|
|
460
|
+
' └────┬────┘',
|
|
461
|
+
' │',
|
|
462
|
+
' ▼',
|
|
463
|
+
' ┌────┴────┐',
|
|
464
|
+
' │ Next │',
|
|
465
|
+
' └─────────┘',
|
|
391
466
|
].join('\n');
|
|
392
467
|
/**
|
|
393
468
|
* Fenced form for raw / code views — tagged with the explicit `diagram`
|
|
@@ -401,6 +476,15 @@ function insertAsciiDiagramBlock(editor: TiptapEditor): void {
|
|
|
401
476
|
insertFenceBlock(editor, DIAGRAM_STARTER_ART, 'diagram');
|
|
402
477
|
}
|
|
403
478
|
|
|
479
|
+
/** Default source for callers that invoke the legacy single-click action. */
|
|
480
|
+
const MERMAID_STARTER_ART = DEFAULT_MERMAID_DIAGRAM_TYPE.starter;
|
|
481
|
+
const MERMAID_STARTER_MARKDOWN = mermaidDiagramMarkdown(MERMAID_STARTER_ART);
|
|
482
|
+
|
|
483
|
+
/** Insert a Mermaid fence that MermaidDiagramExtension turns into the complex canvas. */
|
|
484
|
+
function insertMermaidDiagramBlock(editor: TiptapEditor, source = MERMAID_STARTER_ART): void {
|
|
485
|
+
insertFenceBlock(editor, source, 'mermaid');
|
|
486
|
+
}
|
|
487
|
+
|
|
404
488
|
/**
|
|
405
489
|
* Starter art for a new file tree — an ASCII tree fence (the fence is the
|
|
406
490
|
* source of truth; `TreeViewExtension` mounts the interactive outline).
|
|
@@ -586,6 +670,8 @@ export function Toolbar({
|
|
|
586
670
|
activeSceneText,
|
|
587
671
|
mediaProvider,
|
|
588
672
|
editorMode,
|
|
673
|
+
findMode,
|
|
674
|
+
setFindMode,
|
|
589
675
|
layoutMode,
|
|
590
676
|
activeBlockStartLine,
|
|
591
677
|
versioning,
|
|
@@ -693,9 +779,19 @@ export function Toolbar({
|
|
|
693
779
|
// buttons with a single "+" button. Portaled out to avoid overflow:hidden clipping.
|
|
694
780
|
const insertMenuButtonRef = useRef<HTMLButtonElement | null>(null);
|
|
695
781
|
const insertMenuRef = useRef<HTMLDivElement | null>(null);
|
|
782
|
+
const codeSnippetMenuRef = useRef<HTMLDivElement | null>(null);
|
|
783
|
+
const mermaidTypeMenuRef = useRef<HTMLDivElement | null>(null);
|
|
696
784
|
const [insertMenuAnchor, setInsertMenuAnchor] = useState<{ top: number; left: number } | null>(
|
|
697
785
|
null,
|
|
698
786
|
);
|
|
787
|
+
const [codeSnippetMenuAnchor, setCodeSnippetMenuAnchor] = useState<{
|
|
788
|
+
top: number;
|
|
789
|
+
left: number;
|
|
790
|
+
} | null>(null);
|
|
791
|
+
const [mermaidTypeMenuAnchor, setMermaidTypeMenuAnchor] = useState<{
|
|
792
|
+
top: number;
|
|
793
|
+
left: number;
|
|
794
|
+
} | null>(null);
|
|
699
795
|
|
|
700
796
|
const openInsertMenu = useCallback((trigger?: HTMLElement | null) => {
|
|
701
797
|
const btn = trigger ?? insertMenuButtonRef.current;
|
|
@@ -709,9 +805,47 @@ export function Toolbar({
|
|
|
709
805
|
left = Math.max(margin, rect.right - INSERT_MENU_WIDTH);
|
|
710
806
|
}
|
|
711
807
|
setInsertMenuAnchor({ top: rect.bottom + gap, left });
|
|
808
|
+
setCodeSnippetMenuAnchor(null);
|
|
809
|
+
setMermaidTypeMenuAnchor(null);
|
|
810
|
+
}, []);
|
|
811
|
+
|
|
812
|
+
const openCodeSnippetMenu = useCallback((trigger: HTMLElement) => {
|
|
813
|
+
const rect = trigger.getBoundingClientRect();
|
|
814
|
+
const gap = 4;
|
|
815
|
+
const margin = 8;
|
|
816
|
+
const vw = window.innerWidth;
|
|
817
|
+
const vh = window.innerHeight;
|
|
818
|
+
let left = rect.right + gap;
|
|
819
|
+
if (left + CODE_SNIPPET_MENU_WIDTH + margin > vw) {
|
|
820
|
+
left = Math.max(margin, rect.left - CODE_SNIPPET_MENU_WIDTH - gap);
|
|
821
|
+
}
|
|
822
|
+
const maxMenuHeight = Math.min(440, vh - margin * 2);
|
|
823
|
+
const top = Math.max(margin, Math.min(rect.top, vh - maxMenuHeight - margin));
|
|
824
|
+
setCodeSnippetMenuAnchor({ top, left });
|
|
825
|
+
setMermaidTypeMenuAnchor(null);
|
|
826
|
+
}, []);
|
|
827
|
+
|
|
828
|
+
const openMermaidTypeMenu = useCallback((trigger: HTMLElement) => {
|
|
829
|
+
const rect = trigger.getBoundingClientRect();
|
|
830
|
+
const gap = 4;
|
|
831
|
+
const margin = 8;
|
|
832
|
+
const vw = window.innerWidth;
|
|
833
|
+
const vh = window.innerHeight;
|
|
834
|
+
let left = rect.right + gap;
|
|
835
|
+
if (left + MERMAID_TYPE_MENU_WIDTH + margin > vw) {
|
|
836
|
+
left = Math.max(margin, rect.left - MERMAID_TYPE_MENU_WIDTH - gap);
|
|
837
|
+
}
|
|
838
|
+
const maxMenuHeight = Math.min(600, vh - margin * 2);
|
|
839
|
+
const top = Math.max(margin, Math.min(rect.top, vh - maxMenuHeight - margin));
|
|
840
|
+
setMermaidTypeMenuAnchor({ top, left });
|
|
841
|
+
setCodeSnippetMenuAnchor(null);
|
|
712
842
|
}, []);
|
|
713
843
|
|
|
714
|
-
const closeInsertMenu = useCallback(() =>
|
|
844
|
+
const closeInsertMenu = useCallback(() => {
|
|
845
|
+
setInsertMenuAnchor(null);
|
|
846
|
+
setCodeSnippetMenuAnchor(null);
|
|
847
|
+
setMermaidTypeMenuAnchor(null);
|
|
848
|
+
}, []);
|
|
715
849
|
|
|
716
850
|
// ── Overflow detection ────────────────────────────────
|
|
717
851
|
const actionsRef = useRef<HTMLDivElement>(null);
|
|
@@ -730,6 +864,13 @@ export function Toolbar({
|
|
|
730
864
|
// Custom layout manager dialog (list of doc/library layouts + designer)
|
|
731
865
|
const [showLayoutManager, setShowLayoutManager] = useState(false);
|
|
732
866
|
|
|
867
|
+
useEffect(() => {
|
|
868
|
+
if (!findMode) return;
|
|
869
|
+
setShowOverflow(false);
|
|
870
|
+
closeInsertMenu();
|
|
871
|
+
closeEmojiPicker();
|
|
872
|
+
}, [closeEmojiPicker, closeInsertMenu, findMode]);
|
|
873
|
+
|
|
733
874
|
const overflowIndex = measuredOverflowIndex;
|
|
734
875
|
const clippedContextual = new Set(clippedContextualKey ? clippedContextualKey.split('|') : []);
|
|
735
876
|
|
|
@@ -823,6 +964,8 @@ export function Toolbar({
|
|
|
823
964
|
const handleClick = (e: MouseEvent) => {
|
|
824
965
|
if (insertMenuButtonRef.current?.contains(e.target as Node)) return;
|
|
825
966
|
if (insertMenuRef.current?.contains(e.target as Node)) return;
|
|
967
|
+
if (codeSnippetMenuRef.current?.contains(e.target as Node)) return;
|
|
968
|
+
if (mermaidTypeMenuRef.current?.contains(e.target as Node)) return;
|
|
826
969
|
closeInsertMenu();
|
|
827
970
|
};
|
|
828
971
|
document.addEventListener('mousedown', handleClick);
|
|
@@ -953,6 +1096,11 @@ export function Toolbar({
|
|
|
953
1096
|
// mounts the interactive canvas over the inserted starter.
|
|
954
1097
|
insertAsciiDiagramBlock(tiptapEditor);
|
|
955
1098
|
break;
|
|
1099
|
+
case 'complexdiagram':
|
|
1100
|
+
// Complex diagrams remain full Mermaid source; the Mermaid extension
|
|
1101
|
+
// renders the fence without narrowing it to Squisq's graph model.
|
|
1102
|
+
insertMermaidDiagramBlock(tiptapEditor);
|
|
1103
|
+
break;
|
|
956
1104
|
case 'tree':
|
|
957
1105
|
// File trees are ASCII tree fences; TreeViewExtension mounts the
|
|
958
1106
|
// interactive outline over the inserted starter.
|
|
@@ -1124,6 +1272,11 @@ export function Toolbar({
|
|
|
1124
1272
|
newCursorOffset = replacement.length;
|
|
1125
1273
|
break;
|
|
1126
1274
|
}
|
|
1275
|
+
case 'complexdiagram': {
|
|
1276
|
+
replacement = MERMAID_STARTER_MARKDOWN;
|
|
1277
|
+
newCursorOffset = replacement.length;
|
|
1278
|
+
break;
|
|
1279
|
+
}
|
|
1127
1280
|
case 'tree': {
|
|
1128
1281
|
replacement = TREE_STARTER_MARKDOWN;
|
|
1129
1282
|
newCursorOffset = replacement.length;
|
|
@@ -1214,6 +1367,9 @@ export function Toolbar({
|
|
|
1214
1367
|
case 'diagram':
|
|
1215
1368
|
insertion = DIAGRAM_STARTER_MARKDOWN;
|
|
1216
1369
|
break;
|
|
1370
|
+
case 'complexdiagram':
|
|
1371
|
+
insertion = MERMAID_STARTER_MARKDOWN;
|
|
1372
|
+
break;
|
|
1217
1373
|
case 'tree':
|
|
1218
1374
|
insertion = TREE_STARTER_MARKDOWN;
|
|
1219
1375
|
break;
|
|
@@ -1235,6 +1391,137 @@ export function Toolbar({
|
|
|
1235
1391
|
[monacoEditor, markdownSource, setMarkdownSource],
|
|
1236
1392
|
);
|
|
1237
1393
|
|
|
1394
|
+
const handleCodeSnippetInsert = useCallback(
|
|
1395
|
+
(language: CodeSnippetLanguage) => {
|
|
1396
|
+
if (activeView === 'wysiwyg' && tiptapEditor) {
|
|
1397
|
+
insertFenceBlock(tiptapEditor, language.starter, language.fenceLanguage);
|
|
1398
|
+
closeInsertMenu();
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
if (monacoEditor) {
|
|
1403
|
+
const selection = monacoEditor.getSelection();
|
|
1404
|
+
const model = monacoEditor.getModel();
|
|
1405
|
+
if (!selection || !model) return;
|
|
1406
|
+
const selectedText = model.getValueInRange(selection);
|
|
1407
|
+
const source = selectedText.length > 0 ? selectedText : language.starter;
|
|
1408
|
+
const markdown = codeSnippetMarkdown(language.fenceLanguage, source);
|
|
1409
|
+
monacoEditor.executeEdits('toolbar-code-snippet', [{ range: selection, text: markdown }]);
|
|
1410
|
+
if (selectedText.length === 0) {
|
|
1411
|
+
const sourceOffset = 5 + language.fenceLanguage.length;
|
|
1412
|
+
const sourcePosition = model.getPositionAt(
|
|
1413
|
+
model.getOffsetAt(selection.getStartPosition()) + sourceOffset,
|
|
1414
|
+
);
|
|
1415
|
+
monacoEditor.setPosition(sourcePosition);
|
|
1416
|
+
monacoEditor.revealPositionInCenterIfOutsideViewport(sourcePosition);
|
|
1417
|
+
}
|
|
1418
|
+
monacoEditor.focus();
|
|
1419
|
+
closeInsertMenu();
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
setMarkdownSource(
|
|
1424
|
+
markdownSource + codeSnippetMarkdown(language.fenceLanguage, language.starter),
|
|
1425
|
+
);
|
|
1426
|
+
closeInsertMenu();
|
|
1427
|
+
},
|
|
1428
|
+
[activeView, closeInsertMenu, markdownSource, monacoEditor, setMarkdownSource, tiptapEditor],
|
|
1429
|
+
);
|
|
1430
|
+
|
|
1431
|
+
// ── Selected-text conversion handlers ─────────────────
|
|
1432
|
+
const handleMermaidDiagramInsert = useCallback(
|
|
1433
|
+
(diagramType: MermaidDiagramType) => {
|
|
1434
|
+
if (activeView === 'wysiwyg' && tiptapEditor) {
|
|
1435
|
+
insertMermaidDiagramBlock(tiptapEditor, diagramType.starter);
|
|
1436
|
+
closeInsertMenu();
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
const markdown = mermaidDiagramMarkdown(diagramType.starter);
|
|
1441
|
+
if (monacoEditor) {
|
|
1442
|
+
const selection = monacoEditor.getSelection();
|
|
1443
|
+
const model = monacoEditor.getModel();
|
|
1444
|
+
if (!selection || !model) return;
|
|
1445
|
+
monacoEditor.executeEdits('toolbar-mermaid-diagram', [
|
|
1446
|
+
{ range: selection, text: markdown },
|
|
1447
|
+
]);
|
|
1448
|
+
const endPosition = model.getPositionAt(
|
|
1449
|
+
model.getOffsetAt(selection.getStartPosition()) + markdown.length,
|
|
1450
|
+
);
|
|
1451
|
+
monacoEditor.setPosition(endPosition);
|
|
1452
|
+
monacoEditor.revealPositionInCenterIfOutsideViewport(endPosition);
|
|
1453
|
+
monacoEditor.focus();
|
|
1454
|
+
closeInsertMenu();
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
setMarkdownSource(markdownSource + markdown);
|
|
1459
|
+
closeInsertMenu();
|
|
1460
|
+
},
|
|
1461
|
+
[activeView, closeInsertMenu, markdownSource, monacoEditor, setMarkdownSource, tiptapEditor],
|
|
1462
|
+
);
|
|
1463
|
+
|
|
1464
|
+
const readSelectedText = useCallback((): string => {
|
|
1465
|
+
// Canvas text overlays intentionally have a smaller schema without tables
|
|
1466
|
+
// or task lists. Ignore their selection instead of surfacing actions that
|
|
1467
|
+
// cannot be represented by those layers.
|
|
1468
|
+
if (activeSceneText) return '';
|
|
1469
|
+
|
|
1470
|
+
if (activeView === 'wysiwyg' && tiptapEditor) {
|
|
1471
|
+
const { from, to, empty } = tiptapEditor.state.selection;
|
|
1472
|
+
return empty ? '' : tiptapEditor.state.doc.textBetween(from, to, '\n');
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
if (activeView === 'raw' && monacoEditor) {
|
|
1476
|
+
const selection = monacoEditor.getSelection();
|
|
1477
|
+
const model = monacoEditor.getModel();
|
|
1478
|
+
return selection && model ? model.getValueInRange(selection) : '';
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
return '';
|
|
1482
|
+
}, [activeSceneText, activeView, monacoEditor, tiptapEditor]);
|
|
1483
|
+
|
|
1484
|
+
const handleConvertSelection = useCallback(
|
|
1485
|
+
(target: 'table' | 'tasklist') => {
|
|
1486
|
+
const selectedText = readSelectedText();
|
|
1487
|
+
if (!selectedText.trim()) return;
|
|
1488
|
+
|
|
1489
|
+
if (activeView === 'wysiwyg' && tiptapEditor) {
|
|
1490
|
+
const content =
|
|
1491
|
+
target === 'table'
|
|
1492
|
+
? tableContent(selectionToTable(selectedText).rows)
|
|
1493
|
+
: taskListContent(selectionToTaskItems(selectedText));
|
|
1494
|
+
tiptapEditor
|
|
1495
|
+
.chain()
|
|
1496
|
+
.focus()
|
|
1497
|
+
.insertContentAt(blockConversionRange(tiptapEditor), content)
|
|
1498
|
+
.run();
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
if (activeView === 'raw' && monacoEditor) {
|
|
1503
|
+
const selection = monacoEditor.getSelection();
|
|
1504
|
+
if (!selection) return;
|
|
1505
|
+
const converted =
|
|
1506
|
+
target === 'table'
|
|
1507
|
+
? selectionToTableMarkdown(selectedText)
|
|
1508
|
+
: selectionToTaskListMarkdown(selectedText);
|
|
1509
|
+
if (!converted) return;
|
|
1510
|
+
|
|
1511
|
+
// Monaco line selections commonly include the newline after the last
|
|
1512
|
+
// selected line. Retain selected edge newlines so the replacement
|
|
1513
|
+
// block never runs into the surrounding Markdown.
|
|
1514
|
+
const leadingNewline = /^(?:\r?\n)/.test(selectedText) ? '\n' : '';
|
|
1515
|
+
const trailingNewline = /(?:\r?\n)$/.test(selectedText) ? '\n' : '';
|
|
1516
|
+
monacoEditor.executeEdits('toolbar-convert-selection', [
|
|
1517
|
+
{ range: selection, text: leadingNewline + converted + trailingNewline },
|
|
1518
|
+
]);
|
|
1519
|
+
monacoEditor.focus();
|
|
1520
|
+
}
|
|
1521
|
+
},
|
|
1522
|
+
[activeView, monacoEditor, readSelectedText, tiptapEditor],
|
|
1523
|
+
);
|
|
1524
|
+
|
|
1238
1525
|
// ── Image upload handler ───────────────────────────────
|
|
1239
1526
|
const handleImageFile = useCallback(
|
|
1240
1527
|
async (file: File) => {
|
|
@@ -1471,6 +1758,10 @@ export function Toolbar({
|
|
|
1471
1758
|
if (sceneTextLevel === 'block') return BLOCK_SCENE_BUTTONS.has(id);
|
|
1472
1759
|
return INLINE_SCENE_BUTTONS.has(id);
|
|
1473
1760
|
};
|
|
1761
|
+
const showConvertActions =
|
|
1762
|
+
insertMenuAnchor !== null &&
|
|
1763
|
+
readSelectedText().trim().length > 0 &&
|
|
1764
|
+
CONVERT_BUTTONS.some((button) => buttonAllowed(button.id));
|
|
1474
1765
|
|
|
1475
1766
|
// ── Progressive heading disclosure ───────────────────
|
|
1476
1767
|
// H1\u2013H3 are always visible. H4 appears once the document already
|
|
@@ -1480,14 +1771,12 @@ export function Toolbar({
|
|
|
1480
1771
|
const maxHeadingLevelInDoc = useMemo(() => {
|
|
1481
1772
|
if (!markdownSource) return 0;
|
|
1482
1773
|
let max = 0;
|
|
1483
|
-
|
|
1484
|
-
|
|
1774
|
+
const lines = markdownSource.split(/\r\n|\r|\n/);
|
|
1775
|
+
const fencedLines = markdownFencedCodeLineMask(markdownSource);
|
|
1776
|
+
for (let index = 0; index < lines.length; index++) {
|
|
1777
|
+
if (fencedLines[index]) continue;
|
|
1778
|
+
const rawLine = lines[index];
|
|
1485
1779
|
const line = rawLine.trimEnd();
|
|
1486
|
-
if (/^\s*```/.test(line)) {
|
|
1487
|
-
inFence = !inFence;
|
|
1488
|
-
continue;
|
|
1489
|
-
}
|
|
1490
|
-
if (inFence) continue;
|
|
1491
1780
|
const m = /^(#{1,6})\s+\S/.exec(line);
|
|
1492
1781
|
if (m && m[1].length > max) max = m[1].length;
|
|
1493
1782
|
}
|
|
@@ -1538,6 +1827,12 @@ export function Toolbar({
|
|
|
1538
1827
|
setRawTransition(EMPTY_TRANSITION);
|
|
1539
1828
|
return;
|
|
1540
1829
|
}
|
|
1830
|
+
if (isMarkdownFencedCodeLine(model.getValue(), pos.lineNumber)) {
|
|
1831
|
+
setRawTemplate(null);
|
|
1832
|
+
setRawHeadingLine(null);
|
|
1833
|
+
setRawTransition(EMPTY_TRANSITION);
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1541
1836
|
const line = model.getLineContent(pos.lineNumber);
|
|
1542
1837
|
const headingMatch = line.match(/^#{1,6}\s+(.+)$/);
|
|
1543
1838
|
if (!headingMatch) {
|
|
@@ -1677,6 +1972,7 @@ export function Toolbar({
|
|
|
1677
1972
|
const model = monacoEditor.getModel();
|
|
1678
1973
|
const pos = monacoEditor.getPosition();
|
|
1679
1974
|
if (!model || !pos) return;
|
|
1975
|
+
if (isMarkdownFencedCodeLine(model.getValue(), pos.lineNumber)) return;
|
|
1680
1976
|
const lineNumber = pos.lineNumber;
|
|
1681
1977
|
const lineText = model.getLineContent(lineNumber);
|
|
1682
1978
|
const headingMatch = lineText.match(/^(#{1,6}\s+)(.+)$/);
|
|
@@ -1741,6 +2037,7 @@ export function Toolbar({
|
|
|
1741
2037
|
const model = monacoEditor.getModel();
|
|
1742
2038
|
const pos = monacoEditor.getPosition();
|
|
1743
2039
|
if (!model || !pos) return;
|
|
2040
|
+
if (isMarkdownFencedCodeLine(model.getValue(), pos.lineNumber)) return;
|
|
1744
2041
|
const lineNumber = pos.lineNumber;
|
|
1745
2042
|
const lineText = model.getLineContent(lineNumber);
|
|
1746
2043
|
const newLine = setHeadingLineTransition(lineText, next);
|
|
@@ -1788,12 +2085,17 @@ export function Toolbar({
|
|
|
1788
2085
|
currentTransition !== null && clippedContextual.has('transition');
|
|
1789
2086
|
const showBlockSectionInOverflow = showTemplateInOverflow || showTransitionInOverflow;
|
|
1790
2087
|
const overflowBlockLabel = currentTemplate ? templateLabel(currentTemplate) : 'Heading';
|
|
2088
|
+
const showToolbarOverflow =
|
|
2089
|
+
!findMode &&
|
|
2090
|
+
!isPreview &&
|
|
2091
|
+
!isCodeMode &&
|
|
2092
|
+
(overflowIndex !== null || clippedContextual.size > 0);
|
|
1791
2093
|
|
|
1792
2094
|
return (
|
|
1793
2095
|
<div
|
|
1794
|
-
className={`squisq-toolbar ${className || ''}`}
|
|
2096
|
+
className={`squisq-toolbar${findMode ? ' squisq-toolbar--find' : ''} ${className || ''}`}
|
|
1795
2097
|
role="toolbar"
|
|
1796
|
-
aria-label=
|
|
2098
|
+
aria-label={findMode ? 'Find toolbar' : 'Formatting toolbar'}
|
|
1797
2099
|
>
|
|
1798
2100
|
{/* Hidden file input for image picker */}
|
|
1799
2101
|
<input
|
|
@@ -1809,7 +2111,7 @@ export function Toolbar({
|
|
|
1809
2111
|
}}
|
|
1810
2112
|
/>
|
|
1811
2113
|
{/* Left slot — before view tabs */}
|
|
1812
|
-
{slotLeft}
|
|
2114
|
+
{!findMode && slotLeft}
|
|
1813
2115
|
{/* View tabs — hidden when only one view is available (e.g. code mode). */}
|
|
1814
2116
|
{showViewTabs && (
|
|
1815
2117
|
<div className="squisq-toolbar-view-tabs" role="tablist" aria-label="Editor view">
|
|
@@ -1872,9 +2174,9 @@ export function Toolbar({
|
|
|
1872
2174
|
</div>
|
|
1873
2175
|
)}
|
|
1874
2176
|
{/* After-tabs slot — left side, before formatting or preview controls. */}
|
|
1875
|
-
{slotAfterTabs}
|
|
2177
|
+
{findMode ? <FindToolbar onClose={() => setFindMode(false)} /> : slotAfterTabs}
|
|
1876
2178
|
{/* Formatting buttons — hidden in preview mode and code mode */}
|
|
1877
|
-
{!isPreview && !isCodeMode && (
|
|
2179
|
+
{!findMode && !isPreview && !isCodeMode && (
|
|
1878
2180
|
<div className="squisq-toolbar-actions" ref={actionsRef}>
|
|
1879
2181
|
{groups.map((group, gi) => (
|
|
1880
2182
|
<div key={group} className="squisq-toolbar-group">
|
|
@@ -2125,7 +2427,7 @@ export function Toolbar({
|
|
|
2125
2427
|
{/* Overflow menu — outside the overflow:hidden actions container.
|
|
2126
2428
|
Also appears when a contextual group (template/transition picker,
|
|
2127
2429
|
table controls) is clipped, even if every plain button fits. */}
|
|
2128
|
-
{
|
|
2430
|
+
{showToolbarOverflow && (
|
|
2129
2431
|
<div className="squisq-toolbar-overflow" ref={overflowRef}>
|
|
2130
2432
|
<button
|
|
2131
2433
|
className={`squisq-toolbar-button squisq-toolbar-overflow-trigger${showOverflow ? ' squisq-toolbar-button--active' : ''}`}
|
|
@@ -2295,13 +2597,13 @@ export function Toolbar({
|
|
|
2295
2597
|
)}
|
|
2296
2598
|
|
|
2297
2599
|
{/* After-actions slot — after formatting controls */}
|
|
2298
|
-
{slotAfterActions}
|
|
2600
|
+
{!findMode && slotAfterActions}
|
|
2299
2601
|
{/* Spacer — pushes right-side items to the end when the flex:1 actions
|
|
2300
2602
|
container isn't rendered. In preview mode PreviewToolbarControls
|
|
2301
2603
|
supplies its own flex:1 filler (and measures that leftover width to
|
|
2302
2604
|
decide whether to collapse), so a second spacer here would split the
|
|
2303
2605
|
slack and make the controls collapse too early. */}
|
|
2304
|
-
{isCodeMode && <div style={{ flex: 1 }} />}
|
|
2606
|
+
{isCodeMode && !findMode && <div style={{ flex: 1 }} />}
|
|
2305
2607
|
{/* Version history — renders only when the host enabled versioning
|
|
2306
2608
|
and a container is wired up. The component owns its own button
|
|
2307
2609
|
and popover; we just give it a slot in the toolbar. */}
|
|
@@ -2392,28 +2694,169 @@ export function Toolbar({
|
|
|
2392
2694
|
style={{ position: 'fixed', top: insertMenuAnchor.top, left: insertMenuAnchor.left }}
|
|
2393
2695
|
role="menu"
|
|
2394
2696
|
>
|
|
2395
|
-
|
|
2697
|
+
{showConvertActions && (
|
|
2698
|
+
<>
|
|
2699
|
+
<div className="squisq-insert-menu-header">Convert</div>
|
|
2700
|
+
{CONVERT_BUTTONS.map((btn) => {
|
|
2701
|
+
const label = btn.id === 'table' ? 'Table' : 'Task List';
|
|
2702
|
+
return (
|
|
2703
|
+
<button
|
|
2704
|
+
key={`convert-${btn.id}`}
|
|
2705
|
+
className="squisq-toolbar-overflow-item"
|
|
2706
|
+
disabled={!buttonAllowed(btn.id)}
|
|
2707
|
+
onClick={() => {
|
|
2708
|
+
handleConvertSelection(btn.id as 'table' | 'tasklist');
|
|
2709
|
+
closeInsertMenu();
|
|
2710
|
+
}}
|
|
2711
|
+
role="menuitem"
|
|
2712
|
+
aria-label={`Convert selection to ${label}`}
|
|
2713
|
+
>
|
|
2714
|
+
<span className="squisq-toolbar-overflow-icon">{buttonIcon(btn)}</span>
|
|
2715
|
+
<span>{label}</span>
|
|
2716
|
+
</button>
|
|
2717
|
+
);
|
|
2718
|
+
})}
|
|
2719
|
+
</>
|
|
2720
|
+
)}
|
|
2721
|
+
<div
|
|
2722
|
+
className={`squisq-insert-menu-header${showConvertActions ? ' squisq-insert-menu-header--separated' : ''}`}
|
|
2723
|
+
>
|
|
2724
|
+
Insert
|
|
2725
|
+
</div>
|
|
2396
2726
|
{MEDIA_BUTTONS.filter((b) => isButtonVisible(b.id)).map((btn) => {
|
|
2397
2727
|
const disabled = (btn.id === 'image' && !mediaProvider) || !buttonAllowed(btn.id);
|
|
2398
2728
|
const stripped = btn.title.replace(/^Insert\s+/i, '');
|
|
2399
2729
|
const label = stripped.charAt(0).toUpperCase() + stripped.slice(1);
|
|
2730
|
+
const hasMermaidTypeMenu = btn.id === 'complexdiagram';
|
|
2400
2731
|
return (
|
|
2401
2732
|
<button
|
|
2402
2733
|
key={btn.id}
|
|
2403
2734
|
ref={btn.id === 'emoji' ? emojiButtonRef : undefined}
|
|
2404
2735
|
className="squisq-toolbar-overflow-item"
|
|
2405
2736
|
disabled={disabled}
|
|
2406
|
-
onClick={() => {
|
|
2737
|
+
onClick={(event) => {
|
|
2738
|
+
if (hasMermaidTypeMenu) {
|
|
2739
|
+
if (mermaidTypeMenuAnchor) setMermaidTypeMenuAnchor(null);
|
|
2740
|
+
else openMermaidTypeMenu(event.currentTarget);
|
|
2741
|
+
return;
|
|
2742
|
+
}
|
|
2407
2743
|
handleAction(btn.id);
|
|
2408
2744
|
if (btn.id !== 'emoji') closeInsertMenu();
|
|
2409
2745
|
}}
|
|
2410
2746
|
role="menuitem"
|
|
2747
|
+
aria-haspopup={hasMermaidTypeMenu ? 'menu' : undefined}
|
|
2748
|
+
aria-expanded={hasMermaidTypeMenu ? mermaidTypeMenuAnchor !== null : undefined}
|
|
2411
2749
|
>
|
|
2412
2750
|
<span className="squisq-toolbar-overflow-icon">{buttonIcon(btn)}</span>
|
|
2413
2751
|
<span>{label}</span>
|
|
2752
|
+
{hasMermaidTypeMenu && <Icon icon="fa-solid fa-chevron-right" />}
|
|
2414
2753
|
</button>
|
|
2415
2754
|
);
|
|
2416
2755
|
})}
|
|
2756
|
+
<button
|
|
2757
|
+
className="squisq-toolbar-overflow-item"
|
|
2758
|
+
onClick={(event) => {
|
|
2759
|
+
if (codeSnippetMenuAnchor) setCodeSnippetMenuAnchor(null);
|
|
2760
|
+
else openCodeSnippetMenu(event.currentTarget);
|
|
2761
|
+
}}
|
|
2762
|
+
role="menuitem"
|
|
2763
|
+
aria-label="Insert Code Snippet"
|
|
2764
|
+
aria-haspopup="menu"
|
|
2765
|
+
aria-expanded={codeSnippetMenuAnchor !== null}
|
|
2766
|
+
>
|
|
2767
|
+
<span className="squisq-toolbar-overflow-icon">
|
|
2768
|
+
<Icon icon="fa-solid fa-file-code" />
|
|
2769
|
+
</span>
|
|
2770
|
+
<span>Code Snippet</span>
|
|
2771
|
+
<Icon icon="fa-solid fa-chevron-right" />
|
|
2772
|
+
</button>
|
|
2773
|
+
</div>,
|
|
2774
|
+
document.body,
|
|
2775
|
+
)}
|
|
2776
|
+
|
|
2777
|
+
{mermaidTypeMenuAnchor &&
|
|
2778
|
+
createPortal(
|
|
2779
|
+
<div
|
|
2780
|
+
ref={mermaidTypeMenuRef}
|
|
2781
|
+
className="squisq-insert-menu squisq-mermaid-type-menu"
|
|
2782
|
+
data-theme={colorScheme}
|
|
2783
|
+
style={{
|
|
2784
|
+
position: 'fixed',
|
|
2785
|
+
top: mermaidTypeMenuAnchor.top,
|
|
2786
|
+
left: mermaidTypeMenuAnchor.left,
|
|
2787
|
+
}}
|
|
2788
|
+
role="menu"
|
|
2789
|
+
aria-label="Mermaid diagram type"
|
|
2790
|
+
>
|
|
2791
|
+
<div className="squisq-mermaid-type-menu-heading">
|
|
2792
|
+
<strong>Complex Diagram</strong>
|
|
2793
|
+
<span>Choose a Mermaid diagram type. Flow direction remains a separate edit.</span>
|
|
2794
|
+
</div>
|
|
2795
|
+
{(['Structure', 'Planning', 'Data'] as const).map((category) => (
|
|
2796
|
+
<section key={category} className="squisq-mermaid-type-section">
|
|
2797
|
+
<div className="squisq-insert-menu-header">{category}</div>
|
|
2798
|
+
<div className="squisq-mermaid-type-grid">
|
|
2799
|
+
{MERMAID_DIAGRAM_TYPES.filter((entry) => entry.category === category).map(
|
|
2800
|
+
(entry) => (
|
|
2801
|
+
<button
|
|
2802
|
+
key={entry.id}
|
|
2803
|
+
type="button"
|
|
2804
|
+
className="squisq-mermaid-type-card"
|
|
2805
|
+
onClick={() => handleMermaidDiagramInsert(entry)}
|
|
2806
|
+
role="menuitem"
|
|
2807
|
+
aria-label={`Insert ${entry.label} Mermaid diagram`}
|
|
2808
|
+
>
|
|
2809
|
+
<span className="squisq-mermaid-type-thumbnail">
|
|
2810
|
+
<MermaidDiagramTypeThumbnail preview={entry.preview} />
|
|
2811
|
+
</span>
|
|
2812
|
+
<span className="squisq-mermaid-type-copy">
|
|
2813
|
+
<span className="squisq-mermaid-type-title">
|
|
2814
|
+
{entry.label}
|
|
2815
|
+
{entry.badge && (
|
|
2816
|
+
<span className="squisq-mermaid-type-badge">{entry.badge}</span>
|
|
2817
|
+
)}
|
|
2818
|
+
</span>
|
|
2819
|
+
<span className="squisq-mermaid-type-description">
|
|
2820
|
+
{entry.description}
|
|
2821
|
+
</span>
|
|
2822
|
+
</span>
|
|
2823
|
+
</button>
|
|
2824
|
+
),
|
|
2825
|
+
)}
|
|
2826
|
+
</div>
|
|
2827
|
+
</section>
|
|
2828
|
+
))}
|
|
2829
|
+
</div>,
|
|
2830
|
+
document.body,
|
|
2831
|
+
)}
|
|
2832
|
+
|
|
2833
|
+
{codeSnippetMenuAnchor &&
|
|
2834
|
+
createPortal(
|
|
2835
|
+
<div
|
|
2836
|
+
ref={codeSnippetMenuRef}
|
|
2837
|
+
className="squisq-insert-menu squisq-code-snippet-menu"
|
|
2838
|
+
data-theme={colorScheme}
|
|
2839
|
+
style={{
|
|
2840
|
+
position: 'fixed',
|
|
2841
|
+
top: codeSnippetMenuAnchor.top,
|
|
2842
|
+
left: codeSnippetMenuAnchor.left,
|
|
2843
|
+
}}
|
|
2844
|
+
role="menu"
|
|
2845
|
+
aria-label="Code snippet language"
|
|
2846
|
+
>
|
|
2847
|
+
<div className="squisq-insert-menu-header">Code Snippet</div>
|
|
2848
|
+
{CODE_SNIPPET_LANGUAGES.map((language) => (
|
|
2849
|
+
<button
|
|
2850
|
+
key={language.fenceLanguage}
|
|
2851
|
+
className="squisq-toolbar-overflow-item squisq-code-snippet-language"
|
|
2852
|
+
onClick={() => handleCodeSnippetInsert(language)}
|
|
2853
|
+
role="menuitem"
|
|
2854
|
+
aria-label={`Insert ${language.label} code snippet`}
|
|
2855
|
+
>
|
|
2856
|
+
<span>{language.label}</span>
|
|
2857
|
+
<code>{language.fenceLanguage}</code>
|
|
2858
|
+
</button>
|
|
2859
|
+
))}
|
|
2417
2860
|
</div>,
|
|
2418
2861
|
document.body,
|
|
2419
2862
|
)}
|