@bendyline/squisq-editor-react 1.1.1 → 1.2.1
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.d.ts +1 -1
- package/dist/PreviewControls.d.ts.map +1 -1
- package/dist/PreviewControls.js +36 -17
- package/dist/PreviewControls.js.map +1 -1
- package/dist/RawEditor.d.ts.map +1 -1
- package/dist/RawEditor.js +10 -1
- package/dist/RawEditor.js.map +1 -1
- package/dist/Toolbar.d.ts.map +1 -1
- package/dist/Toolbar.js +155 -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 +23 -23
- package/src/PreviewControls.tsx +116 -87
- package/src/RawEditor.tsx +10 -1
- package/src/Toolbar.tsx +444 -14
- package/src/__tests__/tiptapBridge.test.ts +290 -0
- package/src/styles/editor.css +286 -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,72 @@ 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
|
+
// ── Narrow-screen detection ──────────────────────────
|
|
154
|
+
const [isNarrow, setIsNarrow] = useState(
|
|
155
|
+
() => typeof window !== 'undefined' && window.matchMedia('(max-width: 768px)').matches,
|
|
156
|
+
);
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
const mq = window.matchMedia('(max-width: 768px)');
|
|
159
|
+
const handler = (e: MediaQueryListEvent) => setIsNarrow(e.matches);
|
|
160
|
+
mq.addEventListener('change', handler);
|
|
161
|
+
return () => mq.removeEventListener('change', handler);
|
|
162
|
+
}, []);
|
|
163
|
+
|
|
164
|
+
// ── Overflow detection ────────────────────────────────
|
|
165
|
+
const actionsRef = useRef<HTMLDivElement>(null);
|
|
166
|
+
const [measuredOverflowIndex, setMeasuredOverflowIndex] = useState<number | null>(null);
|
|
167
|
+
const [showOverflow, setShowOverflow] = useState(false);
|
|
168
|
+
const overflowRef = useRef<HTMLDivElement>(null);
|
|
169
|
+
|
|
170
|
+
// On narrow screens, force all buttons into the overflow menu
|
|
171
|
+
const overflowIndex = isNarrow ? 0 : measuredOverflowIndex;
|
|
172
|
+
|
|
173
|
+
useEffect(() => {
|
|
174
|
+
if (isNarrow) return; // Skip measurement on narrow — everything overflows
|
|
175
|
+
const container = actionsRef.current;
|
|
176
|
+
if (!container) return;
|
|
177
|
+
|
|
178
|
+
const measure = () => {
|
|
179
|
+
const containerRight = container.getBoundingClientRect().right;
|
|
180
|
+
const children = container.querySelectorAll<HTMLElement>(
|
|
181
|
+
':scope > .squisq-toolbar-group > .squisq-toolbar-button',
|
|
182
|
+
);
|
|
183
|
+
let firstHidden: number | null = null;
|
|
184
|
+
children.forEach((child, i) => {
|
|
185
|
+
if (firstHidden !== null) return;
|
|
186
|
+
const rect = child.getBoundingClientRect();
|
|
187
|
+
// A button is hidden if its right edge extends past the container
|
|
188
|
+
if (rect.right > containerRight + 2) {
|
|
189
|
+
firstHidden = i;
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
setMeasuredOverflowIndex(firstHidden);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const ro = new ResizeObserver(measure);
|
|
196
|
+
ro.observe(container);
|
|
197
|
+
measure();
|
|
198
|
+
return () => ro.disconnect();
|
|
199
|
+
}, [activeView, isNarrow]);
|
|
200
|
+
|
|
201
|
+
// Close overflow menu on outside click
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
if (!showOverflow) return;
|
|
204
|
+
const handleClick = (e: MouseEvent) => {
|
|
205
|
+
if (overflowRef.current && !overflowRef.current.contains(e.target as Node)) {
|
|
206
|
+
setShowOverflow(false);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
document.addEventListener('mousedown', handleClick);
|
|
210
|
+
return () => document.removeEventListener('mousedown', handleClick);
|
|
211
|
+
}, [showOverflow]);
|
|
212
|
+
|
|
144
213
|
// Force re-render when Tiptap selection or formatting state changes
|
|
145
214
|
const [, forceUpdate] = useReducer((c: number) => c + 1, 0);
|
|
146
215
|
useEffect(() => {
|
|
@@ -202,6 +271,9 @@ export function Toolbar({
|
|
|
202
271
|
}
|
|
203
272
|
break;
|
|
204
273
|
}
|
|
274
|
+
case 'table':
|
|
275
|
+
tiptapEditor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run();
|
|
276
|
+
break;
|
|
205
277
|
}
|
|
206
278
|
},
|
|
207
279
|
[tiptapEditor],
|
|
@@ -296,6 +368,13 @@ export function Toolbar({
|
|
|
296
368
|
}
|
|
297
369
|
break;
|
|
298
370
|
}
|
|
371
|
+
case 'table': {
|
|
372
|
+
const tpl =
|
|
373
|
+
'| Header 1 | Header 2 | Header 3 |\n| --- | --- | --- |\n| Cell | Cell | Cell |\n| Cell | Cell | Cell |';
|
|
374
|
+
replacement = '\n' + tpl + '\n';
|
|
375
|
+
newCursorOffset = 3; // after \n|
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
299
378
|
}
|
|
300
379
|
|
|
301
380
|
// Apply the edit via Monaco's executeEdits for proper undo support
|
|
@@ -307,11 +386,6 @@ export function Toolbar({
|
|
|
307
386
|
const startPos = model.getPositionAt(
|
|
308
387
|
model.getOffsetAt(range.getStartPosition()) + newCursorOffset,
|
|
309
388
|
);
|
|
310
|
-
const _placeholderLen =
|
|
311
|
-
replacement.length -
|
|
312
|
-
newCursorOffset -
|
|
313
|
-
(replacement.length -
|
|
314
|
-
replacement.lastIndexOf(replacement.charAt(replacement.length - 1)));
|
|
315
389
|
// Just place cursor after the prefix
|
|
316
390
|
monacoEditor.setPosition(startPos);
|
|
317
391
|
}
|
|
@@ -360,6 +434,10 @@ export function Toolbar({
|
|
|
360
434
|
case 'link':
|
|
361
435
|
insertion = '[link text](url)';
|
|
362
436
|
break;
|
|
437
|
+
case 'table':
|
|
438
|
+
insertion =
|
|
439
|
+
'\n| Header 1 | Header 2 | Header 3 |\n| --- | --- | --- |\n| Cell | Cell | Cell |\n| Cell | Cell | Cell |\n';
|
|
440
|
+
break;
|
|
363
441
|
}
|
|
364
442
|
if (insertion) {
|
|
365
443
|
setMarkdownSource(markdownSource + insertion);
|
|
@@ -369,8 +447,37 @@ export function Toolbar({
|
|
|
369
447
|
[monacoEditor, markdownSource, setMarkdownSource],
|
|
370
448
|
);
|
|
371
449
|
|
|
450
|
+
// ── Image upload handler ───────────────────────────────
|
|
451
|
+
const handleImageFile = useCallback(
|
|
452
|
+
async (file: File) => {
|
|
453
|
+
if (!mediaProvider) return;
|
|
454
|
+
const buffer = await file.arrayBuffer();
|
|
455
|
+
const relativePath = await mediaProvider.addMedia(file.name, buffer, file.type);
|
|
456
|
+
const altText = file.name.replace(/\.[^.]+$/, '').replace(/[-_]/g, ' ');
|
|
457
|
+
|
|
458
|
+
if (activeView === 'wysiwyg' && tiptapEditor) {
|
|
459
|
+
tiptapEditor.chain().focus().setImage({ src: relativePath, alt: altText }).run();
|
|
460
|
+
} else if (monacoEditor) {
|
|
461
|
+
const selection = monacoEditor.getSelection();
|
|
462
|
+
const model = monacoEditor.getModel();
|
|
463
|
+
if (selection && model) {
|
|
464
|
+
const md = ``;
|
|
465
|
+
monacoEditor.executeEdits('toolbar', [{ range: selection, text: md }]);
|
|
466
|
+
monacoEditor.focus();
|
|
467
|
+
}
|
|
468
|
+
} else {
|
|
469
|
+
setMarkdownSource(markdownSource + `\n\n`);
|
|
470
|
+
}
|
|
471
|
+
},
|
|
472
|
+
[mediaProvider, activeView, tiptapEditor, monacoEditor, markdownSource, setMarkdownSource],
|
|
473
|
+
);
|
|
474
|
+
|
|
372
475
|
const handleAction = useCallback(
|
|
373
476
|
(id: string) => {
|
|
477
|
+
if (id === 'image') {
|
|
478
|
+
imageInputRef.current?.click();
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
374
481
|
if (activeView === 'wysiwyg' && tiptapEditor) {
|
|
375
482
|
handleTiptap(id);
|
|
376
483
|
} else {
|
|
@@ -384,6 +491,9 @@ export function Toolbar({
|
|
|
384
491
|
const isWysiwyg = activeView === 'wysiwyg' && tiptapEditor;
|
|
385
492
|
const isPreview = activeView === 'preview';
|
|
386
493
|
|
|
494
|
+
// Detect whether cursor is inside a table (WYSIWYG mode only)
|
|
495
|
+
const isInTable = isWysiwyg ? tiptapEditor.isActive('table') : false;
|
|
496
|
+
|
|
387
497
|
// Detect current heading template (WYSIWYG mode only)
|
|
388
498
|
const currentTemplate = isWysiwyg
|
|
389
499
|
? tiptapEditor.isActive('heading')
|
|
@@ -405,14 +515,25 @@ export function Toolbar({
|
|
|
405
515
|
}
|
|
406
516
|
};
|
|
407
517
|
|
|
408
|
-
const templateNames = getAvailableTemplates();
|
|
409
|
-
|
|
410
518
|
return (
|
|
411
519
|
<div
|
|
412
520
|
className={`squisq-toolbar ${className || ''}`}
|
|
413
521
|
role="toolbar"
|
|
414
522
|
aria-label="Formatting toolbar"
|
|
415
523
|
>
|
|
524
|
+
{/* Hidden file input for image picker */}
|
|
525
|
+
<input
|
|
526
|
+
ref={imageInputRef}
|
|
527
|
+
type="file"
|
|
528
|
+
accept="image/*"
|
|
529
|
+
style={{ display: 'none' }}
|
|
530
|
+
onChange={(e) => {
|
|
531
|
+
const file = e.target.files?.[0];
|
|
532
|
+
if (file) handleImageFile(file);
|
|
533
|
+
// Reset so the same file can be re-selected
|
|
534
|
+
e.target.value = '';
|
|
535
|
+
}}
|
|
536
|
+
/>
|
|
416
537
|
{/* Left slot — before view tabs */}
|
|
417
538
|
{slotLeft}
|
|
418
539
|
{/* View tabs */}
|
|
@@ -422,6 +543,7 @@ export function Toolbar({
|
|
|
422
543
|
key={view.id}
|
|
423
544
|
role="tab"
|
|
424
545
|
data-view={view.id}
|
|
546
|
+
data-label={view.label}
|
|
425
547
|
aria-selected={activeView === view.id}
|
|
426
548
|
className={`squisq-toolbar-view-tab${activeView === view.id ? ' squisq-toolbar-view-tab--active' : ''}`}
|
|
427
549
|
onClick={() => setActiveView(view.id)}
|
|
@@ -431,25 +553,45 @@ export function Toolbar({
|
|
|
431
553
|
</button>
|
|
432
554
|
))}
|
|
433
555
|
</div>
|
|
434
|
-
{/* Formatting buttons — hidden in preview mode */}
|
|
435
|
-
{!isPreview && (
|
|
436
|
-
<div className="squisq-toolbar-actions">
|
|
556
|
+
{/* Formatting buttons — hidden in preview mode and on narrow screens */}
|
|
557
|
+
{!isPreview && !isNarrow && (
|
|
558
|
+
<div className="squisq-toolbar-actions" ref={actionsRef}>
|
|
437
559
|
{groups.map((group, gi) => (
|
|
438
560
|
<div key={group} className="squisq-toolbar-group">
|
|
439
561
|
{gi > 0 && <div className="squisq-toolbar-separator" />}
|
|
440
562
|
{BUTTONS.filter((b) => b.group === group).map((btn) => {
|
|
441
563
|
const active = isWysiwyg ? isTiptapActive(tiptapEditor, btn.id) : false;
|
|
564
|
+
const disabled = btn.id === 'image' && !mediaProvider;
|
|
442
565
|
return (
|
|
443
566
|
<button
|
|
444
567
|
key={btn.id}
|
|
445
568
|
className={`squisq-toolbar-button${active ? ' squisq-toolbar-button--active' : ''}`}
|
|
446
|
-
title={btn.title}
|
|
569
|
+
title={disabled ? 'Insert image (requires media provider)' : btn.title}
|
|
447
570
|
onClick={() => handleAction(btn.id)}
|
|
448
571
|
aria-label={btn.title}
|
|
449
572
|
aria-pressed={active}
|
|
573
|
+
disabled={disabled}
|
|
450
574
|
style={btn.iconStyle}
|
|
451
575
|
>
|
|
452
|
-
{btn.
|
|
576
|
+
{btn.id === 'table' ? (
|
|
577
|
+
<svg
|
|
578
|
+
width="14"
|
|
579
|
+
height="14"
|
|
580
|
+
viewBox="0 0 14 14"
|
|
581
|
+
fill="none"
|
|
582
|
+
stroke="currentColor"
|
|
583
|
+
strokeWidth="1.4"
|
|
584
|
+
strokeLinecap="round"
|
|
585
|
+
>
|
|
586
|
+
<rect x="1" y="1" width="12" height="12" rx="1" />
|
|
587
|
+
<line x1="1" y1="5" x2="13" y2="5" />
|
|
588
|
+
<line x1="1" y1="9" x2="13" y2="9" />
|
|
589
|
+
<line x1="5" y1="1" x2="5" y2="13" />
|
|
590
|
+
<line x1="9" y1="1" x2="9" y2="13" />
|
|
591
|
+
</svg>
|
|
592
|
+
) : (
|
|
593
|
+
btn.icon
|
|
594
|
+
)}
|
|
453
595
|
</button>
|
|
454
596
|
);
|
|
455
597
|
})}
|
|
@@ -472,7 +614,7 @@ export function Toolbar({
|
|
|
472
614
|
onChange={(e) => handleTemplatePick(e.target.value)}
|
|
473
615
|
>
|
|
474
616
|
<option value="">— none —</option>
|
|
475
|
-
{
|
|
617
|
+
{TEMPLATE_NAMES.map((name) => (
|
|
476
618
|
<option key={name} value={name}>
|
|
477
619
|
{name}
|
|
478
620
|
</option>
|
|
@@ -482,8 +624,296 @@ export function Toolbar({
|
|
|
482
624
|
</div>
|
|
483
625
|
</>
|
|
484
626
|
)}
|
|
627
|
+
|
|
628
|
+
{/* Table controls — visible when cursor is in a table (WYSIWYG) */}
|
|
629
|
+
{isInTable && (
|
|
630
|
+
<>
|
|
631
|
+
<div className="squisq-toolbar-separator" />
|
|
632
|
+
<div className="squisq-toolbar-group squisq-table-controls">
|
|
633
|
+
<span className="squisq-table-controls-label">Table:</span>
|
|
634
|
+
<button
|
|
635
|
+
className="squisq-toolbar-button"
|
|
636
|
+
title="Add column before"
|
|
637
|
+
onClick={() => tiptapEditor!.chain().focus().addColumnBefore().run()}
|
|
638
|
+
aria-label="Add column before"
|
|
639
|
+
>
|
|
640
|
+
<svg
|
|
641
|
+
width="16"
|
|
642
|
+
height="16"
|
|
643
|
+
viewBox="0 0 16 16"
|
|
644
|
+
fill="none"
|
|
645
|
+
stroke="currentColor"
|
|
646
|
+
strokeWidth="1.5"
|
|
647
|
+
strokeLinecap="round"
|
|
648
|
+
>
|
|
649
|
+
<rect x="7" y="2" width="8" height="12" rx="1" />
|
|
650
|
+
<line x1="11" y1="2" x2="11" y2="14" />
|
|
651
|
+
<line x1="1" y1="8" x2="4.5" y2="8" />
|
|
652
|
+
<line x1="2.75" y1="6.25" x2="2.75" y2="9.75" />
|
|
653
|
+
</svg>
|
|
654
|
+
</button>
|
|
655
|
+
<button
|
|
656
|
+
className="squisq-toolbar-button"
|
|
657
|
+
title="Add column after"
|
|
658
|
+
onClick={() => tiptapEditor!.chain().focus().addColumnAfter().run()}
|
|
659
|
+
aria-label="Add column after"
|
|
660
|
+
>
|
|
661
|
+
<svg
|
|
662
|
+
width="16"
|
|
663
|
+
height="16"
|
|
664
|
+
viewBox="0 0 16 16"
|
|
665
|
+
fill="none"
|
|
666
|
+
stroke="currentColor"
|
|
667
|
+
strokeWidth="1.5"
|
|
668
|
+
strokeLinecap="round"
|
|
669
|
+
>
|
|
670
|
+
<rect x="1" y="2" width="8" height="12" rx="1" />
|
|
671
|
+
<line x1="5" y1="2" x2="5" y2="14" />
|
|
672
|
+
<line x1="11.5" y1="8" x2="15" y2="8" />
|
|
673
|
+
<line x1="13.25" y1="6.25" x2="13.25" y2="9.75" />
|
|
674
|
+
</svg>
|
|
675
|
+
</button>
|
|
676
|
+
<button
|
|
677
|
+
className="squisq-toolbar-button"
|
|
678
|
+
title="Delete column"
|
|
679
|
+
onClick={() => tiptapEditor!.chain().focus().deleteColumn().run()}
|
|
680
|
+
aria-label="Delete column"
|
|
681
|
+
>
|
|
682
|
+
<svg
|
|
683
|
+
width="16"
|
|
684
|
+
height="16"
|
|
685
|
+
viewBox="0 0 16 16"
|
|
686
|
+
fill="none"
|
|
687
|
+
stroke="currentColor"
|
|
688
|
+
strokeWidth="1.5"
|
|
689
|
+
strokeLinecap="round"
|
|
690
|
+
>
|
|
691
|
+
<rect x="4" y="1" width="8" height="14" rx="1" />
|
|
692
|
+
<line x1="6" y1="5.5" x2="10" y2="10.5" />
|
|
693
|
+
<line x1="10" y1="5.5" x2="6" y2="10.5" />
|
|
694
|
+
</svg>
|
|
695
|
+
</button>
|
|
696
|
+
<button
|
|
697
|
+
className="squisq-toolbar-button"
|
|
698
|
+
title="Add row above"
|
|
699
|
+
onClick={() => tiptapEditor!.chain().focus().addRowBefore().run()}
|
|
700
|
+
aria-label="Add row above"
|
|
701
|
+
>
|
|
702
|
+
<svg
|
|
703
|
+
width="16"
|
|
704
|
+
height="16"
|
|
705
|
+
viewBox="0 0 16 16"
|
|
706
|
+
fill="none"
|
|
707
|
+
stroke="currentColor"
|
|
708
|
+
strokeWidth="1.5"
|
|
709
|
+
strokeLinecap="round"
|
|
710
|
+
>
|
|
711
|
+
<rect x="2" y="6" width="12" height="9" rx="1" />
|
|
712
|
+
<line x1="2" y1="10.5" x2="14" y2="10.5" />
|
|
713
|
+
<line x1="8" y1="1" x2="8" y2="4.5" />
|
|
714
|
+
<line x1="6.25" y1="2.75" x2="9.75" y2="2.75" />
|
|
715
|
+
</svg>
|
|
716
|
+
</button>
|
|
717
|
+
<button
|
|
718
|
+
className="squisq-toolbar-button"
|
|
719
|
+
title="Add row below"
|
|
720
|
+
onClick={() => tiptapEditor!.chain().focus().addRowAfter().run()}
|
|
721
|
+
aria-label="Add row below"
|
|
722
|
+
>
|
|
723
|
+
<svg
|
|
724
|
+
width="16"
|
|
725
|
+
height="16"
|
|
726
|
+
viewBox="0 0 16 16"
|
|
727
|
+
fill="none"
|
|
728
|
+
stroke="currentColor"
|
|
729
|
+
strokeWidth="1.5"
|
|
730
|
+
strokeLinecap="round"
|
|
731
|
+
>
|
|
732
|
+
<rect x="2" y="1" width="12" height="9" rx="1" />
|
|
733
|
+
<line x1="2" y1="5.5" x2="14" y2="5.5" />
|
|
734
|
+
<line x1="8" y1="11.5" x2="8" y2="15" />
|
|
735
|
+
<line x1="6.25" y1="13.25" x2="9.75" y2="13.25" />
|
|
736
|
+
</svg>
|
|
737
|
+
</button>
|
|
738
|
+
<button
|
|
739
|
+
className="squisq-toolbar-button"
|
|
740
|
+
title="Delete row"
|
|
741
|
+
onClick={() => tiptapEditor!.chain().focus().deleteRow().run()}
|
|
742
|
+
aria-label="Delete row"
|
|
743
|
+
>
|
|
744
|
+
<svg
|
|
745
|
+
width="16"
|
|
746
|
+
height="16"
|
|
747
|
+
viewBox="0 0 16 16"
|
|
748
|
+
fill="none"
|
|
749
|
+
stroke="currentColor"
|
|
750
|
+
strokeWidth="1.5"
|
|
751
|
+
strokeLinecap="round"
|
|
752
|
+
>
|
|
753
|
+
<rect x="1" y="4" width="14" height="8" rx="1" />
|
|
754
|
+
<line x1="5.5" y1="6" x2="10.5" y2="10" />
|
|
755
|
+
<line x1="10.5" y1="6" x2="5.5" y2="10" />
|
|
756
|
+
</svg>
|
|
757
|
+
</button>
|
|
758
|
+
<button
|
|
759
|
+
className="squisq-toolbar-button squisq-toolbar-button--danger"
|
|
760
|
+
title="Delete table"
|
|
761
|
+
onClick={() => tiptapEditor!.chain().focus().deleteTable().run()}
|
|
762
|
+
aria-label="Delete table"
|
|
763
|
+
>
|
|
764
|
+
<svg
|
|
765
|
+
width="16"
|
|
766
|
+
height="16"
|
|
767
|
+
viewBox="0 0 16 16"
|
|
768
|
+
fill="none"
|
|
769
|
+
stroke="currentColor"
|
|
770
|
+
strokeWidth="1.5"
|
|
771
|
+
strokeLinecap="round"
|
|
772
|
+
>
|
|
773
|
+
<rect x="1" y="1" width="14" height="14" rx="1" />
|
|
774
|
+
<line x1="1" y1="5.5" x2="15" y2="5.5" />
|
|
775
|
+
<line x1="5.5" y1="1" x2="5.5" y2="15" />
|
|
776
|
+
<line x1="4.5" y1="4.5" x2="11.5" y2="11.5" strokeWidth="2" />
|
|
777
|
+
<line x1="11.5" y1="4.5" x2="4.5" y2="11.5" strokeWidth="2" />
|
|
778
|
+
</svg>
|
|
779
|
+
</button>
|
|
780
|
+
</div>
|
|
781
|
+
</>
|
|
782
|
+
)}
|
|
783
|
+
</div>
|
|
784
|
+
)}
|
|
785
|
+
|
|
786
|
+
{/* Overflow menu — outside the overflow:hidden actions container */}
|
|
787
|
+
{!isPreview && overflowIndex !== null && (
|
|
788
|
+
<div className="squisq-toolbar-overflow" ref={overflowRef}>
|
|
789
|
+
<button
|
|
790
|
+
className={`squisq-toolbar-button squisq-toolbar-overflow-trigger${showOverflow ? ' squisq-toolbar-button--active' : ''}`}
|
|
791
|
+
title="More actions"
|
|
792
|
+
onClick={() => setShowOverflow((v) => !v)}
|
|
793
|
+
aria-label="More actions"
|
|
794
|
+
aria-expanded={showOverflow}
|
|
795
|
+
>
|
|
796
|
+
···
|
|
797
|
+
</button>
|
|
798
|
+
{showOverflow && (
|
|
799
|
+
<div className="squisq-toolbar-overflow-menu">
|
|
800
|
+
{BUTTONS.slice(overflowIndex).map((btn) => {
|
|
801
|
+
const active = isWysiwyg ? isTiptapActive(tiptapEditor, btn.id) : false;
|
|
802
|
+
const disabled = btn.id === 'image' && !mediaProvider;
|
|
803
|
+
return (
|
|
804
|
+
<button
|
|
805
|
+
key={btn.id}
|
|
806
|
+
className={`squisq-toolbar-overflow-item${active ? ' squisq-toolbar-overflow-item--active' : ''}`}
|
|
807
|
+
title={btn.title}
|
|
808
|
+
onClick={() => {
|
|
809
|
+
handleAction(btn.id);
|
|
810
|
+
setShowOverflow(false);
|
|
811
|
+
}}
|
|
812
|
+
disabled={disabled}
|
|
813
|
+
>
|
|
814
|
+
{btn.id === 'table' ? (
|
|
815
|
+
<svg
|
|
816
|
+
width="14"
|
|
817
|
+
height="14"
|
|
818
|
+
viewBox="0 0 14 14"
|
|
819
|
+
fill="none"
|
|
820
|
+
stroke="currentColor"
|
|
821
|
+
strokeWidth="1.4"
|
|
822
|
+
strokeLinecap="round"
|
|
823
|
+
>
|
|
824
|
+
<rect x="1" y="1" width="12" height="12" rx="1" />
|
|
825
|
+
<line x1="1" y1="5" x2="13" y2="5" />
|
|
826
|
+
<line x1="1" y1="9" x2="13" y2="9" />
|
|
827
|
+
<line x1="5" y1="1" x2="5" y2="13" />
|
|
828
|
+
<line x1="9" y1="1" x2="9" y2="13" />
|
|
829
|
+
</svg>
|
|
830
|
+
) : (
|
|
831
|
+
<span className="squisq-toolbar-overflow-icon" style={btn.iconStyle}>
|
|
832
|
+
{btn.icon}
|
|
833
|
+
</span>
|
|
834
|
+
)}
|
|
835
|
+
<span>{btn.title}</span>
|
|
836
|
+
</button>
|
|
837
|
+
);
|
|
838
|
+
})}
|
|
839
|
+
|
|
840
|
+
{/* Contextual: template picker in overflow */}
|
|
841
|
+
{currentTemplate !== null && (
|
|
842
|
+
<div className="squisq-toolbar-overflow-item squisq-toolbar-overflow-template">
|
|
843
|
+
<span>Template:</span>
|
|
844
|
+
<select
|
|
845
|
+
className="squisq-template-picker-select"
|
|
846
|
+
value={currentTemplate}
|
|
847
|
+
onChange={(e) => {
|
|
848
|
+
handleTemplatePick(e.target.value);
|
|
849
|
+
setShowOverflow(false);
|
|
850
|
+
}}
|
|
851
|
+
>
|
|
852
|
+
<option value="">— none —</option>
|
|
853
|
+
{TEMPLATE_NAMES.map((name) => (
|
|
854
|
+
<option key={name} value={name}>
|
|
855
|
+
{name}
|
|
856
|
+
</option>
|
|
857
|
+
))}
|
|
858
|
+
</select>
|
|
859
|
+
</div>
|
|
860
|
+
)}
|
|
861
|
+
|
|
862
|
+
{/* Contextual: table controls in overflow */}
|
|
863
|
+
{isInTable && (
|
|
864
|
+
<>
|
|
865
|
+
<div
|
|
866
|
+
className="squisq-toolbar-separator"
|
|
867
|
+
style={{ margin: '4px 0', width: '100%', height: 1 }}
|
|
868
|
+
/>
|
|
869
|
+
{[
|
|
870
|
+
{
|
|
871
|
+
label: 'Add column before',
|
|
872
|
+
action: () => tiptapEditor!.chain().focus().addColumnBefore().run(),
|
|
873
|
+
},
|
|
874
|
+
{
|
|
875
|
+
label: 'Add column after',
|
|
876
|
+
action: () => tiptapEditor!.chain().focus().addColumnAfter().run(),
|
|
877
|
+
},
|
|
878
|
+
{
|
|
879
|
+
label: 'Delete column',
|
|
880
|
+
action: () => tiptapEditor!.chain().focus().deleteColumn().run(),
|
|
881
|
+
},
|
|
882
|
+
{
|
|
883
|
+
label: 'Add row above',
|
|
884
|
+
action: () => tiptapEditor!.chain().focus().addRowBefore().run(),
|
|
885
|
+
},
|
|
886
|
+
{
|
|
887
|
+
label: 'Add row below',
|
|
888
|
+
action: () => tiptapEditor!.chain().focus().addRowAfter().run(),
|
|
889
|
+
},
|
|
890
|
+
{
|
|
891
|
+
label: 'Delete row',
|
|
892
|
+
action: () => tiptapEditor!.chain().focus().deleteRow().run(),
|
|
893
|
+
},
|
|
894
|
+
{
|
|
895
|
+
label: 'Delete table',
|
|
896
|
+
action: () => tiptapEditor!.chain().focus().deleteTable().run(),
|
|
897
|
+
},
|
|
898
|
+
].map((item) => (
|
|
899
|
+
<button
|
|
900
|
+
key={item.label}
|
|
901
|
+
className={`squisq-toolbar-overflow-item${item.label.startsWith('Delete') ? ' squisq-toolbar-overflow-item--danger' : ''}`}
|
|
902
|
+
onClick={() => {
|
|
903
|
+
item.action();
|
|
904
|
+
setShowOverflow(false);
|
|
905
|
+
}}
|
|
906
|
+
>
|
|
907
|
+
<span>{item.label}</span>
|
|
908
|
+
</button>
|
|
909
|
+
))}
|
|
910
|
+
</>
|
|
911
|
+
)}
|
|
912
|
+
</div>
|
|
913
|
+
)}
|
|
485
914
|
</div>
|
|
486
915
|
)}
|
|
916
|
+
|
|
487
917
|
{/* After-actions slot — after formatting controls */}
|
|
488
918
|
{slotAfterActions}
|
|
489
919
|
{/* Spacer pushes right-side buttons to the end */}
|