@bendyline/squisq-editor-react 1.6.1 → 2.0.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 +12 -3
- package/dist/index.d.ts +1304 -208
- package/dist/index.js +22838 -14494
- package/dist/index.js.map +1 -1
- package/dist/monaco.d.ts +1 -0
- package/dist/monaco.js +4 -0
- package/dist/monaco.js.map +1 -0
- package/dist/styles/index.css +2384 -408
- package/package.json +9 -5
- package/src/BlockPropertiesPopover.tsx +48 -17
- package/src/DocumentSettingsDialog.tsx +13 -21
- package/src/EditorContext.tsx +199 -42
- package/src/EditorShell.tsx +403 -270
- package/src/ImageEditor.tsx +22 -1
- package/src/InlinePreviewGutter.tsx +16 -8
- package/src/MediaBin.tsx +276 -40
- package/src/OutlinePanel.tsx +202 -7
- package/src/PlainHtmlPreview.tsx +40 -1
- package/src/PreviewControls.tsx +466 -93
- package/src/PreviewPanel.tsx +313 -77
- package/src/RawEditor.tsx +13 -2
- package/src/StatusBar.tsx +5 -2
- package/src/TemplateAnnotation.ts +34 -10
- package/src/TemplateContentPreview.tsx +56 -0
- package/src/TemplatePicker.tsx +355 -134
- package/src/ThemeCustomizerPanel.tsx +30 -15
- package/src/ThemePicker.tsx +10 -5
- package/src/TimelineBlockPreview.tsx +1 -1
- package/src/Toolbar.tsx +719 -238
- package/src/TransitionPicker.tsx +44 -5
- package/src/ViewMenuPanel.tsx +12 -14
- package/src/ViewSwitcher.tsx +4 -4
- package/src/WysiwygEditor.tsx +167 -92
- package/src/__tests__/blockPropertiesPopoverTheme.test.tsx +33 -0
- package/src/__tests__/blockTagActivity.test.ts +183 -0
- package/src/__tests__/buildDocumentPreviewMarkdown.test.ts +75 -0
- package/src/__tests__/buildPreviewDocContent.test.ts +48 -0
- package/src/__tests__/buildPreviewDocTransition.test.ts +22 -3
- package/src/__tests__/canvasSurfaceTextEditing.test.tsx +60 -0
- package/src/__tests__/customLayoutManagerTheme.test.tsx +34 -0
- package/src/__tests__/documentSettingsDialog.test.tsx +29 -1
- package/src/__tests__/editorScrollHandoff.test.tsx +102 -0
- package/src/__tests__/editorScrollSync.test.ts +65 -0
- package/src/__tests__/editorShellProps.test.tsx +390 -1
- package/src/__tests__/emojiPicker.test.tsx +2 -42
- package/src/__tests__/headingTransition.test.ts +53 -15
- package/src/__tests__/imageEditAffordance.test.tsx +5 -1
- package/src/__tests__/imageEditModalContract.test.tsx +119 -0
- package/src/__tests__/imageEditorShell.test.tsx +38 -1
- package/src/__tests__/jsonEditor.test.tsx +56 -1
- package/src/__tests__/layersPanel.test.tsx +75 -5
- package/src/__tests__/majorApiTypes.test.ts +26 -0
- package/src/__tests__/mediaBinDrop.test.tsx +90 -0
- package/src/__tests__/mediaReferences.test.ts +82 -0
- package/src/__tests__/narrationSave.test.ts +148 -0
- package/src/__tests__/outlinePanel.test.tsx +93 -3
- package/src/__tests__/outlineSource.test.ts +217 -0
- package/src/__tests__/plainHtmlPreview.test.tsx +17 -1
- package/src/__tests__/presentationMode.test.tsx +290 -0
- package/src/__tests__/previewControls.test.tsx +312 -6
- package/src/__tests__/previewPanelAudioFailure.test.tsx +46 -0
- package/src/__tests__/previewPanelPresentation.test.tsx +126 -0
- package/src/__tests__/previewPanelTransforms.test.tsx +117 -0
- package/src/__tests__/rawEditorModelIsolation.test.ts +19 -0
- package/src/__tests__/resolveBlockVisual.test.ts +43 -0
- package/src/__tests__/statusBar.test.tsx +27 -0
- package/src/__tests__/teleprompter.test.tsx +190 -0
- package/src/__tests__/templateAnnotationRoundTrip.test.ts +49 -2
- package/src/__tests__/templateContentPreview.test.ts +101 -0
- package/src/__tests__/templatePickerPortal.test.tsx +62 -0
- package/src/__tests__/tiptapBridge.test.ts +70 -0
- package/src/__tests__/tiptapCodeBlockRoundTrip.test.ts +109 -0
- package/src/__tests__/transformStyleId.test.ts +13 -0
- package/src/__tests__/useImageEditor.test.tsx +67 -0
- package/src/__tests__/useMediaRecorder.test.ts +75 -0
- package/src/__tests__/viewMenuPanel.test.tsx +130 -0
- package/src/__tests__/wysiwygImageUpload.test.ts +69 -0
- package/src/__tests__/wysiwygTemplateBadgeFocus.test.tsx +56 -0
- package/src/asciiDiagram/AsciiDiagramExtension.ts +343 -0
- package/src/asciiDiagram/AsciiDiagramWidget.tsx +224 -0
- package/src/asciiDiagram/RepairableDiagramExtension.ts +200 -0
- package/src/asciiDiagram/__tests__/AsciiDiagramExtension.test.ts +145 -0
- package/src/asciiDiagram/__tests__/RepairableDiagramExtension.test.ts +159 -0
- package/src/asciiDiagram/__tests__/asciiDiagramCommands.test.ts +227 -0
- package/src/asciiDiagram/__tests__/asciiDiagramOps.test.ts +114 -0
- package/src/asciiDiagram/__tests__/asciiPaste.test.ts +43 -0
- package/src/asciiDiagram/asciiDiagramCommands.ts +148 -0
- package/src/asciiDiagram/asciiDiagramData.ts +116 -0
- package/src/asciiDiagram/asciiDiagramOps.ts +219 -0
- package/src/asciiDiagram/asciiPaste.ts +19 -0
- package/src/blockTagActivity.ts +233 -0
- package/src/buildDocumentPreviewMarkdown.ts +168 -0
- package/src/buildPreviewDoc.ts +19 -12
- package/src/customTemplates/CustomLayoutManager.tsx +3 -2
- package/src/customTemplates/CustomTemplateContext.tsx +6 -0
- package/src/customTemplates/TemplateDesigner.tsx +7 -4
- package/src/customTemplates/__tests__/library.test.ts +8 -0
- package/src/customTemplates/designer.css +158 -99
- package/src/customTemplates/library.ts +16 -3
- package/src/customThemes/CustomThemeContext.tsx +6 -0
- package/src/customThemes/CustomThemeDialog.tsx +35 -5
- package/src/customThemes/ImportThemeSection.tsx +178 -0
- package/src/customThemes/__tests__/customThemeLibrary.test.ts +9 -0
- package/src/customThemes/__tests__/importThemeSection.test.tsx +192 -0
- package/src/customThemes/customThemeLibrary.ts +7 -4
- package/src/customThemes/index.ts +7 -1
- package/src/customThemes/themeDraft.ts +23 -7
- package/src/diagram/DiagramCanvas.tsx +9 -4
- package/src/diagram/types.ts +35 -0
- package/src/editorScrollSync.ts +73 -0
- package/src/emojiData.ts +3 -23
- package/src/headingTransition.ts +85 -25
- package/src/imageEditor/CanvasSurface.tsx +29 -23
- package/src/imageEditor/LayersPanel.tsx +78 -3
- package/src/imageEditor/Toolbar.tsx +5 -1
- package/src/imageEditor/icons.tsx +4 -0
- package/src/imageEditor/image-editor.css +75 -0
- package/src/imageEditor/layers/SelectionHandles.tsx +1 -1
- package/src/imageEditor/useImageEditor.ts +71 -14
- package/src/index.ts +193 -20
- package/src/jsonEditor/JsonEditorContext.tsx +12 -6
- package/src/jsonEditor/RenderNode.tsx +24 -3
- package/src/jsonEditor/editors.tsx +86 -19
- package/src/mediaEntries.ts +12 -0
- package/src/mediaReferences.ts +299 -0
- package/src/monaco.ts +35 -0
- package/src/monacoWorkers.ts +89 -0
- package/src/outlineSource.ts +171 -0
- package/src/presentation/PresentationMode.tsx +596 -0
- package/src/rawEditorIsolation.ts +18 -0
- package/src/recorder/hooks/useMediaRecorder.ts +97 -53
- package/src/resolveBlockVisual.ts +10 -4
- package/src/scene/Scene.tsx +101 -25
- package/src/scene/SceneBlockExtension.ts +17 -10
- package/src/scene/SceneBlockWidget.tsx +11 -4
- package/src/scene/SceneSelection.tsx +19 -15
- package/src/scene/SceneSideToolbar.tsx +89 -0
- package/src/scene/SceneViewport.tsx +7 -3
- package/src/scene/ShapePalette.tsx +17 -2
- package/src/scene/__tests__/DiagramAdapter.test.ts +86 -0
- package/src/scene/__tests__/SceneBlockExtension.test.ts +33 -0
- package/src/scene/__tests__/sceneIsolation.test.tsx +93 -0
- package/src/scene/adapters/DiagramAdapter.ts +12 -101
- package/src/scene/commands/SceneCommand.ts +4 -1
- package/src/scene/index.ts +1 -6
- package/src/scene/layers/DiagramEdges.tsx +24 -9
- package/src/scene/layers/edgeGeometry.ts +31 -4
- package/src/scene/layers/nodeCard.tsx +27 -8
- package/src/scene/scene.css +145 -2
- package/src/scene/text/SceneTextOverlay.tsx +5 -5
- package/src/scene/text/sceneTextChannel.ts +26 -20
- package/src/scene/text/sceneTextConfig.ts +4 -0
- package/src/scene/tools/ConnectTool.ts +126 -28
- package/src/scene/tools/DrawingConnectTool.ts +86 -16
- package/src/scene/tools/SceneTool.ts +10 -0
- package/src/scene/tools/SelectTool.ts +34 -18
- package/src/scene/tools/ShapeTool.ts +2 -3
- package/src/styles/ascii-diagram.css +79 -0
- package/src/styles/ascii-timeline.css +499 -0
- package/src/styles/editor.css +1566 -219
- package/src/styles/index.css +3 -0
- package/src/styles/tree-view.css +139 -0
- package/src/teleprompter/TeleprompterControls.tsx +218 -0
- package/src/teleprompter/TeleprompterSelfView.tsx +22 -0
- package/src/teleprompter/TeleprompterSurface.tsx +267 -0
- package/src/teleprompter/TeleprompterView.tsx +338 -0
- package/src/teleprompter/canvasRenderer.ts +161 -0
- package/src/teleprompter/floatingWindow.ts +352 -0
- package/src/teleprompter/index.ts +61 -0
- package/src/teleprompter/pcmWorklet.ts +62 -0
- package/src/teleprompter/recording/insertPreamble.ts +80 -0
- package/src/teleprompter/recording/narrationSave.ts +134 -0
- package/src/teleprompter/recording/useNarrationRecorder.ts +367 -0
- package/src/teleprompter/scrollModel.ts +85 -0
- package/src/teleprompter/teleprompterTheme.ts +303 -0
- package/src/teleprompter/types.ts +38 -0
- package/src/teleprompter/useFloatingWindow.ts +74 -0
- package/src/teleprompter/useMicAnalysis.ts +211 -0
- package/src/teleprompter/useTeleprompter.ts +421 -0
- package/src/templateContentPreviewResolver.ts +360 -0
- package/src/timeline/TimelineEditorWidget.tsx +702 -0
- package/src/timeline/TimelineViewExtension.ts +252 -0
- package/src/timeline/__tests__/TimelineEditorWidget.test.tsx +355 -0
- package/src/timeline/__tests__/TimelineViewExtension.test.ts +163 -0
- package/src/timeline/__tests__/timelineCommands.test.ts +327 -0
- package/src/timeline/__tests__/timelineOps.test.ts +222 -0
- package/src/timeline/__tests__/timelinePaste.test.ts +34 -0
- package/src/timeline/timelineCommands.ts +367 -0
- package/src/timeline/timelineData.ts +54 -0
- package/src/timeline/timelineOps.ts +277 -0
- package/src/timeline/timelinePaste.ts +8 -0
- package/src/tiptapBridge.ts +84 -49
- package/src/transformStyleId.ts +17 -0
- package/src/treeview/TreeOutlineWidget.tsx +240 -0
- package/src/treeview/TreeViewExtension.ts +250 -0
- package/src/treeview/__tests__/TreeViewExtension.test.ts +122 -0
- package/src/treeview/__tests__/treeOps.test.ts +125 -0
- package/src/treeview/__tests__/treePaste.test.ts +40 -0
- package/src/treeview/__tests__/treeViewCommands.test.ts +163 -0
- package/src/treeview/treeOps.ts +172 -0
- package/src/treeview/treePaste.ts +13 -0
- package/src/treeview/treeViewCommands.ts +94 -0
- package/src/treeview/treeViewData.ts +41 -0
- package/src/useMonacoLoader.ts +11 -34
- package/src/wysiwygImageUpload.ts +113 -0
- package/src/diagram/DiagramExtension.ts +0 -209
- package/src/diagram/DiagramWidget.tsx +0 -270
- package/src/diagram/useDiagramData.ts +0 -126
package/src/TemplatePicker.tsx
CHANGED
|
@@ -6,12 +6,18 @@
|
|
|
6
6
|
* and a one-sentence description so authors can quickly find the right layout.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
9
|
+
import { useEffect, useId, useMemo, useRef, useState } from 'react';
|
|
10
10
|
import { createPortal } from 'react-dom';
|
|
11
11
|
import type { CustomTemplateDefinition } from '@bendyline/squisq/schemas';
|
|
12
12
|
import { TEMPLATE_METADATA, resolveTemplateName } from '@bendyline/squisq/doc';
|
|
13
|
+
import { extractPlainText } from '@bendyline/squisq/markdown';
|
|
13
14
|
import { useCustomTemplates } from './customTemplates/CustomTemplateContext';
|
|
14
15
|
import { TemplateThumbnail } from './customTemplates/thumbnail';
|
|
16
|
+
import { TemplateContentPreview } from './TemplateContentPreview';
|
|
17
|
+
import {
|
|
18
|
+
resolveTemplateContentPreviewResult,
|
|
19
|
+
type TemplatePreviewSource,
|
|
20
|
+
} from './templateContentPreviewResolver';
|
|
15
21
|
|
|
16
22
|
// ── Template metadata ─────────────────────────────────────────────
|
|
17
23
|
//
|
|
@@ -30,6 +36,21 @@ interface TemplateEntry {
|
|
|
30
36
|
icon: JSX.Element;
|
|
31
37
|
}
|
|
32
38
|
|
|
39
|
+
/** Matches every gallery when multiple editor instances are mounted. */
|
|
40
|
+
export const TEMPLATE_GALLERY_PORTAL_SELECTOR = '[data-squisq-template-gallery-portal]';
|
|
41
|
+
|
|
42
|
+
const TEMPLATE_GALLERY_DIALOG_ID = 'squisq-template-gallery-dialog';
|
|
43
|
+
const TEMPLATE_GALLERY_PORTAL_ID_PREFIX = 'squisq-template-gallery-portal';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Give each portaled gallery a stable, collision-free id. Hosts should match
|
|
47
|
+
* galleries through {@link TEMPLATE_GALLERY_PORTAL_SELECTOR}; a process-wide
|
|
48
|
+
* singleton id cannot identify the right editor when several are mounted.
|
|
49
|
+
*/
|
|
50
|
+
function useTemplateGalleryPortalId(): string {
|
|
51
|
+
return `${TEMPLATE_GALLERY_PORTAL_ID_PREFIX}-${useId().replace(/:/g, '')}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
33
54
|
const W = 56;
|
|
34
55
|
const H = 40;
|
|
35
56
|
|
|
@@ -57,7 +78,7 @@ function TemplateIcon({ children }: { children: React.ReactNode }) {
|
|
|
57
78
|
const NONE_ENTRY: TemplateEntry = {
|
|
58
79
|
name: '',
|
|
59
80
|
label: '— none —',
|
|
60
|
-
description: 'Plain heading block with no visual
|
|
81
|
+
description: 'Plain heading block with no visual treatment.',
|
|
61
82
|
icon: (
|
|
62
83
|
<TemplateIcon>
|
|
63
84
|
<rect
|
|
@@ -404,28 +425,62 @@ export const TEMPLATE_ENTRIES: TemplateEntry[] = [
|
|
|
404
425
|
<rect x={4} y={6} width={16} height={10} rx={2} fill={FA} opacity={0.85} />
|
|
405
426
|
<rect x={36} y={6} width={16} height={10} rx={2} fill={F2} opacity={0.8} />
|
|
406
427
|
<rect x={20} y={24} width={16} height={10} rx={2} fill={F2} opacity={0.8} />
|
|
428
|
+
<path d="M 20 11 L 36 11" stroke={FA} strokeWidth={1.5} fill="none" opacity={0.7} />
|
|
429
|
+
<path d="M 36 11 L 32 8.5 L 32 13.5 Z" fill={FA} opacity={0.7} />
|
|
407
430
|
<path
|
|
408
|
-
d="M
|
|
431
|
+
d="M 12 16 C 12 22, 24 22, 24 24"
|
|
409
432
|
stroke={FA}
|
|
410
433
|
strokeWidth={1.5}
|
|
411
434
|
fill="none"
|
|
412
435
|
opacity={0.7}
|
|
413
|
-
markerEnd="url(#tp-diagram-arrow)"
|
|
414
436
|
/>
|
|
415
437
|
<path
|
|
416
|
-
d="M
|
|
438
|
+
d="M 44 16 C 44 22, 32 22, 32 24"
|
|
417
439
|
stroke={FA}
|
|
418
440
|
strokeWidth={1.5}
|
|
419
441
|
fill="none"
|
|
420
442
|
opacity={0.7}
|
|
421
443
|
/>
|
|
444
|
+
</TemplateIcon>
|
|
445
|
+
),
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
name: 'tree',
|
|
449
|
+
label: 'Tree',
|
|
450
|
+
description:
|
|
451
|
+
'Renders an ASCII file-tree / outline fence as a filesystem-style treeview — edit as an outline.',
|
|
452
|
+
icon: (
|
|
453
|
+
<TemplateIcon>
|
|
454
|
+
<rect x={6} y={5} width={16} height={7} rx={1.5} fill={FA} opacity={0.85} />
|
|
422
455
|
<path
|
|
423
|
-
d="M
|
|
424
|
-
stroke={
|
|
456
|
+
d="M 12 12 L 12 34 M 12 20 L 22 20 M 12 34 L 22 34"
|
|
457
|
+
stroke={F1}
|
|
425
458
|
strokeWidth={1.5}
|
|
426
459
|
fill="none"
|
|
427
460
|
opacity={0.7}
|
|
428
461
|
/>
|
|
462
|
+
<rect x={24} y={16} width={14} height={7} rx={1.5} fill={F2} opacity={0.8} />
|
|
463
|
+
<rect x={24} y={30} width={14} height={7} rx={1.5} fill={F2} opacity={0.8} />
|
|
464
|
+
<rect x={26} y={2} width={14} height={7} rx={1.5} fill={F2} opacity={0.6} />
|
|
465
|
+
</TemplateIcon>
|
|
466
|
+
),
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
name: 'timeline',
|
|
470
|
+
label: 'Timeline',
|
|
471
|
+
description:
|
|
472
|
+
'Plots events across one or more horizontal tracks with callouts and optional branching links.',
|
|
473
|
+
icon: (
|
|
474
|
+
<TemplateIcon>
|
|
475
|
+
<path d="M 5 21 L 51 21" stroke={F1} strokeWidth={1.8} fill="none" />
|
|
476
|
+
<path d="M 47 18 L 52 21 L 47 24" stroke={F1} strokeWidth={1.5} fill="none" />
|
|
477
|
+
<circle cx={13} cy={21} r={3} fill={FA} />
|
|
478
|
+
<circle cx={29} cy={21} r={3} fill={F2} />
|
|
479
|
+
<circle cx={44} cy={21} r={3} fill={FA} />
|
|
480
|
+
<path d="M 13 18 L 13 10 M 29 24 L 29 32 M 44 18 L 44 8" stroke={F1} strokeWidth={1} />
|
|
481
|
+
<rect x={7} y={5} width={12} height={3} rx={1} fill={F2} opacity={0.8} />
|
|
482
|
+
<rect x={23} y={33} width={12} height={3} rx={1} fill={F2} opacity={0.8} />
|
|
483
|
+
<rect x={38} y={3} width={12} height={3} rx={1} fill={F2} opacity={0.8} />
|
|
429
484
|
</TemplateIcon>
|
|
430
485
|
),
|
|
431
486
|
},
|
|
@@ -513,6 +568,8 @@ export function templateLabel(
|
|
|
513
568
|
export interface TemplatePickerProps {
|
|
514
569
|
value: string;
|
|
515
570
|
onChange: (name: string) => void;
|
|
571
|
+
/** Active editor chrome theme, used by the portaled dialog. */
|
|
572
|
+
colorScheme?: 'light' | 'dark';
|
|
516
573
|
/** When true, shows only the trigger button (no popover) — used in the overflow menu. */
|
|
517
574
|
compact?: boolean;
|
|
518
575
|
/**
|
|
@@ -528,48 +585,37 @@ export interface TemplatePickerProps {
|
|
|
528
585
|
* hidden.
|
|
529
586
|
*/
|
|
530
587
|
onOpenDesigner?: () => void;
|
|
588
|
+
/**
|
|
589
|
+
* Active block content used to render live template thumbnails. When omitted,
|
|
590
|
+
* or when a template cannot be meaningfully derived from the block, cards use
|
|
591
|
+
* their static wireframe icons.
|
|
592
|
+
*/
|
|
593
|
+
previewSource?: TemplatePreviewSource;
|
|
531
594
|
}
|
|
532
595
|
|
|
533
596
|
export function TemplatePicker({
|
|
534
597
|
value,
|
|
535
598
|
onChange,
|
|
599
|
+
colorScheme = 'light',
|
|
536
600
|
compact,
|
|
537
601
|
recommended,
|
|
538
602
|
onOpenDesigner,
|
|
603
|
+
previewSource,
|
|
539
604
|
}: TemplatePickerProps) {
|
|
540
605
|
const [open, setOpen] = useState(false);
|
|
541
|
-
const [
|
|
606
|
+
const [dialogStyle, setDialogStyle] = useState<React.CSSProperties>({});
|
|
542
607
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
543
608
|
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
609
|
+
const dialogRef = useRef<HTMLDivElement>(null);
|
|
610
|
+
const dialogId = `${TEMPLATE_GALLERY_DIALOG_ID}-${useId().replace(/:/g, '')}`;
|
|
544
611
|
|
|
545
|
-
|
|
546
|
-
// left edge so the gallery (up to 780px wide) doesn't overflow the
|
|
547
|
-
// viewport when the toolbar trigger sits near the right edge of the
|
|
548
|
-
// window — previously the popover was left-aligned to the trigger,
|
|
549
|
-
// which pushed half the cards off-screen at narrow widths.
|
|
550
|
-
const updatePosition = () => {
|
|
612
|
+
const updateDialogBounds = () => {
|
|
551
613
|
if (!triggerRef.current) return;
|
|
552
|
-
|
|
553
|
-
const popoverEl = document.getElementById('squisq-template-gallery-portal');
|
|
554
|
-
// Use the actual rendered width if the popover is already mounted;
|
|
555
|
-
// otherwise fall back to the CSS-defined max so the first paint
|
|
556
|
-
// doesn't overflow either. Measure the portal element directly —
|
|
557
|
-
// its `firstElementChild` is the (small) "(none)" option, not the
|
|
558
|
-
// gallery itself.
|
|
559
|
-
const popoverWidth = popoverEl?.getBoundingClientRect().width ?? 780;
|
|
560
|
-
const margin = 8;
|
|
561
|
-
const maxLeft = Math.max(margin, window.innerWidth - popoverWidth - margin);
|
|
562
|
-
const left = Math.min(Math.max(margin, rect.left), maxLeft);
|
|
563
|
-
setPopoverStyle({
|
|
564
|
-
position: 'fixed',
|
|
565
|
-
top: rect.bottom + 6,
|
|
566
|
-
left,
|
|
567
|
-
zIndex: 9999,
|
|
568
|
-
});
|
|
614
|
+
setDialogStyle(computeDialogStyle(triggerRef.current));
|
|
569
615
|
};
|
|
570
616
|
|
|
571
617
|
const handleOpen = () => {
|
|
572
|
-
|
|
618
|
+
updateDialogBounds();
|
|
573
619
|
setOpen((v) => !v);
|
|
574
620
|
};
|
|
575
621
|
|
|
@@ -579,8 +625,8 @@ export function TemplatePicker({
|
|
|
579
625
|
const handler = (e: MouseEvent) => {
|
|
580
626
|
const target = e.target as Node;
|
|
581
627
|
const inTrigger = triggerRef.current?.contains(target);
|
|
582
|
-
const
|
|
583
|
-
if (!inTrigger && !
|
|
628
|
+
const inDialog = dialogRef.current?.contains(target);
|
|
629
|
+
if (!inTrigger && !inDialog) {
|
|
584
630
|
setOpen(false);
|
|
585
631
|
}
|
|
586
632
|
};
|
|
@@ -601,10 +647,8 @@ export function TemplatePicker({
|
|
|
601
647
|
// Reposition on scroll/resize while open
|
|
602
648
|
useEffect(() => {
|
|
603
649
|
if (!open) return;
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
requestAnimationFrame(updatePosition);
|
|
607
|
-
const handler = () => updatePosition();
|
|
650
|
+
requestAnimationFrame(updateDialogBounds);
|
|
651
|
+
const handler = () => updateDialogBounds();
|
|
608
652
|
window.addEventListener('scroll', handler, true);
|
|
609
653
|
window.addEventListener('resize', handler);
|
|
610
654
|
return () => {
|
|
@@ -621,6 +665,7 @@ export function TemplatePicker({
|
|
|
621
665
|
const currentLabel = templateLabel(value);
|
|
622
666
|
const currentEntry: TemplateEntry =
|
|
623
667
|
(value && TEMPLATE_ENTRIES.find((e) => e.name === value)) || NONE_ENTRY;
|
|
668
|
+
const dialogTitle = templateDialogTitle(previewSource);
|
|
624
669
|
|
|
625
670
|
if (compact) {
|
|
626
671
|
// In overflow menu, use a simple select for space efficiency
|
|
@@ -642,13 +687,23 @@ export function TemplatePicker({
|
|
|
642
687
|
|
|
643
688
|
const gallery = open
|
|
644
689
|
? createPortal(
|
|
645
|
-
<
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
690
|
+
<TemplateGalleryDialog
|
|
691
|
+
dialogRef={dialogRef}
|
|
692
|
+
dialogId={dialogId}
|
|
693
|
+
title={dialogTitle}
|
|
694
|
+
colorScheme={colorScheme}
|
|
695
|
+
style={dialogStyle}
|
|
696
|
+
onClose={() => setOpen(false)}
|
|
697
|
+
>
|
|
698
|
+
<TemplateGalleryBody
|
|
699
|
+
value={value}
|
|
700
|
+
onSelect={handleSelect}
|
|
701
|
+
style={{}}
|
|
702
|
+
recommended={recommended}
|
|
703
|
+
onOpenDesigner={onOpenDesigner}
|
|
704
|
+
previewSource={previewSource}
|
|
705
|
+
/>
|
|
706
|
+
</TemplateGalleryDialog>,
|
|
652
707
|
document.body,
|
|
653
708
|
)
|
|
654
709
|
: null;
|
|
@@ -660,9 +715,10 @@ export function TemplatePicker({
|
|
|
660
715
|
type="button"
|
|
661
716
|
className={`squisq-template-picker-trigger${open ? ' squisq-template-picker-trigger--open' : ''}`}
|
|
662
717
|
onClick={handleOpen}
|
|
663
|
-
aria-haspopup="
|
|
718
|
+
aria-haspopup="dialog"
|
|
664
719
|
aria-expanded={open}
|
|
665
|
-
|
|
720
|
+
aria-controls={open ? dialogId : undefined}
|
|
721
|
+
title="Choose block type"
|
|
666
722
|
>
|
|
667
723
|
<span className="squisq-template-picker-trigger-label">Block:</span>
|
|
668
724
|
<span className="squisq-template-picker-trigger-thumb" aria-hidden="true">
|
|
@@ -695,8 +751,66 @@ export function TemplatePicker({
|
|
|
695
751
|
|
|
696
752
|
// ── Reusable gallery body ──────────────────────────────────────────
|
|
697
753
|
|
|
754
|
+
function TemplateGalleryDialog({
|
|
755
|
+
dialogRef,
|
|
756
|
+
dialogId,
|
|
757
|
+
children,
|
|
758
|
+
title,
|
|
759
|
+
colorScheme,
|
|
760
|
+
style,
|
|
761
|
+
onClose,
|
|
762
|
+
}: {
|
|
763
|
+
dialogRef: React.RefObject<HTMLDivElement>;
|
|
764
|
+
dialogId: string;
|
|
765
|
+
children: React.ReactNode;
|
|
766
|
+
title: string;
|
|
767
|
+
colorScheme: 'light' | 'dark';
|
|
768
|
+
style: React.CSSProperties;
|
|
769
|
+
onClose: () => void;
|
|
770
|
+
}) {
|
|
771
|
+
return (
|
|
772
|
+
<div
|
|
773
|
+
ref={dialogRef}
|
|
774
|
+
id={dialogId}
|
|
775
|
+
className="squisq-template-gallery-dialog"
|
|
776
|
+
data-theme={colorScheme}
|
|
777
|
+
style={style}
|
|
778
|
+
onMouseDown={(event) => {
|
|
779
|
+
if (event.target === event.currentTarget) onClose();
|
|
780
|
+
}}
|
|
781
|
+
>
|
|
782
|
+
<div
|
|
783
|
+
className="squisq-template-gallery-dialog-panel"
|
|
784
|
+
role="dialog"
|
|
785
|
+
aria-modal="true"
|
|
786
|
+
aria-label={title}
|
|
787
|
+
>
|
|
788
|
+
<div className="squisq-template-gallery-dialog-header">
|
|
789
|
+
<h2 className="squisq-template-gallery-dialog-title">{title}</h2>
|
|
790
|
+
<button
|
|
791
|
+
type="button"
|
|
792
|
+
className="squisq-template-gallery-dialog-close"
|
|
793
|
+
onClick={onClose}
|
|
794
|
+
aria-label="Close block type picker"
|
|
795
|
+
>
|
|
796
|
+
<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true">
|
|
797
|
+
<path
|
|
798
|
+
d="M3.2 3.2l7.6 7.6M10.8 3.2l-7.6 7.6"
|
|
799
|
+
stroke="currentColor"
|
|
800
|
+
strokeWidth="1.7"
|
|
801
|
+
strokeLinecap="round"
|
|
802
|
+
/>
|
|
803
|
+
</svg>
|
|
804
|
+
</button>
|
|
805
|
+
</div>
|
|
806
|
+
<div className="squisq-template-gallery-dialog-body">{children}</div>
|
|
807
|
+
</div>
|
|
808
|
+
</div>
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
|
|
698
812
|
/**
|
|
699
|
-
* The
|
|
813
|
+
* The dialog/grid markup shared by the toolbar `TemplatePicker` and the
|
|
700
814
|
* inline `TemplateBadgeMenu`. Renders all template cards plus the
|
|
701
815
|
* "(none)" option; no positioning logic — callers supply `style` (typically
|
|
702
816
|
* a `position: fixed` rect from `getBoundingClientRect()`).
|
|
@@ -705,10 +819,12 @@ function TemplateCard({
|
|
|
705
819
|
entry,
|
|
706
820
|
value,
|
|
707
821
|
onSelect,
|
|
822
|
+
previewSource,
|
|
708
823
|
}: {
|
|
709
824
|
entry: TemplateEntry;
|
|
710
825
|
value: string;
|
|
711
826
|
onSelect: (name: string) => void;
|
|
827
|
+
previewSource?: TemplatePreviewSource;
|
|
712
828
|
}) {
|
|
713
829
|
return (
|
|
714
830
|
<button
|
|
@@ -719,7 +835,13 @@ function TemplateCard({
|
|
|
719
835
|
onClick={() => onSelect(entry.name)}
|
|
720
836
|
title={entry.description}
|
|
721
837
|
>
|
|
722
|
-
<div className="squisq-template-gallery-card-icon">
|
|
838
|
+
<div className="squisq-template-gallery-card-icon">
|
|
839
|
+
<TemplateContentPreview
|
|
840
|
+
templateName={entry.name}
|
|
841
|
+
source={previewSource}
|
|
842
|
+
fallback={entry.icon}
|
|
843
|
+
/>
|
|
844
|
+
</div>
|
|
723
845
|
<div className="squisq-template-gallery-card-body">
|
|
724
846
|
<span className="squisq-template-gallery-card-name">{entry.label}</span>
|
|
725
847
|
<span className="squisq-template-gallery-card-desc">{entry.description}</span>
|
|
@@ -728,19 +850,40 @@ function TemplateCard({
|
|
|
728
850
|
);
|
|
729
851
|
}
|
|
730
852
|
|
|
853
|
+
function splitBlockTypeEntriesByContent(
|
|
854
|
+
entries: readonly TemplateEntry[],
|
|
855
|
+
previewSource?: TemplatePreviewSource,
|
|
856
|
+
): { blockTypeEntries: TemplateEntry[]; contentNeededEntries: TemplateEntry[] } {
|
|
857
|
+
if (!previewSource) return { blockTypeEntries: [...entries], contentNeededEntries: [] };
|
|
858
|
+
|
|
859
|
+
const blockTypeEntries: TemplateEntry[] = [];
|
|
860
|
+
const contentNeededEntries: TemplateEntry[] = [];
|
|
861
|
+
|
|
862
|
+
for (const entry of entries) {
|
|
863
|
+
const result = resolveTemplateContentPreviewResult(entry.name, previewSource);
|
|
864
|
+
if (result.warning) contentNeededEntries.push(entry);
|
|
865
|
+
else blockTypeEntries.push(entry);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
return { blockTypeEntries, contentNeededEntries };
|
|
869
|
+
}
|
|
870
|
+
|
|
731
871
|
function TemplateGalleryBody({
|
|
732
872
|
value,
|
|
733
873
|
onSelect,
|
|
734
874
|
style,
|
|
735
875
|
recommended,
|
|
736
876
|
onOpenDesigner,
|
|
877
|
+
previewSource,
|
|
737
878
|
}: {
|
|
738
879
|
value: string;
|
|
739
880
|
onSelect: (name: string) => void;
|
|
740
881
|
style: React.CSSProperties;
|
|
741
882
|
recommended?: readonly string[];
|
|
742
883
|
onOpenDesigner?: () => void;
|
|
884
|
+
previewSource?: TemplatePreviewSource;
|
|
743
885
|
}) {
|
|
886
|
+
const galleryId = useTemplateGalleryPortalId();
|
|
744
887
|
const [query, setQuery] = useState('');
|
|
745
888
|
const searchRef = useRef<HTMLInputElement>(null);
|
|
746
889
|
// Pull merged doc + library templates from the surrounding context.
|
|
@@ -779,21 +922,34 @@ function TemplateGalleryBody({
|
|
|
779
922
|
return { built, custom };
|
|
780
923
|
}, [hasQuery, trimmedQuery, customTemplates]);
|
|
781
924
|
|
|
782
|
-
const recommendedSet =
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
const
|
|
787
|
-
? TEMPLATE_ENTRIES.filter((e) =>
|
|
788
|
-
|
|
789
|
-
|
|
925
|
+
const recommendedSet = useMemo(
|
|
926
|
+
() => (recommended && recommended.length > 0 ? new Set(recommended) : null),
|
|
927
|
+
[recommended],
|
|
928
|
+
);
|
|
929
|
+
const recommendedEntries = useMemo(
|
|
930
|
+
() => (recommendedSet ? TEMPLATE_ENTRIES.filter((e) => recommendedSet.has(e.name)) : []),
|
|
931
|
+
[recommendedSet],
|
|
932
|
+
);
|
|
933
|
+
const restEntries = useMemo(
|
|
934
|
+
() =>
|
|
935
|
+
recommendedSet
|
|
936
|
+
? TEMPLATE_ENTRIES.filter((e) => !recommendedSet.has(e.name))
|
|
937
|
+
: TEMPLATE_ENTRIES,
|
|
938
|
+
[recommendedSet],
|
|
939
|
+
);
|
|
940
|
+
const { blockTypeEntries, contentNeededEntries } = useMemo(
|
|
941
|
+
() => splitBlockTypeEntriesByContent(restEntries, previewSource),
|
|
942
|
+
[restEntries, previewSource],
|
|
943
|
+
);
|
|
944
|
+
const grouped = !hasQuery && (recommendedEntries.length > 0 || contentNeededEntries.length > 0);
|
|
790
945
|
|
|
791
946
|
return (
|
|
792
947
|
<div
|
|
793
|
-
id=
|
|
794
|
-
|
|
948
|
+
id={galleryId}
|
|
949
|
+
data-squisq-template-gallery-portal=""
|
|
950
|
+
className={`squisq-template-gallery${grouped ? ' squisq-template-gallery--segmented' : ''}`}
|
|
795
951
|
role="listbox"
|
|
796
|
-
aria-label="Block
|
|
952
|
+
aria-label="Block types"
|
|
797
953
|
style={style}
|
|
798
954
|
>
|
|
799
955
|
<div className="squisq-template-gallery-search">
|
|
@@ -812,10 +968,10 @@ function TemplateGalleryBody({
|
|
|
812
968
|
ref={searchRef}
|
|
813
969
|
type="search"
|
|
814
970
|
className="squisq-template-gallery-search-input"
|
|
815
|
-
placeholder="Search
|
|
971
|
+
placeholder="Search block types…"
|
|
816
972
|
value={query}
|
|
817
973
|
onChange={(e) => setQuery(e.target.value)}
|
|
818
|
-
aria-label="Search block
|
|
974
|
+
aria-label="Search block types"
|
|
819
975
|
/>
|
|
820
976
|
</div>
|
|
821
977
|
|
|
@@ -854,13 +1010,19 @@ function TemplateGalleryBody({
|
|
|
854
1010
|
{matches.built.length > 0 && (
|
|
855
1011
|
<div className="squisq-template-gallery-grid">
|
|
856
1012
|
{matches.built.map((entry) => (
|
|
857
|
-
<TemplateCard
|
|
1013
|
+
<TemplateCard
|
|
1014
|
+
key={entry.name}
|
|
1015
|
+
entry={entry}
|
|
1016
|
+
value={value}
|
|
1017
|
+
onSelect={onSelect}
|
|
1018
|
+
previewSource={previewSource}
|
|
1019
|
+
/>
|
|
858
1020
|
))}
|
|
859
1021
|
</div>
|
|
860
1022
|
)}
|
|
861
1023
|
</>
|
|
862
1024
|
) : (
|
|
863
|
-
<div className="squisq-template-gallery-empty">No
|
|
1025
|
+
<div className="squisq-template-gallery-empty">No block types match "{query}".</div>
|
|
864
1026
|
)
|
|
865
1027
|
) : (
|
|
866
1028
|
<>
|
|
@@ -872,9 +1034,9 @@ function TemplateGalleryBody({
|
|
|
872
1034
|
+
|
|
873
1035
|
</span>
|
|
874
1036
|
<span className="squisq-template-gallery-new-body">
|
|
875
|
-
<span className="squisq-template-gallery-new-label">New
|
|
1037
|
+
<span className="squisq-template-gallery-new-label">New block type</span>
|
|
876
1038
|
<span className="squisq-template-gallery-new-desc">
|
|
877
|
-
Design a reusable
|
|
1039
|
+
Design a reusable block type with placeholders for {'{title}'} and {'{content}'}.
|
|
878
1040
|
</span>
|
|
879
1041
|
</span>
|
|
880
1042
|
</button>
|
|
@@ -891,30 +1053,69 @@ function TemplateGalleryBody({
|
|
|
891
1053
|
</div>
|
|
892
1054
|
)}
|
|
893
1055
|
|
|
894
|
-
{
|
|
1056
|
+
{recommendedEntries.length > 0 && (
|
|
895
1057
|
<div className="squisq-template-gallery-section">
|
|
896
|
-
<h3 className="squisq-template-gallery-section-title">
|
|
1058
|
+
<h3 className="squisq-template-gallery-section-title">Suggested Block Types</h3>
|
|
897
1059
|
<div className="squisq-template-gallery-grid">
|
|
898
1060
|
{recommendedEntries.map((entry) => (
|
|
899
|
-
<TemplateCard
|
|
1061
|
+
<TemplateCard
|
|
1062
|
+
key={entry.name}
|
|
1063
|
+
entry={entry}
|
|
1064
|
+
value={value}
|
|
1065
|
+
onSelect={onSelect}
|
|
1066
|
+
previewSource={previewSource}
|
|
1067
|
+
/>
|
|
900
1068
|
))}
|
|
901
1069
|
</div>
|
|
902
1070
|
</div>
|
|
903
1071
|
)}
|
|
904
1072
|
|
|
905
|
-
{
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
<
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
1073
|
+
{grouped ? (
|
|
1074
|
+
<>
|
|
1075
|
+
{blockTypeEntries.length > 0 && (
|
|
1076
|
+
<div className="squisq-template-gallery-section">
|
|
1077
|
+
<h3 className="squisq-template-gallery-section-title">Block Types</h3>
|
|
1078
|
+
<div className="squisq-template-gallery-grid">
|
|
1079
|
+
{blockTypeEntries.map((entry) => (
|
|
1080
|
+
<TemplateCard
|
|
1081
|
+
key={entry.name}
|
|
1082
|
+
entry={entry}
|
|
1083
|
+
value={value}
|
|
1084
|
+
onSelect={onSelect}
|
|
1085
|
+
previewSource={previewSource}
|
|
1086
|
+
/>
|
|
1087
|
+
))}
|
|
1088
|
+
</div>
|
|
1089
|
+
</div>
|
|
1090
|
+
)}
|
|
1091
|
+
|
|
1092
|
+
{contentNeededEntries.length > 0 && (
|
|
1093
|
+
<div className="squisq-template-gallery-section">
|
|
1094
|
+
<h3 className="squisq-template-gallery-section-title">Block Types for Content</h3>
|
|
1095
|
+
<div className="squisq-template-gallery-grid">
|
|
1096
|
+
{contentNeededEntries.map((entry) => (
|
|
1097
|
+
<TemplateCard
|
|
1098
|
+
key={entry.name}
|
|
1099
|
+
entry={entry}
|
|
1100
|
+
value={value}
|
|
1101
|
+
onSelect={onSelect}
|
|
1102
|
+
previewSource={previewSource}
|
|
1103
|
+
/>
|
|
1104
|
+
))}
|
|
1105
|
+
</div>
|
|
1106
|
+
</div>
|
|
1107
|
+
)}
|
|
1108
|
+
</>
|
|
914
1109
|
) : (
|
|
915
1110
|
<div className="squisq-template-gallery-grid">
|
|
916
|
-
{
|
|
917
|
-
<TemplateCard
|
|
1111
|
+
{blockTypeEntries.map((entry) => (
|
|
1112
|
+
<TemplateCard
|
|
1113
|
+
key={entry.name}
|
|
1114
|
+
entry={entry}
|
|
1115
|
+
value={value}
|
|
1116
|
+
onSelect={onSelect}
|
|
1117
|
+
previewSource={previewSource}
|
|
1118
|
+
/>
|
|
918
1119
|
))}
|
|
919
1120
|
</div>
|
|
920
1121
|
)}
|
|
@@ -957,7 +1158,7 @@ function CustomTemplateCard({
|
|
|
957
1158
|
<div className="squisq-template-gallery-card-body">
|
|
958
1159
|
<span className="squisq-template-gallery-card-name">{def.label}</span>
|
|
959
1160
|
<span className="squisq-template-gallery-card-desc">
|
|
960
|
-
{def.description ?? 'Custom
|
|
1161
|
+
{def.description ?? 'Custom block type'}
|
|
961
1162
|
</span>
|
|
962
1163
|
</div>
|
|
963
1164
|
</button>
|
|
@@ -973,6 +1174,8 @@ export interface TemplateBadgePopoverProps {
|
|
|
973
1174
|
value: string;
|
|
974
1175
|
onChange: (name: string) => void;
|
|
975
1176
|
onClose: () => void;
|
|
1177
|
+
/** Active editor chrome theme, used by the portaled dialog. */
|
|
1178
|
+
colorScheme?: 'light' | 'dark';
|
|
976
1179
|
/** Optional list of template names to surface as "Recommended for this block". */
|
|
977
1180
|
recommended?: readonly string[];
|
|
978
1181
|
/**
|
|
@@ -981,27 +1184,35 @@ export interface TemplateBadgePopoverProps {
|
|
|
981
1184
|
* it closes the popover and opens the designer.
|
|
982
1185
|
*/
|
|
983
1186
|
onOpenDesigner?: () => void;
|
|
1187
|
+
/**
|
|
1188
|
+
* Active block content used to render live template thumbnails. Falls back to
|
|
1189
|
+
* static icons when a candidate cannot be derived.
|
|
1190
|
+
*/
|
|
1191
|
+
previewSource?: TemplatePreviewSource;
|
|
984
1192
|
}
|
|
985
1193
|
|
|
986
1194
|
/**
|
|
987
|
-
* Standalone
|
|
988
|
-
*
|
|
989
|
-
* `.squisq-template-badge` span). Handles its own positioning, outside
|
|
990
|
-
* clicks, Escape, and viewport-edge clamping.
|
|
1195
|
+
* Standalone dialog that mirrors the toolbar `TemplatePicker`'s gallery
|
|
1196
|
+
* when opened from an inline heading badge.
|
|
991
1197
|
*/
|
|
992
1198
|
export function TemplateBadgePopover({
|
|
993
1199
|
anchorRect,
|
|
994
1200
|
value,
|
|
995
1201
|
onChange,
|
|
996
1202
|
onClose,
|
|
1203
|
+
colorScheme = 'light',
|
|
997
1204
|
recommended,
|
|
998
1205
|
onOpenDesigner,
|
|
1206
|
+
previewSource,
|
|
999
1207
|
}: TemplateBadgePopoverProps) {
|
|
1000
|
-
const [style, setStyle] = useState<React.CSSProperties>(() =>
|
|
1208
|
+
const [style, setStyle] = useState<React.CSSProperties>(() => computeDialogStyle(anchorRect));
|
|
1209
|
+
const dialogRef = useRef<HTMLDivElement>(null);
|
|
1210
|
+
const dialogId = `${TEMPLATE_GALLERY_DIALOG_ID}-${useId().replace(/:/g, '')}`;
|
|
1001
1211
|
|
|
1002
|
-
// Reposition once after mount
|
|
1212
|
+
// Reposition once after mount so the dialog covers the current editor shell.
|
|
1003
1213
|
useEffect(() => {
|
|
1004
|
-
requestAnimationFrame(() => setStyle(
|
|
1214
|
+
const frame = requestAnimationFrame(() => setStyle(computeDialogStyle(anchorRect)));
|
|
1215
|
+
return () => cancelAnimationFrame(frame);
|
|
1005
1216
|
}, [anchorRect]);
|
|
1006
1217
|
|
|
1007
1218
|
// Outside click + Escape close
|
|
@@ -1011,8 +1222,8 @@ export function TemplateBadgePopover({
|
|
|
1011
1222
|
};
|
|
1012
1223
|
const onMouse = (e: MouseEvent) => {
|
|
1013
1224
|
const target = e.target as Node;
|
|
1014
|
-
const
|
|
1015
|
-
if (!
|
|
1225
|
+
const inDialog = dialogRef.current?.contains(target);
|
|
1226
|
+
if (!inDialog) onClose();
|
|
1016
1227
|
};
|
|
1017
1228
|
// Defer the mousedown listener by one frame so the click that opened
|
|
1018
1229
|
// us doesn't immediately close us.
|
|
@@ -1032,62 +1243,72 @@ export function TemplateBadgePopover({
|
|
|
1032
1243
|
onClose();
|
|
1033
1244
|
};
|
|
1034
1245
|
|
|
1246
|
+
const dialogTitle = templateDialogTitle(previewSource);
|
|
1247
|
+
|
|
1035
1248
|
return createPortal(
|
|
1036
|
-
<
|
|
1037
|
-
|
|
1038
|
-
|
|
1249
|
+
<TemplateGalleryDialog
|
|
1250
|
+
dialogRef={dialogRef}
|
|
1251
|
+
dialogId={dialogId}
|
|
1252
|
+
title={dialogTitle}
|
|
1253
|
+
colorScheme={colorScheme}
|
|
1039
1254
|
style={style}
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1255
|
+
onClose={onClose}
|
|
1256
|
+
>
|
|
1257
|
+
<TemplateGalleryBody
|
|
1258
|
+
value={value}
|
|
1259
|
+
onSelect={handleSelect}
|
|
1260
|
+
style={{}}
|
|
1261
|
+
recommended={recommended}
|
|
1262
|
+
onOpenDesigner={onOpenDesigner}
|
|
1263
|
+
previewSource={previewSource}
|
|
1264
|
+
/>
|
|
1265
|
+
</TemplateGalleryDialog>,
|
|
1043
1266
|
document.body,
|
|
1044
1267
|
);
|
|
1045
1268
|
}
|
|
1046
1269
|
|
|
1047
|
-
function
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
// `firstElementChild` returns the "(none)" option, which is ~50px tall
|
|
1051
|
-
// and made the "fits below" check incorrectly pass on tall galleries.
|
|
1052
|
-
const popoverEl = document.getElementById('squisq-template-gallery-portal');
|
|
1053
|
-
const popoverRect = popoverEl?.getBoundingClientRect();
|
|
1054
|
-
const popoverWidth = popoverRect?.width ?? 780;
|
|
1055
|
-
const popoverHeight = popoverRect?.height ?? 520;
|
|
1056
|
-
const margin = 8;
|
|
1057
|
-
const gap = 6;
|
|
1058
|
-
const vw = window.innerWidth;
|
|
1059
|
-
const vh = window.innerHeight;
|
|
1270
|
+
function templateDialogTitle(previewSource?: TemplatePreviewSource): string {
|
|
1271
|
+
return `Block Type for ${templateDialogBlockTitle(previewSource)}`;
|
|
1272
|
+
}
|
|
1060
1273
|
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
const
|
|
1066
|
-
const
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
left = Math.min(Math.max(margin, rect.left), maxLeft);
|
|
1077
|
-
} else {
|
|
1078
|
-
// Center it.
|
|
1079
|
-
top = Math.max(margin, Math.floor((vh - popoverHeight) / 2));
|
|
1080
|
-
left = Math.max(margin, Math.floor((vw - popoverWidth) / 2));
|
|
1081
|
-
}
|
|
1274
|
+
function templateDialogBlockTitle(previewSource?: TemplatePreviewSource): string {
|
|
1275
|
+
const block = previewSource?.block;
|
|
1276
|
+
if (!block) return 'this block';
|
|
1277
|
+
|
|
1278
|
+
const headingText = block.sourceHeading ? extractPlainText(block.sourceHeading).trim() : '';
|
|
1279
|
+
const title = (headingText || block.title || '').replace(/\s+/g, ' ').trim();
|
|
1280
|
+
return title || 'this block';
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
function computeDialogStyle(anchor: Element | DOMRect): React.CSSProperties {
|
|
1284
|
+
const shellRect = findEditorShellRect(anchor);
|
|
1285
|
+
const left = Math.max(0, shellRect.left);
|
|
1286
|
+
const top = Math.max(0, shellRect.top);
|
|
1287
|
+
const right = Math.min(window.innerWidth, shellRect.right);
|
|
1288
|
+
const bottom = Math.min(window.innerHeight, shellRect.bottom);
|
|
1082
1289
|
|
|
1083
1290
|
return {
|
|
1084
1291
|
position: 'fixed',
|
|
1085
1292
|
top,
|
|
1086
1293
|
left,
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
maxHeight: `${vh - 2 * margin}px`,
|
|
1090
|
-
overflowY: 'auto',
|
|
1294
|
+
width: Math.max(0, right - left),
|
|
1295
|
+
height: Math.max(0, bottom - top),
|
|
1091
1296
|
zIndex: 9999,
|
|
1092
1297
|
};
|
|
1093
1298
|
}
|
|
1299
|
+
|
|
1300
|
+
function findEditorShellRect(anchor: Element | DOMRect): DOMRect {
|
|
1301
|
+
if (anchor instanceof Element) {
|
|
1302
|
+
const shell = anchor.closest('.squisq-editor-shell');
|
|
1303
|
+
if (shell) return shell.getBoundingClientRect();
|
|
1304
|
+
} else {
|
|
1305
|
+
const x = anchor.left + anchor.width / 2;
|
|
1306
|
+
const y = anchor.top + anchor.height / 2;
|
|
1307
|
+
for (const element of document.elementsFromPoint(x, y)) {
|
|
1308
|
+
const shell = element.closest('.squisq-editor-shell');
|
|
1309
|
+
if (shell) return shell.getBoundingClientRect();
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
return new DOMRect(0, 0, window.innerWidth, window.innerHeight);
|
|
1314
|
+
}
|