@bendyline/squisq-editor-react 1.6.1 → 1.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +87 -41
- package/dist/index.js +8263 -6705
- package/dist/index.js.map +1 -1
- package/dist/styles/index.css +841 -91
- package/package.json +4 -4
- package/src/BlockPropertiesPopover.tsx +23 -7
- package/src/EditorShell.tsx +65 -20
- package/src/MediaBin.tsx +171 -28
- package/src/PreviewControls.tsx +177 -44
- package/src/PreviewPanel.tsx +2 -0
- package/src/TemplateAnnotation.ts +22 -7
- package/src/TemplateContentPreview.tsx +56 -0
- package/src/TemplatePicker.tsx +295 -128
- package/src/ThemeCustomizerPanel.tsx +22 -15
- package/src/Toolbar.tsx +527 -207
- package/src/TransitionPicker.tsx +8 -1
- package/src/ViewSwitcher.tsx +4 -4
- package/src/WysiwygEditor.tsx +45 -3
- package/src/__tests__/buildPreviewDocTransition.test.ts +1 -2
- package/src/__tests__/editorShellProps.test.tsx +268 -1
- package/src/__tests__/headingTransition.test.ts +59 -9
- package/src/__tests__/imageEditorShell.test.tsx +23 -0
- package/src/__tests__/mediaReferences.test.ts +82 -0
- package/src/__tests__/previewControls.test.tsx +94 -1
- package/src/__tests__/templateAnnotationRoundTrip.test.ts +23 -2
- package/src/__tests__/templateContentPreview.test.ts +101 -0
- package/src/__tests__/tiptapBridge.test.ts +47 -0
- package/src/diagram/DiagramWidget.tsx +6 -4
- package/src/headingTransition.ts +96 -21
- package/src/index.ts +2 -1
- package/src/mediaReferences.ts +299 -0
- package/src/scene/Scene.tsx +53 -7
- package/src/scene/SceneBlockWidget.tsx +6 -3
- package/src/scene/SceneSelection.tsx +19 -15
- package/src/scene/SceneSideToolbar.tsx +89 -0
- package/src/scene/layers/DiagramEdges.tsx +4 -3
- package/src/scene/layers/edgeGeometry.ts +23 -4
- package/src/scene/scene.css +142 -2
- package/src/scene/tools/ConnectTool.ts +113 -24
- package/src/scene/tools/DrawingConnectTool.ts +86 -16
- package/src/scene/tools/SceneTool.ts +2 -0
- package/src/styles/editor.css +862 -101
- package/src/templateContentPreviewResolver.ts +353 -0
- package/src/tiptapBridge.ts +60 -33
package/src/Toolbar.tsx
CHANGED
|
@@ -9,21 +9,35 @@
|
|
|
9
9
|
|
|
10
10
|
import type { ReactNode } from 'react';
|
|
11
11
|
import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react';
|
|
12
|
-
import type { Editor as TiptapEditor } from '@tiptap/core';
|
|
12
|
+
import type { Editor as TiptapEditor, JSONContent } from '@tiptap/core';
|
|
13
13
|
import { TextSelection } from '@tiptap/pm/state';
|
|
14
14
|
import { Fragment } from '@tiptap/pm/model';
|
|
15
15
|
import type { IRange } from 'monaco-editor';
|
|
16
|
+
import type { Block } from '@bendyline/squisq/schemas';
|
|
17
|
+
import { VIEWPORT_PRESETS } from '@bendyline/squisq/schemas';
|
|
18
|
+
import { DEFAULT_THEME, flattenBlocks } from '@bendyline/squisq/doc';
|
|
19
|
+
import {
|
|
20
|
+
KNOWN_BLOCK_META_KEYS,
|
|
21
|
+
matchTrailingTemplateAnnotation,
|
|
22
|
+
splitKeyValueToken,
|
|
23
|
+
tokenizeAttrTokens,
|
|
24
|
+
} from '@bendyline/squisq/markdown';
|
|
16
25
|
import { useEditorContext, type EditorView } from './EditorContext';
|
|
17
26
|
import { VersionHistoryPanel } from './VersionHistoryPanel';
|
|
18
27
|
import { RecorderEntry } from './RecorderEntry';
|
|
19
28
|
import { ViewMenuPanel } from './ViewMenuPanel';
|
|
20
|
-
import {
|
|
21
|
-
|
|
29
|
+
import {
|
|
30
|
+
TemplatePicker,
|
|
31
|
+
TEMPLATE_NAMES,
|
|
32
|
+
TEMPLATE_GALLERY_PORTAL_ID,
|
|
33
|
+
templateLabel,
|
|
34
|
+
} from './TemplatePicker';
|
|
35
|
+
import { TransitionPicker, TRANSITION_FLYOUT_PORTAL_ID } from './TransitionPicker';
|
|
22
36
|
import {
|
|
23
37
|
readHeadingLineTransition,
|
|
24
38
|
setHeadingLineTransition,
|
|
25
39
|
readBlockAttrsTransition,
|
|
26
|
-
|
|
40
|
+
setHeadingAttrsTransition,
|
|
27
41
|
EMPTY_TRANSITION,
|
|
28
42
|
type TransitionFields,
|
|
29
43
|
} from './headingTransition';
|
|
@@ -36,25 +50,30 @@ import { CustomLayoutManager } from './customTemplates/CustomLayoutManager';
|
|
|
36
50
|
import { Icon } from './Icon';
|
|
37
51
|
import type { PickerEntry } from './emojiData';
|
|
38
52
|
import { createPortal } from 'react-dom';
|
|
53
|
+
import { usePreviewSettingsOptional } from './PreviewControls';
|
|
39
54
|
|
|
40
55
|
const VIEWS: { id: EditorView; label: string; shortLabel?: string; shortcut: string }[] = [
|
|
41
|
-
{ id: 'wysiwyg', label: '
|
|
42
|
-
{ id: 'raw', label: '
|
|
43
|
-
{ id: 'preview', label: '
|
|
56
|
+
{ id: 'wysiwyg', label: 'Write', shortcut: '⌘1' },
|
|
57
|
+
{ id: 'raw', label: 'Source', shortcut: '⌘2' },
|
|
58
|
+
{ id: 'preview', label: 'Use', shortcut: '⌘3' },
|
|
44
59
|
];
|
|
45
60
|
|
|
61
|
+
const BLOCK_META_KEYS = new Set<string>(Object.keys(KNOWN_BLOCK_META_KEYS));
|
|
62
|
+
|
|
46
63
|
export interface ToolbarProps {
|
|
47
64
|
/** Additional class name */
|
|
48
65
|
className?: string;
|
|
49
66
|
/** Whether the Files panel is currently shown */
|
|
50
67
|
showFiles?: boolean;
|
|
68
|
+
/** Number of files currently available through the MediaProvider. */
|
|
69
|
+
fileCount?: number;
|
|
51
70
|
/** Toggle the Files panel. When provided, a "Files" button appears in the toolbar. */
|
|
52
71
|
onToggleFiles?: () => void;
|
|
53
72
|
/** Content rendered at the left edge of the toolbar, before the view tabs. */
|
|
54
73
|
slotLeft?: ReactNode;
|
|
55
74
|
/** Content rendered immediately after the view tabs, on the left side of the
|
|
56
75
|
* toolbar (before the formatting controls). Used for the preview mode
|
|
57
|
-
* switch in
|
|
76
|
+
* switch in Use view. */
|
|
58
77
|
slotAfterTabs?: ReactNode;
|
|
59
78
|
/** Content rendered after the formatting controls (in the middle area). */
|
|
60
79
|
slotAfterActions?: ReactNode;
|
|
@@ -185,6 +204,14 @@ const BUTTONS: ToolbarButton[] = [
|
|
|
185
204
|
group: 'media',
|
|
186
205
|
faIcon: 'fa-solid fa-table',
|
|
187
206
|
},
|
|
207
|
+
{
|
|
208
|
+
id: 'tasklist',
|
|
209
|
+
label: 'tasks',
|
|
210
|
+
icon: '',
|
|
211
|
+
title: 'Insert Task List',
|
|
212
|
+
group: 'media',
|
|
213
|
+
faIcon: 'fa-solid fa-list-check',
|
|
214
|
+
},
|
|
188
215
|
{
|
|
189
216
|
id: 'diagram',
|
|
190
217
|
label: 'diagram',
|
|
@@ -230,12 +257,51 @@ const BUTTONS: ToolbarButton[] = [
|
|
|
230
257
|
const FIRST_MEDIA_INDEX = BUTTONS.findIndex((b) => b.group === 'media');
|
|
231
258
|
const MEDIA_BUTTONS = BUTTONS.filter((b) => b.group === 'media');
|
|
232
259
|
const INSERT_MENU_WIDTH = 200;
|
|
260
|
+
const TASK_LIST_ITEMS = ['Task 1', 'Task 2', 'Task 3'] as const;
|
|
261
|
+
const TASK_LIST_MARKDOWN = TASK_LIST_ITEMS.map((item) => `- [ ] ${item}`).join('\n');
|
|
262
|
+
|
|
263
|
+
// BUTTONS position per id — stamped on each rendered button as
|
|
264
|
+
// data-btn-index so the overflow measurement can map a clipped DOM button
|
|
265
|
+
// back to its BUTTONS entry. Walking a counter over the DOM instead would
|
|
266
|
+
// drift whenever some entries render no button of their own (hidden H5/H6
|
|
267
|
+
// heading levels, the media group collapsed behind the Insert dropdown).
|
|
268
|
+
const BUTTON_INDEX_BY_ID = new Map(BUTTONS.map((b, i) => [b.id, i]));
|
|
233
269
|
|
|
234
270
|
/** Renders a button's icon: a Font Awesome glyph when set, else the text label. */
|
|
235
271
|
function buttonIcon(btn: ToolbarButton): ReactNode {
|
|
236
272
|
return btn.faIcon ? <Icon icon={btn.faIcon} /> : btn.icon;
|
|
237
273
|
}
|
|
238
274
|
|
|
275
|
+
function fileCountLabel(count: number): string {
|
|
276
|
+
return `${count} ${count === 1 ? 'file' : 'files'}`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function fileCountBadge(count: number): string {
|
|
280
|
+
return count > 99 ? '99+' : String(count);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function taskListContent(): JSONContent {
|
|
284
|
+
return {
|
|
285
|
+
type: 'taskList',
|
|
286
|
+
content: TASK_LIST_ITEMS.map((item) => ({
|
|
287
|
+
type: 'taskItem',
|
|
288
|
+
attrs: { checked: false },
|
|
289
|
+
content: [
|
|
290
|
+
{
|
|
291
|
+
type: 'paragraph',
|
|
292
|
+
content: [{ type: 'text', text: item }],
|
|
293
|
+
},
|
|
294
|
+
],
|
|
295
|
+
})),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function insertTaskList(editor: TiptapEditor): void {
|
|
300
|
+
const supportsTaskList = !!editor.schema.nodes.taskList && !!editor.schema.nodes.taskItem;
|
|
301
|
+
const content = supportsTaskList ? taskListContent() : TASK_LIST_MARKDOWN;
|
|
302
|
+
editor.chain().focus().insertContent(content).run();
|
|
303
|
+
}
|
|
304
|
+
|
|
239
305
|
// ─── Tiptap active-state map ────────────────────────────
|
|
240
306
|
|
|
241
307
|
/** Returns true if the given button id is currently active in Tiptap */
|
|
@@ -365,6 +431,44 @@ function insertLayoutBlock(editor: TiptapEditor): void {
|
|
|
365
431
|
.run();
|
|
366
432
|
}
|
|
367
433
|
|
|
434
|
+
function findBlockBySourceLine(blocks: readonly Block[], lineNumber: number): Block | null {
|
|
435
|
+
for (const block of blocks) {
|
|
436
|
+
const pos = block.sourceHeading?.position;
|
|
437
|
+
if (!pos) continue;
|
|
438
|
+
if (pos.start.line <= lineNumber && pos.end.line >= lineNumber) return block;
|
|
439
|
+
}
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function splitTemplateAnnotationParams(text: string): { text: string; params: string[] } {
|
|
444
|
+
const match = matchTrailingTemplateAnnotation(text);
|
|
445
|
+
if (!match) return { text: text.trimEnd(), params: [] };
|
|
446
|
+
const tokens = tokenizeAttrTokens(match.inner.trim());
|
|
447
|
+
const firstIsParam = tokens.length > 0 && tokens[0].indexOf('=') > 0;
|
|
448
|
+
return {
|
|
449
|
+
text: text.slice(0, match.index).trimEnd(),
|
|
450
|
+
params: keepBlockMetaParamTokens(tokens.slice(firstIsParam ? 0 : 1)),
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function buildTemplateAnnotation(template: string, params: string[]): string | null {
|
|
455
|
+
const parts = template ? [template, ...params] : params;
|
|
456
|
+
return parts.length > 0 ? `{[${parts.join(' ')}]}` : null;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function keepBlockMetaParamTokens(tokens: readonly string[]): string[] {
|
|
460
|
+
return tokens.filter((token) => {
|
|
461
|
+
const kv = splitKeyValueToken(token);
|
|
462
|
+
return kv ? BLOCK_META_KEYS.has(kv.key) : false;
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function keepBlockMetaParamString(inner: string | null | undefined): string | null {
|
|
467
|
+
if (!inner) return null;
|
|
468
|
+
const params = keepBlockMetaParamTokens(tokenizeAttrTokens(inner));
|
|
469
|
+
return params.length > 0 ? params.join(' ') : null;
|
|
470
|
+
}
|
|
471
|
+
|
|
368
472
|
/**
|
|
369
473
|
* Formatting toolbar.
|
|
370
474
|
* - WYSIWYG: calls Tiptap chain commands (toggleBold, etc.)
|
|
@@ -373,6 +477,7 @@ function insertLayoutBlock(editor: TiptapEditor): void {
|
|
|
373
477
|
export function Toolbar({
|
|
374
478
|
className,
|
|
375
479
|
showFiles,
|
|
480
|
+
fileCount,
|
|
376
481
|
onToggleFiles,
|
|
377
482
|
slotLeft,
|
|
378
483
|
slotAfterTabs,
|
|
@@ -384,17 +489,23 @@ export function Toolbar({
|
|
|
384
489
|
activeView,
|
|
385
490
|
setActiveView,
|
|
386
491
|
markdownSource,
|
|
492
|
+
doc,
|
|
387
493
|
setMarkdownSource,
|
|
388
494
|
tiptapEditor,
|
|
389
495
|
monacoEditor,
|
|
390
496
|
activeSceneText,
|
|
391
497
|
mediaProvider,
|
|
392
498
|
editorMode,
|
|
499
|
+
layoutMode,
|
|
500
|
+
activeBlockStartLine,
|
|
393
501
|
versioning,
|
|
394
502
|
allowRecording,
|
|
395
503
|
documentLinkProvider,
|
|
396
504
|
colorScheme,
|
|
505
|
+
mediaRevision,
|
|
506
|
+
bumpMediaRevision,
|
|
397
507
|
} = useEditorContext();
|
|
508
|
+
const previewSettings = usePreviewSettingsOptional();
|
|
398
509
|
// When a canvas textbox is being edited, its Tiptap instance takes over
|
|
399
510
|
// the formatting buttons; otherwise they drive the document editor. The
|
|
400
511
|
// `level` gates which buttons apply (inline labels vs. rich textboxes).
|
|
@@ -409,6 +520,30 @@ export function Toolbar({
|
|
|
409
520
|
return true;
|
|
410
521
|
});
|
|
411
522
|
const showViewTabs = visibleViews.length > 1;
|
|
523
|
+
const [scannedFileCount, setScannedFileCount] = useState(0);
|
|
524
|
+
const resolvedFileCount = fileCount ?? scannedFileCount;
|
|
525
|
+
|
|
526
|
+
useEffect(() => {
|
|
527
|
+
if (fileCount !== undefined) return;
|
|
528
|
+
if (!mediaProvider) {
|
|
529
|
+
setScannedFileCount(0);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
let cancelled = false;
|
|
534
|
+
mediaProvider.listMedia().then(
|
|
535
|
+
(entries) => {
|
|
536
|
+
if (!cancelled) setScannedFileCount(entries.length);
|
|
537
|
+
},
|
|
538
|
+
() => {
|
|
539
|
+
if (!cancelled) setScannedFileCount(0);
|
|
540
|
+
},
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
return () => {
|
|
544
|
+
cancelled = true;
|
|
545
|
+
};
|
|
546
|
+
}, [fileCount, mediaProvider, mediaRevision, showFiles]);
|
|
412
547
|
|
|
413
548
|
// Hidden file input for image picker
|
|
414
549
|
const imageInputRef = useRef<HTMLInputElement>(null);
|
|
@@ -490,6 +625,11 @@ export function Toolbar({
|
|
|
490
625
|
// ── Overflow detection ────────────────────────────────
|
|
491
626
|
const actionsRef = useRef<HTMLDivElement>(null);
|
|
492
627
|
const [measuredOverflowIndex, setMeasuredOverflowIndex] = useState<number | null>(null);
|
|
628
|
+
// '|'-joined data-contextual ids of the contextual groups (template /
|
|
629
|
+
// transition pickers, table controls) whose right edge doesn't fit in the
|
|
630
|
+
// actions row. Stored as a string so identical measurements bail out of
|
|
631
|
+
// re-rendering.
|
|
632
|
+
const [clippedContextualKey, setClippedContextualKey] = useState('');
|
|
493
633
|
const [showOverflow, setShowOverflow] = useState(false);
|
|
494
634
|
const overflowRef = useRef<HTMLDivElement>(null);
|
|
495
635
|
|
|
@@ -500,6 +640,7 @@ export function Toolbar({
|
|
|
500
640
|
const [showLayoutManager, setShowLayoutManager] = useState(false);
|
|
501
641
|
|
|
502
642
|
const overflowIndex = measuredOverflowIndex;
|
|
643
|
+
const clippedContextual = new Set(clippedContextualKey ? clippedContextualKey.split('|') : []);
|
|
503
644
|
|
|
504
645
|
useEffect(() => {
|
|
505
646
|
const container = actionsRef.current;
|
|
@@ -507,39 +648,77 @@ export function Toolbar({
|
|
|
507
648
|
|
|
508
649
|
const measure = () => {
|
|
509
650
|
const containerRight = container.getBoundingClientRect().right;
|
|
651
|
+
// Contextual groups aren't BUTTONS entries, so they must not shift the
|
|
652
|
+
// btnIndex mapping below — measure them separately.
|
|
510
653
|
const children = container.querySelectorAll<HTMLElement>(
|
|
511
|
-
':scope > .squisq-toolbar-group > .squisq-toolbar-button',
|
|
654
|
+
':scope > .squisq-toolbar-group:not(.squisq-toolbar-contextual) > .squisq-toolbar-button',
|
|
512
655
|
);
|
|
513
656
|
let firstHidden: number | null = null;
|
|
514
|
-
let btnIndex = 0;
|
|
515
657
|
children.forEach((child) => {
|
|
516
658
|
if (firstHidden !== null) return;
|
|
517
|
-
//
|
|
518
|
-
//
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
firstHidden = btnIndex;
|
|
659
|
+
// A button is hidden if its right edge extends past the container.
|
|
660
|
+
// Its data-btn-index carries its BUTTONS position (see
|
|
661
|
+
// BUTTON_INDEX_BY_ID for why the DOM can't be walked positionally).
|
|
662
|
+
if (child.getBoundingClientRect().right > containerRight + 2) {
|
|
663
|
+
const index = Number(child.dataset.btnIndex);
|
|
664
|
+
firstHidden = Number.isNaN(index) ? null : index;
|
|
524
665
|
}
|
|
525
|
-
btnIndex += btnCount;
|
|
526
666
|
});
|
|
527
667
|
setMeasuredOverflowIndex(firstHidden);
|
|
668
|
+
// Contextual groups (template/transition pickers, table controls) that
|
|
669
|
+
// don't fit move into the overflow menu instead of painting cropped.
|
|
670
|
+
const clipped: string[] = [];
|
|
671
|
+
container
|
|
672
|
+
.querySelectorAll<HTMLElement>(':scope > .squisq-toolbar-contextual')
|
|
673
|
+
.forEach((group) => {
|
|
674
|
+
if (group.getBoundingClientRect().right > containerRight + 2) {
|
|
675
|
+
clipped.push(group.dataset.contextual ?? '');
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
setClippedContextualKey(clipped.join('|'));
|
|
528
679
|
};
|
|
529
680
|
|
|
681
|
+
// Observe the row and every group in it: contextual groups mount/unmount
|
|
682
|
+
// with the cursor context and change width with their value, all without
|
|
683
|
+
// resizing the flex:1 container itself — a container-only observer would
|
|
684
|
+
// go stale.
|
|
530
685
|
const ro = new ResizeObserver(measure);
|
|
531
|
-
|
|
686
|
+
const observeAll = () => {
|
|
687
|
+
ro.disconnect();
|
|
688
|
+
ro.observe(container);
|
|
689
|
+
container
|
|
690
|
+
.querySelectorAll<HTMLElement>(':scope > .squisq-toolbar-group')
|
|
691
|
+
.forEach((group) => ro.observe(group));
|
|
692
|
+
};
|
|
693
|
+
const mo = new MutationObserver(() => {
|
|
694
|
+
observeAll();
|
|
695
|
+
measure();
|
|
696
|
+
});
|
|
697
|
+
mo.observe(container, { childList: true });
|
|
698
|
+
observeAll();
|
|
532
699
|
measure();
|
|
533
|
-
return () =>
|
|
700
|
+
return () => {
|
|
701
|
+
ro.disconnect();
|
|
702
|
+
mo.disconnect();
|
|
703
|
+
};
|
|
534
704
|
}, [activeView]);
|
|
535
705
|
|
|
536
|
-
// Close overflow menu on outside click
|
|
706
|
+
// Close overflow menu on outside click. Clicks inside the template
|
|
707
|
+
// gallery / transition flyout portals count as inside: those popovers are
|
|
708
|
+
// hosted by pickers living in this menu, and closing the menu on their
|
|
709
|
+
// mousedown would unmount them before the click completes.
|
|
537
710
|
useEffect(() => {
|
|
538
711
|
if (!showOverflow) return;
|
|
539
712
|
const handleClick = (e: MouseEvent) => {
|
|
540
|
-
|
|
541
|
-
|
|
713
|
+
const target = e.target as Node;
|
|
714
|
+
if (overflowRef.current?.contains(target)) return;
|
|
715
|
+
if (
|
|
716
|
+
target instanceof Element &&
|
|
717
|
+
target.closest(`#${TEMPLATE_GALLERY_PORTAL_ID}, #${TRANSITION_FLYOUT_PORTAL_ID}`)
|
|
718
|
+
) {
|
|
719
|
+
return;
|
|
542
720
|
}
|
|
721
|
+
setShowOverflow(false);
|
|
543
722
|
};
|
|
544
723
|
document.addEventListener('mousedown', handleClick);
|
|
545
724
|
return () => document.removeEventListener('mousedown', handleClick);
|
|
@@ -673,6 +852,9 @@ export function Toolbar({
|
|
|
673
852
|
case 'table':
|
|
674
853
|
tiptapEditor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run();
|
|
675
854
|
break;
|
|
855
|
+
case 'tasklist':
|
|
856
|
+
insertTaskList(tiptapEditor);
|
|
857
|
+
break;
|
|
676
858
|
case 'diagram':
|
|
677
859
|
// Empty diagram is usable via its "+ Add first node" affordance;
|
|
678
860
|
// sub-headings under this block become nodes.
|
|
@@ -827,6 +1009,11 @@ export function Toolbar({
|
|
|
827
1009
|
newCursorOffset = 3; // after \n|
|
|
828
1010
|
break;
|
|
829
1011
|
}
|
|
1012
|
+
case 'tasklist': {
|
|
1013
|
+
replacement = `\n${TASK_LIST_MARKDOWN}\n`;
|
|
1014
|
+
newCursorOffset = replacement.length;
|
|
1015
|
+
break;
|
|
1016
|
+
}
|
|
830
1017
|
case 'diagram': {
|
|
831
1018
|
// A diagram is just a heading with the `{[diagram]}` template
|
|
832
1019
|
// annotation; the WYSIWYG view renders its editable canvas.
|
|
@@ -908,6 +1095,9 @@ export function Toolbar({
|
|
|
908
1095
|
insertion =
|
|
909
1096
|
'\n| Header 1 | Header 2 | Header 3 |\n| --- | --- | --- |\n| Cell | Cell | Cell |\n| Cell | Cell | Cell |\n';
|
|
910
1097
|
break;
|
|
1098
|
+
case 'tasklist':
|
|
1099
|
+
insertion = `\n${TASK_LIST_MARKDOWN}\n`;
|
|
1100
|
+
break;
|
|
911
1101
|
case 'diagram':
|
|
912
1102
|
insertion = '\n## Diagram {[diagram]}\n';
|
|
913
1103
|
break;
|
|
@@ -932,6 +1122,7 @@ export function Toolbar({
|
|
|
932
1122
|
if (!mediaProvider) return;
|
|
933
1123
|
const buffer = await file.arrayBuffer();
|
|
934
1124
|
const relativePath = await mediaProvider.addMedia(file.name, buffer, file.type);
|
|
1125
|
+
bumpMediaRevision();
|
|
935
1126
|
const altText = file.name.replace(/\.[^.]+$/, '').replace(/[-_]/g, ' ');
|
|
936
1127
|
|
|
937
1128
|
if (activeView === 'wysiwyg' && tiptapEditor) {
|
|
@@ -948,7 +1139,15 @@ export function Toolbar({
|
|
|
948
1139
|
setMarkdownSource(markdownSource + `\n\n`);
|
|
949
1140
|
}
|
|
950
1141
|
},
|
|
951
|
-
[
|
|
1142
|
+
[
|
|
1143
|
+
mediaProvider,
|
|
1144
|
+
activeView,
|
|
1145
|
+
tiptapEditor,
|
|
1146
|
+
monacoEditor,
|
|
1147
|
+
markdownSource,
|
|
1148
|
+
setMarkdownSource,
|
|
1149
|
+
bumpMediaRevision,
|
|
1150
|
+
],
|
|
952
1151
|
);
|
|
953
1152
|
|
|
954
1153
|
const handleAction = useCallback(
|
|
@@ -1230,11 +1429,11 @@ export function Toolbar({
|
|
|
1230
1429
|
}
|
|
1231
1430
|
setRawHeadingLine(pos.lineNumber);
|
|
1232
1431
|
setRawTransition(readHeadingLineTransition(line));
|
|
1233
|
-
const annotMatch = headingMatch[1]
|
|
1432
|
+
const annotMatch = matchTrailingTemplateAnnotation(headingMatch[1]);
|
|
1234
1433
|
if (annotMatch) {
|
|
1235
|
-
|
|
1236
|
-
const
|
|
1237
|
-
setRawTemplate(
|
|
1434
|
+
const tokens = tokenizeAttrTokens(annotMatch.inner.trim());
|
|
1435
|
+
const firstIsParam = tokens.length > 0 && tokens[0].indexOf('=') > 0;
|
|
1436
|
+
setRawTemplate(firstIsParam ? '' : (tokens[0] ?? ''));
|
|
1238
1437
|
} else {
|
|
1239
1438
|
setRawTemplate('');
|
|
1240
1439
|
}
|
|
@@ -1302,6 +1501,55 @@ export function Toolbar({
|
|
|
1302
1501
|
return recommendTemplatesForBlock(profile, TEMPLATE_NAMES).recommended;
|
|
1303
1502
|
}, [currentTemplate, isRawView, isWysiwyg, rawHeadingLine, wysiwygHeadingIndex, markdownSource]);
|
|
1304
1503
|
|
|
1504
|
+
const templatePreviewBlock = useMemo(() => {
|
|
1505
|
+
if (!doc || currentTemplate === null) return null;
|
|
1506
|
+
const flat = flattenBlocks(doc.blocks);
|
|
1507
|
+
if (flat.length === 0) return null;
|
|
1508
|
+
|
|
1509
|
+
if (isRawView && rawHeadingLine !== null) {
|
|
1510
|
+
return findBlockBySourceLine(flat, rawHeadingLine);
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
if (isWysiwyg) {
|
|
1514
|
+
if (layoutMode !== 'document' && activeBlockStartLine !== null) {
|
|
1515
|
+
return findBlockBySourceLine(flat, activeBlockStartLine);
|
|
1516
|
+
}
|
|
1517
|
+
if (wysiwygHeadingIndex !== null) {
|
|
1518
|
+
return flat[wysiwygHeadingIndex] ?? null;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
return null;
|
|
1523
|
+
}, [
|
|
1524
|
+
activeBlockStartLine,
|
|
1525
|
+
currentTemplate,
|
|
1526
|
+
doc,
|
|
1527
|
+
isRawView,
|
|
1528
|
+
isWysiwyg,
|
|
1529
|
+
layoutMode,
|
|
1530
|
+
rawHeadingLine,
|
|
1531
|
+
wysiwygHeadingIndex,
|
|
1532
|
+
]);
|
|
1533
|
+
|
|
1534
|
+
const templatePreviewSource = useMemo(
|
|
1535
|
+
() =>
|
|
1536
|
+
templatePreviewBlock
|
|
1537
|
+
? {
|
|
1538
|
+
block: templatePreviewBlock,
|
|
1539
|
+
theme: previewSettings?.activeTheme ?? DEFAULT_THEME,
|
|
1540
|
+
viewport: previewSettings?.activeViewport ?? VIEWPORT_PRESETS.landscape,
|
|
1541
|
+
basePath: '/',
|
|
1542
|
+
mediaProvider,
|
|
1543
|
+
}
|
|
1544
|
+
: undefined,
|
|
1545
|
+
[
|
|
1546
|
+
mediaProvider,
|
|
1547
|
+
previewSettings?.activeTheme,
|
|
1548
|
+
previewSettings?.activeViewport,
|
|
1549
|
+
templatePreviewBlock,
|
|
1550
|
+
],
|
|
1551
|
+
);
|
|
1552
|
+
|
|
1305
1553
|
const handleTemplatePick = (value: string) => {
|
|
1306
1554
|
// Raw (Monaco) — rewrite the heading line's annotation suffix in place.
|
|
1307
1555
|
if (isRawView && monacoEditor) {
|
|
@@ -1313,9 +1561,9 @@ export function Toolbar({
|
|
|
1313
1561
|
const headingMatch = lineText.match(/^(#{1,6}\s+)(.+)$/);
|
|
1314
1562
|
if (!headingMatch) return;
|
|
1315
1563
|
const prefix = headingMatch[1];
|
|
1316
|
-
|
|
1317
|
-
const
|
|
1318
|
-
const newLine =
|
|
1564
|
+
const { text: bareText, params } = splitTemplateAnnotationParams(headingMatch[2]);
|
|
1565
|
+
const annotation = buildTemplateAnnotation(value, params);
|
|
1566
|
+
const newLine = annotation ? `${prefix}${bareText} ${annotation}` : `${prefix}${bareText}`;
|
|
1319
1567
|
monacoEditor.executeEdits('toolbar-template-pick', [
|
|
1320
1568
|
{
|
|
1321
1569
|
range: {
|
|
@@ -1332,14 +1580,22 @@ export function Toolbar({
|
|
|
1332
1580
|
}
|
|
1333
1581
|
// WYSIWYG — update the heading node attributes.
|
|
1334
1582
|
if (!tiptapEditor) return;
|
|
1583
|
+
const attrs = tiptapEditor.getAttributes('heading') as {
|
|
1584
|
+
dataTemplateParams?: string | null;
|
|
1585
|
+
};
|
|
1586
|
+
const dataTemplateParams = keepBlockMetaParamString(attrs.dataTemplateParams);
|
|
1335
1587
|
if (value === '') {
|
|
1336
1588
|
tiptapEditor
|
|
1337
1589
|
.chain()
|
|
1338
1590
|
.focus()
|
|
1339
|
-
.updateAttributes('heading', { dataTemplate: null, dataTemplateParams
|
|
1591
|
+
.updateAttributes('heading', { dataTemplate: null, dataTemplateParams })
|
|
1340
1592
|
.run();
|
|
1341
1593
|
} else {
|
|
1342
|
-
tiptapEditor
|
|
1594
|
+
tiptapEditor
|
|
1595
|
+
.chain()
|
|
1596
|
+
.focus()
|
|
1597
|
+
.updateAttributes('heading', { dataTemplate: value, dataTemplateParams })
|
|
1598
|
+
.run();
|
|
1343
1599
|
}
|
|
1344
1600
|
};
|
|
1345
1601
|
|
|
@@ -1382,13 +1638,36 @@ export function Toolbar({
|
|
|
1382
1638
|
monacoEditor.focus();
|
|
1383
1639
|
return;
|
|
1384
1640
|
}
|
|
1385
|
-
// WYSIWYG —
|
|
1641
|
+
// WYSIWYG — write transitions into `{[…]}` params and migrate legacy
|
|
1642
|
+
// Pandoc transition keys out of `dataBlockAttrs`.
|
|
1386
1643
|
if (!tiptapEditor) return;
|
|
1387
|
-
const attrs = tiptapEditor.getAttributes('heading') as {
|
|
1388
|
-
|
|
1389
|
-
|
|
1644
|
+
const attrs = tiptapEditor.getAttributes('heading') as {
|
|
1645
|
+
dataBlockAttrs?: string | null;
|
|
1646
|
+
dataTemplateParams?: string | null;
|
|
1647
|
+
};
|
|
1648
|
+
const updated = setHeadingAttrsTransition(attrs.dataBlockAttrs, attrs.dataTemplateParams, next);
|
|
1649
|
+
tiptapEditor
|
|
1650
|
+
.chain()
|
|
1651
|
+
.focus()
|
|
1652
|
+
.updateAttributes('heading', {
|
|
1653
|
+
dataBlockAttrs: updated.blockAttrsInner,
|
|
1654
|
+
dataTemplateParams: updated.templateParams,
|
|
1655
|
+
})
|
|
1656
|
+
.run();
|
|
1390
1657
|
};
|
|
1391
1658
|
|
|
1659
|
+
// ── Per-block controls in the overflow menu ────────────
|
|
1660
|
+
// The template + transition pickers configure the active heading block, so
|
|
1661
|
+
// when either is pushed into the ··· menu we group them under a labeled
|
|
1662
|
+
// "<name> Block:" heading. `currentTemplate`/`currentTransition` are non-null
|
|
1663
|
+
// together (both gated on a heading being active); an empty template string
|
|
1664
|
+
// is a heading with no visual template, labeled "Heading".
|
|
1665
|
+
const showTemplateInOverflow = currentTemplate !== null && clippedContextual.has('template');
|
|
1666
|
+
const showTransitionInOverflow =
|
|
1667
|
+
currentTransition !== null && clippedContextual.has('transition');
|
|
1668
|
+
const showBlockSectionInOverflow = showTemplateInOverflow || showTransitionInOverflow;
|
|
1669
|
+
const overflowBlockLabel = currentTemplate ? templateLabel(currentTemplate) : 'Heading';
|
|
1670
|
+
|
|
1392
1671
|
return (
|
|
1393
1672
|
<div
|
|
1394
1673
|
className={`squisq-toolbar ${className || ''}`}
|
|
@@ -1462,6 +1741,7 @@ export function Toolbar({
|
|
|
1462
1741
|
key={btn.id}
|
|
1463
1742
|
ref={btn.id === 'emoji' ? emojiButtonRef : undefined}
|
|
1464
1743
|
className={`squisq-toolbar-button${active ? ' squisq-toolbar-button--active' : ''}`}
|
|
1744
|
+
data-btn-index={BUTTON_INDEX_BY_ID.get(btn.id)}
|
|
1465
1745
|
data-tooltip={disabled ? 'Insert image (requires media provider)' : btn.title}
|
|
1466
1746
|
onClick={() => handleAction(btn.id)}
|
|
1467
1747
|
aria-label={btn.title}
|
|
@@ -1481,7 +1761,7 @@ export function Toolbar({
|
|
|
1481
1761
|
<button
|
|
1482
1762
|
ref={insertMenuButtonRef}
|
|
1483
1763
|
className={`squisq-toolbar-button${insertMenuAnchor ? ' squisq-toolbar-button--active' : ''}`}
|
|
1484
|
-
data-btn-
|
|
1764
|
+
data-btn-index={FIRST_MEDIA_INDEX}
|
|
1485
1765
|
data-tooltip="Insert..."
|
|
1486
1766
|
onClick={() => (insertMenuAnchor ? closeInsertMenu() : openInsertMenu())}
|
|
1487
1767
|
aria-label="Insert"
|
|
@@ -1494,192 +1774,202 @@ export function Toolbar({
|
|
|
1494
1774
|
|
|
1495
1775
|
{/* Template picker — visible when the cursor is in a heading.
|
|
1496
1776
|
In WYSIWYG, reads from the heading node's `dataTemplate`; in
|
|
1497
|
-
Markdown view, parses the `{[...]}` suffix on the cursor's line.
|
|
1777
|
+
Markdown view, parses the `{[...]}` suffix on the cursor's line.
|
|
1778
|
+
The `squisq-toolbar-contextual` class + data-contextual id opt
|
|
1779
|
+
this group (and the two below) into the clipping measurement:
|
|
1780
|
+
when it doesn't fit it hides here and moves into the ··· menu. */}
|
|
1498
1781
|
{currentTemplate !== null && (
|
|
1499
|
-
|
|
1782
|
+
<div
|
|
1783
|
+
className={`squisq-toolbar-group squisq-toolbar-contextual squisq-template-picker${clippedContextual.has('template') ? ' squisq-toolbar-contextual--clipped' : ''}`}
|
|
1784
|
+
data-contextual="template"
|
|
1785
|
+
>
|
|
1500
1786
|
<div className="squisq-toolbar-separator" />
|
|
1501
|
-
<
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1787
|
+
<TemplatePicker
|
|
1788
|
+
value={currentTemplate}
|
|
1789
|
+
onChange={handleTemplatePick}
|
|
1790
|
+
colorScheme={colorScheme}
|
|
1791
|
+
recommended={recommendedTemplates}
|
|
1792
|
+
previewSource={templatePreviewSource}
|
|
1793
|
+
/>
|
|
1794
|
+
</div>
|
|
1509
1795
|
)}
|
|
1510
1796
|
|
|
1511
1797
|
{/* Transition picker — visible alongside the template picker when a
|
|
1512
1798
|
block (heading) is active. Writes `transition=` into the block's
|
|
1513
1799
|
Pandoc `{…}` attribute block (the canonical home for transitions). */}
|
|
1514
1800
|
{currentTransition !== null && (
|
|
1515
|
-
|
|
1801
|
+
<div
|
|
1802
|
+
className={`squisq-toolbar-group squisq-toolbar-contextual squisq-transition-picker-group${clippedContextual.has('transition') ? ' squisq-toolbar-contextual--clipped' : ''}`}
|
|
1803
|
+
data-contextual="transition"
|
|
1804
|
+
>
|
|
1516
1805
|
<div className="squisq-toolbar-separator" />
|
|
1517
|
-
<
|
|
1518
|
-
|
|
1519
|
-
</div>
|
|
1520
|
-
</>
|
|
1806
|
+
<TransitionPicker value={currentTransition} onChange={handleTransitionPick} />
|
|
1807
|
+
</div>
|
|
1521
1808
|
)}
|
|
1522
1809
|
|
|
1523
1810
|
{/* Table controls — visible when cursor is in a table (WYSIWYG) */}
|
|
1524
1811
|
{isInTable && (
|
|
1525
|
-
|
|
1812
|
+
<div
|
|
1813
|
+
className={`squisq-toolbar-group squisq-toolbar-contextual squisq-table-controls${clippedContextual.has('table') ? ' squisq-toolbar-contextual--clipped' : ''}`}
|
|
1814
|
+
data-contextual="table"
|
|
1815
|
+
>
|
|
1526
1816
|
<div className="squisq-toolbar-separator" />
|
|
1527
|
-
<
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1817
|
+
<span className="squisq-table-controls-label">Table:</span>
|
|
1818
|
+
<button
|
|
1819
|
+
className="squisq-toolbar-button"
|
|
1820
|
+
data-tooltip="Add column before"
|
|
1821
|
+
onClick={() => tiptapEditor!.chain().focus().addColumnBefore().run()}
|
|
1822
|
+
aria-label="Add column before"
|
|
1823
|
+
>
|
|
1824
|
+
<svg
|
|
1825
|
+
width="16"
|
|
1826
|
+
height="16"
|
|
1827
|
+
viewBox="0 0 16 16"
|
|
1828
|
+
fill="none"
|
|
1829
|
+
stroke="currentColor"
|
|
1830
|
+
strokeWidth="1.5"
|
|
1831
|
+
strokeLinecap="round"
|
|
1534
1832
|
>
|
|
1535
|
-
<
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1833
|
+
<rect x="7" y="2" width="8" height="12" rx="1" />
|
|
1834
|
+
<line x1="11" y1="2" x2="11" y2="14" />
|
|
1835
|
+
<line x1="1" y1="8" x2="4.5" y2="8" />
|
|
1836
|
+
<line x1="2.75" y1="6.25" x2="2.75" y2="9.75" />
|
|
1837
|
+
</svg>
|
|
1838
|
+
</button>
|
|
1839
|
+
<button
|
|
1840
|
+
className="squisq-toolbar-button"
|
|
1841
|
+
data-tooltip="Add column after"
|
|
1842
|
+
onClick={() => tiptapEditor!.chain().focus().addColumnAfter().run()}
|
|
1843
|
+
aria-label="Add column after"
|
|
1844
|
+
>
|
|
1845
|
+
<svg
|
|
1846
|
+
width="16"
|
|
1847
|
+
height="16"
|
|
1848
|
+
viewBox="0 0 16 16"
|
|
1849
|
+
fill="none"
|
|
1850
|
+
stroke="currentColor"
|
|
1851
|
+
strokeWidth="1.5"
|
|
1852
|
+
strokeLinecap="round"
|
|
1555
1853
|
>
|
|
1556
|
-
<
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1854
|
+
<rect x="1" y="2" width="8" height="12" rx="1" />
|
|
1855
|
+
<line x1="5" y1="2" x2="5" y2="14" />
|
|
1856
|
+
<line x1="11.5" y1="8" x2="15" y2="8" />
|
|
1857
|
+
<line x1="13.25" y1="6.25" x2="13.25" y2="9.75" />
|
|
1858
|
+
</svg>
|
|
1859
|
+
</button>
|
|
1860
|
+
<button
|
|
1861
|
+
className="squisq-toolbar-button"
|
|
1862
|
+
data-tooltip="Delete column"
|
|
1863
|
+
onClick={() => tiptapEditor!.chain().focus().deleteColumn().run()}
|
|
1864
|
+
aria-label="Delete column"
|
|
1865
|
+
>
|
|
1866
|
+
<svg
|
|
1867
|
+
width="16"
|
|
1868
|
+
height="16"
|
|
1869
|
+
viewBox="0 0 16 16"
|
|
1870
|
+
fill="none"
|
|
1871
|
+
stroke="currentColor"
|
|
1872
|
+
strokeWidth="1.5"
|
|
1873
|
+
strokeLinecap="round"
|
|
1576
1874
|
>
|
|
1577
|
-
<
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1875
|
+
<rect x="4" y="1" width="8" height="14" rx="1" />
|
|
1876
|
+
<line x1="6" y1="5.5" x2="10" y2="10.5" />
|
|
1877
|
+
<line x1="10" y1="5.5" x2="6" y2="10.5" />
|
|
1878
|
+
</svg>
|
|
1879
|
+
</button>
|
|
1880
|
+
<button
|
|
1881
|
+
className="squisq-toolbar-button"
|
|
1882
|
+
data-tooltip="Add row above"
|
|
1883
|
+
onClick={() => tiptapEditor!.chain().focus().addRowBefore().run()}
|
|
1884
|
+
aria-label="Add row above"
|
|
1885
|
+
>
|
|
1886
|
+
<svg
|
|
1887
|
+
width="16"
|
|
1888
|
+
height="16"
|
|
1889
|
+
viewBox="0 0 16 16"
|
|
1890
|
+
fill="none"
|
|
1891
|
+
stroke="currentColor"
|
|
1892
|
+
strokeWidth="1.5"
|
|
1893
|
+
strokeLinecap="round"
|
|
1596
1894
|
>
|
|
1597
|
-
<
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1895
|
+
<rect x="2" y="6" width="12" height="9" rx="1" />
|
|
1896
|
+
<line x1="2" y1="10.5" x2="14" y2="10.5" />
|
|
1897
|
+
<line x1="8" y1="1" x2="8" y2="4.5" />
|
|
1898
|
+
<line x1="6.25" y1="2.75" x2="9.75" y2="2.75" />
|
|
1899
|
+
</svg>
|
|
1900
|
+
</button>
|
|
1901
|
+
<button
|
|
1902
|
+
className="squisq-toolbar-button"
|
|
1903
|
+
data-tooltip="Add row below"
|
|
1904
|
+
onClick={() => tiptapEditor!.chain().focus().addRowAfter().run()}
|
|
1905
|
+
aria-label="Add row below"
|
|
1906
|
+
>
|
|
1907
|
+
<svg
|
|
1908
|
+
width="16"
|
|
1909
|
+
height="16"
|
|
1910
|
+
viewBox="0 0 16 16"
|
|
1911
|
+
fill="none"
|
|
1912
|
+
stroke="currentColor"
|
|
1913
|
+
strokeWidth="1.5"
|
|
1914
|
+
strokeLinecap="round"
|
|
1617
1915
|
>
|
|
1618
|
-
<
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1916
|
+
<rect x="2" y="1" width="12" height="9" rx="1" />
|
|
1917
|
+
<line x1="2" y1="5.5" x2="14" y2="5.5" />
|
|
1918
|
+
<line x1="8" y1="11.5" x2="8" y2="15" />
|
|
1919
|
+
<line x1="6.25" y1="13.25" x2="9.75" y2="13.25" />
|
|
1920
|
+
</svg>
|
|
1921
|
+
</button>
|
|
1922
|
+
<button
|
|
1923
|
+
className="squisq-toolbar-button"
|
|
1924
|
+
data-tooltip="Delete row"
|
|
1925
|
+
onClick={() => tiptapEditor!.chain().focus().deleteRow().run()}
|
|
1926
|
+
aria-label="Delete row"
|
|
1927
|
+
>
|
|
1928
|
+
<svg
|
|
1929
|
+
width="16"
|
|
1930
|
+
height="16"
|
|
1931
|
+
viewBox="0 0 16 16"
|
|
1932
|
+
fill="none"
|
|
1933
|
+
stroke="currentColor"
|
|
1934
|
+
strokeWidth="1.5"
|
|
1935
|
+
strokeLinecap="round"
|
|
1638
1936
|
>
|
|
1639
|
-
<
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1937
|
+
<rect x="1" y="4" width="14" height="8" rx="1" />
|
|
1938
|
+
<line x1="5.5" y1="6" x2="10.5" y2="10" />
|
|
1939
|
+
<line x1="10.5" y1="6" x2="5.5" y2="10" />
|
|
1940
|
+
</svg>
|
|
1941
|
+
</button>
|
|
1942
|
+
<button
|
|
1943
|
+
className="squisq-toolbar-button squisq-toolbar-button--danger"
|
|
1944
|
+
data-tooltip="Delete table"
|
|
1945
|
+
onClick={() => tiptapEditor!.chain().focus().deleteTable().run()}
|
|
1946
|
+
aria-label="Delete table"
|
|
1947
|
+
>
|
|
1948
|
+
<svg
|
|
1949
|
+
width="16"
|
|
1950
|
+
height="16"
|
|
1951
|
+
viewBox="0 0 16 16"
|
|
1952
|
+
fill="none"
|
|
1953
|
+
stroke="currentColor"
|
|
1954
|
+
strokeWidth="1.5"
|
|
1955
|
+
strokeLinecap="round"
|
|
1658
1956
|
>
|
|
1659
|
-
<
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
>
|
|
1668
|
-
<rect x="1" y="1" width="14" height="14" rx="1" />
|
|
1669
|
-
<line x1="1" y1="5.5" x2="15" y2="5.5" />
|
|
1670
|
-
<line x1="5.5" y1="1" x2="5.5" y2="15" />
|
|
1671
|
-
<line x1="4.5" y1="4.5" x2="11.5" y2="11.5" strokeWidth="2" />
|
|
1672
|
-
<line x1="11.5" y1="4.5" x2="4.5" y2="11.5" strokeWidth="2" />
|
|
1673
|
-
</svg>
|
|
1674
|
-
</button>
|
|
1675
|
-
</div>
|
|
1676
|
-
</>
|
|
1957
|
+
<rect x="1" y="1" width="14" height="14" rx="1" />
|
|
1958
|
+
<line x1="1" y1="5.5" x2="15" y2="5.5" />
|
|
1959
|
+
<line x1="5.5" y1="1" x2="5.5" y2="15" />
|
|
1960
|
+
<line x1="4.5" y1="4.5" x2="11.5" y2="11.5" strokeWidth="2" />
|
|
1961
|
+
<line x1="11.5" y1="4.5" x2="4.5" y2="11.5" strokeWidth="2" />
|
|
1962
|
+
</svg>
|
|
1963
|
+
</button>
|
|
1964
|
+
</div>
|
|
1677
1965
|
)}
|
|
1678
1966
|
</div>
|
|
1679
1967
|
)}
|
|
1680
1968
|
|
|
1681
|
-
{/* Overflow menu — outside the overflow:hidden actions container
|
|
1682
|
-
|
|
1969
|
+
{/* Overflow menu — outside the overflow:hidden actions container.
|
|
1970
|
+
Also appears when a contextual group (template/transition picker,
|
|
1971
|
+
table controls) is clipped, even if every plain button fits. */}
|
|
1972
|
+
{!isPreview && !isCodeMode && (overflowIndex !== null || clippedContextual.size > 0) && (
|
|
1683
1973
|
<div className="squisq-toolbar-overflow" ref={overflowRef}>
|
|
1684
1974
|
<button
|
|
1685
1975
|
className={`squisq-toolbar-button squisq-toolbar-overflow-trigger${showOverflow ? ' squisq-toolbar-button--active' : ''}`}
|
|
@@ -1694,7 +1984,7 @@ export function Toolbar({
|
|
|
1694
1984
|
<div
|
|
1695
1985
|
className={`squisq-toolbar-overflow-menu squisq-toolbar-overflow-menu--${overflowPlacement}`}
|
|
1696
1986
|
>
|
|
1697
|
-
{BUTTONS.slice(overflowIndex)
|
|
1987
|
+
{BUTTONS.slice(overflowIndex ?? BUTTONS.length)
|
|
1698
1988
|
.filter((b) => isButtonVisible(b.id))
|
|
1699
1989
|
// Media buttons are represented by the synthetic Insert dropdown
|
|
1700
1990
|
// in both the visible toolbar and the overflow menu.
|
|
@@ -1748,23 +2038,48 @@ export function Toolbar({
|
|
|
1748
2038
|
</button>
|
|
1749
2039
|
)}
|
|
1750
2040
|
|
|
1751
|
-
{/*
|
|
1752
|
-
|
|
2041
|
+
{/* Per-block section — the template + transition pickers move
|
|
2042
|
+
here when clipped, under a divider and a "<name> Block:"
|
|
2043
|
+
heading that names the block they configure. */}
|
|
2044
|
+
{showBlockSectionInOverflow && (
|
|
2045
|
+
<>
|
|
2046
|
+
<div
|
|
2047
|
+
className="squisq-toolbar-separator"
|
|
2048
|
+
style={{ margin: '4px 0', width: '100%', height: 1 }}
|
|
2049
|
+
/>
|
|
2050
|
+
<div className="squisq-toolbar-overflow-heading">{overflowBlockLabel} Block:</div>
|
|
2051
|
+
</>
|
|
2052
|
+
)}
|
|
2053
|
+
|
|
2054
|
+
{/* Contextual: template picker in overflow — only when the
|
|
2055
|
+
inline one is clipped, so the control lives in exactly one
|
|
2056
|
+
visible place at a time. */}
|
|
2057
|
+
{showTemplateInOverflow && (
|
|
1753
2058
|
<div className="squisq-toolbar-overflow-item squisq-toolbar-overflow-template">
|
|
1754
|
-
<span>Template:</span>
|
|
1755
2059
|
<TemplatePicker
|
|
1756
|
-
value={currentTemplate}
|
|
2060
|
+
value={currentTemplate!}
|
|
1757
2061
|
onChange={(v) => {
|
|
1758
2062
|
handleTemplatePick(v);
|
|
1759
2063
|
setShowOverflow(false);
|
|
1760
2064
|
}}
|
|
2065
|
+
colorScheme={colorScheme}
|
|
1761
2066
|
recommended={recommendedTemplates}
|
|
2067
|
+
previewSource={templatePreviewSource}
|
|
1762
2068
|
/>
|
|
1763
2069
|
</div>
|
|
1764
2070
|
)}
|
|
1765
2071
|
|
|
2072
|
+
{/* Contextual: transition picker in overflow — mirrors the
|
|
2073
|
+
template picker above. The menu stays open on change since
|
|
2074
|
+
the flyout carries follow-up controls (direction/duration). */}
|
|
2075
|
+
{showTransitionInOverflow && (
|
|
2076
|
+
<div className="squisq-toolbar-overflow-item squisq-toolbar-overflow-template">
|
|
2077
|
+
<TransitionPicker value={currentTransition!} onChange={handleTransitionPick} />
|
|
2078
|
+
</div>
|
|
2079
|
+
)}
|
|
2080
|
+
|
|
1766
2081
|
{/* Contextual: table controls in overflow */}
|
|
1767
|
-
{isInTable && (
|
|
2082
|
+
{isInTable && clippedContextual.has('table') && (
|
|
1768
2083
|
<>
|
|
1769
2084
|
<div
|
|
1770
2085
|
className="squisq-toolbar-separator"
|
|
@@ -1863,11 +2178,16 @@ export function Toolbar({
|
|
|
1863
2178
|
<button
|
|
1864
2179
|
className={`squisq-toolbar-button squisq-toolbar-files-toggle${showFiles ? ' squisq-toolbar-button--active' : ''}`}
|
|
1865
2180
|
onClick={onToggleFiles}
|
|
1866
|
-
data-tooltip={showFiles ? 'Hide
|
|
2181
|
+
data-tooltip={`${showFiles ? 'Hide' : 'Show'} Files panel${resolvedFileCount > 0 ? ` (${fileCountLabel(resolvedFileCount)})` : ''}`}
|
|
1867
2182
|
aria-pressed={showFiles}
|
|
1868
|
-
aria-label=
|
|
2183
|
+
aria-label={`Toggle Files panel${resolvedFileCount > 0 ? `, ${fileCountLabel(resolvedFileCount)}` : ''}`}
|
|
1869
2184
|
>
|
|
1870
2185
|
<Icon icon="fa-solid fa-paperclip" />
|
|
2186
|
+
{resolvedFileCount > 0 && (
|
|
2187
|
+
<span className="squisq-toolbar-files-badge" aria-hidden="true">
|
|
2188
|
+
{fileCountBadge(resolvedFileCount)}
|
|
2189
|
+
</span>
|
|
2190
|
+
)}
|
|
1871
2191
|
</button>
|
|
1872
2192
|
)}
|
|
1873
2193
|
{/* Right slot — rightmost end of toolbar */}
|