@bendyline/squisq-editor-react 1.5.3 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -10
- package/dist/index.d.ts +1114 -52
- package/dist/index.js +18446 -8050
- package/dist/index.js.map +1 -1
- package/dist/styles/fa-brands-400-AHOAZHCU.woff2 +0 -0
- package/dist/styles/fa-regular-400-VRZYIBIZ.woff2 +0 -0
- package/dist/styles/fa-solid-900-MDEYK55F.woff2 +0 -0
- package/dist/styles/fa-v4compatibility-ETEVP6IB.woff2 +0 -0
- package/dist/styles/index.css +13867 -0
- package/package.json +15 -7
- package/src/BlockCardView.tsx +121 -0
- package/src/BlockPreviewPanel.tsx +69 -0
- package/src/BlockPropertiesPopover.tsx +191 -0
- package/src/EditorContext.tsx +165 -16
- package/src/EditorShell.tsx +265 -144
- package/src/FolderView.tsx +131 -0
- package/src/Icon.tsx +26 -0
- package/src/ImageEditor.tsx +69 -22
- package/src/OutlinePanel.tsx +64 -7
- package/src/PlainHtmlPreview.tsx +30 -3
- package/src/PreviewControls.tsx +491 -122
- package/src/PreviewPanel.tsx +15 -9
- package/src/RawEditor.tsx +179 -17
- 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 +544 -180
- package/src/Tooltip.tsx +22 -4
- package/src/TransitionPicker.tsx +351 -0
- package/src/VersionHistoryPanel.tsx +4 -16
- 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__/codeContextSectionView.test.tsx +95 -0
- package/src/__tests__/codeContextZoneManager.test.ts +127 -0
- package/src/__tests__/createShapeLayer.test.ts +46 -0
- package/src/__tests__/diffContextSections.test.ts +39 -0
- package/src/__tests__/drawingShapeRoundTrip.test.ts +49 -0
- package/src/__tests__/editorShellCodeContext.test.tsx +86 -0
- package/src/__tests__/editorShellProps.test.tsx +96 -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__/previewControls.test.tsx +70 -0
- 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__/useJsonEditorTokens.test.ts +59 -0
- package/src/__tests__/useMediaRecorder.test.ts +41 -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/codeContext/CodeContextSectionView.tsx +124 -0
- package/src/codeContext/CodeContextZoneManager.ts +149 -0
- package/src/codeContext/CodeContextZones.tsx +121 -0
- package/src/codeContext/diffContextSections.ts +38 -0
- package/src/codeContext/types.ts +75 -0
- 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 +109 -1
- package/src/jsonEditor/useJsonEditorTokens.ts +13 -43
- package/src/recorder/RecorderModal.tsx +120 -53
- package/src/recorder/RecorderPanel.tsx +2 -26
- package/src/recorder/hooks/useMediaRecorder.ts +17 -11
- 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/code-context.css +155 -0
- package/src/styles/diagram.css +183 -0
- package/src/styles/editor.css +1708 -150
- package/src/styles/folder-view.css +210 -0
- package/src/styles/image-edit-affordance.css +2 -2
- package/src/styles/index.css +5 -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 +23 -1
- package/src/useTimelineClock.ts +76 -0
- package/src/utils/dropUtils.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bendyline/squisq-editor-react",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.1",
|
|
4
4
|
"description": "React editor shell with raw/WYSIWYG/preview modes for Squisq documents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Bendyline",
|
|
@@ -34,10 +34,10 @@
|
|
|
34
34
|
"types": "./dist/index.d.ts",
|
|
35
35
|
"import": "./dist/index.js"
|
|
36
36
|
},
|
|
37
|
-
"./styles": "./
|
|
37
|
+
"./styles": "./dist/styles/index.css"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
|
-
"build": "tsup",
|
|
40
|
+
"build": "tsup && node ../../scripts/build-styles.mjs",
|
|
41
41
|
"dev": "concurrently -n js,dts -c blue,gray -r \"tsup --watch --no-dts --no-clean\" \"tsup --watch --dts-only --no-clean\"",
|
|
42
42
|
"typecheck": "tsc --noEmit"
|
|
43
43
|
},
|
|
@@ -46,10 +46,15 @@
|
|
|
46
46
|
"react": "^18.0.0 || ^19.0.0",
|
|
47
47
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
48
48
|
},
|
|
49
|
+
"peerDependenciesMeta": {
|
|
50
|
+
"monaco-editor": {
|
|
51
|
+
"optional": true
|
|
52
|
+
}
|
|
53
|
+
},
|
|
49
54
|
"dependencies": {
|
|
50
|
-
"@bendyline/squisq": "1.
|
|
51
|
-
"@bendyline/squisq-formats": "1.
|
|
52
|
-
"@bendyline/squisq-react": "1.
|
|
55
|
+
"@bendyline/squisq": "1.5.1",
|
|
56
|
+
"@bendyline/squisq-formats": "1.4.1",
|
|
57
|
+
"@bendyline/squisq-react": "1.4.1",
|
|
53
58
|
"@fortawesome/fontawesome-free": "7.2.0",
|
|
54
59
|
"@monaco-editor/react": "4.7.0",
|
|
55
60
|
"@tiptap/extension-image": "2.27.2",
|
|
@@ -74,5 +79,8 @@
|
|
|
74
79
|
"react-dom": "18.3.1",
|
|
75
80
|
"tsup": "8.5.1",
|
|
76
81
|
"typescript": "5.9.3"
|
|
77
|
-
}
|
|
82
|
+
},
|
|
83
|
+
"sideEffects": [
|
|
84
|
+
"**/*.css"
|
|
85
|
+
]
|
|
78
86
|
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BlockCardView
|
|
3
|
+
*
|
|
4
|
+
* Presentational chrome for the block-at-a-time view: a single card holding
|
|
5
|
+
* one block's editor surface, with Previous / Next navigation, a
|
|
6
|
+
* "Block N of M" position indicator, and an optional "Add block" affordance.
|
|
7
|
+
*
|
|
8
|
+
* It is intentionally decoupled from `EditorContext` / `EditorShell` — it
|
|
9
|
+
* takes plain props (typically sourced from {@link useBlockNavigator}) and
|
|
10
|
+
* renders arbitrary `children`, so any host can reuse the block-at-a-time UX
|
|
11
|
+
* around its own editor or content.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { ReactNode } from 'react';
|
|
15
|
+
|
|
16
|
+
export interface BlockCardViewProps {
|
|
17
|
+
/** Total number of navigable blocks. */
|
|
18
|
+
blockCount: number;
|
|
19
|
+
/** Index of the block currently shown (0-based). */
|
|
20
|
+
activeBlockKey: number;
|
|
21
|
+
/** Move to the previous block. */
|
|
22
|
+
onPrev: () => void;
|
|
23
|
+
/** Move to the next block. */
|
|
24
|
+
onNext: () => void;
|
|
25
|
+
/** Insert a new block after the current one. Omit to hide the affordance. */
|
|
26
|
+
onAdd?: () => void;
|
|
27
|
+
/** The editor surface (or any content) for the active block. */
|
|
28
|
+
children: ReactNode;
|
|
29
|
+
/** Optional extra class for the outer container. */
|
|
30
|
+
className?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function BlockCardView({
|
|
34
|
+
blockCount,
|
|
35
|
+
activeBlockKey,
|
|
36
|
+
onPrev,
|
|
37
|
+
onNext,
|
|
38
|
+
onAdd,
|
|
39
|
+
children,
|
|
40
|
+
className,
|
|
41
|
+
}: BlockCardViewProps) {
|
|
42
|
+
const atStart = activeBlockKey <= 0;
|
|
43
|
+
const atEnd = activeBlockKey >= blockCount - 1;
|
|
44
|
+
// 1-based for humans; clamp to ≥1 so an empty doc still reads "Block 1 of 1".
|
|
45
|
+
const position = Math.min(activeBlockKey + 1, Math.max(blockCount, 1));
|
|
46
|
+
const total = Math.max(blockCount, 1);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div
|
|
50
|
+
className={`squisq-block-card${className ? ` ${className}` : ''}`}
|
|
51
|
+
data-testid="block-card-view"
|
|
52
|
+
>
|
|
53
|
+
<div className="squisq-block-card-body">{children}</div>
|
|
54
|
+
<div className="squisq-block-card-nav" role="toolbar" aria-label="Block navigation">
|
|
55
|
+
<button
|
|
56
|
+
type="button"
|
|
57
|
+
className="squisq-block-card-button squisq-block-card-prev"
|
|
58
|
+
onClick={onPrev}
|
|
59
|
+
disabled={atStart}
|
|
60
|
+
aria-label="Previous block"
|
|
61
|
+
data-tooltip="Previous block"
|
|
62
|
+
>
|
|
63
|
+
<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true">
|
|
64
|
+
<path
|
|
65
|
+
d="M9 2.5 L4.5 7 L9 11.5"
|
|
66
|
+
stroke="currentColor"
|
|
67
|
+
strokeWidth="1.6"
|
|
68
|
+
strokeLinecap="round"
|
|
69
|
+
strokeLinejoin="round"
|
|
70
|
+
fill="none"
|
|
71
|
+
/>
|
|
72
|
+
</svg>
|
|
73
|
+
<span>Previous</span>
|
|
74
|
+
</button>
|
|
75
|
+
<span className="squisq-block-card-position" aria-live="polite">
|
|
76
|
+
Block {position} of {total}
|
|
77
|
+
</span>
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
className="squisq-block-card-button squisq-block-card-next"
|
|
81
|
+
onClick={onNext}
|
|
82
|
+
disabled={atEnd}
|
|
83
|
+
aria-label="Next block"
|
|
84
|
+
data-tooltip="Next block"
|
|
85
|
+
>
|
|
86
|
+
<span>Next</span>
|
|
87
|
+
<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true">
|
|
88
|
+
<path
|
|
89
|
+
d="M5 2.5 L9.5 7 L5 11.5"
|
|
90
|
+
stroke="currentColor"
|
|
91
|
+
strokeWidth="1.6"
|
|
92
|
+
strokeLinecap="round"
|
|
93
|
+
strokeLinejoin="round"
|
|
94
|
+
fill="none"
|
|
95
|
+
/>
|
|
96
|
+
</svg>
|
|
97
|
+
</button>
|
|
98
|
+
{onAdd && (
|
|
99
|
+
<button
|
|
100
|
+
type="button"
|
|
101
|
+
className="squisq-block-card-button squisq-block-card-add"
|
|
102
|
+
onClick={onAdd}
|
|
103
|
+
aria-label="Add block"
|
|
104
|
+
data-tooltip="Add block after this one"
|
|
105
|
+
>
|
|
106
|
+
<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true">
|
|
107
|
+
<path
|
|
108
|
+
d="M7 3 L7 11 M3 7 L11 7"
|
|
109
|
+
stroke="currentColor"
|
|
110
|
+
strokeWidth="1.6"
|
|
111
|
+
strokeLinecap="round"
|
|
112
|
+
fill="none"
|
|
113
|
+
/>
|
|
114
|
+
</svg>
|
|
115
|
+
<span>Add block</span>
|
|
116
|
+
</button>
|
|
117
|
+
)}
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BlockPreviewPanel
|
|
3
|
+
*
|
|
4
|
+
* A large, live preview of the active block, shown to the right of the
|
|
5
|
+
* block-at-a-time / timeline editing card when "Show block previews" is on.
|
|
6
|
+
* In document mode that toggle drives the {@link InlinePreviewGutter} (a
|
|
7
|
+
* column of small cards aligned to headings); in card mode there's only one
|
|
8
|
+
* block in view, so a single full-size preview reads better than a gutter.
|
|
9
|
+
*
|
|
10
|
+
* Reuses the shared preview pipeline (`resolveBlockVisual` → `BlockRenderer`)
|
|
11
|
+
* so the frame matches the slideshow and the timeline thumbnails exactly,
|
|
12
|
+
* honoring the active theme and viewport from the preview settings.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useMemo } from 'react';
|
|
16
|
+
import type { ViewportConfig } from '@bendyline/squisq/schemas';
|
|
17
|
+
import { VIEWPORT_PRESETS } from '@bendyline/squisq/schemas';
|
|
18
|
+
import { flattenBlocks, DEFAULT_THEME } from '@bendyline/squisq/doc';
|
|
19
|
+
import { useEditorContext } from './EditorContext';
|
|
20
|
+
import { usePreviewSettingsOptional } from './PreviewControls';
|
|
21
|
+
import { BlockThumbnail } from './TimelineBlockPreview';
|
|
22
|
+
import { resolveBlockVisual } from './resolveBlockVisual';
|
|
23
|
+
|
|
24
|
+
export interface BlockPreviewPanelProps {
|
|
25
|
+
basePath?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function BlockPreviewPanel({ basePath = '/' }: BlockPreviewPanelProps) {
|
|
29
|
+
const { doc, activeBlockStartLine, mediaProvider } = useEditorContext();
|
|
30
|
+
const previewSettings = usePreviewSettingsOptional();
|
|
31
|
+
const theme = previewSettings?.activeTheme ?? DEFAULT_THEME;
|
|
32
|
+
const viewport: ViewportConfig = previewSettings?.activeViewport ?? VIEWPORT_PRESETS.landscape;
|
|
33
|
+
|
|
34
|
+
// The block the card editor is scoped to — matched by its heading's source
|
|
35
|
+
// line (same key the timeline + outline use), falling back to the first
|
|
36
|
+
// block (e.g. when a heading-less preamble is active).
|
|
37
|
+
const block = useMemo(() => {
|
|
38
|
+
if (!doc) return null;
|
|
39
|
+
const blocks = flattenBlocks(doc.blocks);
|
|
40
|
+
if (blocks.length === 0) return null;
|
|
41
|
+
return (
|
|
42
|
+
blocks.find((b) => b.sourceHeading?.position?.start.line === activeBlockStartLine) ??
|
|
43
|
+
blocks[0]
|
|
44
|
+
);
|
|
45
|
+
}, [doc, activeBlockStartLine]);
|
|
46
|
+
|
|
47
|
+
const visual = useMemo(
|
|
48
|
+
() => (doc && block ? resolveBlockVisual(doc, block, theme, viewport) : null),
|
|
49
|
+
[doc, block, theme, viewport],
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
if (!visual) return null;
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div className="squisq-block-preview-panel" data-testid="block-preview-panel" aria-hidden>
|
|
56
|
+
<div
|
|
57
|
+
className="squisq-block-preview-frame"
|
|
58
|
+
style={{ aspectRatio: `${viewport.width} / ${viewport.height}` }}
|
|
59
|
+
>
|
|
60
|
+
<BlockThumbnail
|
|
61
|
+
visual={visual}
|
|
62
|
+
viewport={viewport}
|
|
63
|
+
basePath={basePath}
|
|
64
|
+
mediaProvider={mediaProvider}
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BlockPropertiesPopover
|
|
3
|
+
*
|
|
4
|
+
* The on-canvas "block properties" palette — the sibling of the block-template
|
|
5
|
+
* badge. Anchored at the `.squisq-props-badge` chip on a heading, it edits the
|
|
6
|
+
* block's playback/animation metadata, all stored in the heading's Pandoc `{…}`
|
|
7
|
+
* attribute block (`dataBlockAttrs`):
|
|
8
|
+
*
|
|
9
|
+
* - Transition (type / direction / duration) — reuses `TransitionPicker`
|
|
10
|
+
* - Duration (`duration`) — how long the block is shown
|
|
11
|
+
* - Start time (`startTime`) — timeline position
|
|
12
|
+
*
|
|
13
|
+
* The popover holds the `dataBlockAttrs` inner string as working state and
|
|
14
|
+
* re-derives each control from it, so successive edits compose. Every change
|
|
15
|
+
* serializes a new inner and bubbles up through `onChange`; the host applies it
|
|
16
|
+
* to the heading node. Positioning/portal/outside-click mirror
|
|
17
|
+
* `TemplateBadgePopover`.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { useEffect, useState } from 'react';
|
|
21
|
+
import { createPortal } from 'react-dom';
|
|
22
|
+
import { TransitionPicker } from './TransitionPicker';
|
|
23
|
+
import {
|
|
24
|
+
readBlockAttrsTransition,
|
|
25
|
+
setBlockAttrsTransition,
|
|
26
|
+
type TransitionFields,
|
|
27
|
+
} from './headingTransition';
|
|
28
|
+
import { readBlockAttrsValue, setBlockAttrsValue } from './blockProperties';
|
|
29
|
+
|
|
30
|
+
export interface BlockPropertiesPopoverProps {
|
|
31
|
+
/** DOMRect of the badge that triggered the popover (viewport coords). */
|
|
32
|
+
anchorRect: DOMRect;
|
|
33
|
+
/** Current `dataBlockAttrs` inner (Pandoc), or null when unset. */
|
|
34
|
+
blockAttrs: string | null;
|
|
35
|
+
/** `dataTemplateParams`, so a hand-typed `{[… transition=]}` reads through. */
|
|
36
|
+
templateParams: string | null;
|
|
37
|
+
/** Apply a new `dataBlockAttrs` inner to the heading (null clears it). */
|
|
38
|
+
onChange: (nextInner: string | null) => void;
|
|
39
|
+
onClose: () => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const PANEL_ID = 'squisq-block-props-portal';
|
|
43
|
+
/** The nested transition flyout is portaled out here; don't treat it as "outside". */
|
|
44
|
+
const TRANSITION_FLYOUT = '.squisq-transition-flyout';
|
|
45
|
+
|
|
46
|
+
export function BlockPropertiesPopover({
|
|
47
|
+
anchorRect,
|
|
48
|
+
blockAttrs,
|
|
49
|
+
templateParams,
|
|
50
|
+
onChange,
|
|
51
|
+
onClose,
|
|
52
|
+
}: BlockPropertiesPopoverProps) {
|
|
53
|
+
// Working copy of the Pandoc inner; successive edits compose off it.
|
|
54
|
+
const [inner, setInner] = useState<string | null>(blockAttrs);
|
|
55
|
+
const [style, setStyle] = useState<React.CSSProperties>(() => computeStyle(anchorRect));
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
requestAnimationFrame(() => setStyle(computeStyle(anchorRect)));
|
|
59
|
+
}, [anchorRect]);
|
|
60
|
+
|
|
61
|
+
// Outside click + Escape close. A click inside the popover OR inside the
|
|
62
|
+
// nested transition flyout (portaled elsewhere in the DOM) is kept open.
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
const onKey = (e: KeyboardEvent) => {
|
|
65
|
+
if (e.key === 'Escape') onClose();
|
|
66
|
+
};
|
|
67
|
+
const onMouse = (e: MouseEvent) => {
|
|
68
|
+
const target = e.target as HTMLElement;
|
|
69
|
+
const inPanel = document.getElementById(PANEL_ID)?.contains(target);
|
|
70
|
+
const inFlyout = target.closest?.(TRANSITION_FLYOUT);
|
|
71
|
+
if (!inPanel && !inFlyout) onClose();
|
|
72
|
+
};
|
|
73
|
+
const id = requestAnimationFrame(() => document.addEventListener('mousedown', onMouse));
|
|
74
|
+
document.addEventListener('keydown', onKey);
|
|
75
|
+
return () => {
|
|
76
|
+
cancelAnimationFrame(id);
|
|
77
|
+
document.removeEventListener('mousedown', onMouse);
|
|
78
|
+
document.removeEventListener('keydown', onKey);
|
|
79
|
+
};
|
|
80
|
+
}, [onClose]);
|
|
81
|
+
|
|
82
|
+
const apply = (next: string | null) => {
|
|
83
|
+
setInner(next);
|
|
84
|
+
onChange(next);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const transition = readBlockAttrsTransition(inner, templateParams);
|
|
88
|
+
const duration = readBlockAttrsValue(inner, 'duration');
|
|
89
|
+
const startTime = readBlockAttrsValue(inner, 'startTime');
|
|
90
|
+
|
|
91
|
+
const onTransition = (next: TransitionFields) => apply(setBlockAttrsTransition(inner, next));
|
|
92
|
+
const onDuration = (v: string) => apply(setBlockAttrsValue(inner, 'duration', v));
|
|
93
|
+
const onStartTime = (v: string) => apply(setBlockAttrsValue(inner, 'startTime', v));
|
|
94
|
+
|
|
95
|
+
return createPortal(
|
|
96
|
+
<div
|
|
97
|
+
id={PANEL_ID}
|
|
98
|
+
className="squisq-block-props-popover"
|
|
99
|
+
role="dialog"
|
|
100
|
+
aria-label="Block properties"
|
|
101
|
+
style={style}
|
|
102
|
+
>
|
|
103
|
+
<div className="squisq-block-props-row">
|
|
104
|
+
<span className="squisq-block-props-label">Transition</span>
|
|
105
|
+
<TransitionPicker value={transition} onChange={onTransition} />
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<div className="squisq-block-props-row">
|
|
109
|
+
<span className="squisq-block-props-label">Duration</span>
|
|
110
|
+
<NumberField
|
|
111
|
+
value={duration}
|
|
112
|
+
placeholder="auto"
|
|
113
|
+
unit="sec"
|
|
114
|
+
ariaLabel="Block duration in seconds"
|
|
115
|
+
onChange={onDuration}
|
|
116
|
+
/>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<div className="squisq-block-props-row">
|
|
120
|
+
<span className="squisq-block-props-label">Start time</span>
|
|
121
|
+
<NumberField
|
|
122
|
+
value={startTime}
|
|
123
|
+
placeholder="—"
|
|
124
|
+
unit="sec"
|
|
125
|
+
ariaLabel="Block start time in seconds"
|
|
126
|
+
onChange={onStartTime}
|
|
127
|
+
/>
|
|
128
|
+
</div>
|
|
129
|
+
</div>,
|
|
130
|
+
document.body,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function NumberField({
|
|
135
|
+
value,
|
|
136
|
+
placeholder,
|
|
137
|
+
unit,
|
|
138
|
+
ariaLabel,
|
|
139
|
+
onChange,
|
|
140
|
+
}: {
|
|
141
|
+
value: string;
|
|
142
|
+
placeholder: string;
|
|
143
|
+
unit: string;
|
|
144
|
+
ariaLabel: string;
|
|
145
|
+
onChange: (v: string) => void;
|
|
146
|
+
}) {
|
|
147
|
+
return (
|
|
148
|
+
<div className="squisq-block-props-numfield">
|
|
149
|
+
<input
|
|
150
|
+
type="number"
|
|
151
|
+
min="0"
|
|
152
|
+
step="0.1"
|
|
153
|
+
className="squisq-block-props-input"
|
|
154
|
+
placeholder={placeholder}
|
|
155
|
+
value={value}
|
|
156
|
+
onChange={(e) => onChange(e.target.value)}
|
|
157
|
+
aria-label={ariaLabel}
|
|
158
|
+
/>
|
|
159
|
+
<span className="squisq-block-props-unit">{unit}</span>
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Place the panel below the badge, flipping above / clamping to the viewport. */
|
|
165
|
+
function computeStyle(rect: DOMRect): React.CSSProperties {
|
|
166
|
+
const panel = document.getElementById(PANEL_ID)?.getBoundingClientRect();
|
|
167
|
+
const width = panel?.width ?? 280;
|
|
168
|
+
const height = panel?.height ?? 160;
|
|
169
|
+
const margin = 8;
|
|
170
|
+
const gap = 6;
|
|
171
|
+
const vw = window.innerWidth;
|
|
172
|
+
const vh = window.innerHeight;
|
|
173
|
+
|
|
174
|
+
const spaceBelow = vh - rect.bottom - margin;
|
|
175
|
+
let top: number;
|
|
176
|
+
if (height + gap <= spaceBelow || rect.top - margin < height + gap) {
|
|
177
|
+
top = rect.bottom + gap;
|
|
178
|
+
} else {
|
|
179
|
+
top = rect.top - height - gap;
|
|
180
|
+
}
|
|
181
|
+
const maxLeft = Math.max(margin, vw - width - margin);
|
|
182
|
+
const left = Math.min(Math.max(margin, rect.left), maxLeft);
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
position: 'fixed',
|
|
186
|
+
top,
|
|
187
|
+
left,
|
|
188
|
+
maxHeight: `${vh - 2 * margin}px`,
|
|
189
|
+
zIndex: 9999,
|
|
190
|
+
};
|
|
191
|
+
}
|