@bendyline/squisq-editor-react 1.1.1 → 1.2.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/dist/PreviewControls.js +1 -1
- package/dist/PreviewControls.js.map +1 -1
- package/dist/Toolbar.d.ts.map +1 -1
- package/dist/Toolbar.js +143 -10
- package/dist/Toolbar.js.map +1 -1
- package/dist/__tests__/tiptapBridge.test.d.ts +2 -0
- package/dist/__tests__/tiptapBridge.test.d.ts.map +1 -0
- package/dist/__tests__/tiptapBridge.test.js +241 -0
- package/dist/__tests__/tiptapBridge.test.js.map +1 -0
- package/dist/tiptapBridge.d.ts.map +1 -1
- package/dist/tiptapBridge.js +142 -0
- package/dist/tiptapBridge.js.map +1 -1
- package/package.json +4 -4
- package/src/PreviewControls.tsx +1 -1
- package/src/Toolbar.tsx +427 -12
- package/src/__tests__/tiptapBridge.test.ts +290 -0
- package/src/styles/editor.css +229 -11
- package/src/tiptapBridge.ts +159 -0
package/src/Toolbar.tsx
CHANGED
|
@@ -8,11 +8,14 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { ReactNode } from 'react';
|
|
11
|
-
import { useCallback, useEffect, useReducer } from 'react';
|
|
11
|
+
import { useCallback, useEffect, useReducer, useRef, useState } from 'react';
|
|
12
12
|
import type { Editor as TiptapEditor } from '@tiptap/core';
|
|
13
13
|
import { useEditorContext, type EditorView } from './EditorContext';
|
|
14
14
|
import { getAvailableTemplates } from '@bendyline/squisq/doc';
|
|
15
15
|
|
|
16
|
+
/** Template names are static — computed once at module load. */
|
|
17
|
+
const TEMPLATE_NAMES = getAvailableTemplates();
|
|
18
|
+
|
|
16
19
|
const VIEWS: { id: EditorView; label: string; shortcut: string }[] = [
|
|
17
20
|
{ id: 'wysiwyg', label: 'Editor', shortcut: '⌘1' },
|
|
18
21
|
{ id: 'raw', label: 'Raw', shortcut: '⌘2' },
|
|
@@ -84,6 +87,8 @@ const BUTTONS: ToolbarButton[] = [
|
|
|
84
87
|
{ id: 'codeblock', label: '{ }', icon: '{ }', title: 'Code block', group: 'insert' },
|
|
85
88
|
{ id: 'hr', label: '—', icon: '—', title: 'Horizontal rule', group: 'insert' },
|
|
86
89
|
{ id: 'link', label: '🔗', icon: '🔗', title: 'Insert link', group: 'insert' },
|
|
90
|
+
{ id: 'table', label: 'table', icon: '', title: 'Insert table', group: 'insert' },
|
|
91
|
+
{ id: 'image', label: '🖼', icon: '🖼', title: 'Insert image', group: 'insert' },
|
|
87
92
|
];
|
|
88
93
|
|
|
89
94
|
// ─── Tiptap active-state map ────────────────────────────
|
|
@@ -139,8 +144,57 @@ export function Toolbar({
|
|
|
139
144
|
setMarkdownSource,
|
|
140
145
|
tiptapEditor,
|
|
141
146
|
monacoEditor,
|
|
147
|
+
mediaProvider,
|
|
142
148
|
} = useEditorContext();
|
|
143
149
|
|
|
150
|
+
// Hidden file input for image picker
|
|
151
|
+
const imageInputRef = useRef<HTMLInputElement>(null);
|
|
152
|
+
|
|
153
|
+
// ── Overflow detection ────────────────────────────────
|
|
154
|
+
const actionsRef = useRef<HTMLDivElement>(null);
|
|
155
|
+
const [overflowIndex, setOverflowIndex] = useState<number | null>(null);
|
|
156
|
+
const [showOverflow, setShowOverflow] = useState(false);
|
|
157
|
+
const overflowRef = useRef<HTMLDivElement>(null);
|
|
158
|
+
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
const container = actionsRef.current;
|
|
161
|
+
if (!container) return;
|
|
162
|
+
|
|
163
|
+
const measure = () => {
|
|
164
|
+
const containerRight = container.getBoundingClientRect().right;
|
|
165
|
+
const children = container.querySelectorAll<HTMLElement>(
|
|
166
|
+
':scope > .squisq-toolbar-group > .squisq-toolbar-button',
|
|
167
|
+
);
|
|
168
|
+
let firstHidden: number | null = null;
|
|
169
|
+
children.forEach((child, i) => {
|
|
170
|
+
if (firstHidden !== null) return;
|
|
171
|
+
const rect = child.getBoundingClientRect();
|
|
172
|
+
// A button is hidden if its right edge extends past the container
|
|
173
|
+
if (rect.right > containerRight + 2) {
|
|
174
|
+
firstHidden = i;
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
setOverflowIndex(firstHidden);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const ro = new ResizeObserver(measure);
|
|
181
|
+
ro.observe(container);
|
|
182
|
+
measure();
|
|
183
|
+
return () => ro.disconnect();
|
|
184
|
+
}, [activeView]);
|
|
185
|
+
|
|
186
|
+
// Close overflow menu on outside click
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
if (!showOverflow) return;
|
|
189
|
+
const handleClick = (e: MouseEvent) => {
|
|
190
|
+
if (overflowRef.current && !overflowRef.current.contains(e.target as Node)) {
|
|
191
|
+
setShowOverflow(false);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
document.addEventListener('mousedown', handleClick);
|
|
195
|
+
return () => document.removeEventListener('mousedown', handleClick);
|
|
196
|
+
}, [showOverflow]);
|
|
197
|
+
|
|
144
198
|
// Force re-render when Tiptap selection or formatting state changes
|
|
145
199
|
const [, forceUpdate] = useReducer((c: number) => c + 1, 0);
|
|
146
200
|
useEffect(() => {
|
|
@@ -202,6 +256,9 @@ export function Toolbar({
|
|
|
202
256
|
}
|
|
203
257
|
break;
|
|
204
258
|
}
|
|
259
|
+
case 'table':
|
|
260
|
+
tiptapEditor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run();
|
|
261
|
+
break;
|
|
205
262
|
}
|
|
206
263
|
},
|
|
207
264
|
[tiptapEditor],
|
|
@@ -296,6 +353,13 @@ export function Toolbar({
|
|
|
296
353
|
}
|
|
297
354
|
break;
|
|
298
355
|
}
|
|
356
|
+
case 'table': {
|
|
357
|
+
const tpl =
|
|
358
|
+
'| Header 1 | Header 2 | Header 3 |\n| --- | --- | --- |\n| Cell | Cell | Cell |\n| Cell | Cell | Cell |';
|
|
359
|
+
replacement = '\n' + tpl + '\n';
|
|
360
|
+
newCursorOffset = 3; // after \n|
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
299
363
|
}
|
|
300
364
|
|
|
301
365
|
// Apply the edit via Monaco's executeEdits for proper undo support
|
|
@@ -307,11 +371,6 @@ export function Toolbar({
|
|
|
307
371
|
const startPos = model.getPositionAt(
|
|
308
372
|
model.getOffsetAt(range.getStartPosition()) + newCursorOffset,
|
|
309
373
|
);
|
|
310
|
-
const _placeholderLen =
|
|
311
|
-
replacement.length -
|
|
312
|
-
newCursorOffset -
|
|
313
|
-
(replacement.length -
|
|
314
|
-
replacement.lastIndexOf(replacement.charAt(replacement.length - 1)));
|
|
315
374
|
// Just place cursor after the prefix
|
|
316
375
|
monacoEditor.setPosition(startPos);
|
|
317
376
|
}
|
|
@@ -360,6 +419,10 @@ export function Toolbar({
|
|
|
360
419
|
case 'link':
|
|
361
420
|
insertion = '[link text](url)';
|
|
362
421
|
break;
|
|
422
|
+
case 'table':
|
|
423
|
+
insertion =
|
|
424
|
+
'\n| Header 1 | Header 2 | Header 3 |\n| --- | --- | --- |\n| Cell | Cell | Cell |\n| Cell | Cell | Cell |\n';
|
|
425
|
+
break;
|
|
363
426
|
}
|
|
364
427
|
if (insertion) {
|
|
365
428
|
setMarkdownSource(markdownSource + insertion);
|
|
@@ -369,8 +432,37 @@ export function Toolbar({
|
|
|
369
432
|
[monacoEditor, markdownSource, setMarkdownSource],
|
|
370
433
|
);
|
|
371
434
|
|
|
435
|
+
// ── Image upload handler ───────────────────────────────
|
|
436
|
+
const handleImageFile = useCallback(
|
|
437
|
+
async (file: File) => {
|
|
438
|
+
if (!mediaProvider) return;
|
|
439
|
+
const buffer = await file.arrayBuffer();
|
|
440
|
+
const relativePath = await mediaProvider.addMedia(file.name, buffer, file.type);
|
|
441
|
+
const altText = file.name.replace(/\.[^.]+$/, '').replace(/[-_]/g, ' ');
|
|
442
|
+
|
|
443
|
+
if (activeView === 'wysiwyg' && tiptapEditor) {
|
|
444
|
+
tiptapEditor.chain().focus().setImage({ src: relativePath, alt: altText }).run();
|
|
445
|
+
} else if (monacoEditor) {
|
|
446
|
+
const selection = monacoEditor.getSelection();
|
|
447
|
+
const model = monacoEditor.getModel();
|
|
448
|
+
if (selection && model) {
|
|
449
|
+
const md = ``;
|
|
450
|
+
monacoEditor.executeEdits('toolbar', [{ range: selection, text: md }]);
|
|
451
|
+
monacoEditor.focus();
|
|
452
|
+
}
|
|
453
|
+
} else {
|
|
454
|
+
setMarkdownSource(markdownSource + `\n\n`);
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
[mediaProvider, activeView, tiptapEditor, monacoEditor, markdownSource, setMarkdownSource],
|
|
458
|
+
);
|
|
459
|
+
|
|
372
460
|
const handleAction = useCallback(
|
|
373
461
|
(id: string) => {
|
|
462
|
+
if (id === 'image') {
|
|
463
|
+
imageInputRef.current?.click();
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
374
466
|
if (activeView === 'wysiwyg' && tiptapEditor) {
|
|
375
467
|
handleTiptap(id);
|
|
376
468
|
} else {
|
|
@@ -384,6 +476,9 @@ export function Toolbar({
|
|
|
384
476
|
const isWysiwyg = activeView === 'wysiwyg' && tiptapEditor;
|
|
385
477
|
const isPreview = activeView === 'preview';
|
|
386
478
|
|
|
479
|
+
// Detect whether cursor is inside a table (WYSIWYG mode only)
|
|
480
|
+
const isInTable = isWysiwyg ? tiptapEditor.isActive('table') : false;
|
|
481
|
+
|
|
387
482
|
// Detect current heading template (WYSIWYG mode only)
|
|
388
483
|
const currentTemplate = isWysiwyg
|
|
389
484
|
? tiptapEditor.isActive('heading')
|
|
@@ -405,14 +500,25 @@ export function Toolbar({
|
|
|
405
500
|
}
|
|
406
501
|
};
|
|
407
502
|
|
|
408
|
-
const templateNames = getAvailableTemplates();
|
|
409
|
-
|
|
410
503
|
return (
|
|
411
504
|
<div
|
|
412
505
|
className={`squisq-toolbar ${className || ''}`}
|
|
413
506
|
role="toolbar"
|
|
414
507
|
aria-label="Formatting toolbar"
|
|
415
508
|
>
|
|
509
|
+
{/* Hidden file input for image picker */}
|
|
510
|
+
<input
|
|
511
|
+
ref={imageInputRef}
|
|
512
|
+
type="file"
|
|
513
|
+
accept="image/*"
|
|
514
|
+
style={{ display: 'none' }}
|
|
515
|
+
onChange={(e) => {
|
|
516
|
+
const file = e.target.files?.[0];
|
|
517
|
+
if (file) handleImageFile(file);
|
|
518
|
+
// Reset so the same file can be re-selected
|
|
519
|
+
e.target.value = '';
|
|
520
|
+
}}
|
|
521
|
+
/>
|
|
416
522
|
{/* Left slot — before view tabs */}
|
|
417
523
|
{slotLeft}
|
|
418
524
|
{/* View tabs */}
|
|
@@ -422,6 +528,7 @@ export function Toolbar({
|
|
|
422
528
|
key={view.id}
|
|
423
529
|
role="tab"
|
|
424
530
|
data-view={view.id}
|
|
531
|
+
data-label={view.label}
|
|
425
532
|
aria-selected={activeView === view.id}
|
|
426
533
|
className={`squisq-toolbar-view-tab${activeView === view.id ? ' squisq-toolbar-view-tab--active' : ''}`}
|
|
427
534
|
onClick={() => setActiveView(view.id)}
|
|
@@ -433,23 +540,43 @@ export function Toolbar({
|
|
|
433
540
|
</div>
|
|
434
541
|
{/* Formatting buttons — hidden in preview mode */}
|
|
435
542
|
{!isPreview && (
|
|
436
|
-
<div className="squisq-toolbar-actions">
|
|
543
|
+
<div className="squisq-toolbar-actions" ref={actionsRef}>
|
|
437
544
|
{groups.map((group, gi) => (
|
|
438
545
|
<div key={group} className="squisq-toolbar-group">
|
|
439
546
|
{gi > 0 && <div className="squisq-toolbar-separator" />}
|
|
440
547
|
{BUTTONS.filter((b) => b.group === group).map((btn) => {
|
|
441
548
|
const active = isWysiwyg ? isTiptapActive(tiptapEditor, btn.id) : false;
|
|
549
|
+
const disabled = btn.id === 'image' && !mediaProvider;
|
|
442
550
|
return (
|
|
443
551
|
<button
|
|
444
552
|
key={btn.id}
|
|
445
553
|
className={`squisq-toolbar-button${active ? ' squisq-toolbar-button--active' : ''}`}
|
|
446
|
-
title={btn.title}
|
|
554
|
+
title={disabled ? 'Insert image (requires media provider)' : btn.title}
|
|
447
555
|
onClick={() => handleAction(btn.id)}
|
|
448
556
|
aria-label={btn.title}
|
|
449
557
|
aria-pressed={active}
|
|
558
|
+
disabled={disabled}
|
|
450
559
|
style={btn.iconStyle}
|
|
451
560
|
>
|
|
452
|
-
{btn.
|
|
561
|
+
{btn.id === 'table' ? (
|
|
562
|
+
<svg
|
|
563
|
+
width="14"
|
|
564
|
+
height="14"
|
|
565
|
+
viewBox="0 0 14 14"
|
|
566
|
+
fill="none"
|
|
567
|
+
stroke="currentColor"
|
|
568
|
+
strokeWidth="1.4"
|
|
569
|
+
strokeLinecap="round"
|
|
570
|
+
>
|
|
571
|
+
<rect x="1" y="1" width="12" height="12" rx="1" />
|
|
572
|
+
<line x1="1" y1="5" x2="13" y2="5" />
|
|
573
|
+
<line x1="1" y1="9" x2="13" y2="9" />
|
|
574
|
+
<line x1="5" y1="1" x2="5" y2="13" />
|
|
575
|
+
<line x1="9" y1="1" x2="9" y2="13" />
|
|
576
|
+
</svg>
|
|
577
|
+
) : (
|
|
578
|
+
btn.icon
|
|
579
|
+
)}
|
|
453
580
|
</button>
|
|
454
581
|
);
|
|
455
582
|
})}
|
|
@@ -472,7 +599,7 @@ export function Toolbar({
|
|
|
472
599
|
onChange={(e) => handleTemplatePick(e.target.value)}
|
|
473
600
|
>
|
|
474
601
|
<option value="">— none —</option>
|
|
475
|
-
{
|
|
602
|
+
{TEMPLATE_NAMES.map((name) => (
|
|
476
603
|
<option key={name} value={name}>
|
|
477
604
|
{name}
|
|
478
605
|
</option>
|
|
@@ -482,8 +609,296 @@ export function Toolbar({
|
|
|
482
609
|
</div>
|
|
483
610
|
</>
|
|
484
611
|
)}
|
|
612
|
+
|
|
613
|
+
{/* Table controls — visible when cursor is in a table (WYSIWYG) */}
|
|
614
|
+
{isInTable && (
|
|
615
|
+
<>
|
|
616
|
+
<div className="squisq-toolbar-separator" />
|
|
617
|
+
<div className="squisq-toolbar-group squisq-table-controls">
|
|
618
|
+
<span className="squisq-table-controls-label">Table:</span>
|
|
619
|
+
<button
|
|
620
|
+
className="squisq-toolbar-button"
|
|
621
|
+
title="Add column before"
|
|
622
|
+
onClick={() => tiptapEditor!.chain().focus().addColumnBefore().run()}
|
|
623
|
+
aria-label="Add column before"
|
|
624
|
+
>
|
|
625
|
+
<svg
|
|
626
|
+
width="16"
|
|
627
|
+
height="16"
|
|
628
|
+
viewBox="0 0 16 16"
|
|
629
|
+
fill="none"
|
|
630
|
+
stroke="currentColor"
|
|
631
|
+
strokeWidth="1.5"
|
|
632
|
+
strokeLinecap="round"
|
|
633
|
+
>
|
|
634
|
+
<rect x="7" y="2" width="8" height="12" rx="1" />
|
|
635
|
+
<line x1="11" y1="2" x2="11" y2="14" />
|
|
636
|
+
<line x1="1" y1="8" x2="4.5" y2="8" />
|
|
637
|
+
<line x1="2.75" y1="6.25" x2="2.75" y2="9.75" />
|
|
638
|
+
</svg>
|
|
639
|
+
</button>
|
|
640
|
+
<button
|
|
641
|
+
className="squisq-toolbar-button"
|
|
642
|
+
title="Add column after"
|
|
643
|
+
onClick={() => tiptapEditor!.chain().focus().addColumnAfter().run()}
|
|
644
|
+
aria-label="Add column after"
|
|
645
|
+
>
|
|
646
|
+
<svg
|
|
647
|
+
width="16"
|
|
648
|
+
height="16"
|
|
649
|
+
viewBox="0 0 16 16"
|
|
650
|
+
fill="none"
|
|
651
|
+
stroke="currentColor"
|
|
652
|
+
strokeWidth="1.5"
|
|
653
|
+
strokeLinecap="round"
|
|
654
|
+
>
|
|
655
|
+
<rect x="1" y="2" width="8" height="12" rx="1" />
|
|
656
|
+
<line x1="5" y1="2" x2="5" y2="14" />
|
|
657
|
+
<line x1="11.5" y1="8" x2="15" y2="8" />
|
|
658
|
+
<line x1="13.25" y1="6.25" x2="13.25" y2="9.75" />
|
|
659
|
+
</svg>
|
|
660
|
+
</button>
|
|
661
|
+
<button
|
|
662
|
+
className="squisq-toolbar-button"
|
|
663
|
+
title="Delete column"
|
|
664
|
+
onClick={() => tiptapEditor!.chain().focus().deleteColumn().run()}
|
|
665
|
+
aria-label="Delete column"
|
|
666
|
+
>
|
|
667
|
+
<svg
|
|
668
|
+
width="16"
|
|
669
|
+
height="16"
|
|
670
|
+
viewBox="0 0 16 16"
|
|
671
|
+
fill="none"
|
|
672
|
+
stroke="currentColor"
|
|
673
|
+
strokeWidth="1.5"
|
|
674
|
+
strokeLinecap="round"
|
|
675
|
+
>
|
|
676
|
+
<rect x="4" y="1" width="8" height="14" rx="1" />
|
|
677
|
+
<line x1="6" y1="5.5" x2="10" y2="10.5" />
|
|
678
|
+
<line x1="10" y1="5.5" x2="6" y2="10.5" />
|
|
679
|
+
</svg>
|
|
680
|
+
</button>
|
|
681
|
+
<button
|
|
682
|
+
className="squisq-toolbar-button"
|
|
683
|
+
title="Add row above"
|
|
684
|
+
onClick={() => tiptapEditor!.chain().focus().addRowBefore().run()}
|
|
685
|
+
aria-label="Add row above"
|
|
686
|
+
>
|
|
687
|
+
<svg
|
|
688
|
+
width="16"
|
|
689
|
+
height="16"
|
|
690
|
+
viewBox="0 0 16 16"
|
|
691
|
+
fill="none"
|
|
692
|
+
stroke="currentColor"
|
|
693
|
+
strokeWidth="1.5"
|
|
694
|
+
strokeLinecap="round"
|
|
695
|
+
>
|
|
696
|
+
<rect x="2" y="6" width="12" height="9" rx="1" />
|
|
697
|
+
<line x1="2" y1="10.5" x2="14" y2="10.5" />
|
|
698
|
+
<line x1="8" y1="1" x2="8" y2="4.5" />
|
|
699
|
+
<line x1="6.25" y1="2.75" x2="9.75" y2="2.75" />
|
|
700
|
+
</svg>
|
|
701
|
+
</button>
|
|
702
|
+
<button
|
|
703
|
+
className="squisq-toolbar-button"
|
|
704
|
+
title="Add row below"
|
|
705
|
+
onClick={() => tiptapEditor!.chain().focus().addRowAfter().run()}
|
|
706
|
+
aria-label="Add row below"
|
|
707
|
+
>
|
|
708
|
+
<svg
|
|
709
|
+
width="16"
|
|
710
|
+
height="16"
|
|
711
|
+
viewBox="0 0 16 16"
|
|
712
|
+
fill="none"
|
|
713
|
+
stroke="currentColor"
|
|
714
|
+
strokeWidth="1.5"
|
|
715
|
+
strokeLinecap="round"
|
|
716
|
+
>
|
|
717
|
+
<rect x="2" y="1" width="12" height="9" rx="1" />
|
|
718
|
+
<line x1="2" y1="5.5" x2="14" y2="5.5" />
|
|
719
|
+
<line x1="8" y1="11.5" x2="8" y2="15" />
|
|
720
|
+
<line x1="6.25" y1="13.25" x2="9.75" y2="13.25" />
|
|
721
|
+
</svg>
|
|
722
|
+
</button>
|
|
723
|
+
<button
|
|
724
|
+
className="squisq-toolbar-button"
|
|
725
|
+
title="Delete row"
|
|
726
|
+
onClick={() => tiptapEditor!.chain().focus().deleteRow().run()}
|
|
727
|
+
aria-label="Delete row"
|
|
728
|
+
>
|
|
729
|
+
<svg
|
|
730
|
+
width="16"
|
|
731
|
+
height="16"
|
|
732
|
+
viewBox="0 0 16 16"
|
|
733
|
+
fill="none"
|
|
734
|
+
stroke="currentColor"
|
|
735
|
+
strokeWidth="1.5"
|
|
736
|
+
strokeLinecap="round"
|
|
737
|
+
>
|
|
738
|
+
<rect x="1" y="4" width="14" height="8" rx="1" />
|
|
739
|
+
<line x1="5.5" y1="6" x2="10.5" y2="10" />
|
|
740
|
+
<line x1="10.5" y1="6" x2="5.5" y2="10" />
|
|
741
|
+
</svg>
|
|
742
|
+
</button>
|
|
743
|
+
<button
|
|
744
|
+
className="squisq-toolbar-button squisq-toolbar-button--danger"
|
|
745
|
+
title="Delete table"
|
|
746
|
+
onClick={() => tiptapEditor!.chain().focus().deleteTable().run()}
|
|
747
|
+
aria-label="Delete table"
|
|
748
|
+
>
|
|
749
|
+
<svg
|
|
750
|
+
width="16"
|
|
751
|
+
height="16"
|
|
752
|
+
viewBox="0 0 16 16"
|
|
753
|
+
fill="none"
|
|
754
|
+
stroke="currentColor"
|
|
755
|
+
strokeWidth="1.5"
|
|
756
|
+
strokeLinecap="round"
|
|
757
|
+
>
|
|
758
|
+
<rect x="1" y="1" width="14" height="14" rx="1" />
|
|
759
|
+
<line x1="1" y1="5.5" x2="15" y2="5.5" />
|
|
760
|
+
<line x1="5.5" y1="1" x2="5.5" y2="15" />
|
|
761
|
+
<line x1="4.5" y1="4.5" x2="11.5" y2="11.5" strokeWidth="2" />
|
|
762
|
+
<line x1="11.5" y1="4.5" x2="4.5" y2="11.5" strokeWidth="2" />
|
|
763
|
+
</svg>
|
|
764
|
+
</button>
|
|
765
|
+
</div>
|
|
766
|
+
</>
|
|
767
|
+
)}
|
|
768
|
+
</div>
|
|
769
|
+
)}
|
|
770
|
+
|
|
771
|
+
{/* Overflow menu — outside the overflow:hidden actions container */}
|
|
772
|
+
{!isPreview && overflowIndex !== null && (
|
|
773
|
+
<div className="squisq-toolbar-overflow" ref={overflowRef}>
|
|
774
|
+
<button
|
|
775
|
+
className={`squisq-toolbar-button squisq-toolbar-overflow-trigger${showOverflow ? ' squisq-toolbar-button--active' : ''}`}
|
|
776
|
+
title="More actions"
|
|
777
|
+
onClick={() => setShowOverflow((v) => !v)}
|
|
778
|
+
aria-label="More actions"
|
|
779
|
+
aria-expanded={showOverflow}
|
|
780
|
+
>
|
|
781
|
+
···
|
|
782
|
+
</button>
|
|
783
|
+
{showOverflow && (
|
|
784
|
+
<div className="squisq-toolbar-overflow-menu">
|
|
785
|
+
{BUTTONS.slice(overflowIndex).map((btn) => {
|
|
786
|
+
const active = isWysiwyg ? isTiptapActive(tiptapEditor, btn.id) : false;
|
|
787
|
+
const disabled = btn.id === 'image' && !mediaProvider;
|
|
788
|
+
return (
|
|
789
|
+
<button
|
|
790
|
+
key={btn.id}
|
|
791
|
+
className={`squisq-toolbar-overflow-item${active ? ' squisq-toolbar-overflow-item--active' : ''}`}
|
|
792
|
+
title={btn.title}
|
|
793
|
+
onClick={() => {
|
|
794
|
+
handleAction(btn.id);
|
|
795
|
+
setShowOverflow(false);
|
|
796
|
+
}}
|
|
797
|
+
disabled={disabled}
|
|
798
|
+
>
|
|
799
|
+
{btn.id === 'table' ? (
|
|
800
|
+
<svg
|
|
801
|
+
width="14"
|
|
802
|
+
height="14"
|
|
803
|
+
viewBox="0 0 14 14"
|
|
804
|
+
fill="none"
|
|
805
|
+
stroke="currentColor"
|
|
806
|
+
strokeWidth="1.4"
|
|
807
|
+
strokeLinecap="round"
|
|
808
|
+
>
|
|
809
|
+
<rect x="1" y="1" width="12" height="12" rx="1" />
|
|
810
|
+
<line x1="1" y1="5" x2="13" y2="5" />
|
|
811
|
+
<line x1="1" y1="9" x2="13" y2="9" />
|
|
812
|
+
<line x1="5" y1="1" x2="5" y2="13" />
|
|
813
|
+
<line x1="9" y1="1" x2="9" y2="13" />
|
|
814
|
+
</svg>
|
|
815
|
+
) : (
|
|
816
|
+
<span className="squisq-toolbar-overflow-icon" style={btn.iconStyle}>
|
|
817
|
+
{btn.icon}
|
|
818
|
+
</span>
|
|
819
|
+
)}
|
|
820
|
+
<span>{btn.title}</span>
|
|
821
|
+
</button>
|
|
822
|
+
);
|
|
823
|
+
})}
|
|
824
|
+
|
|
825
|
+
{/* Contextual: template picker in overflow */}
|
|
826
|
+
{currentTemplate !== null && (
|
|
827
|
+
<div className="squisq-toolbar-overflow-item squisq-toolbar-overflow-template">
|
|
828
|
+
<span>Template:</span>
|
|
829
|
+
<select
|
|
830
|
+
className="squisq-template-picker-select"
|
|
831
|
+
value={currentTemplate}
|
|
832
|
+
onChange={(e) => {
|
|
833
|
+
handleTemplatePick(e.target.value);
|
|
834
|
+
setShowOverflow(false);
|
|
835
|
+
}}
|
|
836
|
+
>
|
|
837
|
+
<option value="">— none —</option>
|
|
838
|
+
{TEMPLATE_NAMES.map((name) => (
|
|
839
|
+
<option key={name} value={name}>
|
|
840
|
+
{name}
|
|
841
|
+
</option>
|
|
842
|
+
))}
|
|
843
|
+
</select>
|
|
844
|
+
</div>
|
|
845
|
+
)}
|
|
846
|
+
|
|
847
|
+
{/* Contextual: table controls in overflow */}
|
|
848
|
+
{isInTable && (
|
|
849
|
+
<>
|
|
850
|
+
<div
|
|
851
|
+
className="squisq-toolbar-separator"
|
|
852
|
+
style={{ margin: '4px 0', width: '100%', height: 1 }}
|
|
853
|
+
/>
|
|
854
|
+
{[
|
|
855
|
+
{
|
|
856
|
+
label: 'Add column before',
|
|
857
|
+
action: () => tiptapEditor!.chain().focus().addColumnBefore().run(),
|
|
858
|
+
},
|
|
859
|
+
{
|
|
860
|
+
label: 'Add column after',
|
|
861
|
+
action: () => tiptapEditor!.chain().focus().addColumnAfter().run(),
|
|
862
|
+
},
|
|
863
|
+
{
|
|
864
|
+
label: 'Delete column',
|
|
865
|
+
action: () => tiptapEditor!.chain().focus().deleteColumn().run(),
|
|
866
|
+
},
|
|
867
|
+
{
|
|
868
|
+
label: 'Add row above',
|
|
869
|
+
action: () => tiptapEditor!.chain().focus().addRowBefore().run(),
|
|
870
|
+
},
|
|
871
|
+
{
|
|
872
|
+
label: 'Add row below',
|
|
873
|
+
action: () => tiptapEditor!.chain().focus().addRowAfter().run(),
|
|
874
|
+
},
|
|
875
|
+
{
|
|
876
|
+
label: 'Delete row',
|
|
877
|
+
action: () => tiptapEditor!.chain().focus().deleteRow().run(),
|
|
878
|
+
},
|
|
879
|
+
{
|
|
880
|
+
label: 'Delete table',
|
|
881
|
+
action: () => tiptapEditor!.chain().focus().deleteTable().run(),
|
|
882
|
+
},
|
|
883
|
+
].map((item) => (
|
|
884
|
+
<button
|
|
885
|
+
key={item.label}
|
|
886
|
+
className={`squisq-toolbar-overflow-item${item.label.startsWith('Delete') ? ' squisq-toolbar-overflow-item--danger' : ''}`}
|
|
887
|
+
onClick={() => {
|
|
888
|
+
item.action();
|
|
889
|
+
setShowOverflow(false);
|
|
890
|
+
}}
|
|
891
|
+
>
|
|
892
|
+
<span>{item.label}</span>
|
|
893
|
+
</button>
|
|
894
|
+
))}
|
|
895
|
+
</>
|
|
896
|
+
)}
|
|
897
|
+
</div>
|
|
898
|
+
)}
|
|
485
899
|
</div>
|
|
486
900
|
)}
|
|
901
|
+
|
|
487
902
|
{/* After-actions slot — after formatting controls */}
|
|
488
903
|
{slotAfterActions}
|
|
489
904
|
{/* Spacer pushes right-side buttons to the end */}
|