@bendyline/squisq-editor-react 2.1.0 → 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/dist/index.d.ts +188 -55
- package/dist/index.js +5094 -2948
- package/dist/monaco.js +0 -1
- package/dist/styles/index.css +234 -145
- package/package.json +9 -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 -1274
- package/src/EditorShell.tsx +0 -1358
- 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 -1265
- package/src/PreviewPanel.tsx +0 -426
- package/src/RawEditor.tsx +0 -743
- package/src/RecorderEntry.tsx +0 -162
- 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 -2885
- 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 -806
- 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 -65
- 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 -550
- 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__/findMode.test.tsx +0 -104
- package/src/__tests__/findModel.test.ts +0 -55
- 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__/markdownCodeFence.test.ts +0 -45
- package/src/__tests__/mediaAttachmentFlow.test.ts +0 -110
- package/src/__tests__/mediaBinDrop.test.tsx +0 -90
- package/src/__tests__/mediaReferences.test.ts +0 -97
- 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 -571
- 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 -575
- 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 -190
- 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__/writeCanvasSettings.test.ts +0 -15
- 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 -164
- 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 -407
- 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/codeSnippet/CodeSnippetExtension.ts +0 -205
- package/src/codeSnippet/CodeSnippetWidget.tsx +0 -109
- package/src/codeSnippet/__tests__/CodeSnippetExtension.test.ts +0 -95
- package/src/codeSnippet/__tests__/codeSnippetLanguages.test.ts +0 -50
- package/src/codeSnippet/codeSnippetCommands.ts +0 -21
- package/src/codeSnippet/codeSnippetData.ts +0 -43
- package/src/codeSnippet/codeSnippetLanguages.ts +0 -216
- 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 -260
- 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/find/FindHighlightExtension.ts +0 -81
- package/src/find/FindToolbar.tsx +0 -314
- package/src/find/findModel.ts +0 -77
- 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 -559
- 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/markdownCodeFence.ts +0 -72
- package/src/mediaDragMime.ts +0 -32
- package/src/mediaEntries.ts +0 -12
- package/src/mediaReferences.ts +0 -301
- package/src/mermaid/MermaidDiagramCanvas.tsx +0 -693
- package/src/mermaid/MermaidDiagramExtension.ts +0 -243
- package/src/mermaid/MermaidDiagramTypeThumbnail.tsx +0 -184
- package/src/mermaid/MermaidDiagramWidget.tsx +0 -653
- package/src/mermaid/MermaidShapePalette.tsx +0 -245
- package/src/mermaid/__tests__/MermaidDiagramExtension.test.ts +0 -361
- package/src/mermaid/__tests__/mermaidDiagramTypes.test.ts +0 -35
- package/src/mermaid/__tests__/mermaidRenderer.test.ts +0 -49
- package/src/mermaid/__tests__/mermaidSourceOps.test.ts +0 -213
- package/src/mermaid/__tests__/mermaidSyntax.test.ts +0 -71
- package/src/mermaid/mermaidCommands.ts +0 -34
- package/src/mermaid/mermaidData.ts +0 -31
- package/src/mermaid/mermaidDiagramTypes.ts +0 -325
- package/src/mermaid/mermaidModel.ts +0 -31
- package/src/mermaid/mermaidRenderer.ts +0 -181
- package/src/mermaid/mermaidShapes.ts +0 -113
- package/src/mermaid/mermaidSourceOps.ts +0 -454
- 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 -647
- package/src/scene/SceneBlockExtension.ts +0 -205
- package/src/scene/SceneBlockToolbar.tsx +0 -216
- 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/SceneViewControls.tsx +0 -43
- 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__/fitScale.test.ts +0 -19
- 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 -130
- 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/fitScale.ts +0 -17
- package/src/scene/hooks/useSceneHitTest.ts +0 -105
- package/src/scene/hooks/useScenePanZoom.ts +0 -154
- 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 -874
- 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/code-snippet.css +0 -76
- package/src/styles/diagram.css +0 -183
- package/src/styles/editor.css +0 -7131
- package/src/styles/folder-view.css +0 -210
- package/src/styles/image-edit-affordance.css +0 -201
- package/src/styles/index.css +0 -20
- package/src/styles/mermaid-diagram.css +0 -487
- 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/writeCanvasSettings.ts +0 -30
- package/src/wysiwygImageUpload.ts +0 -113
|
@@ -1,396 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ImageVersionHistoryDropdown
|
|
3
|
-
*
|
|
4
|
-
* Toolbar-anchored popover that lists image-edit version snapshots from
|
|
5
|
-
* the sidecar and lets the user revert to any of them.
|
|
6
|
-
*
|
|
7
|
-
* For each snapshot we render a small thumbnail (rasterized via
|
|
8
|
-
* `exportImageEditDoc` at a thumbnail-sized scale) plus a one-line diff
|
|
9
|
-
* summary describing what changed relative to the *previous* (older)
|
|
10
|
-
* snapshot — e.g. "Resized 4800×920 → 1200×920", "Added text layer",
|
|
11
|
-
* "Removed Background". This makes it possible to identify the right
|
|
12
|
-
* snapshot to revert to without having to revert blindly.
|
|
13
|
-
*
|
|
14
|
-
* Thumbnails and diffs are computed lazily and cached by version path.
|
|
15
|
-
* Loads happen in the background after the popover opens.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
19
|
-
import {
|
|
20
|
-
exportImageEditDoc,
|
|
21
|
-
type ImageEditDoc,
|
|
22
|
-
type ImageEditLayer,
|
|
23
|
-
type ImageEditVersionManager,
|
|
24
|
-
} from '@bendyline/squisq/imageEdit';
|
|
25
|
-
import type { ContentContainer } from '@bendyline/squisq/storage';
|
|
26
|
-
import type { Version } from '@bendyline/squisq/versions';
|
|
27
|
-
|
|
28
|
-
interface Props {
|
|
29
|
-
versioning: ImageEditVersionManager;
|
|
30
|
-
/**
|
|
31
|
-
* Sidecar container backing the snapshots — same one passed to the
|
|
32
|
-
* `<ImageEditor>`. Required for thumbnail rendering since
|
|
33
|
-
* `exportImageEditDoc` reads asset bytes from the container.
|
|
34
|
-
*/
|
|
35
|
-
container: ContentContainer;
|
|
36
|
-
/** Called when the user picks a snapshot to load into the editor. */
|
|
37
|
-
onRevert: (version: Version) => void | Promise<void>;
|
|
38
|
-
/**
|
|
39
|
-
* Persistence revision counter. When the host saves a new snapshot
|
|
40
|
-
* (e.g. on flush), bumping this triggers a re-list so the popover
|
|
41
|
-
* stays in sync without polling.
|
|
42
|
-
*/
|
|
43
|
-
refreshKey?: number;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
interface VersionMeta {
|
|
47
|
-
doc: ImageEditDoc;
|
|
48
|
-
thumbUrl: string | null;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const THUMB_MAX_DIM = 96;
|
|
52
|
-
|
|
53
|
-
export function ImageVersionHistoryDropdown({
|
|
54
|
-
versioning,
|
|
55
|
-
container,
|
|
56
|
-
onRevert,
|
|
57
|
-
refreshKey,
|
|
58
|
-
}: Props) {
|
|
59
|
-
const [open, setOpen] = useState(false);
|
|
60
|
-
const [versions, setVersions] = useState<Version[]>([]);
|
|
61
|
-
const [loading, setLoading] = useState(false);
|
|
62
|
-
const [busyTimestamp, setBusyTimestamp] = useState<number | null>(null);
|
|
63
|
-
// Per-version metadata (doc + thumbnail). Keyed by version.path.
|
|
64
|
-
const [meta, setMeta] = useState<Record<string, VersionMeta>>({});
|
|
65
|
-
const popoverRef = useRef<HTMLDivElement | null>(null);
|
|
66
|
-
const triggerRef = useRef<HTMLButtonElement | null>(null);
|
|
67
|
-
// Track object URLs so we can revoke them on unmount / cache reset.
|
|
68
|
-
const urlsRef = useRef<Set<string>>(new Set());
|
|
69
|
-
|
|
70
|
-
// Re-load the list when the popover opens or when the host bumps refreshKey.
|
|
71
|
-
useEffect(() => {
|
|
72
|
-
if (!open) return;
|
|
73
|
-
let cancelled = false;
|
|
74
|
-
setLoading(true);
|
|
75
|
-
versioning
|
|
76
|
-
.listVersions()
|
|
77
|
-
.then((list) => {
|
|
78
|
-
if (cancelled) return;
|
|
79
|
-
// Newest first.
|
|
80
|
-
const sorted = [...list].sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
|
81
|
-
setVersions(sorted);
|
|
82
|
-
})
|
|
83
|
-
.catch(() => {
|
|
84
|
-
if (cancelled) return;
|
|
85
|
-
setVersions([]);
|
|
86
|
-
})
|
|
87
|
-
.finally(() => {
|
|
88
|
-
if (!cancelled) setLoading(false);
|
|
89
|
-
});
|
|
90
|
-
return () => {
|
|
91
|
-
cancelled = true;
|
|
92
|
-
};
|
|
93
|
-
}, [open, versioning, refreshKey]);
|
|
94
|
-
|
|
95
|
-
// Lazily load doc + thumbnail for any version we don't have cached yet.
|
|
96
|
-
// We do this sequentially to avoid hammering the canvas pipeline.
|
|
97
|
-
useEffect(() => {
|
|
98
|
-
if (!open) return;
|
|
99
|
-
if (versions.length === 0) return;
|
|
100
|
-
let cancelled = false;
|
|
101
|
-
(async () => {
|
|
102
|
-
for (const v of versions) {
|
|
103
|
-
if (cancelled) return;
|
|
104
|
-
if (meta[v.path]) continue;
|
|
105
|
-
try {
|
|
106
|
-
const doc = await versioning.readVersion(v);
|
|
107
|
-
if (cancelled) return;
|
|
108
|
-
if (!doc) {
|
|
109
|
-
setMeta((m) => ({ ...m, [v.path]: { doc: emptyDoc(), thumbUrl: null } }));
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
let thumbUrl: string | null = null;
|
|
113
|
-
try {
|
|
114
|
-
const scale = computeThumbScale(doc.canvas.width, doc.canvas.height);
|
|
115
|
-
const blob = await exportImageEditDoc(doc, container, {
|
|
116
|
-
format: 'png',
|
|
117
|
-
scale,
|
|
118
|
-
});
|
|
119
|
-
if (cancelled) return;
|
|
120
|
-
thumbUrl = URL.createObjectURL(blob);
|
|
121
|
-
urlsRef.current.add(thumbUrl);
|
|
122
|
-
} catch {
|
|
123
|
-
// Best-effort thumbnail — fall back to a placeholder.
|
|
124
|
-
thumbUrl = null;
|
|
125
|
-
}
|
|
126
|
-
setMeta((m) => ({ ...m, [v.path]: { doc, thumbUrl } }));
|
|
127
|
-
} catch {
|
|
128
|
-
// Skip on read error; the row will render without a thumb.
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
})();
|
|
132
|
-
return () => {
|
|
133
|
-
cancelled = true;
|
|
134
|
-
};
|
|
135
|
-
}, [open, versions, versioning, container, meta]);
|
|
136
|
-
|
|
137
|
-
// Revoke object URLs on unmount.
|
|
138
|
-
useEffect(() => {
|
|
139
|
-
const urls = urlsRef.current;
|
|
140
|
-
return () => {
|
|
141
|
-
for (const url of urls) URL.revokeObjectURL(url);
|
|
142
|
-
urls.clear();
|
|
143
|
-
};
|
|
144
|
-
}, []);
|
|
145
|
-
|
|
146
|
-
// When refreshKey changes, blow away the cache so newly-saved
|
|
147
|
-
// snapshots are re-thumbnailed (and old thumbs aren't stale).
|
|
148
|
-
useEffect(() => {
|
|
149
|
-
for (const url of urlsRef.current) URL.revokeObjectURL(url);
|
|
150
|
-
urlsRef.current.clear();
|
|
151
|
-
setMeta({});
|
|
152
|
-
}, [refreshKey]);
|
|
153
|
-
|
|
154
|
-
// Close on outside click / Escape.
|
|
155
|
-
useEffect(() => {
|
|
156
|
-
if (!open) return;
|
|
157
|
-
function onDocClick(e: MouseEvent) {
|
|
158
|
-
const t = e.target as Node | null;
|
|
159
|
-
if (!t) return;
|
|
160
|
-
if (popoverRef.current?.contains(t)) return;
|
|
161
|
-
if (triggerRef.current?.contains(t)) return;
|
|
162
|
-
setOpen(false);
|
|
163
|
-
}
|
|
164
|
-
function onKey(e: KeyboardEvent) {
|
|
165
|
-
if (e.key === 'Escape') setOpen(false);
|
|
166
|
-
}
|
|
167
|
-
document.addEventListener('mousedown', onDocClick);
|
|
168
|
-
document.addEventListener('keydown', onKey);
|
|
169
|
-
return () => {
|
|
170
|
-
document.removeEventListener('mousedown', onDocClick);
|
|
171
|
-
document.removeEventListener('keydown', onKey);
|
|
172
|
-
};
|
|
173
|
-
}, [open]);
|
|
174
|
-
|
|
175
|
-
const handleRevert = useCallback(
|
|
176
|
-
async (v: Version) => {
|
|
177
|
-
setBusyTimestamp(v.timestamp.getTime());
|
|
178
|
-
try {
|
|
179
|
-
await onRevert(v);
|
|
180
|
-
setOpen(false);
|
|
181
|
-
} catch (err: unknown) {
|
|
182
|
-
console.warn(
|
|
183
|
-
'[squisq-editor] image-edit revert failed:',
|
|
184
|
-
err instanceof Error ? err.message : err,
|
|
185
|
-
);
|
|
186
|
-
} finally {
|
|
187
|
-
setBusyTimestamp(null);
|
|
188
|
-
}
|
|
189
|
-
},
|
|
190
|
-
[onRevert],
|
|
191
|
-
);
|
|
192
|
-
|
|
193
|
-
return (
|
|
194
|
-
<div className="squisq-image-editor-version-dropdown">
|
|
195
|
-
<button
|
|
196
|
-
ref={triggerRef}
|
|
197
|
-
type="button"
|
|
198
|
-
className="squisq-image-editor-tool-button squisq-image-editor-tool-button--with-label"
|
|
199
|
-
onClick={() => setOpen((o) => !o)}
|
|
200
|
-
aria-expanded={open}
|
|
201
|
-
aria-haspopup="menu"
|
|
202
|
-
title="Version history"
|
|
203
|
-
data-testid="image-editor-history-trigger"
|
|
204
|
-
>
|
|
205
|
-
<span>History</span>
|
|
206
|
-
<span aria-hidden="true" style={{ fontSize: '0.8em' }}>
|
|
207
|
-
▾
|
|
208
|
-
</span>
|
|
209
|
-
</button>
|
|
210
|
-
{open && (
|
|
211
|
-
<div
|
|
212
|
-
ref={popoverRef}
|
|
213
|
-
className="squisq-image-editor-version-popover"
|
|
214
|
-
role="menu"
|
|
215
|
-
data-testid="image-editor-history-popover"
|
|
216
|
-
>
|
|
217
|
-
<div className="squisq-image-editor-version-popover__title">Version history</div>
|
|
218
|
-
{loading && <div className="squisq-image-editor-version-popover__empty">Loading…</div>}
|
|
219
|
-
{!loading && versions.length === 0 && (
|
|
220
|
-
<div className="squisq-image-editor-version-popover__empty">No snapshots yet</div>
|
|
221
|
-
)}
|
|
222
|
-
{!loading && versions.length > 0 && (
|
|
223
|
-
<ul className="squisq-image-editor-version-popover__list">
|
|
224
|
-
{versions.map((v, i) => {
|
|
225
|
-
const ts = v.timestamp.getTime();
|
|
226
|
-
const m = meta[v.path];
|
|
227
|
-
const isCurrent = i === 0;
|
|
228
|
-
const isOriginal = i + 1 >= versions.length;
|
|
229
|
-
// Diff is computed against the *next* (older) version in
|
|
230
|
-
// the newest-first list. The oldest snapshot has no
|
|
231
|
-
// older neighbor, so we label it "Original".
|
|
232
|
-
const olderMeta = i + 1 < versions.length ? meta[versions[i + 1]!.path] : undefined;
|
|
233
|
-
const summary = isOriginal
|
|
234
|
-
? 'Original'
|
|
235
|
-
: m && olderMeta
|
|
236
|
-
? summarizeDiff(olderMeta.doc, m.doc)
|
|
237
|
-
: '';
|
|
238
|
-
return (
|
|
239
|
-
<li
|
|
240
|
-
key={ts}
|
|
241
|
-
className={
|
|
242
|
-
'squisq-image-editor-version-popover__row' +
|
|
243
|
-
(isCurrent ? ' squisq-image-editor-version-popover__row--current' : '')
|
|
244
|
-
}
|
|
245
|
-
>
|
|
246
|
-
<div className="squisq-image-editor-version-popover__thumb">
|
|
247
|
-
{m?.thumbUrl ? (
|
|
248
|
-
<img
|
|
249
|
-
src={m.thumbUrl}
|
|
250
|
-
alt=""
|
|
251
|
-
className="squisq-image-editor-version-popover__thumb-img"
|
|
252
|
-
/>
|
|
253
|
-
) : (
|
|
254
|
-
<div
|
|
255
|
-
className="squisq-image-editor-version-popover__thumb-placeholder"
|
|
256
|
-
aria-hidden="true"
|
|
257
|
-
/>
|
|
258
|
-
)}
|
|
259
|
-
</div>
|
|
260
|
-
<div className="squisq-image-editor-version-popover__info">
|
|
261
|
-
<div className="squisq-image-editor-version-popover__when">
|
|
262
|
-
{isCurrent && (
|
|
263
|
-
<span className="squisq-image-editor-version-popover__badge">
|
|
264
|
-
Current
|
|
265
|
-
</span>
|
|
266
|
-
)}
|
|
267
|
-
{formatTimestamp(v.timestamp)}
|
|
268
|
-
</div>
|
|
269
|
-
<div className="squisq-image-editor-version-popover__summary" title={summary}>
|
|
270
|
-
{summary || (m ? '' : 'Loading…')}
|
|
271
|
-
</div>
|
|
272
|
-
</div>
|
|
273
|
-
<button
|
|
274
|
-
type="button"
|
|
275
|
-
className="squisq-image-editor-tool-button"
|
|
276
|
-
onClick={() => handleRevert(v)}
|
|
277
|
-
disabled={isCurrent || busyTimestamp === ts}
|
|
278
|
-
title={isCurrent ? 'This is the current version' : 'Revert to this version'}
|
|
279
|
-
>
|
|
280
|
-
{busyTimestamp === ts ? 'Loading…' : 'Revert'}
|
|
281
|
-
</button>
|
|
282
|
-
</li>
|
|
283
|
-
);
|
|
284
|
-
})}
|
|
285
|
-
</ul>
|
|
286
|
-
)}
|
|
287
|
-
</div>
|
|
288
|
-
)}
|
|
289
|
-
</div>
|
|
290
|
-
);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* Pick a `scale` factor for `exportImageEditDoc` so the rendered
|
|
295
|
-
* thumbnail's longest side is at most `THUMB_MAX_DIM` pixels. Always
|
|
296
|
-
* ≤1 to avoid up-rendering small canvases.
|
|
297
|
-
*/
|
|
298
|
-
function computeThumbScale(width: number, height: number): number {
|
|
299
|
-
const longest = Math.max(width, height);
|
|
300
|
-
if (longest <= THUMB_MAX_DIM) return 1;
|
|
301
|
-
return THUMB_MAX_DIM / longest;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* Convert a Date to a human-readable local time.
|
|
306
|
-
*/
|
|
307
|
-
function formatTimestamp(stamp: Date): string {
|
|
308
|
-
if (Number.isNaN(stamp.getTime())) return String(stamp);
|
|
309
|
-
return stamp.toLocaleString(undefined, {
|
|
310
|
-
month: 'short',
|
|
311
|
-
day: 'numeric',
|
|
312
|
-
hour: 'numeric',
|
|
313
|
-
minute: '2-digit',
|
|
314
|
-
second: '2-digit',
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/**
|
|
319
|
-
* Build a one-line, human-readable summary of what changed between
|
|
320
|
-
* two snapshots. Reports (in priority order):
|
|
321
|
-
*
|
|
322
|
-
* 1. Canvas size change — "Resized 4800×920 → 1200×920"
|
|
323
|
-
* 2. Layer additions/removals — "Added text", "Removed Background"
|
|
324
|
-
* 3. Per-layer position/size — "Moved Text", "Resized Image"
|
|
325
|
-
* 4. Per-layer content/style — "Edited Text"
|
|
326
|
-
*
|
|
327
|
-
* Returns "No changes" if the two docs are byte-equivalent in the
|
|
328
|
-
* fields we compare.
|
|
329
|
-
*/
|
|
330
|
-
function summarizeDiff(prev: ImageEditDoc, next: ImageEditDoc): string {
|
|
331
|
-
const parts: string[] = [];
|
|
332
|
-
|
|
333
|
-
// 1. Canvas size.
|
|
334
|
-
if (prev.canvas.width !== next.canvas.width || prev.canvas.height !== next.canvas.height) {
|
|
335
|
-
parts.push(
|
|
336
|
-
`Resized ${prev.canvas.width}×${prev.canvas.height} → ${next.canvas.width}×${next.canvas.height}`,
|
|
337
|
-
);
|
|
338
|
-
} else if (prev.canvas.background !== next.canvas.background) {
|
|
339
|
-
parts.push('Changed background');
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// 2. Layer add / remove (by id).
|
|
343
|
-
const prevIds = new Set(prev.layers.map((l) => l.id));
|
|
344
|
-
const nextIds = new Set(next.layers.map((l) => l.id));
|
|
345
|
-
const added = next.layers.filter((l) => !prevIds.has(l.id));
|
|
346
|
-
const removed = prev.layers.filter((l) => !nextIds.has(l.id));
|
|
347
|
-
for (const l of added) parts.push(`Added ${describeLayer(l)}`);
|
|
348
|
-
for (const l of removed) parts.push(`Removed ${describeLayer(l)}`);
|
|
349
|
-
|
|
350
|
-
// 3 & 4. Per-layer changes for layers present in both.
|
|
351
|
-
for (const n of next.layers) {
|
|
352
|
-
const p = prev.layers.find((l) => l.id === n.id);
|
|
353
|
-
if (!p) continue;
|
|
354
|
-
const change = describeLayerChange(p, n);
|
|
355
|
-
if (change) parts.push(change);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
if (parts.length === 0) return 'No changes';
|
|
359
|
-
// Cap to a sane length — the popover row is one line.
|
|
360
|
-
return parts.slice(0, 3).join(' · ') + (parts.length > 3 ? ` (+${parts.length - 3})` : '');
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
function describeLayer(layer: ImageEditLayer): string {
|
|
364
|
-
const name = layer.name?.trim();
|
|
365
|
-
return name && name.length > 0 ? `“${name}”` : layer.type;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
function describeLayerChange(p: ImageEditLayer, n: ImageEditLayer): string | null {
|
|
369
|
-
const label = describeLayer(n);
|
|
370
|
-
const posChanged =
|
|
371
|
-
p.position.x !== n.position.x ||
|
|
372
|
-
p.position.y !== n.position.y ||
|
|
373
|
-
p.position.width !== n.position.width ||
|
|
374
|
-
p.position.height !== n.position.height;
|
|
375
|
-
const sizeChanged =
|
|
376
|
-
p.position.width !== n.position.width || p.position.height !== n.position.height;
|
|
377
|
-
// Treat content + style as a single "edited" bucket.
|
|
378
|
-
const contentChanged = JSON.stringify(p.content) !== JSON.stringify(n.content);
|
|
379
|
-
|
|
380
|
-
if (sizeChanged && !contentChanged) return `Resized ${label}`;
|
|
381
|
-
if (posChanged && !contentChanged) return `Moved ${label}`;
|
|
382
|
-
if (contentChanged) return `Edited ${label}`;
|
|
383
|
-
return null;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
/**
|
|
387
|
-
* Fallback used when a snapshot fails to load — keeps render code
|
|
388
|
-
* branch-free without polluting the diff summary with errors.
|
|
389
|
-
*/
|
|
390
|
-
function emptyDoc(): ImageEditDoc {
|
|
391
|
-
return {
|
|
392
|
-
version: 1,
|
|
393
|
-
canvas: { width: 0, height: 0, background: 'transparent' },
|
|
394
|
-
layers: [],
|
|
395
|
-
};
|
|
396
|
-
}
|
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LayersPanel — list of layers with visibility / lock / reorder / delete
|
|
3
|
-
* controls. Newest layer is rendered last (top of the SVG stack), so the
|
|
4
|
-
* panel shows the array reversed so "top" appears at the top of the list.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { useEffect, useRef, useState } from 'react';
|
|
8
|
-
import type { ImageEditDoc } from '@bendyline/squisq/schemas';
|
|
9
|
-
import type { ImageEditorAction } from './state.js';
|
|
10
|
-
import {
|
|
11
|
-
ChevronDownIcon,
|
|
12
|
-
ChevronUpIcon,
|
|
13
|
-
CloseIcon,
|
|
14
|
-
EyeIcon,
|
|
15
|
-
EyeOffIcon,
|
|
16
|
-
ImageIcon,
|
|
17
|
-
LockIcon,
|
|
18
|
-
PlusIcon,
|
|
19
|
-
ShapeIcon,
|
|
20
|
-
TextIcon,
|
|
21
|
-
UnlockIcon,
|
|
22
|
-
} from './icons.js';
|
|
23
|
-
|
|
24
|
-
export type AddableLayerKind = 'text' | 'shape' | 'image';
|
|
25
|
-
|
|
26
|
-
export interface LayersPanelProps {
|
|
27
|
-
doc: ImageEditDoc;
|
|
28
|
-
selectedLayerId: string | null;
|
|
29
|
-
dispatch: (action: ImageEditorAction) => void;
|
|
30
|
-
onAddLayer: (kind: AddableLayerKind) => void;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const ADD_LAYER_OPTIONS: ReadonlyArray<{
|
|
34
|
-
kind: AddableLayerKind;
|
|
35
|
-
label: string;
|
|
36
|
-
icon: React.ReactNode;
|
|
37
|
-
}> = [
|
|
38
|
-
{ kind: 'text', label: 'Text layer', icon: <TextIcon /> },
|
|
39
|
-
{ kind: 'shape', label: 'Shape layer', icon: <ShapeIcon /> },
|
|
40
|
-
{ kind: 'image', label: 'Image layer…', icon: <ImageIcon /> },
|
|
41
|
-
];
|
|
42
|
-
|
|
43
|
-
export function LayersPanel({ doc, selectedLayerId, dispatch, onAddLayer }: LayersPanelProps) {
|
|
44
|
-
const [addMenuOpen, setAddMenuOpen] = useState(false);
|
|
45
|
-
const addMenuRef = useRef<HTMLSpanElement | null>(null);
|
|
46
|
-
|
|
47
|
-
useEffect(() => {
|
|
48
|
-
if (!addMenuOpen) return;
|
|
49
|
-
|
|
50
|
-
const closeOnOutsideClick = (event: MouseEvent) => {
|
|
51
|
-
if (!addMenuRef.current?.contains(event.target as Node)) setAddMenuOpen(false);
|
|
52
|
-
};
|
|
53
|
-
const closeOnEscape = (event: KeyboardEvent) => {
|
|
54
|
-
if (event.key === 'Escape') setAddMenuOpen(false);
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
document.addEventListener('mousedown', closeOnOutsideClick);
|
|
58
|
-
document.addEventListener('keydown', closeOnEscape);
|
|
59
|
-
return () => {
|
|
60
|
-
document.removeEventListener('mousedown', closeOnOutsideClick);
|
|
61
|
-
document.removeEventListener('keydown', closeOnEscape);
|
|
62
|
-
};
|
|
63
|
-
}, [addMenuOpen]);
|
|
64
|
-
|
|
65
|
-
// Visual order: top of stack first.
|
|
66
|
-
const ordered = doc.layers.slice().reverse();
|
|
67
|
-
|
|
68
|
-
return (
|
|
69
|
-
<div className="squisq-image-editor-layers" data-testid="image-editor-layers">
|
|
70
|
-
<div className="squisq-image-editor-panel-header squisq-image-editor-layers-header">
|
|
71
|
-
<span>Layers</span>
|
|
72
|
-
<span ref={addMenuRef} className="squisq-image-editor-layer-add">
|
|
73
|
-
<button
|
|
74
|
-
type="button"
|
|
75
|
-
className="squisq-image-editor-layer-add-button"
|
|
76
|
-
onClick={() => setAddMenuOpen((open) => !open)}
|
|
77
|
-
aria-label="Add layer"
|
|
78
|
-
title="Add layer"
|
|
79
|
-
aria-haspopup="menu"
|
|
80
|
-
aria-expanded={addMenuOpen}
|
|
81
|
-
>
|
|
82
|
-
<PlusIcon />
|
|
83
|
-
</button>
|
|
84
|
-
{addMenuOpen && (
|
|
85
|
-
<div className="squisq-image-editor-layer-add-menu" role="menu">
|
|
86
|
-
{ADD_LAYER_OPTIONS.map(({ kind, label, icon }) => (
|
|
87
|
-
<button
|
|
88
|
-
key={kind}
|
|
89
|
-
type="button"
|
|
90
|
-
className="squisq-image-editor-layer-add-menu-item"
|
|
91
|
-
role="menuitem"
|
|
92
|
-
onClick={() => {
|
|
93
|
-
setAddMenuOpen(false);
|
|
94
|
-
onAddLayer(kind);
|
|
95
|
-
}}
|
|
96
|
-
>
|
|
97
|
-
{icon}
|
|
98
|
-
<span>{label}</span>
|
|
99
|
-
</button>
|
|
100
|
-
))}
|
|
101
|
-
</div>
|
|
102
|
-
)}
|
|
103
|
-
</span>
|
|
104
|
-
</div>
|
|
105
|
-
<ul className="squisq-image-editor-layer-list">
|
|
106
|
-
{ordered.length === 0 && <li className="squisq-image-editor-layer-empty">No layers yet</li>}
|
|
107
|
-
{ordered.map((layer) => {
|
|
108
|
-
const visible = layer.visible !== false;
|
|
109
|
-
const locked = !!layer.locked;
|
|
110
|
-
const stackIndex = doc.layers.findIndex((l) => l.id === layer.id);
|
|
111
|
-
const canMoveUp = stackIndex < doc.layers.length - 1;
|
|
112
|
-
const canMoveDown = stackIndex > 0;
|
|
113
|
-
const isSelected = selectedLayerId === layer.id;
|
|
114
|
-
const layerName = layer.name ?? defaultLayerName(layer);
|
|
115
|
-
|
|
116
|
-
return (
|
|
117
|
-
<li
|
|
118
|
-
key={layer.id}
|
|
119
|
-
className={[
|
|
120
|
-
'squisq-image-editor-layer-item',
|
|
121
|
-
isSelected ? 'is-selected' : '',
|
|
122
|
-
visible ? '' : 'is-hidden',
|
|
123
|
-
]
|
|
124
|
-
.filter(Boolean)
|
|
125
|
-
.join(' ')}
|
|
126
|
-
>
|
|
127
|
-
<button
|
|
128
|
-
type="button"
|
|
129
|
-
className="squisq-image-editor-layer-toggle"
|
|
130
|
-
onClick={() =>
|
|
131
|
-
dispatch({
|
|
132
|
-
type: 'update-layer',
|
|
133
|
-
layerId: layer.id,
|
|
134
|
-
patch: { visible: !visible },
|
|
135
|
-
})
|
|
136
|
-
}
|
|
137
|
-
aria-label={visible ? 'Hide layer' : 'Show layer'}
|
|
138
|
-
title={visible ? 'Hide layer' : 'Show layer'}
|
|
139
|
-
>
|
|
140
|
-
{visible ? <EyeIcon /> : <EyeOffIcon />}
|
|
141
|
-
</button>
|
|
142
|
-
<button
|
|
143
|
-
type="button"
|
|
144
|
-
className="squisq-image-editor-layer-toggle"
|
|
145
|
-
onClick={() =>
|
|
146
|
-
dispatch({
|
|
147
|
-
type: 'update-layer',
|
|
148
|
-
layerId: layer.id,
|
|
149
|
-
patch: { locked: !locked },
|
|
150
|
-
})
|
|
151
|
-
}
|
|
152
|
-
aria-label={locked ? 'Unlock layer' : 'Lock layer'}
|
|
153
|
-
title={locked ? 'Unlock layer' : 'Lock layer'}
|
|
154
|
-
>
|
|
155
|
-
{locked ? <LockIcon /> : <UnlockIcon />}
|
|
156
|
-
</button>
|
|
157
|
-
<button
|
|
158
|
-
type="button"
|
|
159
|
-
className="squisq-image-editor-layer-name"
|
|
160
|
-
onClick={() => dispatch({ type: 'select', layerId: layer.id })}
|
|
161
|
-
title={layerName}
|
|
162
|
-
>
|
|
163
|
-
<span className="squisq-image-editor-layer-name-text">{layerName}</span>
|
|
164
|
-
<span className="squisq-image-editor-layer-kind">{layer.type}</span>
|
|
165
|
-
</button>
|
|
166
|
-
<button
|
|
167
|
-
type="button"
|
|
168
|
-
className="squisq-image-editor-layer-toggle"
|
|
169
|
-
disabled={!canMoveUp}
|
|
170
|
-
onClick={() =>
|
|
171
|
-
dispatch({ type: 'reorder-layer', layerId: layer.id, toIndex: stackIndex + 1 })
|
|
172
|
-
}
|
|
173
|
-
aria-label="Move layer up"
|
|
174
|
-
title="Move layer up"
|
|
175
|
-
>
|
|
176
|
-
<ChevronUpIcon />
|
|
177
|
-
</button>
|
|
178
|
-
<button
|
|
179
|
-
type="button"
|
|
180
|
-
className="squisq-image-editor-layer-toggle"
|
|
181
|
-
disabled={!canMoveDown}
|
|
182
|
-
onClick={() =>
|
|
183
|
-
dispatch({ type: 'reorder-layer', layerId: layer.id, toIndex: stackIndex - 1 })
|
|
184
|
-
}
|
|
185
|
-
aria-label="Move layer down"
|
|
186
|
-
title="Move layer down"
|
|
187
|
-
>
|
|
188
|
-
<ChevronDownIcon />
|
|
189
|
-
</button>
|
|
190
|
-
<button
|
|
191
|
-
type="button"
|
|
192
|
-
className="squisq-image-editor-layer-toggle"
|
|
193
|
-
onClick={() => dispatch({ type: 'remove-layer', layerId: layer.id })}
|
|
194
|
-
aria-label="Delete layer"
|
|
195
|
-
title="Delete layer"
|
|
196
|
-
>
|
|
197
|
-
<CloseIcon />
|
|
198
|
-
</button>
|
|
199
|
-
</li>
|
|
200
|
-
);
|
|
201
|
-
})}
|
|
202
|
-
</ul>
|
|
203
|
-
</div>
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function defaultLayerName(layer: { type: string; content?: unknown }): string {
|
|
208
|
-
if (layer.type === 'text') {
|
|
209
|
-
const c = layer.content as { text?: string } | undefined;
|
|
210
|
-
return c?.text?.split('\n')[0]?.slice(0, 24) || 'Text';
|
|
211
|
-
}
|
|
212
|
-
if (layer.type === 'image') return 'Image';
|
|
213
|
-
if (layer.type === 'shape') {
|
|
214
|
-
const c = layer.content as { shape?: string } | undefined;
|
|
215
|
-
return c?.shape ? c.shape[0]!.toUpperCase() + c.shape.slice(1) : 'Shape';
|
|
216
|
-
}
|
|
217
|
-
return layer.type;
|
|
218
|
-
}
|