@bendyline/squisq-editor-react 2.0.1 → 2.2.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/LICENSE +21 -0
- package/NOTICE.md +40 -0
- package/README.md +10 -2
- package/dist/index.d.ts +683 -60
- package/dist/index.js +9648 -3750
- package/dist/monaco.js +0 -1
- package/dist/styles/index.css +1160 -241
- package/package.json +10 -6
- package/dist/index.js.map +0 -1
- package/dist/monaco.js.map +0 -1
- package/src/BlockCardView.tsx +0 -121
- package/src/BlockPreviewPanel.tsx +0 -69
- package/src/BlockPropertiesPopover.tsx +0 -222
- package/src/DocumentSettingsDialog.tsx +0 -272
- package/src/DropZoneOverlay.tsx +0 -137
- package/src/EditorContext.tsx +0 -1247
- package/src/EditorShell.tsx +0 -1334
- package/src/EmojiPicker.tsx +0 -332
- package/src/FolderView.tsx +0 -131
- package/src/Icon.tsx +0 -26
- package/src/ImageEditor.tsx +0 -395
- package/src/ImageNodeView.tsx +0 -284
- package/src/ImageViewer.tsx +0 -221
- package/src/InlineIcon.ts +0 -84
- package/src/InlinePreviewGutter.tsx +0 -590
- package/src/LinkDialog.tsx +0 -276
- package/src/MediaBin.tsx +0 -500
- package/src/MentionExtension.tsx +0 -261
- package/src/OutlinePanel.tsx +0 -547
- package/src/PlainHtmlPreview.tsx +0 -277
- package/src/PreviewControls.tsx +0 -1250
- package/src/PreviewPanel.tsx +0 -422
- package/src/RawEditor.tsx +0 -731
- package/src/RecorderEntry.tsx +0 -159
- package/src/StatusBar.tsx +0 -51
- package/src/TemplateAnnotation.ts +0 -167
- package/src/TemplateContentPreview.tsx +0 -56
- package/src/TemplatePicker.tsx +0 -1314
- package/src/ThemeCustomizerPanel.tsx +0 -304
- package/src/ThemePicker.tsx +0 -433
- package/src/TimelineBlockPreview.tsx +0 -37
- package/src/TimelineTrack.tsx +0 -671
- package/src/Toolbar.tsx +0 -2587
- package/src/Tooltip.tsx +0 -142
- package/src/TransitionPicker.tsx +0 -390
- package/src/VersionHistoryPanel.tsx +0 -359
- package/src/ViewMenuPanel.tsx +0 -189
- package/src/ViewSwitcher.tsx +0 -56
- package/src/WysiwygEditor.tsx +0 -792
- package/src/__tests__/blockProperties.test.ts +0 -92
- package/src/__tests__/blockPropertiesPopoverTheme.test.tsx +0 -33
- package/src/__tests__/blockRange.test.ts +0 -105
- package/src/__tests__/blockTagActivity.test.ts +0 -183
- package/src/__tests__/buildDocumentPreviewMarkdown.test.ts +0 -75
- package/src/__tests__/buildPreviewDocContent.test.ts +0 -48
- package/src/__tests__/buildPreviewDocTransition.test.ts +0 -92
- package/src/__tests__/canvasSurfaceTextEditing.test.tsx +0 -60
- package/src/__tests__/codeContextSectionView.test.tsx +0 -97
- package/src/__tests__/codeContextZoneManager.test.ts +0 -127
- package/src/__tests__/createShapeLayer.test.ts +0 -46
- package/src/__tests__/customLayoutManagerTheme.test.tsx +0 -34
- package/src/__tests__/detectMarkdown.test.ts +0 -73
- package/src/__tests__/diffContextSections.test.ts +0 -39
- package/src/__tests__/documentSettingsDialog.test.tsx +0 -197
- package/src/__tests__/drawingShapeRoundTrip.test.ts +0 -49
- package/src/__tests__/editorScrollHandoff.test.tsx +0 -102
- package/src/__tests__/editorScrollSync.test.ts +0 -65
- package/src/__tests__/editorShellCodeContext.test.tsx +0 -86
- package/src/__tests__/editorShellProps.test.tsx +0 -485
- package/src/__tests__/embeddedMedia.test.ts +0 -48
- package/src/__tests__/emojiPicker.test.tsx +0 -93
- package/src/__tests__/fileKind.test.ts +0 -112
- package/src/__tests__/headingTransition.test.ts +0 -176
- package/src/__tests__/imageEditAffordance.test.tsx +0 -272
- package/src/__tests__/imageEditModalContract.test.tsx +0 -119
- package/src/__tests__/imageEditorShell.test.tsx +0 -94
- package/src/__tests__/imageEditorState.test.ts +0 -171
- package/src/__tests__/inlinePreviewGutter.test.tsx +0 -62
- package/src/__tests__/inlinePreviewGutterAllBlocks.test.tsx +0 -103
- package/src/__tests__/jsonEditor.test.tsx +0 -223
- package/src/__tests__/layersPanel.test.tsx +0 -167
- package/src/__tests__/layoutChildRoundTrip.test.ts +0 -71
- package/src/__tests__/linkDialogDocPicker.test.tsx +0 -137
- package/src/__tests__/majorApiTypes.test.ts +0 -26
- package/src/__tests__/mediaAttachmentFlow.test.ts +0 -110
- package/src/__tests__/mediaBinDrop.test.tsx +0 -90
- package/src/__tests__/mediaReferences.test.ts +0 -82
- package/src/__tests__/narrationSave.test.ts +0 -148
- package/src/__tests__/outlinePanel.test.tsx +0 -169
- package/src/__tests__/outlineSource.test.ts +0 -217
- package/src/__tests__/plainHtmlPreview.test.tsx +0 -125
- package/src/__tests__/presentationMode.test.tsx +0 -290
- package/src/__tests__/previewControls.test.tsx +0 -540
- package/src/__tests__/previewPanelAudioFailure.test.tsx +0 -46
- package/src/__tests__/previewPanelPresentation.test.tsx +0 -126
- package/src/__tests__/previewPanelTransforms.test.tsx +0 -117
- package/src/__tests__/propertiesPanel.test.tsx +0 -69
- package/src/__tests__/rawEditorModelIsolation.test.ts +0 -19
- package/src/__tests__/recorderFormats.test.ts +0 -146
- package/src/__tests__/recorderMediaInsert.test.ts +0 -86
- package/src/__tests__/recorderTheme.test.tsx +0 -42
- package/src/__tests__/recorderTimingJson.test.ts +0 -41
- package/src/__tests__/resolveBlockVisual.test.ts +0 -43
- package/src/__tests__/selectionConversions.test.ts +0 -80
- package/src/__tests__/statusBar.test.tsx +0 -27
- package/src/__tests__/teleprompter.test.tsx +0 -190
- package/src/__tests__/templateAnnotationRoundTrip.test.ts +0 -99
- package/src/__tests__/templateContentPreview.test.ts +0 -101
- package/src/__tests__/templatePickerMetadata.test.ts +0 -32
- package/src/__tests__/templatePickerPortal.test.tsx +0 -62
- package/src/__tests__/timelineSource.test.ts +0 -134
- package/src/__tests__/tiptapBridge.test.ts +0 -566
- package/src/__tests__/tiptapBridgeConformance.test.ts +0 -47
- package/src/__tests__/tiptapCodeBlockRoundTrip.test.ts +0 -109
- package/src/__tests__/tiptapImageRoundTrip.test.ts +0 -73
- package/src/__tests__/toolbarSelectionConversion.test.tsx +0 -164
- package/src/__tests__/tooltip.test.tsx +0 -72
- package/src/__tests__/transformStyleId.test.ts +0 -13
- package/src/__tests__/transitionCatalog.test.ts +0 -64
- package/src/__tests__/useBlockNavigator.test.tsx +0 -67
- package/src/__tests__/useImageEditor.test.tsx +0 -226
- package/src/__tests__/useJsonEditorTokens.test.ts +0 -59
- package/src/__tests__/useMediaRecorder.test.ts +0 -302
- package/src/__tests__/useTimelineClock.test.ts +0 -21
- package/src/__tests__/versionHistory.test.tsx +0 -197
- package/src/__tests__/viewMenuPanel.test.tsx +0 -130
- package/src/__tests__/wysiwygImageUpload.test.ts +0 -69
- package/src/__tests__/wysiwygTemplateBadgeFocus.test.tsx +0 -56
- package/src/asciiDiagram/AsciiDiagramExtension.ts +0 -343
- package/src/asciiDiagram/AsciiDiagramWidget.tsx +0 -254
- package/src/asciiDiagram/RepairableDiagramExtension.ts +0 -200
- package/src/asciiDiagram/__tests__/AsciiDiagramExtension.test.ts +0 -145
- package/src/asciiDiagram/__tests__/RepairableDiagramExtension.test.ts +0 -159
- package/src/asciiDiagram/__tests__/asciiDiagramCommands.test.ts +0 -284
- package/src/asciiDiagram/__tests__/asciiDiagramOps.test.ts +0 -114
- package/src/asciiDiagram/__tests__/asciiPaste.test.ts +0 -43
- package/src/asciiDiagram/asciiDiagramCommands.ts +0 -174
- package/src/asciiDiagram/asciiDiagramData.ts +0 -149
- package/src/asciiDiagram/asciiDiagramOps.ts +0 -238
- package/src/asciiDiagram/asciiPaste.ts +0 -19
- package/src/blockProperties.ts +0 -88
- package/src/blockRange.ts +0 -132
- package/src/blockSlice.ts +0 -75
- package/src/blockTagActivity.ts +0 -233
- package/src/buildDocumentPreviewMarkdown.ts +0 -168
- package/src/buildPreviewDoc.ts +0 -405
- package/src/codeContext/CodeContextSectionView.tsx +0 -124
- package/src/codeContext/CodeContextZoneManager.ts +0 -149
- package/src/codeContext/CodeContextZones.tsx +0 -121
- package/src/codeContext/diffContextSections.ts +0 -38
- package/src/codeContext/types.ts +0 -75
- package/src/customTemplates/AddBin.tsx +0 -126
- package/src/customTemplates/CustomLayoutManager.tsx +0 -234
- package/src/customTemplates/CustomTemplateContext.tsx +0 -188
- package/src/customTemplates/LayerToolbar.tsx +0 -580
- package/src/customTemplates/ShapeGlyph.tsx +0 -47
- package/src/customTemplates/TemplateDesigner.tsx +0 -433
- package/src/customTemplates/__tests__/library.test.ts +0 -96
- package/src/customTemplates/__tests__/normalizePositions.test.ts +0 -109
- package/src/customTemplates/__tests__/shapeDefs.test.ts +0 -49
- package/src/customTemplates/__tests__/useMemoryLayerAdapter.test.ts +0 -108
- package/src/customTemplates/designer.css +0 -732
- package/src/customTemplates/index.ts +0 -31
- package/src/customTemplates/library.ts +0 -110
- package/src/customTemplates/normalizePositions.ts +0 -75
- package/src/customTemplates/shapeDefs.ts +0 -131
- package/src/customTemplates/thumbnail.tsx +0 -63
- package/src/customTemplates/tokenDefs.ts +0 -60
- package/src/customTemplates/useDocCustomTemplates.ts +0 -52
- package/src/customTemplates/useMemoryLayerAdapter.ts +0 -129
- package/src/customThemes/CustomThemeContext.tsx +0 -185
- package/src/customThemes/CustomThemeDialog.tsx +0 -316
- package/src/customThemes/ImportThemeSection.tsx +0 -178
- package/src/customThemes/__tests__/CustomThemeContext.test.tsx +0 -64
- package/src/customThemes/__tests__/CustomThemeDialog.test.tsx +0 -47
- package/src/customThemes/__tests__/customThemeLibrary.test.ts +0 -60
- package/src/customThemes/__tests__/importThemeSection.test.tsx +0 -192
- package/src/customThemes/customThemeLibrary.ts +0 -100
- package/src/customThemes/index.ts +0 -37
- package/src/customThemes/themeControls.tsx +0 -229
- package/src/customThemes/themeDraft.ts +0 -288
- package/src/customThemes/useDocCustomThemes.ts +0 -49
- package/src/detectMarkdown.ts +0 -62
- package/src/diagram/DiagramCanvas.tsx +0 -259
- package/src/diagram/DiagramMaximizedOverlay.tsx +0 -46
- package/src/diagram/diagramCommands.ts +0 -604
- package/src/diagram/diagramConstants.ts +0 -17
- package/src/diagram/types.ts +0 -35
- package/src/editorScrollSync.ts +0 -73
- package/src/embeddedMedia.ts +0 -78
- package/src/emojiData.ts +0 -1317
- package/src/fileKind.ts +0 -158
- package/src/frontmatter.ts +0 -29
- package/src/frontmatterSettings.ts +0 -23
- package/src/headingTransition.ts +0 -291
- package/src/hooks/useFileDrop.ts +0 -266
- package/src/imageEditor/CanvasSurface.tsx +0 -703
- package/src/imageEditor/ImageVersionHistoryDropdown.tsx +0 -396
- package/src/imageEditor/LayersPanel.tsx +0 -218
- package/src/imageEditor/PropertiesPanel.tsx +0 -474
- package/src/imageEditor/Toolbar.tsx +0 -459
- package/src/imageEditor/createShapeLayer.ts +0 -280
- package/src/imageEditor/icons.tsx +0 -68
- package/src/imageEditor/image-editor.css +0 -574
- package/src/imageEditor/layers/EditorImageLayer.tsx +0 -45
- package/src/imageEditor/layers/EditorShapeLayer.tsx +0 -62
- package/src/imageEditor/layers/EditorTextLayer.tsx +0 -45
- package/src/imageEditor/layers/SelectionHandles.tsx +0 -86
- package/src/imageEditor/state.ts +0 -173
- package/src/imageEditor/useImageEditor.ts +0 -385
- package/src/imageEditor/useImageEditorTokens.ts +0 -70
- package/src/index.ts +0 -470
- package/src/jsonEditor/EmbeddedRichTextField.tsx +0 -81
- package/src/jsonEditor/JsonEditor.tsx +0 -81
- package/src/jsonEditor/JsonEditorContext.tsx +0 -81
- package/src/jsonEditor/RenderNode.tsx +0 -87
- package/src/jsonEditor/editors.tsx +0 -745
- package/src/jsonEditor/index.ts +0 -2
- package/src/jsonEditor/json-editor.css +0 -463
- package/src/jsonEditor/useJsonEditorTokens.ts +0 -33
- package/src/mediaDragMime.ts +0 -32
- package/src/mediaEntries.ts +0 -12
- package/src/mediaReferences.ts +0 -299
- package/src/monaco.ts +0 -35
- package/src/monacoWorkers.ts +0 -89
- package/src/outlineSource.ts +0 -171
- package/src/presentation/PresentationMode.tsx +0 -596
- package/src/rawEditorIsolation.ts +0 -18
- package/src/recorder/RecorderButton.tsx +0 -80
- package/src/recorder/RecorderModal.tsx +0 -706
- package/src/recorder/RecorderPanel.tsx +0 -77
- package/src/recorder/formats.ts +0 -159
- package/src/recorder/hooks/useMediaRecorder.ts +0 -436
- package/src/recorder/hooks/useStreamPreview.ts +0 -47
- package/src/recorder/insertMediaBlock.ts +0 -30
- package/src/recorder/sources/cameraStream.ts +0 -32
- package/src/recorder/sources/micStream.ts +0 -25
- package/src/recorder/sources/screenStream.ts +0 -162
- package/src/recorder/timingJson.ts +0 -66
- package/src/resolveBlockVisual.ts +0 -39
- package/src/scene/Scene.tsx +0 -616
- package/src/scene/SceneBlockExtension.ts +0 -205
- package/src/scene/SceneBlockToolbar.tsx +0 -201
- package/src/scene/SceneBlockWidget.tsx +0 -441
- package/src/scene/ScenePropsBar.tsx +0 -85
- package/src/scene/SceneSelection.tsx +0 -111
- package/src/scene/SceneSideToolbar.tsx +0 -89
- package/src/scene/SceneViewport.tsx +0 -106
- package/src/scene/ShapePalette.tsx +0 -196
- package/src/scene/__tests__/DiagramAdapter.test.ts +0 -142
- package/src/scene/__tests__/SceneBlockExtension.test.ts +0 -33
- package/src/scene/__tests__/bezierEdit.test.ts +0 -85
- package/src/scene/__tests__/blockLayers.test.ts +0 -57
- package/src/scene/__tests__/sceneIsolation.test.tsx +0 -119
- package/src/scene/__tests__/shapeLayers.test.ts +0 -106
- package/src/scene/__tests__/useSceneHitTest.test.ts +0 -90
- package/src/scene/__tests__/useScenePanZoom.test.ts +0 -103
- package/src/scene/adapters/DiagramAdapter.ts +0 -79
- package/src/scene/adapters/DrawingAdapter.ts +0 -420
- package/src/scene/adapters/LayoutAdapter.ts +0 -313
- package/src/scene/adapters/blockLayers.ts +0 -159
- package/src/scene/commands/SceneCommand.ts +0 -84
- package/src/scene/commands/drawingCommands.ts +0 -318
- package/src/scene/commands/layoutCommands.ts +0 -301
- package/src/scene/hooks/useSceneHitTest.ts +0 -105
- package/src/scene/hooks/useScenePanZoom.ts +0 -147
- package/src/scene/hooks/useSceneSelection.ts +0 -62
- package/src/scene/index.ts +0 -90
- package/src/scene/layers/DiagramEdges.tsx +0 -142
- package/src/scene/layers/edgeGeometry.ts +0 -104
- package/src/scene/layers/nodeCard.tsx +0 -164
- package/src/scene/layers/renderLayer.tsx +0 -70
- package/src/scene/layers/shapeLayers.ts +0 -201
- package/src/scene/paths/bezierEdit.ts +0 -208
- package/src/scene/scene.css +0 -792
- package/src/scene/text/SceneTextOverlay.tsx +0 -161
- package/src/scene/text/sceneTextChannel.ts +0 -46
- package/src/scene/text/sceneTextConfig.ts +0 -31
- package/src/scene/text/sceneTiptap.ts +0 -36
- package/src/scene/text/useSceneTextEditing.ts +0 -39
- package/src/scene/tools/ConnectTool.ts +0 -209
- package/src/scene/tools/DrawingConnectTool.ts +0 -231
- package/src/scene/tools/PathTool.ts +0 -158
- package/src/scene/tools/PlaceTool.ts +0 -47
- package/src/scene/tools/SceneTool.ts +0 -85
- package/src/scene/tools/SelectTool.ts +0 -300
- package/src/scene/tools/ShapeTool.ts +0 -143
- package/src/scene/tools/TextTool.ts +0 -72
- package/src/scene/tools/TokenTool.ts +0 -95
- package/src/scene/tools/createDrawShapeTool.ts +0 -82
- package/src/selectionConversions.ts +0 -155
- package/src/styles/ascii-diagram.css +0 -79
- package/src/styles/ascii-timeline.css +0 -596
- package/src/styles/code-context.css +0 -155
- package/src/styles/diagram.css +0 -183
- package/src/styles/editor.css +0 -6811
- package/src/styles/folder-view.css +0 -210
- package/src/styles/image-edit-affordance.css +0 -201
- package/src/styles/index.css +0 -18
- package/src/styles/tree-view.css +0 -189
- package/src/teleprompter/TeleprompterControls.tsx +0 -218
- package/src/teleprompter/TeleprompterSelfView.tsx +0 -22
- package/src/teleprompter/TeleprompterSurface.tsx +0 -267
- package/src/teleprompter/TeleprompterView.tsx +0 -338
- package/src/teleprompter/canvasRenderer.ts +0 -161
- package/src/teleprompter/floatingWindow.ts +0 -352
- package/src/teleprompter/index.ts +0 -61
- package/src/teleprompter/pcmWorklet.ts +0 -62
- package/src/teleprompter/recording/insertPreamble.ts +0 -80
- package/src/teleprompter/recording/narrationSave.ts +0 -134
- package/src/teleprompter/recording/useNarrationRecorder.ts +0 -367
- package/src/teleprompter/scrollModel.ts +0 -85
- package/src/teleprompter/teleprompterTheme.ts +0 -303
- package/src/teleprompter/types.ts +0 -38
- package/src/teleprompter/useFloatingWindow.ts +0 -74
- package/src/teleprompter/useMicAnalysis.ts +0 -211
- package/src/teleprompter/useTeleprompter.ts +0 -421
- package/src/templateContentPreviewResolver.ts +0 -360
- package/src/timeline/TimelineEditorWidget.tsx +0 -861
- package/src/timeline/TimelineViewExtension.ts +0 -252
- package/src/timeline/__tests__/TimelineEditorWidget.test.tsx +0 -414
- package/src/timeline/__tests__/TimelineViewExtension.test.ts +0 -163
- package/src/timeline/__tests__/timelineCommands.test.ts +0 -359
- package/src/timeline/__tests__/timelineOps.test.ts +0 -277
- package/src/timeline/__tests__/timelinePaste.test.ts +0 -34
- package/src/timeline/timelineCommands.ts +0 -421
- package/src/timeline/timelineData.ts +0 -54
- package/src/timeline/timelineOps.ts +0 -381
- package/src/timeline/timelinePaste.ts +0 -8
- package/src/timelineSource.ts +0 -244
- package/src/tiptap/TiptapAudio.tsx +0 -86
- package/src/tiptap/TiptapVideo.tsx +0 -119
- package/src/tiptap/useResolvedMediaSrc.ts +0 -47
- package/src/tiptapBridge.ts +0 -986
- package/src/tooltipPlacement.ts +0 -13
- package/src/transformStyleId.ts +0 -17
- package/src/transitionCatalog.ts +0 -159
- package/src/treeview/TreeOutlineWidget.tsx +0 -390
- package/src/treeview/TreeViewExtension.ts +0 -250
- package/src/treeview/__tests__/TreeOutlineWidget.test.tsx +0 -156
- package/src/treeview/__tests__/TreeViewExtension.test.ts +0 -122
- package/src/treeview/__tests__/treeOps.test.ts +0 -177
- package/src/treeview/__tests__/treePaste.test.ts +0 -40
- package/src/treeview/__tests__/treeViewCommands.test.ts +0 -179
- package/src/treeview/treeOps.ts +0 -231
- package/src/treeview/treePaste.ts +0 -13
- package/src/treeview/treeViewCommands.ts +0 -99
- package/src/treeview/treeViewData.ts +0 -41
- package/src/types/monaco-shims.d.ts +0 -10
- package/src/useBlockNavigator.ts +0 -153
- package/src/useHeadingLayout.ts +0 -294
- package/src/useMonacoLoader.ts +0 -82
- package/src/useTimelineClock.ts +0 -76
- package/src/utils/collectInlineFontAwesomeCss.ts +0 -69
- package/src/utils/dropUtils.ts +0 -136
- package/src/utils/normalizeMalformedAssetUrl.ts +0 -22
- package/src/wysiwygImageUpload.ts +0 -113
package/src/buildPreviewDoc.ts
DELETED
|
@@ -1,405 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* buildPreviewDoc — Converts a markdown-derived Doc into a player-ready Doc
|
|
3
|
-
* with TemplateBlock slides and interleaved images.
|
|
4
|
-
*
|
|
5
|
-
* Shared between PreviewPanel (live preview) and export flows (HTML/video).
|
|
6
|
-
*
|
|
7
|
-
* Pipeline:
|
|
8
|
-
* 1. Flatten hierarchical blocks into a linear slide sequence
|
|
9
|
-
* 2. Convert each block into a TemplateBlock-compatible object
|
|
10
|
-
* 3. Interleave images as standalone imageWithCaption slides
|
|
11
|
-
* 4. Synthesize a dummy audio segment for timer-based playback
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { deriveTemplateInputs, flattenRenderableBlocks, hasTemplate } from '@bendyline/squisq/doc';
|
|
15
|
-
import { extractPlainText, KNOWN_BLOCK_META_KEYS } from '@bendyline/squisq/markdown';
|
|
16
|
-
import { getChildren } from '@bendyline/squisq/markdown';
|
|
17
|
-
import { iconMarker } from '@bendyline/squisq/icon-marker';
|
|
18
|
-
import type { IconFamily } from '@bendyline/squisq/icons';
|
|
19
|
-
import type { Block, Doc } from '@bendyline/squisq/schemas';
|
|
20
|
-
import type { MarkdownBlockNode, MarkdownList, MarkdownNode } from '@bendyline/squisq/markdown';
|
|
21
|
-
|
|
22
|
-
// ── Helpers ────────────────────────────────────────────────────────
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Like `extractPlainText`, but preserves inline icons as encoded markers so
|
|
26
|
-
* template text can render them as glyphs downstream (see `iconMarker` and
|
|
27
|
-
* `TextLayer`). Mirrors `extractPlainText`'s value/child/list handling; the
|
|
28
|
-
* only addition is the `inlineIcon` interception.
|
|
29
|
-
*/
|
|
30
|
-
function extractRichText(node: MarkdownNode): string {
|
|
31
|
-
if (node.type === 'inlineIcon') {
|
|
32
|
-
const icon = node as unknown as { family: IconFamily; name: string };
|
|
33
|
-
return iconMarker(icon.family, icon.name);
|
|
34
|
-
}
|
|
35
|
-
if ('value' in node && typeof (node as { value?: unknown }).value === 'string') {
|
|
36
|
-
return (node as { value: string }).value;
|
|
37
|
-
}
|
|
38
|
-
const children = getChildren(node);
|
|
39
|
-
const separator = node.type === 'list' || node.type === 'listItem' ? '\n' : '';
|
|
40
|
-
return children.map(extractRichText).join(separator);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function extractBodyText(contents: MarkdownBlockNode[] | undefined): string {
|
|
44
|
-
if (!contents || contents.length === 0) return '';
|
|
45
|
-
const parts: string[] = [];
|
|
46
|
-
for (const node of contents) {
|
|
47
|
-
parts.push(extractRichText(node));
|
|
48
|
-
}
|
|
49
|
-
return parts.join('\n').trim();
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
interface ExtractedImage {
|
|
53
|
-
src: string;
|
|
54
|
-
alt: string;
|
|
55
|
-
/** Explicit width from `<img width>` (markdown shorthand has none). */
|
|
56
|
-
width?: number;
|
|
57
|
-
/** Explicit height from `<img height>`. */
|
|
58
|
-
height?: number;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function parseDim(raw: string | undefined): number | undefined {
|
|
62
|
-
if (raw === undefined) return undefined;
|
|
63
|
-
const n = parseFloat(raw);
|
|
64
|
-
return Number.isFinite(n) && n > 0 ? n : undefined;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function extractBlockImages(contents: MarkdownBlockNode[] | undefined): ExtractedImage[] {
|
|
68
|
-
if (!contents || contents.length === 0) return [];
|
|
69
|
-
const images: ExtractedImage[] = [];
|
|
70
|
-
|
|
71
|
-
function walkHtml(node: unknown): void {
|
|
72
|
-
if (!node || typeof node !== 'object') return;
|
|
73
|
-
const n = node as Record<string, unknown>;
|
|
74
|
-
if (n.type === 'htmlElement' && (n.tagName as string).toLowerCase() === 'img') {
|
|
75
|
-
const attrs = n.attributes as Record<string, string> | undefined;
|
|
76
|
-
const src = attrs?.src;
|
|
77
|
-
if (typeof src === 'string' && src) {
|
|
78
|
-
images.push({
|
|
79
|
-
src,
|
|
80
|
-
alt: typeof attrs?.alt === 'string' ? attrs.alt : '',
|
|
81
|
-
width: parseDim(attrs?.width),
|
|
82
|
-
height: parseDim(attrs?.height),
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
if (Array.isArray(n.children)) {
|
|
87
|
-
for (const child of n.children) walkHtml(child);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function walk(node: MarkdownNode): void {
|
|
92
|
-
if ('type' in node && node.type === 'image' && 'url' in node) {
|
|
93
|
-
const img = node as { url: string; alt?: string };
|
|
94
|
-
if (img.url) {
|
|
95
|
-
images.push({ src: img.url, alt: img.alt ?? '' });
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
// Resized images round-trip as raw HTML. Pick them up so feature
|
|
99
|
-
// and slideshow previews show the actual image rather than a blank
|
|
100
|
-
// placeholder.
|
|
101
|
-
if ('type' in node && (node.type === 'htmlBlock' || node.type === 'htmlInline')) {
|
|
102
|
-
const html = node as unknown as { htmlChildren?: unknown[] };
|
|
103
|
-
for (const child of html.htmlChildren ?? []) walkHtml(child);
|
|
104
|
-
}
|
|
105
|
-
for (const child of getChildren(node)) {
|
|
106
|
-
walk(child);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
for (const node of contents) {
|
|
111
|
-
walk(node);
|
|
112
|
-
}
|
|
113
|
-
return images;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function collectAllDocImages(blocks: Block[]): Array<{ src: string; alt: string }> {
|
|
117
|
-
const seen = new Set<string>();
|
|
118
|
-
const images: Array<{ src: string; alt: string }> = [];
|
|
119
|
-
|
|
120
|
-
function walkBlocks(blockList: Block[]): void {
|
|
121
|
-
for (const block of blockList) {
|
|
122
|
-
for (const img of extractBlockImages(block.contents)) {
|
|
123
|
-
if (!seen.has(img.src)) {
|
|
124
|
-
seen.add(img.src);
|
|
125
|
-
images.push(img);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
if (block.children) {
|
|
129
|
-
walkBlocks(block.children);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
walkBlocks(blocks);
|
|
135
|
-
return images;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function extractListItems(contents: MarkdownBlockNode[] | undefined): string[] {
|
|
139
|
-
if (!contents) return [];
|
|
140
|
-
const items: string[] = [];
|
|
141
|
-
for (const node of contents) {
|
|
142
|
-
if (node.type === 'list') {
|
|
143
|
-
for (const item of (node as MarkdownList).children) {
|
|
144
|
-
const text = extractPlainText(item).trim();
|
|
145
|
-
if (text) items.push(text);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
return items;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function getTemplateDefaults(
|
|
153
|
-
templateName: string,
|
|
154
|
-
headingText: string,
|
|
155
|
-
block: Block,
|
|
156
|
-
): Record<string, unknown> {
|
|
157
|
-
const body = extractBodyText(block.contents);
|
|
158
|
-
|
|
159
|
-
switch (templateName) {
|
|
160
|
-
case 'statHighlight':
|
|
161
|
-
return (
|
|
162
|
-
deriveTemplateInputs(templateName, headingText, block.contents) ?? {
|
|
163
|
-
stat: headingText,
|
|
164
|
-
description: body || headingText,
|
|
165
|
-
}
|
|
166
|
-
);
|
|
167
|
-
case 'quote':
|
|
168
|
-
case 'fullBleedQuote':
|
|
169
|
-
case 'pullQuote':
|
|
170
|
-
return { quote: body || headingText };
|
|
171
|
-
case 'factCard':
|
|
172
|
-
return { fact: headingText, explanation: body || headingText };
|
|
173
|
-
case 'comparisonBar':
|
|
174
|
-
return { leftLabel: 'A', leftValue: 60, rightLabel: 'B', rightValue: 40 };
|
|
175
|
-
case 'list': {
|
|
176
|
-
const items = extractListItems(block.contents);
|
|
177
|
-
return { items: items.length > 0 ? items : ['Item 1', 'Item 2', 'Item 3'] };
|
|
178
|
-
}
|
|
179
|
-
case 'definitionCard':
|
|
180
|
-
return { term: headingText, definition: body || headingText };
|
|
181
|
-
case 'dateEvent':
|
|
182
|
-
return { date: headingText, description: body || headingText };
|
|
183
|
-
case 'leftFeature':
|
|
184
|
-
case 'rightFeature': {
|
|
185
|
-
// Feature blocks need imageSrc from body content and use the
|
|
186
|
-
// heading text as the visible title alongside the body paragraph.
|
|
187
|
-
const images = extractBlockImages(block.contents);
|
|
188
|
-
const img = images[0];
|
|
189
|
-
return {
|
|
190
|
-
imageSrc: img?.src ?? '',
|
|
191
|
-
imageAlt: img?.alt || headingText,
|
|
192
|
-
imageWidth: img?.width,
|
|
193
|
-
imageHeight: img?.height,
|
|
194
|
-
title: headingText,
|
|
195
|
-
body: body || headingText,
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
default:
|
|
199
|
-
return {};
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function blockToSlide(
|
|
204
|
-
block: Block,
|
|
205
|
-
index: number,
|
|
206
|
-
knownTemplates?: ReadonlySet<string>,
|
|
207
|
-
): Record<string, unknown> {
|
|
208
|
-
const headingText = block.sourceHeading
|
|
209
|
-
? extractPlainText(block.sourceHeading)
|
|
210
|
-
: block.title || block.id || `Slide ${index + 1}`;
|
|
211
|
-
|
|
212
|
-
const requestedTemplate = block.template || 'sectionHeader';
|
|
213
|
-
// A template is recognized if it's a built-in OR a user-defined
|
|
214
|
-
// template carried in the doc's `customTemplates` set. Without this,
|
|
215
|
-
// an annotated `{[hero]}` heading would silently fall back to
|
|
216
|
-
// `sectionHeader` because `hasTemplate` only knows built-ins.
|
|
217
|
-
const isCustomTemplate = knownTemplates?.has(requestedTemplate) ?? false;
|
|
218
|
-
const recognized = hasTemplate(requestedTemplate) || isCustomTemplate;
|
|
219
|
-
const template = recognized ? requestedTemplate : 'sectionHeader';
|
|
220
|
-
const defaults = getTemplateDefaults(template, headingText, block);
|
|
221
|
-
|
|
222
|
-
const {
|
|
223
|
-
id: _id,
|
|
224
|
-
startTime: _st,
|
|
225
|
-
duration: _d,
|
|
226
|
-
audioSegment: _as,
|
|
227
|
-
layers: _l,
|
|
228
|
-
transition: _tr,
|
|
229
|
-
template: _t,
|
|
230
|
-
title: _ti,
|
|
231
|
-
children: _c,
|
|
232
|
-
contents: _co,
|
|
233
|
-
sourceHeading: _sh,
|
|
234
|
-
templateOverrides: _to,
|
|
235
|
-
templateData: _td,
|
|
236
|
-
...extraFields
|
|
237
|
-
} = block as unknown as Record<string, unknown>;
|
|
238
|
-
|
|
239
|
-
return {
|
|
240
|
-
id: block.id,
|
|
241
|
-
template,
|
|
242
|
-
duration: block.duration,
|
|
243
|
-
audioSegment: 0,
|
|
244
|
-
// Respect the block's authored transition (set via the toolbar / on-canvas
|
|
245
|
-
// properties palette → `{…}` block attrs). Only fall back to a default fade
|
|
246
|
-
// for blocks past the first when the author hasn't chosen one; the first
|
|
247
|
-
// block has no previous slide to transition in from.
|
|
248
|
-
transition: block.transition ?? (index > 0 ? { type: 'fade', duration: 0.5 } : undefined),
|
|
249
|
-
title: headingText,
|
|
250
|
-
// Custom templates need access to the source block's body content
|
|
251
|
-
// + children so their token resolver (`{content}`, `{children}`,
|
|
252
|
-
// `{image:N}`) substitutes against the user's prose, not just the
|
|
253
|
-
// heading. Built-in templates don't read these fields and risk
|
|
254
|
-
// surprising overlap with their typed inputs, so we only attach
|
|
255
|
-
// them when the slide actually maps to a custom template.
|
|
256
|
-
...(isCustomTemplate && block.contents ? { contents: block.contents } : {}),
|
|
257
|
-
...(isCustomTemplate && block.children ? { children: block.children } : {}),
|
|
258
|
-
...defaults,
|
|
259
|
-
...extraFields,
|
|
260
|
-
// Structured body data (```json data fences, GFM tables for dataTable)
|
|
261
|
-
// carries typed values; `{[…]}` string overrides win last so an explicit
|
|
262
|
-
// annotation param can still pin any field.
|
|
263
|
-
//
|
|
264
|
-
// Block-meta keys (transition, startTime, duration, …) are the exception:
|
|
265
|
-
// they were already coerced to typed block fields above (e.g.
|
|
266
|
-
// `block.transition` → `{ type, duration, direction }`). Their raw string
|
|
267
|
-
// form also rides along in `templateData`/`templateOverrides` because the
|
|
268
|
-
// author wrote them inside `{[…]}`; left un-stripped, that string would
|
|
269
|
-
// spread back over the typed value here and clobber it — turning
|
|
270
|
-
// `transition=vortex` into the string `"vortex"`, which the player can't
|
|
271
|
-
// animate. Omit them from the content spreads so the typed fields win.
|
|
272
|
-
...omitBlockMeta(block.templateData),
|
|
273
|
-
...omitBlockMeta(block.templateOverrides),
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/** Keys coerced to typed block fields; must not be re-applied as raw strings. */
|
|
278
|
-
const BLOCK_META_KEYS: ReadonlySet<string> = new Set(Object.keys(KNOWN_BLOCK_META_KEYS));
|
|
279
|
-
|
|
280
|
-
/** Copy of a template-param record with block-meta keys removed. */
|
|
281
|
-
function omitBlockMeta(
|
|
282
|
-
data: Record<string, unknown> | undefined,
|
|
283
|
-
): Record<string, unknown> | undefined {
|
|
284
|
-
if (!data) return data;
|
|
285
|
-
let hit = false;
|
|
286
|
-
const out: Record<string, unknown> = {};
|
|
287
|
-
for (const key of Object.keys(data)) {
|
|
288
|
-
if (BLOCK_META_KEYS.has(key)) {
|
|
289
|
-
hit = true;
|
|
290
|
-
continue;
|
|
291
|
-
}
|
|
292
|
-
out[key] = data[key];
|
|
293
|
-
}
|
|
294
|
-
return hit ? out : data;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const IMAGE_MOTIONS: Array<'zoomIn' | 'zoomOut' | 'panLeft' | 'panRight'> = [
|
|
298
|
-
'zoomIn',
|
|
299
|
-
'zoomOut',
|
|
300
|
-
'panLeft',
|
|
301
|
-
'panRight',
|
|
302
|
-
];
|
|
303
|
-
|
|
304
|
-
// ── Public API ─────────────────────────────────────────────────────
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* Build a player-ready Doc from a markdown-derived Doc.
|
|
308
|
-
*
|
|
309
|
-
* Flattens hierarchical blocks, converts each to a TemplateBlock-compatible
|
|
310
|
-
* slide, interleaves images, recalculates timing, and adds a synthetic
|
|
311
|
-
* audio segment.
|
|
312
|
-
*/
|
|
313
|
-
export function buildPreviewDoc(doc: Doc): Doc {
|
|
314
|
-
// Container templates (`diagram`, `drawing`) render their children as
|
|
315
|
-
// nodes/shapes, so those children must not also become preview slides.
|
|
316
|
-
const flat = flattenRenderableBlocks(doc.blocks);
|
|
317
|
-
const allImages = collectAllDocImages(doc.blocks);
|
|
318
|
-
const usedImageSrcs = new Set<string>();
|
|
319
|
-
// Names of user-defined templates carried by the doc — passed into
|
|
320
|
-
// `blockToSlide` so heading annotations like `{[hero]}` aren't
|
|
321
|
-
// silently downgraded to `sectionHeader` when the template doesn't
|
|
322
|
-
// exist in the built-in registry.
|
|
323
|
-
const knownTemplates = doc.customTemplates
|
|
324
|
-
? new Set(doc.customTemplates.map((d) => d.name))
|
|
325
|
-
: undefined;
|
|
326
|
-
|
|
327
|
-
const slides: Record<string, unknown>[] = [];
|
|
328
|
-
let motionIndex = 0;
|
|
329
|
-
|
|
330
|
-
for (let i = 0; i < flat.length; i++) {
|
|
331
|
-
const block = flat[i];
|
|
332
|
-
const blockImages = extractBlockImages(block.contents);
|
|
333
|
-
const slide = blockToSlide(block, i, knownTemplates);
|
|
334
|
-
|
|
335
|
-
if (blockImages.length > 0 && slide.template === 'sectionHeader') {
|
|
336
|
-
const img = blockImages[0];
|
|
337
|
-
usedImageSrcs.add(img.src);
|
|
338
|
-
slide.template = 'imageWithCaption';
|
|
339
|
-
slide.imageSrc = img.src;
|
|
340
|
-
slide.imageAlt = img.alt;
|
|
341
|
-
slide.caption = slide.title as string;
|
|
342
|
-
slide.captionPosition = 'bottom';
|
|
343
|
-
slide.ambientMotion = IMAGE_MOTIONS[motionIndex++ % IMAGE_MOTIONS.length];
|
|
344
|
-
} else if (blockImages.length > 0) {
|
|
345
|
-
const img = blockImages[0];
|
|
346
|
-
usedImageSrcs.add(img.src);
|
|
347
|
-
if (!slide.accentImage) {
|
|
348
|
-
slide.accentImage = {
|
|
349
|
-
src: img.src,
|
|
350
|
-
alt: img.alt,
|
|
351
|
-
position: 'left-strip',
|
|
352
|
-
ambientMotion: IMAGE_MOTIONS[motionIndex++ % IMAGE_MOTIONS.length],
|
|
353
|
-
};
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
slides.push(slide);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// Interleave unused images
|
|
361
|
-
const unusedImages = allImages.filter((img) => !usedImageSrcs.has(img.src));
|
|
362
|
-
if (unusedImages.length > 0 && slides.length > 0) {
|
|
363
|
-
const interval = Math.max(2, Math.floor(slides.length / (unusedImages.length + 1)));
|
|
364
|
-
let insertOffset = 0;
|
|
365
|
-
for (let imgIdx = 0; imgIdx < unusedImages.length; imgIdx++) {
|
|
366
|
-
const insertAt = Math.min((imgIdx + 1) * interval + insertOffset, slides.length);
|
|
367
|
-
const img = unusedImages[imgIdx];
|
|
368
|
-
slides.splice(insertAt, 0, {
|
|
369
|
-
id: `img-interleave-${imgIdx}`,
|
|
370
|
-
template: 'imageWithCaption',
|
|
371
|
-
duration: 5,
|
|
372
|
-
audioSegment: 0,
|
|
373
|
-
imageSrc: img.src,
|
|
374
|
-
imageAlt: img.alt,
|
|
375
|
-
ambientMotion: IMAGE_MOTIONS[motionIndex++ % IMAGE_MOTIONS.length],
|
|
376
|
-
transition: { type: 'fade', duration: 0.5 },
|
|
377
|
-
});
|
|
378
|
-
insertOffset++;
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// Recalculate timing
|
|
383
|
-
let t = 0;
|
|
384
|
-
for (const slide of slides) {
|
|
385
|
-
slide.startTime = t;
|
|
386
|
-
t += slide.duration as number;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
const audio =
|
|
390
|
-
doc.audio?.segments?.length > 0
|
|
391
|
-
? doc.audio
|
|
392
|
-
: {
|
|
393
|
-
segments: t > 0 ? [{ src: '', name: 'preview', duration: t, startTime: 0 }] : [],
|
|
394
|
-
};
|
|
395
|
-
|
|
396
|
-
return {
|
|
397
|
-
// Preserve document-wide capabilities (custom themes, persistent layers,
|
|
398
|
-
// scheduled media, frontmatter, captions, and future schema fields).
|
|
399
|
-
// Preview preparation should replace only the slide/timing projection.
|
|
400
|
-
...doc,
|
|
401
|
-
duration: t,
|
|
402
|
-
blocks: slides as unknown as Block[],
|
|
403
|
-
audio,
|
|
404
|
-
};
|
|
405
|
-
}
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import { parseMarkdown } from '@bendyline/squisq/markdown';
|
|
2
|
-
import { MarkdownRenderer } from '@bendyline/squisq-react';
|
|
3
|
-
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
4
|
-
import type { CodeContext, CodeContextSection } from './types';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* One context section rendered inside a Monaco view zone: a single-line
|
|
8
|
-
* disclosure strip, plus the full markdown body while expanded. The body is
|
|
9
|
-
* rendered lazily — a file with 100 collapsed sections parses 100 one-liners,
|
|
10
|
-
* nothing more.
|
|
11
|
-
*/
|
|
12
|
-
export interface CodeContextSectionViewProps {
|
|
13
|
-
section: Omit<CodeContextSection, 'line'>;
|
|
14
|
-
expanded: boolean;
|
|
15
|
-
onToggle: (id: string) => void;
|
|
16
|
-
linkSchemes?: readonly string[] | undefined;
|
|
17
|
-
onLinkClick?: CodeContext['onLinkClick'] | undefined;
|
|
18
|
-
/** Native `#L<n>` handling: reveal that line in the editor. */
|
|
19
|
-
onRevealLine: (line: number) => void;
|
|
20
|
-
/** Reports the rendered content height so the zone can be resized to fit. */
|
|
21
|
-
onMeasure: (id: string, px: number) => void;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function CodeContextSectionView({
|
|
25
|
-
section,
|
|
26
|
-
expanded,
|
|
27
|
-
onToggle,
|
|
28
|
-
linkSchemes,
|
|
29
|
-
onLinkClick,
|
|
30
|
-
onRevealLine,
|
|
31
|
-
onMeasure,
|
|
32
|
-
}: CodeContextSectionViewProps) {
|
|
33
|
-
const rootRef = useRef<HTMLDivElement | null>(null);
|
|
34
|
-
|
|
35
|
-
const stripNodes = useMemo(
|
|
36
|
-
() => parseMarkdown(section.summaryMarkdown).children,
|
|
37
|
-
[section.summaryMarkdown],
|
|
38
|
-
);
|
|
39
|
-
const bodyNodes = useMemo(
|
|
40
|
-
() => (expanded && section.markdown ? parseMarkdown(section.markdown).children : null),
|
|
41
|
-
[expanded, section.markdown],
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
// Height feedback loop: report the content's real height whenever it
|
|
45
|
-
// changes. Monaco display:none's offscreen zones and ResizeObserver reports
|
|
46
|
-
// 0×0 for those — ignore zeros so scrolled-away zones keep their height.
|
|
47
|
-
useEffect(() => {
|
|
48
|
-
const el = rootRef.current;
|
|
49
|
-
if (!el || typeof ResizeObserver === 'undefined') return;
|
|
50
|
-
const report = () => {
|
|
51
|
-
const h = el.offsetHeight;
|
|
52
|
-
if (h > 0) onMeasure(section.id, h);
|
|
53
|
-
};
|
|
54
|
-
report();
|
|
55
|
-
const ro = new ResizeObserver(report);
|
|
56
|
-
ro.observe(el);
|
|
57
|
-
return () => ro.disconnect();
|
|
58
|
-
}, [section.id, onMeasure]);
|
|
59
|
-
|
|
60
|
-
// Delegated link interception. `#L<n>` reveals natively; everything else
|
|
61
|
-
// goes to the host callback (returning false opts back into default
|
|
62
|
-
// navigation).
|
|
63
|
-
const handleClick = useCallback(
|
|
64
|
-
(e: React.MouseEvent) => {
|
|
65
|
-
const anchor = (e.target as HTMLElement).closest?.('a');
|
|
66
|
-
if (!anchor) return;
|
|
67
|
-
const href = anchor.getAttribute('href') ?? '';
|
|
68
|
-
const lineMatch = /^#L(\d+)$/.exec(href);
|
|
69
|
-
if (lineMatch) {
|
|
70
|
-
e.preventDefault();
|
|
71
|
-
e.stopPropagation();
|
|
72
|
-
onRevealLine(Number(lineMatch[1]));
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
if (onLinkClick) {
|
|
76
|
-
const handled = onLinkClick(href, { sectionId: section.id });
|
|
77
|
-
if (handled !== false) {
|
|
78
|
-
e.preventDefault();
|
|
79
|
-
e.stopPropagation();
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
},
|
|
83
|
-
[onLinkClick, onRevealLine, section.id],
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
// Keep Monaco's container-level mousedown handler from hijacking clicks
|
|
87
|
-
// and text selection inside the section.
|
|
88
|
-
const stopMouseDown = useCallback((e: React.MouseEvent) => {
|
|
89
|
-
e.stopPropagation();
|
|
90
|
-
}, []);
|
|
91
|
-
|
|
92
|
-
return (
|
|
93
|
-
// Click handler is delegated anchor interception only; keyboard users
|
|
94
|
-
// reach links natively and the strip itself is a real <button>.
|
|
95
|
-
<div
|
|
96
|
-
ref={rootRef}
|
|
97
|
-
className={`squisq-ccx-section${expanded ? ' squisq-ccx-section--expanded' : ''}`}
|
|
98
|
-
onClick={handleClick}
|
|
99
|
-
onMouseDown={stopMouseDown}
|
|
100
|
-
>
|
|
101
|
-
<button
|
|
102
|
-
type="button"
|
|
103
|
-
className="squisq-ccx-strip"
|
|
104
|
-
aria-expanded={expanded}
|
|
105
|
-
onClick={() => onToggle(section.id)}
|
|
106
|
-
>
|
|
107
|
-
<span className="squisq-ccx-chevron" aria-hidden="true">
|
|
108
|
-
{expanded ? '▾' : '▸'}
|
|
109
|
-
</span>
|
|
110
|
-
<span className="squisq-ccx-strip-text">
|
|
111
|
-
<MarkdownRenderer nodes={stripNodes} {...(linkSchemes ? { linkSchemes } : {})} />
|
|
112
|
-
</span>
|
|
113
|
-
</button>
|
|
114
|
-
{expanded &&
|
|
115
|
-
(bodyNodes ? (
|
|
116
|
-
<div className="squisq-ccx-body">
|
|
117
|
-
<MarkdownRenderer nodes={bodyNodes} {...(linkSchemes ? { linkSchemes } : {})} />
|
|
118
|
-
</div>
|
|
119
|
-
) : (
|
|
120
|
-
<div className="squisq-ccx-body squisq-ccx-body--loading">Loading…</div>
|
|
121
|
-
))}
|
|
122
|
-
</div>
|
|
123
|
-
);
|
|
124
|
-
}
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import type { editor as MonacoEditorNs } from 'monaco-editor';
|
|
2
|
-
import { type ZoneSpec, diffContextSections } from './diffContextSections';
|
|
3
|
-
|
|
4
|
-
type MonacoEditor = MonacoEditorNs.IStandaloneCodeEditor;
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Owns the Monaco view zones behind the code-context sections. React-free —
|
|
8
|
-
* the React layer portals content into the zone dom nodes it hands out and
|
|
9
|
-
* feeds measured heights back via {@link setHeight}.
|
|
10
|
-
*
|
|
11
|
-
* Zone mechanics this leans on (monaco 0.50):
|
|
12
|
-
* - `changeViewZones` batches adds/removes/layouts into one whitespace
|
|
13
|
-
* re-layout.
|
|
14
|
-
* - `layoutZone(id)` re-reads the zone delegate, so mutating our own
|
|
15
|
-
* delegate's `afterLineNumber`/`heightInPx` then calling layoutZone both
|
|
16
|
-
* moves and resizes without remove/re-add churn (expanded DOM survives).
|
|
17
|
-
* - Zone dom nodes live in `.view-zones` (absolutely positioned, offscreen
|
|
18
|
-
* ones display:none'd by Monaco). They sit BELOW `.view-lines` in hit-test
|
|
19
|
-
* order, so the stylesheet raises `.squisq-ccx-zone` with a z-index and
|
|
20
|
-
* `pointer-events: auto` to make section content clickable.
|
|
21
|
-
* - Model swaps invalidate zone ids; the owner listens to onDidChangeModel
|
|
22
|
-
* and rebuilds.
|
|
23
|
-
*/
|
|
24
|
-
export class CodeContextZoneManager {
|
|
25
|
-
private readonly editor: MonacoEditor;
|
|
26
|
-
private entries = new Map<
|
|
27
|
-
string,
|
|
28
|
-
{
|
|
29
|
-
zoneId: string;
|
|
30
|
-
delegate: MonacoEditorNs.IViewZone;
|
|
31
|
-
domNode: HTMLDivElement;
|
|
32
|
-
spec: ZoneSpec;
|
|
33
|
-
}
|
|
34
|
-
>();
|
|
35
|
-
private listeners = new Set<() => void>();
|
|
36
|
-
private modelListener: { dispose(): void } | null = null;
|
|
37
|
-
private disposed = false;
|
|
38
|
-
|
|
39
|
-
/** Initial height estimate for a strip; the ResizeObserver corrects it. */
|
|
40
|
-
static readonly INITIAL_HEIGHT_PX = 28;
|
|
41
|
-
|
|
42
|
-
constructor(editor: MonacoEditor) {
|
|
43
|
-
this.editor = editor;
|
|
44
|
-
// A model swap (file change / external reset) invalidates every zone id.
|
|
45
|
-
// Drop our bookkeeping; the next sync() recreates zones against the new
|
|
46
|
-
// model.
|
|
47
|
-
this.modelListener = editor.onDidChangeModel(() => {
|
|
48
|
-
this.entries.clear();
|
|
49
|
-
this.emit();
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** Reconcile the live zones against `specs` in one changeViewZones batch. */
|
|
54
|
-
sync(specs: ZoneSpec[]): void {
|
|
55
|
-
if (this.disposed) return;
|
|
56
|
-
const prev = [...this.entries.values()].map((e) => e.spec);
|
|
57
|
-
const { add, remove, move } = diffContextSections(prev, specs);
|
|
58
|
-
if (add.length === 0 && remove.length === 0 && move.length === 0) return;
|
|
59
|
-
|
|
60
|
-
this.editor.changeViewZones((accessor) => {
|
|
61
|
-
for (const id of remove) {
|
|
62
|
-
const entry = this.entries.get(id);
|
|
63
|
-
if (!entry) continue;
|
|
64
|
-
accessor.removeZone(entry.zoneId);
|
|
65
|
-
this.entries.delete(id);
|
|
66
|
-
}
|
|
67
|
-
for (const spec of move) {
|
|
68
|
-
const entry = this.entries.get(spec.id);
|
|
69
|
-
if (!entry) continue;
|
|
70
|
-
entry.spec = spec;
|
|
71
|
-
entry.delegate.afterLineNumber = Math.max(spec.line - 1, 0);
|
|
72
|
-
setOrdinal(entry.delegate, spec.ordinal);
|
|
73
|
-
accessor.layoutZone(entry.zoneId);
|
|
74
|
-
}
|
|
75
|
-
for (const spec of add) {
|
|
76
|
-
const domNode = document.createElement('div');
|
|
77
|
-
domNode.className = 'squisq-ccx-zone';
|
|
78
|
-
domNode.dataset.sectionId = spec.id;
|
|
79
|
-
const delegate: MonacoEditorNs.IViewZone = {
|
|
80
|
-
afterLineNumber: Math.max(spec.line - 1, 0),
|
|
81
|
-
heightInPx: CodeContextZoneManager.INITIAL_HEIGHT_PX,
|
|
82
|
-
domNode,
|
|
83
|
-
suppressMouseDown: true,
|
|
84
|
-
};
|
|
85
|
-
setOrdinal(delegate, spec.ordinal);
|
|
86
|
-
const zoneId = accessor.addZone(delegate);
|
|
87
|
-
this.entries.set(spec.id, { zoneId, delegate, domNode, spec });
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
this.emit();
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** Measured-content feedback: resize the zone to fit its rendered DOM. */
|
|
94
|
-
setHeight(id: string, px: number): void {
|
|
95
|
-
if (this.disposed) return;
|
|
96
|
-
const entry = this.entries.get(id);
|
|
97
|
-
if (!entry) return;
|
|
98
|
-
const height = Math.max(Math.ceil(px), 1);
|
|
99
|
-
if (Math.abs((entry.delegate.heightInPx ?? 0) - height) < 1) return;
|
|
100
|
-
entry.delegate.heightInPx = height;
|
|
101
|
-
this.editor.changeViewZones((accessor) => {
|
|
102
|
-
accessor.layoutZone(entry.zoneId);
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
getDomNode(id: string): HTMLDivElement | undefined {
|
|
107
|
-
return this.entries.get(id)?.domNode;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
liveIds(): string[] {
|
|
111
|
-
return [...this.entries.keys()];
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/** Fires after sync/model-change so the React layer rebuilds its portals. */
|
|
115
|
-
onDidChangeZones(cb: () => void): () => void {
|
|
116
|
-
this.listeners.add(cb);
|
|
117
|
-
return () => this.listeners.delete(cb);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
dispose(): void {
|
|
121
|
-
if (this.disposed) return;
|
|
122
|
-
this.disposed = true;
|
|
123
|
-
this.modelListener?.dispose();
|
|
124
|
-
this.modelListener = null;
|
|
125
|
-
if (this.entries.size > 0) {
|
|
126
|
-
try {
|
|
127
|
-
this.editor.changeViewZones((accessor) => {
|
|
128
|
-
for (const entry of this.entries.values()) accessor.removeZone(entry.zoneId);
|
|
129
|
-
});
|
|
130
|
-
} catch {
|
|
131
|
-
// Editor already disposed — its zones died with it.
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
this.entries.clear();
|
|
135
|
-
this.listeners.clear();
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
private emit(): void {
|
|
139
|
-
for (const cb of [...this.listeners]) cb();
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* `ordinal` orders zones sharing an afterLineNumber (monaco 0.50 supports it
|
|
145
|
-
* but the public IViewZone typing lags behind) — hence the structural write.
|
|
146
|
-
*/
|
|
147
|
-
function setOrdinal(zone: MonacoEditorNs.IViewZone, ordinal: number): void {
|
|
148
|
-
(zone as MonacoEditorNs.IViewZone & { ordinal?: number }).ordinal = ordinal;
|
|
149
|
-
}
|