@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/PreviewControls.tsx
CHANGED
|
@@ -62,6 +62,10 @@ export interface PreviewSettings {
|
|
|
62
62
|
* that style. The single entry point so the toggle buttons persist in one
|
|
63
63
|
* frontmatter write. */
|
|
64
64
|
setCaptionMode: (mode: CaptionMode) => void;
|
|
65
|
+
/** Whether Squisq should synthesize and show its managed cover slide. */
|
|
66
|
+
activeCoverSlide: boolean;
|
|
67
|
+
/** Enable/disable the managed cover slide. */
|
|
68
|
+
setCoverSlideEnabled: (enabled: boolean) => void;
|
|
65
69
|
/** User-authored themes (doc + browser library) for the picker's "Custom" group. */
|
|
66
70
|
customThemes: Theme[];
|
|
67
71
|
/** Open the custom-theme designer for a theme (or null to create a new one). */
|
|
@@ -171,6 +175,15 @@ function resolveFrontmatterCaptionMode(value: unknown): CaptionMode | null {
|
|
|
171
175
|
return null;
|
|
172
176
|
}
|
|
173
177
|
|
|
178
|
+
function resolveFrontmatterBoolean(value: unknown): boolean | null {
|
|
179
|
+
if (typeof value === 'boolean') return value;
|
|
180
|
+
if (typeof value !== 'string') return null;
|
|
181
|
+
const v = value.trim().toLowerCase();
|
|
182
|
+
if (v === 'true' || v === 'yes' || v === 'on' || v === 'show' || v === 'visible') return true;
|
|
183
|
+
if (v === 'false' || v === 'no' || v === 'off' || v === 'hide' || v === 'hidden') return false;
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
174
187
|
// ── Provider ─────────────────────────────────────────────────────
|
|
175
188
|
|
|
176
189
|
export interface PreviewSettingsProviderProps {
|
|
@@ -192,6 +205,7 @@ const FM_KEYS = {
|
|
|
192
205
|
theme: { canonical: 'squisq-theme', legacy: 'theme' as const },
|
|
193
206
|
transform: { canonical: 'squisq-transform', legacy: 'transform-style' as const },
|
|
194
207
|
captions: { canonical: 'squisq-captions', legacy: 'caption-style' as const },
|
|
208
|
+
coverSlide: { canonical: 'squisq-cover-slide', legacy: 'cover-slide' as const },
|
|
195
209
|
} as const;
|
|
196
210
|
|
|
197
211
|
function readFrontmatterKey(
|
|
@@ -366,6 +380,26 @@ export function PreviewSettingsProvider({
|
|
|
366
380
|
[persistFrontmatter],
|
|
367
381
|
);
|
|
368
382
|
|
|
383
|
+
// Managed cover slide — generated from the document startBlock. Defaults on
|
|
384
|
+
// for existing documents; authors can persist an explicit off switch.
|
|
385
|
+
const fmCoverSlide = useMemo(
|
|
386
|
+
() =>
|
|
387
|
+
resolveFrontmatterBoolean(
|
|
388
|
+
readFrontmatterKey(frontmatter, FM_KEYS.coverSlide.canonical, FM_KEYS.coverSlide.legacy),
|
|
389
|
+
),
|
|
390
|
+
[frontmatter],
|
|
391
|
+
);
|
|
392
|
+
const [selectedCoverSlide, setSelectedCoverSlide] = useState<boolean | null>(null);
|
|
393
|
+
useEffect(() => setSelectedCoverSlide(null), [fmCoverSlide]);
|
|
394
|
+
const activeCoverSlide = selectedCoverSlide ?? fmCoverSlide ?? true;
|
|
395
|
+
const handleSetCoverSlideEnabled = useCallback(
|
|
396
|
+
(enabled: boolean) => {
|
|
397
|
+
setSelectedCoverSlide(enabled);
|
|
398
|
+
persistFrontmatter({ [FM_KEYS.coverSlide.canonical]: enabled ? 'true' : 'false' });
|
|
399
|
+
},
|
|
400
|
+
[persistFrontmatter],
|
|
401
|
+
);
|
|
402
|
+
|
|
369
403
|
// Config for the docked designer (rendered by `<ThemeDesignerDock>` in the
|
|
370
404
|
// editor content row). Null when closed. setPreviewTheme is a stable setter.
|
|
371
405
|
const themeDesigner = useMemo<ThemeDesignerConfig | null>(
|
|
@@ -396,6 +430,8 @@ export function PreviewSettingsProvider({
|
|
|
396
430
|
activeCaptionStyle,
|
|
397
431
|
activeCaptionsEnabled,
|
|
398
432
|
setCaptionMode: handleSetCaptionMode,
|
|
433
|
+
activeCoverSlide,
|
|
434
|
+
setCoverSlideEnabled: handleSetCoverSlideEnabled,
|
|
399
435
|
customThemes,
|
|
400
436
|
openThemeDesigner,
|
|
401
437
|
deleteCustomTheme,
|
|
@@ -410,9 +446,11 @@ export function PreviewSettingsProvider({
|
|
|
410
446
|
activeTransformStyle,
|
|
411
447
|
activeCaptionStyle,
|
|
412
448
|
activeCaptionsEnabled,
|
|
449
|
+
activeCoverSlide,
|
|
413
450
|
handleSetThemeId,
|
|
414
451
|
handleSetTransformStyle,
|
|
415
452
|
handleSetCaptionMode,
|
|
453
|
+
handleSetCoverSlideEnabled,
|
|
416
454
|
customThemes,
|
|
417
455
|
openThemeDesigner,
|
|
418
456
|
deleteCustomTheme,
|
|
@@ -473,15 +511,28 @@ const TRANSFORM_STYLE_OPTIONS = [
|
|
|
473
511
|
/**
|
|
474
512
|
* Left-to-right priority order for the preview controls. As the toolbar
|
|
475
513
|
* narrows, controls drop into the overflow menu from the END of this list
|
|
476
|
-
* first (
|
|
477
|
-
*
|
|
478
|
-
*
|
|
479
|
-
* Display mode and aspect ratio are not here — they're surfaced separately as
|
|
480
|
-
* the segmented {@link PreviewModeSwitch} / {@link PreviewFormatSwitch} on the
|
|
481
|
-
* left of the toolbar.
|
|
514
|
+
* first (Cover, then Captions, …). Display mode and aspect ratio stay inline
|
|
515
|
+
* the longest, but still collapse into the same menu when the toolbar is very
|
|
516
|
+
* constrained.
|
|
482
517
|
*/
|
|
483
|
-
type ControlKey = 'theme' | 'transform' | 'captions';
|
|
484
|
-
const CONTROL_KEYS: ControlKey[] = ['theme', 'transform', 'captions'];
|
|
518
|
+
type ControlKey = 'mode' | 'format' | 'theme' | 'transform' | 'captions' | 'cover';
|
|
519
|
+
const CONTROL_KEYS: ControlKey[] = ['mode', 'format', 'theme', 'transform', 'captions', 'cover'];
|
|
520
|
+
|
|
521
|
+
const PREVIEW_POPOVER_GAP = 4;
|
|
522
|
+
const PREVIEW_POPOVER_MARGIN = 8;
|
|
523
|
+
const PREVIEW_POPOVER_FALLBACK_WIDTH = 220;
|
|
524
|
+
|
|
525
|
+
function clampPreviewPopoverLeft(
|
|
526
|
+
triggerRect: DOMRect,
|
|
527
|
+
popoverWidth: number,
|
|
528
|
+
viewportWidth: number,
|
|
529
|
+
): number {
|
|
530
|
+
const maxLeft = Math.max(
|
|
531
|
+
PREVIEW_POPOVER_MARGIN,
|
|
532
|
+
viewportWidth - popoverWidth - PREVIEW_POPOVER_MARGIN,
|
|
533
|
+
);
|
|
534
|
+
return Math.min(Math.max(PREVIEW_POPOVER_MARGIN, triggerRect.right - popoverWidth), maxLeft);
|
|
535
|
+
}
|
|
485
536
|
|
|
486
537
|
// ── Shared styles ────────────────────────────────────────────────
|
|
487
538
|
|
|
@@ -510,7 +561,7 @@ const selectStyle: React.CSSProperties = {
|
|
|
510
561
|
* whole row in and out at a fixed window-width breakpoint, the controls
|
|
511
562
|
* measure how many of them actually fit in the width the toolbar gives them
|
|
512
563
|
* and keep that many inline, folding the rest — from the low-priority end of
|
|
513
|
-
* {@link CONTROL_KEYS} — into a single
|
|
564
|
+
* {@link CONTROL_KEYS} — into a single ellipsis button's popover. As
|
|
514
565
|
* the toolbar widens or narrows, controls migrate one at a time between the
|
|
515
566
|
* inline row and the menu, so the available space is always well used and the
|
|
516
567
|
* row never wraps onto a second line.
|
|
@@ -526,6 +577,27 @@ export function PreviewToolbarControls() {
|
|
|
526
577
|
// inline and overflow is computed from these per-control measurements.
|
|
527
578
|
const probeRef = useRef<HTMLDivElement>(null);
|
|
528
579
|
const popoverRef = useRef<HTMLDivElement>(null);
|
|
580
|
+
const popoverTriggerRef = useRef<HTMLButtonElement>(null);
|
|
581
|
+
const popoverPanelRef = useRef<HTMLDivElement>(null);
|
|
582
|
+
const [popoverAnchor, setPopoverAnchor] = useState<{ top: number; left: number } | null>(null);
|
|
583
|
+
|
|
584
|
+
const updatePopoverPosition = useCallback(() => {
|
|
585
|
+
const trigger = popoverTriggerRef.current;
|
|
586
|
+
if (!trigger) return;
|
|
587
|
+
const triggerRect = trigger.getBoundingClientRect();
|
|
588
|
+
const measuredWidth =
|
|
589
|
+
popoverPanelRef.current?.getBoundingClientRect().width ?? PREVIEW_POPOVER_FALLBACK_WIDTH;
|
|
590
|
+
const popoverWidth = Math.min(measuredWidth, window.innerWidth - PREVIEW_POPOVER_MARGIN * 2);
|
|
591
|
+
setPopoverAnchor({
|
|
592
|
+
top: triggerRect.bottom + PREVIEW_POPOVER_GAP,
|
|
593
|
+
left: clampPreviewPopoverLeft(triggerRect, popoverWidth, window.innerWidth),
|
|
594
|
+
});
|
|
595
|
+
}, []);
|
|
596
|
+
|
|
597
|
+
const closePopover = useCallback(() => {
|
|
598
|
+
setPopoverOpen(false);
|
|
599
|
+
setPopoverAnchor(null);
|
|
600
|
+
}, []);
|
|
529
601
|
|
|
530
602
|
// Fit detection: keep as many controls inline as fit, overflow the rest.
|
|
531
603
|
useLayoutEffect(() => {
|
|
@@ -534,7 +606,7 @@ export function PreviewToolbarControls() {
|
|
|
534
606
|
if (!root || !probe) return;
|
|
535
607
|
const GAP = 6; // matches the row's flex `gap`
|
|
536
608
|
const LEAD_PAD = 9; // root's left padding, eaten before any control
|
|
537
|
-
const
|
|
609
|
+
const OVERFLOW_TRIGGER_RESERVE = 40; // width kept for the ellipsis button (+ its gap)
|
|
538
610
|
const SAFETY = 2;
|
|
539
611
|
const measure = () => {
|
|
540
612
|
const available = root.clientWidth - LEAD_PAD;
|
|
@@ -549,8 +621,8 @@ export function PreviewToolbarControls() {
|
|
|
549
621
|
setVisibleCount(widths.length);
|
|
550
622
|
return;
|
|
551
623
|
}
|
|
552
|
-
// Otherwise reserve room for the
|
|
553
|
-
const budget = available -
|
|
624
|
+
// Otherwise reserve room for the ellipsis and fit as many as possible.
|
|
625
|
+
const budget = available - OVERFLOW_TRIGGER_RESERVE - GAP - SAFETY;
|
|
554
626
|
let count = 0;
|
|
555
627
|
while (count < widths.length && rowWidth(count + 1) <= budget) count++;
|
|
556
628
|
setVisibleCount(count);
|
|
@@ -568,13 +640,29 @@ export function PreviewToolbarControls() {
|
|
|
568
640
|
useEffect(() => {
|
|
569
641
|
if (!popoverOpen) return;
|
|
570
642
|
const handler = (e: MouseEvent) => {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
643
|
+
const target = e.target as Node;
|
|
644
|
+
if (popoverRef.current?.contains(target)) return;
|
|
645
|
+
if (popoverPanelRef.current?.contains(target)) return;
|
|
646
|
+
closePopover();
|
|
574
647
|
};
|
|
575
648
|
document.addEventListener('mousedown', handler);
|
|
576
649
|
return () => document.removeEventListener('mousedown', handler);
|
|
577
|
-
}, [popoverOpen]);
|
|
650
|
+
}, [closePopover, popoverOpen]);
|
|
651
|
+
|
|
652
|
+
useLayoutEffect(() => {
|
|
653
|
+
if (!popoverOpen) return;
|
|
654
|
+
updatePopoverPosition();
|
|
655
|
+
}, [popoverOpen, updatePopoverPosition, visibleCount]);
|
|
656
|
+
|
|
657
|
+
useEffect(() => {
|
|
658
|
+
if (!popoverOpen) return;
|
|
659
|
+
window.addEventListener('resize', updatePopoverPosition);
|
|
660
|
+
window.addEventListener('scroll', updatePopoverPosition, true);
|
|
661
|
+
return () => {
|
|
662
|
+
window.removeEventListener('resize', updatePopoverPosition);
|
|
663
|
+
window.removeEventListener('scroll', updatePopoverPosition, true);
|
|
664
|
+
};
|
|
665
|
+
}, [popoverOpen, updatePopoverPosition]);
|
|
578
666
|
|
|
579
667
|
// "Edit theme" pencil next to the dropdown. A custom theme opens directly in
|
|
580
668
|
// the designer; a built-in seeds a NEW "Modified <name>" custom theme based
|
|
@@ -593,6 +681,26 @@ export function PreviewToolbarControls() {
|
|
|
593
681
|
// label-over-control layout used inside the overflow popover.
|
|
594
682
|
const renderControl = (key: ControlKey, compact: boolean): ReactNode => {
|
|
595
683
|
switch (key) {
|
|
684
|
+
case 'mode':
|
|
685
|
+
return (
|
|
686
|
+
<div
|
|
687
|
+
key="mode"
|
|
688
|
+
className={`squisq-preview-control squisq-preview-control--seg${compact ? ' squisq-preview-control--compact' : ''}`}
|
|
689
|
+
>
|
|
690
|
+
{compact && <label style={labelStyle}>Mode:</label>}
|
|
691
|
+
<PreviewModeSwitch />
|
|
692
|
+
</div>
|
|
693
|
+
);
|
|
694
|
+
case 'format':
|
|
695
|
+
return (
|
|
696
|
+
<div
|
|
697
|
+
key="format"
|
|
698
|
+
className={`squisq-preview-control squisq-preview-control--seg${compact ? ' squisq-preview-control--compact' : ''}`}
|
|
699
|
+
>
|
|
700
|
+
{compact && <label style={labelStyle}>Format:</label>}
|
|
701
|
+
<PreviewFormatSwitch />
|
|
702
|
+
</div>
|
|
703
|
+
);
|
|
596
704
|
case 'theme':
|
|
597
705
|
return (
|
|
598
706
|
<div
|
|
@@ -684,6 +792,23 @@ export function PreviewToolbarControls() {
|
|
|
684
792
|
</div>
|
|
685
793
|
);
|
|
686
794
|
}
|
|
795
|
+
case 'cover':
|
|
796
|
+
return (
|
|
797
|
+
<div
|
|
798
|
+
key="cover"
|
|
799
|
+
className={`squisq-preview-control${compact ? ' squisq-preview-control--compact' : ''}`}
|
|
800
|
+
>
|
|
801
|
+
<label style={labelStyle}>Cover:</label>
|
|
802
|
+
<label className="squisq-preview-checkbox">
|
|
803
|
+
<input
|
|
804
|
+
type="checkbox"
|
|
805
|
+
checked={s.activeCoverSlide}
|
|
806
|
+
onChange={(e) => s.setCoverSlideEnabled(e.target.checked)}
|
|
807
|
+
/>
|
|
808
|
+
<span>Cover slide</span>
|
|
809
|
+
</label>
|
|
810
|
+
</div>
|
|
811
|
+
);
|
|
687
812
|
}
|
|
688
813
|
};
|
|
689
814
|
|
|
@@ -691,10 +816,18 @@ export function PreviewToolbarControls() {
|
|
|
691
816
|
const visibleKeys = CONTROL_KEYS.slice(0, visibleCount);
|
|
692
817
|
const overflowKeys = CONTROL_KEYS.slice(visibleCount);
|
|
693
818
|
|
|
819
|
+
useEffect(() => {
|
|
820
|
+
if (!hasOverflow && popoverOpen) closePopover();
|
|
821
|
+
}, [closePopover, hasOverflow, popoverOpen]);
|
|
822
|
+
|
|
694
823
|
// The root is a flex:1 filler so it always spans the toolbar's leftover
|
|
695
824
|
// width (which is what the fit measurement reads).
|
|
696
825
|
return (
|
|
697
|
-
<div
|
|
826
|
+
<div
|
|
827
|
+
className="squisq-preview-controls"
|
|
828
|
+
data-has-overflow={hasOverflow ? 'true' : undefined}
|
|
829
|
+
ref={rootRef}
|
|
830
|
+
>
|
|
698
831
|
{/* Hidden probe — every control at natural width, measured to decide the
|
|
699
832
|
inline/overflow split. Absolutely positioned so it never affects
|
|
700
833
|
layout. */}
|
|
@@ -702,35 +835,37 @@ export function PreviewToolbarControls() {
|
|
|
702
835
|
{CONTROL_KEYS.map((key) => renderControl(key, false))}
|
|
703
836
|
</div>
|
|
704
837
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
838
|
+
{visibleKeys.length > 0 && (
|
|
839
|
+
<div className="squisq-preview-controls-inline">
|
|
840
|
+
{visibleKeys.map((key) => renderControl(key, false))}
|
|
841
|
+
</div>
|
|
842
|
+
)}
|
|
708
843
|
|
|
709
844
|
{hasOverflow && (
|
|
710
845
|
<div className="squisq-preview-controls-compact" ref={popoverRef}>
|
|
711
846
|
<button
|
|
847
|
+
ref={popoverTriggerRef}
|
|
712
848
|
className={`squisq-toolbar-button${popoverOpen ? ' squisq-toolbar-button--active' : ''}`}
|
|
713
|
-
onClick={() =>
|
|
849
|
+
onClick={() => {
|
|
850
|
+
if (popoverOpen) {
|
|
851
|
+
closePopover();
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
updatePopoverPosition();
|
|
855
|
+
setPopoverOpen(true);
|
|
856
|
+
}}
|
|
714
857
|
aria-label="More preview settings"
|
|
715
858
|
title="More preview settings"
|
|
716
859
|
aria-expanded={popoverOpen}
|
|
717
860
|
>
|
|
718
|
-
<
|
|
719
|
-
width="16"
|
|
720
|
-
height="16"
|
|
721
|
-
viewBox="0 0 16 16"
|
|
722
|
-
fill="none"
|
|
723
|
-
stroke="currentColor"
|
|
724
|
-
strokeWidth="1.5"
|
|
725
|
-
strokeLinecap="round"
|
|
726
|
-
strokeLinejoin="round"
|
|
727
|
-
>
|
|
728
|
-
<circle cx="8" cy="8" r="2.5" />
|
|
729
|
-
<path d="M13.5 8a5.5 5.5 0 01-.4 1.8l1.2 1.2-1.6 1.6-1.2-1.2A5.5 5.5 0 018 13.5a5.5 5.5 0 01-3.5-1.3L3.3 13.4 1.7 11.8l1.2-1.2A5.5 5.5 0 012.5 8c0-.6.1-1.2.4-1.8L1.7 5 3.3 3.4l1.2 1.2A5.5 5.5 0 018 2.5c1.3 0 2.5.5 3.5 1.3l1.2-1.2 1.6 1.6-1.2 1.2c.3.6.4 1.2.4 1.6z" />
|
|
730
|
-
</svg>
|
|
861
|
+
<Icon icon="fa-solid fa-ellipsis" />
|
|
731
862
|
</button>
|
|
732
|
-
{popoverOpen && (
|
|
733
|
-
<div
|
|
863
|
+
{popoverOpen && popoverAnchor && (
|
|
864
|
+
<div
|
|
865
|
+
ref={popoverPanelRef}
|
|
866
|
+
className="squisq-preview-controls-popover"
|
|
867
|
+
style={{ top: popoverAnchor.top, left: popoverAnchor.left }}
|
|
868
|
+
>
|
|
734
869
|
{overflowKeys.map((key) => renderControl(key, true))}
|
|
735
870
|
</div>
|
|
736
871
|
)}
|
|
@@ -741,10 +876,9 @@ export function PreviewToolbarControls() {
|
|
|
741
876
|
}
|
|
742
877
|
|
|
743
878
|
/**
|
|
744
|
-
* Segmented display-mode switch (Video / Slideshow / Document / Page)
|
|
745
|
-
*
|
|
746
|
-
*
|
|
747
|
-
* `activeDisplayMode` in preview settings.
|
|
879
|
+
* Segmented display-mode switch (Video / Slideshow / Document / Page), used
|
|
880
|
+
* inline in the Use toolbar and inside its overflow popover. Reads and writes
|
|
881
|
+
* the same `activeDisplayMode` in preview settings.
|
|
748
882
|
*/
|
|
749
883
|
export function PreviewModeSwitch() {
|
|
750
884
|
const s = usePreviewSettings();
|
|
@@ -787,10 +921,9 @@ function AspectIcon({ w, h }: { w: number; h: number }) {
|
|
|
787
921
|
}
|
|
788
922
|
|
|
789
923
|
/**
|
|
790
|
-
* Segmented aspect-ratio switch (16:9 / 1:1 / 9:16 / 4:3)
|
|
791
|
-
*
|
|
792
|
-
*
|
|
793
|
-
* same `activePreset` in preview settings.
|
|
924
|
+
* Segmented aspect-ratio switch (16:9 / 1:1 / 9:16 / 4:3), used inline in the
|
|
925
|
+
* Use toolbar and inside its overflow popover. Reads and writes the same
|
|
926
|
+
* `activePreset` in preview settings.
|
|
794
927
|
*/
|
|
795
928
|
export function PreviewFormatSwitch() {
|
|
796
929
|
const s = usePreviewSettings();
|
package/src/PreviewPanel.tsx
CHANGED
|
@@ -49,6 +49,7 @@ export function PreviewPanel({ basePath = '/', className, workspaceContainer }:
|
|
|
49
49
|
activeTransformStyle,
|
|
50
50
|
activeCaptionStyle,
|
|
51
51
|
activeCaptionsEnabled,
|
|
52
|
+
activeCoverSlide,
|
|
52
53
|
} = usePreviewSettings();
|
|
53
54
|
|
|
54
55
|
// Build the player-ready Doc whenever the parsed doc changes.
|
|
@@ -178,6 +179,7 @@ export function PreviewPanel({ basePath = '/', className, workspaceContainer }:
|
|
|
178
179
|
theme={activeTheme}
|
|
179
180
|
captionStyle={activeCaptionStyle}
|
|
180
181
|
captionsEnabled={activeCaptionsEnabled}
|
|
182
|
+
showCoverSlide={activeCoverSlide}
|
|
181
183
|
/>
|
|
182
184
|
)}
|
|
183
185
|
</div>
|
|
@@ -63,10 +63,10 @@ export const HeadingWithTemplate = Heading.extend({
|
|
|
63
63
|
(HTMLAttributes['data-template-params'] as string | undefined) ?? null,
|
|
64
64
|
);
|
|
65
65
|
|
|
66
|
-
// Render heading with a trailing badge span. The badge
|
|
67
|
-
// content —
|
|
68
|
-
//
|
|
69
|
-
//
|
|
66
|
+
// Render heading with a trailing badge span. The badge and its inner core
|
|
67
|
+
// have no real text content — labels/braces are painted via CSS so the
|
|
68
|
+
// template name never becomes part of serialized heading text (which
|
|
69
|
+
// would leak into markdown on round-trip).
|
|
70
70
|
//
|
|
71
71
|
// When no template is set we still render a subtle "empty" badge so
|
|
72
72
|
// authors have a visible affordance for opening the template picker
|
|
@@ -86,10 +86,17 @@ export const HeadingWithTemplate = Heading.extend({
|
|
|
86
86
|
role: 'button',
|
|
87
87
|
tabindex: '0',
|
|
88
88
|
'aria-haspopup': 'listbox',
|
|
89
|
-
title: 'Change block
|
|
89
|
+
title: 'Change block type',
|
|
90
90
|
'data-template': templateName,
|
|
91
|
-
'data-template-label': templateLabel(templateName),
|
|
92
91
|
},
|
|
92
|
+
[
|
|
93
|
+
'span',
|
|
94
|
+
{
|
|
95
|
+
class: 'squisq-template-badge-core',
|
|
96
|
+
'data-template-label': templateLabel(templateName),
|
|
97
|
+
'aria-hidden': 'true',
|
|
98
|
+
},
|
|
99
|
+
],
|
|
93
100
|
],
|
|
94
101
|
propsBadgeSpec(propsSummary),
|
|
95
102
|
];
|
|
@@ -107,8 +114,16 @@ export const HeadingWithTemplate = Heading.extend({
|
|
|
107
114
|
role: 'button',
|
|
108
115
|
tabindex: '0',
|
|
109
116
|
'aria-haspopup': 'listbox',
|
|
110
|
-
title: 'Choose block
|
|
117
|
+
title: 'Choose block type',
|
|
111
118
|
},
|
|
119
|
+
[
|
|
120
|
+
'span',
|
|
121
|
+
{
|
|
122
|
+
class: 'squisq-template-badge-core',
|
|
123
|
+
'data-template-label': 'Block',
|
|
124
|
+
'aria-hidden': 'true',
|
|
125
|
+
},
|
|
126
|
+
],
|
|
112
127
|
],
|
|
113
128
|
propsBadgeSpec(propsSummary),
|
|
114
129
|
];
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { BlockRenderer, MediaContext } from '@bendyline/squisq-react';
|
|
3
|
+
import {
|
|
4
|
+
resolveTemplateContentPreviewResult,
|
|
5
|
+
type TemplatePreviewSource,
|
|
6
|
+
} from './templateContentPreviewResolver';
|
|
7
|
+
|
|
8
|
+
export interface TemplateContentPreviewProps {
|
|
9
|
+
templateName: string;
|
|
10
|
+
source?: TemplatePreviewSource;
|
|
11
|
+
fallback: JSX.Element;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function TemplateContentPreview({
|
|
15
|
+
templateName,
|
|
16
|
+
source,
|
|
17
|
+
fallback,
|
|
18
|
+
}: TemplateContentPreviewProps) {
|
|
19
|
+
const preview = useMemo(
|
|
20
|
+
() => (source ? resolveTemplateContentPreviewResult(templateName, source) : null),
|
|
21
|
+
[templateName, source],
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
if (!source) return fallback;
|
|
25
|
+
|
|
26
|
+
if (!preview?.visual) {
|
|
27
|
+
if (!preview?.warning) return fallback;
|
|
28
|
+
return (
|
|
29
|
+
<div
|
|
30
|
+
className="squisq-template-gallery-content-preview squisq-template-gallery-content-preview--fallback"
|
|
31
|
+
style={{ aspectRatio: `${source.viewport.width} / ${source.viewport.height}` }}
|
|
32
|
+
aria-hidden="true"
|
|
33
|
+
>
|
|
34
|
+
<div className="squisq-template-gallery-content-preview-fallback">{fallback}</div>
|
|
35
|
+
<span className="squisq-template-gallery-content-preview-warning">{preview.warning}</span>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div
|
|
42
|
+
className="squisq-template-gallery-content-preview"
|
|
43
|
+
style={{ aspectRatio: `${source.viewport.width} / ${source.viewport.height}` }}
|
|
44
|
+
aria-hidden="true"
|
|
45
|
+
>
|
|
46
|
+
<MediaContext.Provider value={source.mediaProvider ?? null}>
|
|
47
|
+
<BlockRenderer
|
|
48
|
+
block={preview.visual}
|
|
49
|
+
blockTime={0}
|
|
50
|
+
basePath={source.basePath ?? '/'}
|
|
51
|
+
viewport={source.viewport}
|
|
52
|
+
/>
|
|
53
|
+
</MediaContext.Provider>
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|