@bendyline/squisq-editor-react 1.5.2 → 1.6.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/index.d.ts +757 -20
- package/dist/index.js +16910 -6844
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/BlockCardView.tsx +121 -0
- package/src/BlockPreviewPanel.tsx +69 -0
- package/src/BlockPropertiesPopover.tsx +191 -0
- package/src/EditorContext.tsx +143 -0
- package/src/EditorShell.tsx +200 -120
- package/src/FolderView.tsx +131 -0
- package/src/Icon.tsx +26 -0
- package/src/ImageEditor.tsx +69 -22
- package/src/OutlinePanel.tsx +38 -3
- package/src/PlainHtmlPreview.tsx +30 -3
- package/src/PreviewControls.tsx +180 -8
- package/src/RawEditor.tsx +216 -29
- package/src/RecorderEntry.tsx +9 -16
- package/src/TemplateAnnotation.ts +44 -0
- package/src/TemplatePicker.tsx +329 -54
- package/src/ThemeCustomizerPanel.tsx +30 -336
- package/src/ThemePicker.tsx +112 -3
- package/src/TimelineBlockPreview.tsx +37 -0
- package/src/TimelineTrack.tsx +671 -0
- package/src/Toolbar.tsx +528 -174
- package/src/Tooltip.tsx +22 -4
- package/src/TransitionPicker.tsx +351 -0
- package/src/VersionHistoryPanel.tsx +61 -31
- package/src/ViewMenuPanel.tsx +17 -14
- package/src/WysiwygEditor.tsx +161 -65
- package/src/__tests__/blockProperties.test.ts +92 -0
- package/src/__tests__/blockRange.test.ts +105 -0
- package/src/__tests__/buildPreviewDocTransition.test.ts +73 -0
- package/src/__tests__/createShapeLayer.test.ts +46 -0
- package/src/__tests__/drawingShapeRoundTrip.test.ts +49 -0
- package/src/__tests__/embeddedMedia.test.ts +48 -0
- package/src/__tests__/headingTransition.test.ts +138 -0
- package/src/__tests__/layoutChildRoundTrip.test.ts +71 -0
- package/src/__tests__/plainHtmlPreview.test.tsx +10 -8
- package/src/__tests__/recorderMediaInsert.test.ts +86 -0
- package/src/__tests__/templateAnnotationRoundTrip.test.ts +18 -0
- package/src/__tests__/templatePickerMetadata.test.ts +32 -0
- package/src/__tests__/timelineSource.test.ts +134 -0
- package/src/__tests__/tiptapBridge.test.ts +92 -0
- package/src/__tests__/tiptapBridgeConformance.test.ts +47 -0
- package/src/__tests__/tooltip.test.tsx +72 -0
- package/src/__tests__/transitionCatalog.test.ts +64 -0
- package/src/__tests__/useBlockNavigator.test.tsx +67 -0
- package/src/__tests__/useMediaRecorder.test.ts +24 -0
- package/src/__tests__/useTimelineClock.test.ts +21 -0
- package/src/blockProperties.ts +88 -0
- package/src/blockRange.ts +132 -0
- package/src/buildPreviewDoc.ts +98 -9
- package/src/customTemplates/AddBin.tsx +126 -0
- package/src/customTemplates/CustomLayoutManager.tsx +233 -0
- package/src/customTemplates/CustomTemplateContext.tsx +182 -0
- package/src/customTemplates/LayerToolbar.tsx +580 -0
- package/src/customTemplates/ShapeGlyph.tsx +47 -0
- package/src/customTemplates/TemplateDesigner.tsx +430 -0
- package/src/customTemplates/__tests__/library.test.ts +88 -0
- package/src/customTemplates/__tests__/normalizePositions.test.ts +109 -0
- package/src/customTemplates/__tests__/shapeDefs.test.ts +49 -0
- package/src/customTemplates/__tests__/useMemoryLayerAdapter.test.ts +95 -0
- package/src/customTemplates/designer.css +673 -0
- package/src/customTemplates/index.ts +31 -0
- package/src/customTemplates/library.ts +97 -0
- package/src/customTemplates/normalizePositions.ts +75 -0
- package/src/customTemplates/shapeDefs.ts +131 -0
- package/src/customTemplates/thumbnail.tsx +63 -0
- package/src/customTemplates/tokenDefs.ts +60 -0
- package/src/customTemplates/useDocCustomTemplates.ts +52 -0
- package/src/customTemplates/useMemoryLayerAdapter.ts +123 -0
- package/src/customThemes/CustomThemeContext.tsx +179 -0
- package/src/customThemes/CustomThemeDialog.tsx +286 -0
- package/src/customThemes/__tests__/CustomThemeContext.test.tsx +64 -0
- package/src/customThemes/__tests__/CustomThemeDialog.test.tsx +47 -0
- package/src/customThemes/__tests__/customThemeLibrary.test.ts +51 -0
- package/src/customThemes/customThemeLibrary.ts +97 -0
- package/src/customThemes/index.ts +31 -0
- package/src/customThemes/themeControls.tsx +229 -0
- package/src/customThemes/themeDraft.ts +272 -0
- package/src/customThemes/useDocCustomThemes.ts +49 -0
- package/src/diagram/DiagramCanvas.tsx +240 -0
- package/src/diagram/DiagramExtension.ts +209 -0
- package/src/diagram/DiagramMaximizedOverlay.tsx +46 -0
- package/src/diagram/DiagramWidget.tsx +270 -0
- package/src/diagram/diagramCommands.ts +604 -0
- package/src/diagram/diagramConstants.ts +17 -0
- package/src/diagram/useDiagramData.ts +126 -0
- package/src/embeddedMedia.ts +78 -0
- package/src/frontmatter.ts +29 -0
- package/src/headingTransition.ts +231 -0
- package/src/imageEditor/CanvasSurface.tsx +383 -88
- package/src/imageEditor/PropertiesPanel.tsx +47 -1
- package/src/imageEditor/Toolbar.tsx +229 -16
- package/src/imageEditor/createShapeLayer.ts +280 -0
- package/src/imageEditor/icons.tsx +34 -114
- package/src/imageEditor/image-editor.css +54 -5
- package/src/imageEditor/state.ts +23 -3
- package/src/index.ts +77 -0
- package/src/recorder/RecorderModal.tsx +120 -53
- package/src/recorder/RecorderPanel.tsx +2 -26
- package/src/recorder/hooks/useMediaRecorder.ts +17 -2
- package/src/recorder/insertMediaBlock.ts +30 -0
- package/src/resolveBlockVisual.ts +33 -0
- package/src/scene/Scene.tsx +540 -0
- package/src/scene/SceneBlockExtension.ts +198 -0
- package/src/scene/SceneBlockToolbar.tsx +201 -0
- package/src/scene/SceneBlockWidget.tsx +434 -0
- package/src/scene/ScenePropsBar.tsx +85 -0
- package/src/scene/SceneSelection.tsx +107 -0
- package/src/scene/SceneViewport.tsx +102 -0
- package/src/scene/ShapePalette.tsx +181 -0
- package/src/scene/__tests__/DiagramAdapter.test.ts +56 -0
- package/src/scene/__tests__/bezierEdit.test.ts +85 -0
- package/src/scene/__tests__/blockLayers.test.ts +57 -0
- package/src/scene/__tests__/shapeLayers.test.ts +106 -0
- package/src/scene/__tests__/useSceneHitTest.test.ts +90 -0
- package/src/scene/__tests__/useScenePanZoom.test.ts +103 -0
- package/src/scene/adapters/DiagramAdapter.ts +168 -0
- package/src/scene/adapters/DrawingAdapter.ts +415 -0
- package/src/scene/adapters/LayoutAdapter.ts +310 -0
- package/src/scene/adapters/blockLayers.ts +159 -0
- package/src/scene/commands/SceneCommand.ts +70 -0
- package/src/scene/commands/drawingCommands.ts +318 -0
- package/src/scene/commands/layoutCommands.ts +301 -0
- package/src/scene/hooks/useSceneHitTest.ts +105 -0
- package/src/scene/hooks/useScenePanZoom.ts +147 -0
- package/src/scene/hooks/useSceneSelection.ts +62 -0
- package/src/scene/index.ts +95 -0
- package/src/scene/layers/DiagramEdges.tsx +127 -0
- package/src/scene/layers/edgeGeometry.ts +77 -0
- package/src/scene/layers/nodeCard.tsx +145 -0
- package/src/scene/layers/renderLayer.tsx +70 -0
- package/src/scene/layers/shapeLayers.ts +201 -0
- package/src/scene/paths/bezierEdit.ts +208 -0
- package/src/scene/scene.css +649 -0
- package/src/scene/text/SceneTextOverlay.tsx +161 -0
- package/src/scene/text/sceneTextChannel.ts +40 -0
- package/src/scene/text/sceneTextConfig.ts +27 -0
- package/src/scene/text/sceneTiptap.ts +36 -0
- package/src/scene/text/useSceneTextEditing.ts +39 -0
- package/src/scene/tools/ConnectTool.ts +111 -0
- package/src/scene/tools/DrawingConnectTool.ts +161 -0
- package/src/scene/tools/PathTool.ts +158 -0
- package/src/scene/tools/PlaceTool.ts +47 -0
- package/src/scene/tools/SceneTool.ts +75 -0
- package/src/scene/tools/SelectTool.ts +284 -0
- package/src/scene/tools/ShapeTool.ts +144 -0
- package/src/scene/tools/TextTool.ts +72 -0
- package/src/scene/tools/TokenTool.ts +95 -0
- package/src/scene/tools/createDrawShapeTool.ts +82 -0
- package/src/styles/diagram.css +183 -0
- package/src/styles/editor.css +1615 -203
- package/src/styles/folder-view.css +210 -0
- package/src/styles/image-edit-affordance.css +2 -2
- package/src/styles/index.css +4 -0
- package/src/timelineSource.ts +244 -0
- package/src/tiptapBridge.ts +115 -34
- package/src/tooltipPlacement.ts +13 -0
- package/src/transitionCatalog.ts +159 -0
- package/src/types/monaco-shims.d.ts +10 -0
- package/src/useBlockNavigator.ts +153 -0
- package/src/useMonacoLoader.ts +105 -0
- package/src/useTimelineClock.ts +76 -0
- package/src/utils/dropUtils.ts +1 -1
package/src/TemplatePicker.tsx
CHANGED
|
@@ -6,10 +6,22 @@
|
|
|
6
6
|
* and a one-sentence description so authors can quickly find the right layout.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { useEffect, useRef, useState } from 'react';
|
|
9
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
10
10
|
import { createPortal } from 'react-dom';
|
|
11
|
+
import type { CustomTemplateDefinition } from '@bendyline/squisq/schemas';
|
|
12
|
+
import { TEMPLATE_METADATA, resolveTemplateName } from '@bendyline/squisq/doc';
|
|
13
|
+
import { useCustomTemplates } from './customTemplates/CustomTemplateContext';
|
|
14
|
+
import { TemplateThumbnail } from './customTemplates/thumbnail';
|
|
11
15
|
|
|
12
16
|
// ── Template metadata ─────────────────────────────────────────────
|
|
17
|
+
//
|
|
18
|
+
// The canonical list of built-in templates (which ids exist, their labels,
|
|
19
|
+
// and their descriptions) lives in core's `TEMPLATE_METADATA`. The entries
|
|
20
|
+
// below add the one thing core can't hold — a React/SVG preview icon per id —
|
|
21
|
+
// and MUST stay 1:1 with `TEMPLATE_METADATA` (same ids, same order, same
|
|
22
|
+
// label/description). `templatePickerMetadata.test.ts` enforces this, so a
|
|
23
|
+
// template added to the core registry without a picker icon (or vice versa)
|
|
24
|
+
// fails the build instead of silently disappearing from the gallery.
|
|
13
25
|
|
|
14
26
|
interface TemplateEntry {
|
|
15
27
|
name: string;
|
|
@@ -65,7 +77,8 @@ const NONE_ENTRY: TemplateEntry = {
|
|
|
65
77
|
),
|
|
66
78
|
};
|
|
67
79
|
|
|
68
|
-
|
|
80
|
+
// eslint-disable-next-line react-refresh/only-export-components
|
|
81
|
+
export const TEMPLATE_ENTRIES: TemplateEntry[] = [
|
|
69
82
|
{
|
|
70
83
|
name: 'title',
|
|
71
84
|
label: 'Title',
|
|
@@ -381,6 +394,85 @@ const TEMPLATE_ENTRIES: TemplateEntry[] = [
|
|
|
381
394
|
</TemplateIcon>
|
|
382
395
|
),
|
|
383
396
|
},
|
|
397
|
+
{
|
|
398
|
+
name: 'diagram',
|
|
399
|
+
label: 'Diagram',
|
|
400
|
+
description:
|
|
401
|
+
'Renders sub-headings as connected nodes — edit visually as a node-and-edge diagram.',
|
|
402
|
+
icon: (
|
|
403
|
+
<TemplateIcon>
|
|
404
|
+
<rect x={4} y={6} width={16} height={10} rx={2} fill={FA} opacity={0.85} />
|
|
405
|
+
<rect x={36} y={6} width={16} height={10} rx={2} fill={F2} opacity={0.8} />
|
|
406
|
+
<rect x={20} y={24} width={16} height={10} rx={2} fill={F2} opacity={0.8} />
|
|
407
|
+
<path
|
|
408
|
+
d="M 20 11 L 36 11"
|
|
409
|
+
stroke={FA}
|
|
410
|
+
strokeWidth={1.5}
|
|
411
|
+
fill="none"
|
|
412
|
+
opacity={0.7}
|
|
413
|
+
markerEnd="url(#tp-diagram-arrow)"
|
|
414
|
+
/>
|
|
415
|
+
<path
|
|
416
|
+
d="M 12 16 C 12 22, 24 22, 24 24"
|
|
417
|
+
stroke={FA}
|
|
418
|
+
strokeWidth={1.5}
|
|
419
|
+
fill="none"
|
|
420
|
+
opacity={0.7}
|
|
421
|
+
/>
|
|
422
|
+
<path
|
|
423
|
+
d="M 44 16 C 44 22, 32 22, 32 24"
|
|
424
|
+
stroke={FA}
|
|
425
|
+
strokeWidth={1.5}
|
|
426
|
+
fill="none"
|
|
427
|
+
opacity={0.7}
|
|
428
|
+
/>
|
|
429
|
+
</TemplateIcon>
|
|
430
|
+
),
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
name: 'layout',
|
|
434
|
+
label: 'Layout',
|
|
435
|
+
description:
|
|
436
|
+
"Free-form 2D canvas — drag layers into place. Use for one-off block layouts that don't fit a template.",
|
|
437
|
+
icon: (
|
|
438
|
+
<TemplateIcon>
|
|
439
|
+
<rect
|
|
440
|
+
x={3}
|
|
441
|
+
y={3}
|
|
442
|
+
width={50}
|
|
443
|
+
height={34}
|
|
444
|
+
rx={2}
|
|
445
|
+
fill="none"
|
|
446
|
+
stroke={F1}
|
|
447
|
+
strokeWidth={1}
|
|
448
|
+
strokeDasharray="3 2"
|
|
449
|
+
/>
|
|
450
|
+
<rect x={7} y={7} width={20} height={12} rx={1.5} fill={FA} opacity={0.85} />
|
|
451
|
+
<rect x={30} y={7} width={20} height={26} rx={1.5} fill={F2} opacity={0.7} />
|
|
452
|
+
<rect x={7} y={22} width={20} height={11} rx={1.5} fill={F1} opacity={0.7} />
|
|
453
|
+
</TemplateIcon>
|
|
454
|
+
),
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
name: 'drawing',
|
|
458
|
+
label: 'Drawing',
|
|
459
|
+
description: 'Free-form sketches — draw shapes, paths, and text directly on a 2D surface.',
|
|
460
|
+
icon: (
|
|
461
|
+
<TemplateIcon>
|
|
462
|
+
<rect x={3} y={3} width={50} height={34} rx={2} fill="none" stroke={F1} strokeWidth={1} />
|
|
463
|
+
<path
|
|
464
|
+
d="M 8 28 C 14 12, 26 14, 30 22 S 44 32, 50 14"
|
|
465
|
+
stroke={FA}
|
|
466
|
+
strokeWidth={2}
|
|
467
|
+
fill="none"
|
|
468
|
+
opacity={0.85}
|
|
469
|
+
strokeLinecap="round"
|
|
470
|
+
/>
|
|
471
|
+
<circle cx={10} cy={10} r={2.5} fill={F2} />
|
|
472
|
+
<rect x={40} y={28} width={8} height={6} rx={1} fill={F2} opacity={0.7} />
|
|
473
|
+
</TemplateIcon>
|
|
474
|
+
),
|
|
475
|
+
},
|
|
384
476
|
];
|
|
385
477
|
|
|
386
478
|
/**
|
|
@@ -399,27 +491,23 @@ export const TEMPLATE_NAMES: readonly string[] = TEMPLATE_ENTRIES.map((e) => e.n
|
|
|
399
491
|
* normalizing their annotations.
|
|
400
492
|
*/
|
|
401
493
|
// eslint-disable-next-line react-refresh/only-export-components
|
|
402
|
-
export function templateLabel(
|
|
494
|
+
export function templateLabel(
|
|
495
|
+
name: string,
|
|
496
|
+
customTemplates?: readonly CustomTemplateDefinition[],
|
|
497
|
+
): string {
|
|
403
498
|
if (!name) return '— none —';
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
499
|
+
// `resolveTemplateName` + `TEMPLATE_METADATA` are the canonical source of
|
|
500
|
+
// truth in core; both legacy long ids (`titleBlock`) and short ids resolve
|
|
501
|
+
// through it, so the picker never has to maintain its own alias/label table.
|
|
502
|
+
const resolved = resolveTemplateName(name);
|
|
503
|
+
const meta = TEMPLATE_METADATA[resolved];
|
|
504
|
+
if (meta) return meta.label;
|
|
505
|
+
const custom = customTemplates?.find((t) => t.name === resolved);
|
|
506
|
+
if (custom) return custom.label;
|
|
407
507
|
// Fallback: split camelCase
|
|
408
508
|
return resolved.replace(/([A-Z])/g, ' $1').replace(/^./, (s) => s.toUpperCase());
|
|
409
509
|
}
|
|
410
510
|
|
|
411
|
-
/**
|
|
412
|
-
* Legacy template id → canonical short id. Kept inline to avoid pulling
|
|
413
|
-
* the core package's runtime registry into the picker's bundle — the
|
|
414
|
-
* picker only needs this for label resolution.
|
|
415
|
-
*/
|
|
416
|
-
const TEMPLATE_NAME_ALIASES: Readonly<Record<string, string>> = {
|
|
417
|
-
titleBlock: 'title',
|
|
418
|
-
quoteBlock: 'quote',
|
|
419
|
-
mapBlock: 'map',
|
|
420
|
-
listBlock: 'list',
|
|
421
|
-
};
|
|
422
|
-
|
|
423
511
|
// ── Component ─────────────────────────────────────────────────────
|
|
424
512
|
|
|
425
513
|
export interface TemplatePickerProps {
|
|
@@ -433,9 +521,22 @@ export interface TemplatePickerProps {
|
|
|
433
521
|
* single ungrouped grid (legacy behavior).
|
|
434
522
|
*/
|
|
435
523
|
recommended?: readonly string[];
|
|
524
|
+
/**
|
|
525
|
+
* Optional callback fired when the user clicks the "+ New custom
|
|
526
|
+
* template" card pinned at the top of the gallery. The host wires this
|
|
527
|
+
* to open the modal `TemplateDesigner`. When omitted, the card is
|
|
528
|
+
* hidden.
|
|
529
|
+
*/
|
|
530
|
+
onOpenDesigner?: () => void;
|
|
436
531
|
}
|
|
437
532
|
|
|
438
|
-
export function TemplatePicker({
|
|
533
|
+
export function TemplatePicker({
|
|
534
|
+
value,
|
|
535
|
+
onChange,
|
|
536
|
+
compact,
|
|
537
|
+
recommended,
|
|
538
|
+
onOpenDesigner,
|
|
539
|
+
}: TemplatePickerProps) {
|
|
439
540
|
const [open, setOpen] = useState(false);
|
|
440
541
|
const [popoverStyle, setPopoverStyle] = useState<React.CSSProperties>({});
|
|
441
542
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
@@ -546,6 +647,7 @@ export function TemplatePicker({ value, onChange, compact, recommended }: Templa
|
|
|
546
647
|
onSelect={handleSelect}
|
|
547
648
|
style={popoverStyle}
|
|
548
649
|
recommended={recommended}
|
|
650
|
+
onOpenDesigner={onOpenDesigner}
|
|
549
651
|
/>,
|
|
550
652
|
document.body,
|
|
551
653
|
)
|
|
@@ -631,12 +733,52 @@ function TemplateGalleryBody({
|
|
|
631
733
|
onSelect,
|
|
632
734
|
style,
|
|
633
735
|
recommended,
|
|
736
|
+
onOpenDesigner,
|
|
634
737
|
}: {
|
|
635
738
|
value: string;
|
|
636
739
|
onSelect: (name: string) => void;
|
|
637
740
|
style: React.CSSProperties;
|
|
638
741
|
recommended?: readonly string[];
|
|
742
|
+
onOpenDesigner?: () => void;
|
|
639
743
|
}) {
|
|
744
|
+
const [query, setQuery] = useState('');
|
|
745
|
+
const searchRef = useRef<HTMLInputElement>(null);
|
|
746
|
+
// Pull merged doc + library templates from the surrounding context.
|
|
747
|
+
// When the host hasn't wrapped the editor in a CustomTemplateProvider
|
|
748
|
+
// we silently degrade to "no custom templates", which preserves the
|
|
749
|
+
// legacy behaviour for any caller that hasn't opted in.
|
|
750
|
+
const customCtx = useCustomTemplates();
|
|
751
|
+
const customTemplates = useMemo(() => customCtx?.allTemplates ?? [], [customCtx]);
|
|
752
|
+
|
|
753
|
+
// Auto-focus the search input when the gallery mounts so the user can
|
|
754
|
+
// start typing immediately to filter templates.
|
|
755
|
+
useEffect(() => {
|
|
756
|
+
searchRef.current?.focus();
|
|
757
|
+
}, []);
|
|
758
|
+
|
|
759
|
+
const trimmedQuery = query.trim().toLowerCase();
|
|
760
|
+
const hasQuery = trimmedQuery.length > 0;
|
|
761
|
+
|
|
762
|
+
// For search results we match both built-in entries and custom
|
|
763
|
+
// templates by label/name/description so the user can find a custom
|
|
764
|
+
// template by typing its name.
|
|
765
|
+
const matches = useMemo(() => {
|
|
766
|
+
if (!hasQuery) return null;
|
|
767
|
+
const built = TEMPLATE_ENTRIES.filter(
|
|
768
|
+
(e) =>
|
|
769
|
+
e.label.toLowerCase().includes(trimmedQuery) ||
|
|
770
|
+
e.description.toLowerCase().includes(trimmedQuery) ||
|
|
771
|
+
e.name.toLowerCase().includes(trimmedQuery),
|
|
772
|
+
);
|
|
773
|
+
const custom = customTemplates.filter(
|
|
774
|
+
(t) =>
|
|
775
|
+
t.label.toLowerCase().includes(trimmedQuery) ||
|
|
776
|
+
t.name.toLowerCase().includes(trimmedQuery) ||
|
|
777
|
+
(t.description ?? '').toLowerCase().includes(trimmedQuery),
|
|
778
|
+
);
|
|
779
|
+
return { built, custom };
|
|
780
|
+
}, [hasQuery, trimmedQuery, customTemplates]);
|
|
781
|
+
|
|
640
782
|
const recommendedSet = recommended && recommended.length > 0 ? new Set(recommended) : null;
|
|
641
783
|
const recommendedEntries = recommendedSet
|
|
642
784
|
? TEMPLATE_ENTRIES.filter((e) => recommendedSet.has(e.name))
|
|
@@ -644,7 +786,7 @@ function TemplateGalleryBody({
|
|
|
644
786
|
const restEntries = recommendedSet
|
|
645
787
|
? TEMPLATE_ENTRIES.filter((e) => !recommendedSet.has(e.name))
|
|
646
788
|
: TEMPLATE_ENTRIES;
|
|
647
|
-
const segmented = recommendedEntries.length > 0;
|
|
789
|
+
const segmented = !hasQuery && recommendedEntries.length > 0;
|
|
648
790
|
|
|
649
791
|
return (
|
|
650
792
|
<div
|
|
@@ -654,49 +796,174 @@ function TemplateGalleryBody({
|
|
|
654
796
|
aria-label="Block templates"
|
|
655
797
|
style={style}
|
|
656
798
|
>
|
|
657
|
-
<
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
799
|
+
<div className="squisq-template-gallery-search">
|
|
800
|
+
<svg
|
|
801
|
+
className="squisq-template-gallery-search-icon"
|
|
802
|
+
width="14"
|
|
803
|
+
height="14"
|
|
804
|
+
viewBox="0 0 16 16"
|
|
805
|
+
fill="none"
|
|
806
|
+
aria-hidden="true"
|
|
807
|
+
>
|
|
808
|
+
<circle cx="7" cy="7" r="5" stroke="currentColor" strokeWidth="1.5" />
|
|
809
|
+
<path d="M11 11l3 3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
810
|
+
</svg>
|
|
811
|
+
<input
|
|
812
|
+
ref={searchRef}
|
|
813
|
+
type="search"
|
|
814
|
+
className="squisq-template-gallery-search-input"
|
|
815
|
+
placeholder="Search templates…"
|
|
816
|
+
value={query}
|
|
817
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
818
|
+
aria-label="Search block templates"
|
|
819
|
+
/>
|
|
820
|
+
</div>
|
|
668
821
|
|
|
669
|
-
{
|
|
670
|
-
<
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
822
|
+
{!hasQuery && (
|
|
823
|
+
<button
|
|
824
|
+
type="button"
|
|
825
|
+
role="option"
|
|
826
|
+
aria-selected={value === ''}
|
|
827
|
+
className={`squisq-template-gallery-none${value === '' ? ' squisq-template-gallery-card--selected' : ''}`}
|
|
828
|
+
onClick={() => onSelect('')}
|
|
829
|
+
>
|
|
830
|
+
{NONE_ENTRY.icon}
|
|
831
|
+
<span className="squisq-template-gallery-none-label">{NONE_ENTRY.label}</span>
|
|
832
|
+
<span className="squisq-template-gallery-none-desc">{NONE_ENTRY.description}</span>
|
|
833
|
+
</button>
|
|
678
834
|
)}
|
|
679
835
|
|
|
680
|
-
{
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
836
|
+
{hasQuery ? (
|
|
837
|
+
matches && (matches.built.length > 0 || matches.custom.length > 0) ? (
|
|
838
|
+
<>
|
|
839
|
+
{matches.custom.length > 0 && (
|
|
840
|
+
<div className="squisq-template-gallery-section">
|
|
841
|
+
<h3 className="squisq-template-gallery-section-title">Custom</h3>
|
|
842
|
+
<div className="squisq-template-gallery-grid">
|
|
843
|
+
{matches.custom.map((def) => (
|
|
844
|
+
<CustomTemplateCard
|
|
845
|
+
key={def.name}
|
|
846
|
+
def={def}
|
|
847
|
+
value={value}
|
|
848
|
+
onSelect={onSelect}
|
|
849
|
+
/>
|
|
850
|
+
))}
|
|
851
|
+
</div>
|
|
852
|
+
</div>
|
|
853
|
+
)}
|
|
854
|
+
{matches.built.length > 0 && (
|
|
855
|
+
<div className="squisq-template-gallery-grid">
|
|
856
|
+
{matches.built.map((entry) => (
|
|
857
|
+
<TemplateCard key={entry.name} entry={entry} value={value} onSelect={onSelect} />
|
|
858
|
+
))}
|
|
859
|
+
</div>
|
|
860
|
+
)}
|
|
861
|
+
</>
|
|
862
|
+
) : (
|
|
863
|
+
<div className="squisq-template-gallery-empty">No templates match "{query}".</div>
|
|
864
|
+
)
|
|
689
865
|
) : (
|
|
690
|
-
|
|
691
|
-
{
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
866
|
+
<>
|
|
867
|
+
{/* "+ New custom template" card pinned at the top — only shown
|
|
868
|
+
when the host has wired up `onOpenDesigner`. */}
|
|
869
|
+
{onOpenDesigner && (
|
|
870
|
+
<button type="button" className="squisq-template-gallery-new" onClick={onOpenDesigner}>
|
|
871
|
+
<span className="squisq-template-gallery-new-plus" aria-hidden="true">
|
|
872
|
+
+
|
|
873
|
+
</span>
|
|
874
|
+
<span className="squisq-template-gallery-new-body">
|
|
875
|
+
<span className="squisq-template-gallery-new-label">New layout</span>
|
|
876
|
+
<span className="squisq-template-gallery-new-desc">
|
|
877
|
+
Design a reusable layout with placeholders for {'{title}'} and {'{content}'}.
|
|
878
|
+
</span>
|
|
879
|
+
</span>
|
|
880
|
+
</button>
|
|
881
|
+
)}
|
|
882
|
+
|
|
883
|
+
{customTemplates.length > 0 && (
|
|
884
|
+
<div className="squisq-template-gallery-section">
|
|
885
|
+
<h3 className="squisq-template-gallery-section-title">Custom</h3>
|
|
886
|
+
<div className="squisq-template-gallery-grid">
|
|
887
|
+
{customTemplates.map((def) => (
|
|
888
|
+
<CustomTemplateCard key={def.name} def={def} value={value} onSelect={onSelect} />
|
|
889
|
+
))}
|
|
890
|
+
</div>
|
|
891
|
+
</div>
|
|
892
|
+
)}
|
|
893
|
+
|
|
894
|
+
{segmented && (
|
|
895
|
+
<div className="squisq-template-gallery-section">
|
|
896
|
+
<h3 className="squisq-template-gallery-section-title">Recommended for this block</h3>
|
|
897
|
+
<div className="squisq-template-gallery-grid">
|
|
898
|
+
{recommendedEntries.map((entry) => (
|
|
899
|
+
<TemplateCard key={entry.name} entry={entry} value={value} onSelect={onSelect} />
|
|
900
|
+
))}
|
|
901
|
+
</div>
|
|
902
|
+
</div>
|
|
903
|
+
)}
|
|
904
|
+
|
|
905
|
+
{segmented ? (
|
|
906
|
+
<div className="squisq-template-gallery-section">
|
|
907
|
+
<h3 className="squisq-template-gallery-section-title">All templates</h3>
|
|
908
|
+
<div className="squisq-template-gallery-grid">
|
|
909
|
+
{restEntries.map((entry) => (
|
|
910
|
+
<TemplateCard key={entry.name} entry={entry} value={value} onSelect={onSelect} />
|
|
911
|
+
))}
|
|
912
|
+
</div>
|
|
913
|
+
</div>
|
|
914
|
+
) : (
|
|
915
|
+
<div className="squisq-template-gallery-grid">
|
|
916
|
+
{restEntries.map((entry) => (
|
|
917
|
+
<TemplateCard key={entry.name} entry={entry} value={value} onSelect={onSelect} />
|
|
918
|
+
))}
|
|
919
|
+
</div>
|
|
920
|
+
)}
|
|
921
|
+
</>
|
|
695
922
|
)}
|
|
696
923
|
</div>
|
|
697
924
|
);
|
|
698
925
|
}
|
|
699
926
|
|
|
927
|
+
/**
|
|
928
|
+
* Picker card for a user-authored template. Mirrors `TemplateCard`'s
|
|
929
|
+
* shape so it slots into the gallery grid, but draws the thumbnail by
|
|
930
|
+
* rendering the template's layers via `<TemplateThumbnail>`. Selecting
|
|
931
|
+
* it dispatches the template name; the host's `applyTemplate` (called
|
|
932
|
+
* separately by the badge update flow) inlines the def into the doc.
|
|
933
|
+
*/
|
|
934
|
+
function CustomTemplateCard({
|
|
935
|
+
def,
|
|
936
|
+
value,
|
|
937
|
+
onSelect,
|
|
938
|
+
}: {
|
|
939
|
+
def: CustomTemplateDefinition;
|
|
940
|
+
value: string;
|
|
941
|
+
onSelect: (name: string) => void;
|
|
942
|
+
}) {
|
|
943
|
+
return (
|
|
944
|
+
<button
|
|
945
|
+
type="button"
|
|
946
|
+
role="option"
|
|
947
|
+
aria-selected={value === def.name}
|
|
948
|
+
className={`squisq-template-gallery-card${
|
|
949
|
+
value === def.name ? ' squisq-template-gallery-card--selected' : ''
|
|
950
|
+
}`}
|
|
951
|
+
onClick={() => onSelect(def.name)}
|
|
952
|
+
title={def.description ?? def.label}
|
|
953
|
+
>
|
|
954
|
+
<div className="squisq-template-gallery-card-icon">
|
|
955
|
+
<TemplateThumbnail def={def} />
|
|
956
|
+
</div>
|
|
957
|
+
<div className="squisq-template-gallery-card-body">
|
|
958
|
+
<span className="squisq-template-gallery-card-name">{def.label}</span>
|
|
959
|
+
<span className="squisq-template-gallery-card-desc">
|
|
960
|
+
{def.description ?? 'Custom template'}
|
|
961
|
+
</span>
|
|
962
|
+
</div>
|
|
963
|
+
</button>
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
|
|
700
967
|
// ── Inline badge popover (anchored at a heading badge) ─────────────
|
|
701
968
|
|
|
702
969
|
export interface TemplateBadgePopoverProps {
|
|
@@ -708,6 +975,12 @@ export interface TemplateBadgePopoverProps {
|
|
|
708
975
|
onClose: () => void;
|
|
709
976
|
/** Optional list of template names to surface as "Recommended for this block". */
|
|
710
977
|
recommended?: readonly string[];
|
|
978
|
+
/**
|
|
979
|
+
* Optional callback for the "+ New custom template" card. When
|
|
980
|
+
* supplied, the gallery shows the card pinned at the top; clicking
|
|
981
|
+
* it closes the popover and opens the designer.
|
|
982
|
+
*/
|
|
983
|
+
onOpenDesigner?: () => void;
|
|
711
984
|
}
|
|
712
985
|
|
|
713
986
|
/**
|
|
@@ -722,6 +995,7 @@ export function TemplateBadgePopover({
|
|
|
722
995
|
onChange,
|
|
723
996
|
onClose,
|
|
724
997
|
recommended,
|
|
998
|
+
onOpenDesigner,
|
|
725
999
|
}: TemplateBadgePopoverProps) {
|
|
726
1000
|
const [style, setStyle] = useState<React.CSSProperties>(() => computePopoverStyle(anchorRect));
|
|
727
1001
|
|
|
@@ -764,6 +1038,7 @@ export function TemplateBadgePopover({
|
|
|
764
1038
|
onSelect={handleSelect}
|
|
765
1039
|
style={style}
|
|
766
1040
|
recommended={recommended}
|
|
1041
|
+
onOpenDesigner={onOpenDesigner}
|
|
767
1042
|
/>,
|
|
768
1043
|
document.body,
|
|
769
1044
|
);
|