@bendyline/squisq-editor-react 1.5.2 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +757 -20
- package/dist/index.js +16910 -6844
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/BlockCardView.tsx +121 -0
- package/src/BlockPreviewPanel.tsx +69 -0
- package/src/BlockPropertiesPopover.tsx +191 -0
- package/src/EditorContext.tsx +143 -0
- package/src/EditorShell.tsx +200 -120
- package/src/FolderView.tsx +131 -0
- package/src/Icon.tsx +26 -0
- package/src/ImageEditor.tsx +69 -22
- package/src/OutlinePanel.tsx +38 -3
- package/src/PlainHtmlPreview.tsx +30 -3
- package/src/PreviewControls.tsx +180 -8
- package/src/RawEditor.tsx +216 -29
- package/src/RecorderEntry.tsx +9 -16
- package/src/TemplateAnnotation.ts +44 -0
- package/src/TemplatePicker.tsx +329 -54
- package/src/ThemeCustomizerPanel.tsx +30 -336
- package/src/ThemePicker.tsx +112 -3
- package/src/TimelineBlockPreview.tsx +37 -0
- package/src/TimelineTrack.tsx +671 -0
- package/src/Toolbar.tsx +528 -174
- package/src/Tooltip.tsx +22 -4
- package/src/TransitionPicker.tsx +351 -0
- package/src/VersionHistoryPanel.tsx +61 -31
- package/src/ViewMenuPanel.tsx +17 -14
- package/src/WysiwygEditor.tsx +161 -65
- package/src/__tests__/blockProperties.test.ts +92 -0
- package/src/__tests__/blockRange.test.ts +105 -0
- package/src/__tests__/buildPreviewDocTransition.test.ts +73 -0
- package/src/__tests__/createShapeLayer.test.ts +46 -0
- package/src/__tests__/drawingShapeRoundTrip.test.ts +49 -0
- package/src/__tests__/embeddedMedia.test.ts +48 -0
- package/src/__tests__/headingTransition.test.ts +138 -0
- package/src/__tests__/layoutChildRoundTrip.test.ts +71 -0
- package/src/__tests__/plainHtmlPreview.test.tsx +10 -8
- package/src/__tests__/recorderMediaInsert.test.ts +86 -0
- package/src/__tests__/templateAnnotationRoundTrip.test.ts +18 -0
- package/src/__tests__/templatePickerMetadata.test.ts +32 -0
- package/src/__tests__/timelineSource.test.ts +134 -0
- package/src/__tests__/tiptapBridge.test.ts +92 -0
- package/src/__tests__/tiptapBridgeConformance.test.ts +47 -0
- package/src/__tests__/tooltip.test.tsx +72 -0
- package/src/__tests__/transitionCatalog.test.ts +64 -0
- package/src/__tests__/useBlockNavigator.test.tsx +67 -0
- package/src/__tests__/useMediaRecorder.test.ts +24 -0
- package/src/__tests__/useTimelineClock.test.ts +21 -0
- package/src/blockProperties.ts +88 -0
- package/src/blockRange.ts +132 -0
- package/src/buildPreviewDoc.ts +98 -9
- package/src/customTemplates/AddBin.tsx +126 -0
- package/src/customTemplates/CustomLayoutManager.tsx +233 -0
- package/src/customTemplates/CustomTemplateContext.tsx +182 -0
- package/src/customTemplates/LayerToolbar.tsx +580 -0
- package/src/customTemplates/ShapeGlyph.tsx +47 -0
- package/src/customTemplates/TemplateDesigner.tsx +430 -0
- package/src/customTemplates/__tests__/library.test.ts +88 -0
- package/src/customTemplates/__tests__/normalizePositions.test.ts +109 -0
- package/src/customTemplates/__tests__/shapeDefs.test.ts +49 -0
- package/src/customTemplates/__tests__/useMemoryLayerAdapter.test.ts +95 -0
- package/src/customTemplates/designer.css +673 -0
- package/src/customTemplates/index.ts +31 -0
- package/src/customTemplates/library.ts +97 -0
- package/src/customTemplates/normalizePositions.ts +75 -0
- package/src/customTemplates/shapeDefs.ts +131 -0
- package/src/customTemplates/thumbnail.tsx +63 -0
- package/src/customTemplates/tokenDefs.ts +60 -0
- package/src/customTemplates/useDocCustomTemplates.ts +52 -0
- package/src/customTemplates/useMemoryLayerAdapter.ts +123 -0
- package/src/customThemes/CustomThemeContext.tsx +179 -0
- package/src/customThemes/CustomThemeDialog.tsx +286 -0
- package/src/customThemes/__tests__/CustomThemeContext.test.tsx +64 -0
- package/src/customThemes/__tests__/CustomThemeDialog.test.tsx +47 -0
- package/src/customThemes/__tests__/customThemeLibrary.test.ts +51 -0
- package/src/customThemes/customThemeLibrary.ts +97 -0
- package/src/customThemes/index.ts +31 -0
- package/src/customThemes/themeControls.tsx +229 -0
- package/src/customThemes/themeDraft.ts +272 -0
- package/src/customThemes/useDocCustomThemes.ts +49 -0
- package/src/diagram/DiagramCanvas.tsx +240 -0
- package/src/diagram/DiagramExtension.ts +209 -0
- package/src/diagram/DiagramMaximizedOverlay.tsx +46 -0
- package/src/diagram/DiagramWidget.tsx +270 -0
- package/src/diagram/diagramCommands.ts +604 -0
- package/src/diagram/diagramConstants.ts +17 -0
- package/src/diagram/useDiagramData.ts +126 -0
- package/src/embeddedMedia.ts +78 -0
- package/src/frontmatter.ts +29 -0
- package/src/headingTransition.ts +231 -0
- package/src/imageEditor/CanvasSurface.tsx +383 -88
- package/src/imageEditor/PropertiesPanel.tsx +47 -1
- package/src/imageEditor/Toolbar.tsx +229 -16
- package/src/imageEditor/createShapeLayer.ts +280 -0
- package/src/imageEditor/icons.tsx +34 -114
- package/src/imageEditor/image-editor.css +54 -5
- package/src/imageEditor/state.ts +23 -3
- package/src/index.ts +77 -0
- package/src/recorder/RecorderModal.tsx +120 -53
- package/src/recorder/RecorderPanel.tsx +2 -26
- package/src/recorder/hooks/useMediaRecorder.ts +17 -2
- package/src/recorder/insertMediaBlock.ts +30 -0
- package/src/resolveBlockVisual.ts +33 -0
- package/src/scene/Scene.tsx +540 -0
- package/src/scene/SceneBlockExtension.ts +198 -0
- package/src/scene/SceneBlockToolbar.tsx +201 -0
- package/src/scene/SceneBlockWidget.tsx +434 -0
- package/src/scene/ScenePropsBar.tsx +85 -0
- package/src/scene/SceneSelection.tsx +107 -0
- package/src/scene/SceneViewport.tsx +102 -0
- package/src/scene/ShapePalette.tsx +181 -0
- package/src/scene/__tests__/DiagramAdapter.test.ts +56 -0
- package/src/scene/__tests__/bezierEdit.test.ts +85 -0
- package/src/scene/__tests__/blockLayers.test.ts +57 -0
- package/src/scene/__tests__/shapeLayers.test.ts +106 -0
- package/src/scene/__tests__/useSceneHitTest.test.ts +90 -0
- package/src/scene/__tests__/useScenePanZoom.test.ts +103 -0
- package/src/scene/adapters/DiagramAdapter.ts +168 -0
- package/src/scene/adapters/DrawingAdapter.ts +415 -0
- package/src/scene/adapters/LayoutAdapter.ts +310 -0
- package/src/scene/adapters/blockLayers.ts +159 -0
- package/src/scene/commands/SceneCommand.ts +70 -0
- package/src/scene/commands/drawingCommands.ts +318 -0
- package/src/scene/commands/layoutCommands.ts +301 -0
- package/src/scene/hooks/useSceneHitTest.ts +105 -0
- package/src/scene/hooks/useScenePanZoom.ts +147 -0
- package/src/scene/hooks/useSceneSelection.ts +62 -0
- package/src/scene/index.ts +95 -0
- package/src/scene/layers/DiagramEdges.tsx +127 -0
- package/src/scene/layers/edgeGeometry.ts +77 -0
- package/src/scene/layers/nodeCard.tsx +145 -0
- package/src/scene/layers/renderLayer.tsx +70 -0
- package/src/scene/layers/shapeLayers.ts +201 -0
- package/src/scene/paths/bezierEdit.ts +208 -0
- package/src/scene/scene.css +649 -0
- package/src/scene/text/SceneTextOverlay.tsx +161 -0
- package/src/scene/text/sceneTextChannel.ts +40 -0
- package/src/scene/text/sceneTextConfig.ts +27 -0
- package/src/scene/text/sceneTiptap.ts +36 -0
- package/src/scene/text/useSceneTextEditing.ts +39 -0
- package/src/scene/tools/ConnectTool.ts +111 -0
- package/src/scene/tools/DrawingConnectTool.ts +161 -0
- package/src/scene/tools/PathTool.ts +158 -0
- package/src/scene/tools/PlaceTool.ts +47 -0
- package/src/scene/tools/SceneTool.ts +75 -0
- package/src/scene/tools/SelectTool.ts +284 -0
- package/src/scene/tools/ShapeTool.ts +144 -0
- package/src/scene/tools/TextTool.ts +72 -0
- package/src/scene/tools/TokenTool.ts +95 -0
- package/src/scene/tools/createDrawShapeTool.ts +82 -0
- package/src/styles/diagram.css +183 -0
- package/src/styles/editor.css +1615 -203
- package/src/styles/folder-view.css +210 -0
- package/src/styles/image-edit-affordance.css +2 -2
- package/src/styles/index.css +4 -0
- package/src/timelineSource.ts +244 -0
- package/src/tiptapBridge.ts +115 -34
- package/src/tooltipPlacement.ts +13 -0
- package/src/transitionCatalog.ts +159 -0
- package/src/types/monaco-shims.d.ts +10 -0
- package/src/useBlockNavigator.ts +153 -0
- package/src/useMonacoLoader.ts +105 -0
- package/src/useTimelineClock.ts +76 -0
- package/src/utils/dropUtils.ts +1 -1
package/src/EditorShell.tsx
CHANGED
|
@@ -22,11 +22,19 @@ import { StatusBar } from './StatusBar';
|
|
|
22
22
|
import { RawEditor } from './RawEditor';
|
|
23
23
|
import { WysiwygEditor } from './WysiwygEditor';
|
|
24
24
|
import { InlinePreviewGutter } from './InlinePreviewGutter';
|
|
25
|
+
import { BlockPreviewPanel } from './BlockPreviewPanel';
|
|
25
26
|
import { OutlinePanel } from './OutlinePanel';
|
|
27
|
+
import { BlockCardView } from './BlockCardView';
|
|
28
|
+
import { TimelineTrack } from './TimelineTrack';
|
|
26
29
|
import { PreviewPanel } from './PreviewPanel';
|
|
27
30
|
import { ImageViewer } from './ImageViewer';
|
|
28
31
|
import { ImageEditor } from './ImageEditor';
|
|
29
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
PreviewSettingsProvider,
|
|
34
|
+
PreviewToolbarControls,
|
|
35
|
+
ThemeDesignerDock,
|
|
36
|
+
} from './PreviewControls';
|
|
37
|
+
import { CustomThemeProvider, useDocCustomThemes } from './customThemes';
|
|
30
38
|
import { MediaBin } from './MediaBin';
|
|
31
39
|
import { DropZoneOverlay } from './DropZoneOverlay';
|
|
32
40
|
import { TooltipLayer } from './Tooltip';
|
|
@@ -575,14 +583,31 @@ function EditorShellInner({
|
|
|
575
583
|
inlinePreviewVisible,
|
|
576
584
|
statusBarVisible,
|
|
577
585
|
outlineVisible,
|
|
586
|
+
layoutMode,
|
|
587
|
+
blockCount,
|
|
588
|
+
activeBlockKey,
|
|
589
|
+
prevBlock,
|
|
590
|
+
nextBlock,
|
|
591
|
+
addBlock,
|
|
578
592
|
imageEditTarget,
|
|
579
593
|
closeImageEdit,
|
|
580
594
|
bumpMediaRevision,
|
|
581
595
|
} = useEditorContext();
|
|
596
|
+
// Dual-catalog custom themes (doc + browser library), mirroring how
|
|
597
|
+
// CustomTemplateProvider is fed. Wraps the preview subtree so the theme
|
|
598
|
+
// picker + designer can read/write both pools.
|
|
599
|
+
const { docThemes, onDocThemesChange } = useDocCustomThemes();
|
|
582
600
|
const isPreview = activeView === 'preview';
|
|
583
601
|
const isCodeMode = editorMode === 'code';
|
|
584
602
|
const isImageMode = editorMode === 'image';
|
|
585
603
|
const isMarkdownMode = editorMode === 'markdown';
|
|
604
|
+
// The block card (one block at a time) applies to the editing surfaces (raw /
|
|
605
|
+
// wysiwyg) in markdown mode — never to preview, code, or image. Both the
|
|
606
|
+
// block-at-a-time and timeline layouts scope the editor to a single block.
|
|
607
|
+
const isCardMode =
|
|
608
|
+
isMarkdownMode && (layoutMode === 'block' || layoutMode === 'timeline') && !isPreview;
|
|
609
|
+
// Timeline mode additionally shows the horizontal timeline track below.
|
|
610
|
+
const isTimelineMode = isMarkdownMode && layoutMode === 'timeline' && !isPreview;
|
|
586
611
|
const [showFiles, setShowFiles] = useState(false);
|
|
587
612
|
const [mediaRefreshKey, setMediaRefreshKey] = useState(0);
|
|
588
613
|
// Persistent fallback container for image-edit sidecars when the host
|
|
@@ -760,135 +785,184 @@ function EditorShellInner({
|
|
|
760
785
|
}}
|
|
761
786
|
{...containerProps}
|
|
762
787
|
>
|
|
763
|
-
<
|
|
764
|
-
{
|
|
788
|
+
<CustomThemeProvider docThemes={docThemes} onDocThemesChange={onDocThemesChange}>
|
|
789
|
+
<PreviewSettingsProvider doc={doc} themeOverride={themeOverride}>
|
|
790
|
+
{/* Header. In image mode the full markdown/code Toolbar is replaced
|
|
765
791
|
with a minimal slot bar — view tabs, formatting, and preview
|
|
766
792
|
controls don't apply to a binary asset. */}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
793
|
+
{isImageMode ? (
|
|
794
|
+
(toolbarSlotLeft || toolbarSlotRight) && (
|
|
795
|
+
<div className="squisq-editor-header squisq-editor-header--image">
|
|
796
|
+
{toolbarSlotLeft}
|
|
797
|
+
<div style={{ flex: 1 }} />
|
|
798
|
+
{toolbarSlotRight}
|
|
799
|
+
</div>
|
|
800
|
+
)
|
|
801
|
+
) : (
|
|
802
|
+
<div className="squisq-editor-header">
|
|
803
|
+
<Toolbar
|
|
804
|
+
showFiles={showFiles}
|
|
805
|
+
onToggleFiles={!isCodeMode && filesToggleEnabled ? handleToggleFiles : undefined}
|
|
806
|
+
slotLeft={toolbarSlotLeft}
|
|
807
|
+
slotAfterActions={
|
|
808
|
+
<>
|
|
809
|
+
{toolbarSlotAfterActions}
|
|
810
|
+
{!isCodeMode && isPreview && <PreviewToolbarControls />}
|
|
811
|
+
</>
|
|
812
|
+
}
|
|
813
|
+
slotRight={toolbarSlotRight}
|
|
814
|
+
showPlayTab={showPlayTab}
|
|
815
|
+
/>
|
|
773
816
|
</div>
|
|
774
|
-
)
|
|
775
|
-
) : (
|
|
776
|
-
<div className="squisq-editor-header">
|
|
777
|
-
<Toolbar
|
|
778
|
-
showFiles={showFiles}
|
|
779
|
-
onToggleFiles={!isCodeMode && filesToggleEnabled ? handleToggleFiles : undefined}
|
|
780
|
-
slotLeft={toolbarSlotLeft}
|
|
781
|
-
slotAfterActions={
|
|
782
|
-
<>
|
|
783
|
-
{toolbarSlotAfterActions}
|
|
784
|
-
{!isCodeMode && isPreview && <PreviewToolbarControls />}
|
|
785
|
-
</>
|
|
786
|
-
}
|
|
787
|
-
slotRight={toolbarSlotRight}
|
|
788
|
-
showPlayTab={showPlayTab}
|
|
789
|
-
/>
|
|
790
|
-
</div>
|
|
791
|
-
)}
|
|
817
|
+
)}
|
|
792
818
|
|
|
793
|
-
|
|
794
|
-
<div
|
|
795
|
-
className="squisq-editor-content"
|
|
796
|
-
style={{
|
|
797
|
-
flex: autoGrow ? '1 1 auto' : 1,
|
|
798
|
-
overflowY: autoGrow ? 'auto' : 'hidden',
|
|
799
|
-
overflowX: 'hidden',
|
|
800
|
-
minHeight: 0,
|
|
801
|
-
position: 'relative',
|
|
802
|
-
display: 'flex',
|
|
803
|
-
}}
|
|
804
|
-
>
|
|
819
|
+
{/* Main content area */}
|
|
805
820
|
<div
|
|
821
|
+
className="squisq-editor-content"
|
|
806
822
|
style={{
|
|
807
823
|
flex: autoGrow ? '1 1 auto' : 1,
|
|
808
|
-
|
|
824
|
+
overflowY: autoGrow ? 'auto' : 'hidden',
|
|
825
|
+
overflowX: 'hidden',
|
|
809
826
|
minHeight: 0,
|
|
810
827
|
position: 'relative',
|
|
828
|
+
display: 'flex',
|
|
811
829
|
}}
|
|
812
830
|
>
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
831
|
+
<div
|
|
832
|
+
style={{
|
|
833
|
+
flex: autoGrow ? '1 1 auto' : 1,
|
|
834
|
+
overflow: autoGrow ? 'visible' : 'hidden',
|
|
835
|
+
minHeight: 0,
|
|
836
|
+
position: 'relative',
|
|
837
|
+
}}
|
|
838
|
+
>
|
|
839
|
+
{isImageMode &&
|
|
840
|
+
imageSrc &&
|
|
841
|
+
(imageMode === 'edit' && imageEditorContainer ? (
|
|
842
|
+
<ImageEditor
|
|
843
|
+
filesContainer={imageEditorContainer}
|
|
844
|
+
initialSrc={imageSrc}
|
|
845
|
+
allowVersioning={allowVersioning}
|
|
846
|
+
versioningAutoSaveIdleMs={versioningAutoSaveIdleMs}
|
|
847
|
+
onExport={onImageExport}
|
|
848
|
+
/>
|
|
849
|
+
) : (
|
|
850
|
+
<ImageViewer src={imageSrc} alt={imageAlt} theme={theme} />
|
|
851
|
+
))}
|
|
852
|
+
{/* Raw (Monaco) view. Always wrapped in `.squisq-editor-with-gutter`
|
|
827
853
|
so toggling a pane on/off doesn't change the editor's tree
|
|
828
854
|
position — Monaco stays mounted and `monacoEditor` in
|
|
829
855
|
context stays stable, which is what `useHeadingLayout` needs
|
|
830
856
|
to compute positions. */}
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
857
|
+
{!isImageMode && activeView === 'raw' && (
|
|
858
|
+
<div className="squisq-editor-with-gutter" key="raw-shell">
|
|
859
|
+
{isMarkdownMode && outlineVisible && (
|
|
860
|
+
<OutlinePanel key="outline" width={outlineWidth} />
|
|
861
|
+
)}
|
|
862
|
+
{isCardMode ? (
|
|
863
|
+
<BlockCardView
|
|
864
|
+
key="raw-card"
|
|
865
|
+
blockCount={blockCount}
|
|
866
|
+
activeBlockKey={activeBlockKey}
|
|
867
|
+
onPrev={prevBlock}
|
|
868
|
+
onNext={nextBlock}
|
|
869
|
+
onAdd={addBlock}
|
|
870
|
+
>
|
|
871
|
+
<div key="raw-editor" className="squisq-raw-editor-container">
|
|
872
|
+
<RawEditor
|
|
873
|
+
theme={theme === 'dark' ? 'vs-dark' : 'vs'}
|
|
874
|
+
submitOnEnter={submitOnEnter}
|
|
875
|
+
readOnly={readOnly}
|
|
876
|
+
/>
|
|
877
|
+
</div>
|
|
878
|
+
</BlockCardView>
|
|
879
|
+
) : (
|
|
880
|
+
<div key="raw-editor" className="squisq-raw-editor-container">
|
|
881
|
+
<RawEditor
|
|
882
|
+
theme={theme === 'dark' ? 'vs-dark' : 'vs'}
|
|
883
|
+
submitOnEnter={submitOnEnter}
|
|
884
|
+
readOnly={readOnly}
|
|
885
|
+
/>
|
|
886
|
+
</div>
|
|
887
|
+
)}
|
|
888
|
+
{isMarkdownMode && isCardMode && inlinePreviewVisible && (
|
|
889
|
+
<BlockPreviewPanel key="block-preview" basePath={basePath} />
|
|
890
|
+
)}
|
|
891
|
+
{isMarkdownMode && !isCardMode && inlinePreviewVisible && (
|
|
892
|
+
<InlinePreviewGutter
|
|
893
|
+
key="inline"
|
|
894
|
+
width={inlinePreviewWidth}
|
|
895
|
+
basePath={basePath}
|
|
896
|
+
mediaProvider={mediaProvider}
|
|
897
|
+
/>
|
|
898
|
+
)}
|
|
842
899
|
</div>
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
key="inline"
|
|
846
|
-
width={inlinePreviewWidth}
|
|
847
|
-
basePath={basePath}
|
|
848
|
-
mediaProvider={mediaProvider}
|
|
849
|
-
/>
|
|
850
|
-
)}
|
|
851
|
-
</div>
|
|
852
|
-
)}
|
|
853
|
-
{/* WYSIWYG + Preview are markdown-only surfaces — skip them
|
|
900
|
+
)}
|
|
901
|
+
{/* WYSIWYG + Preview are markdown-only surfaces — skip them
|
|
854
902
|
entirely in code or image mode so Tiptap never initializes
|
|
855
903
|
and the preview pipeline stays idle. Same always-wrapped
|
|
856
904
|
pattern as the Raw branch above so pane toggles don't
|
|
857
905
|
remount Tiptap. */}
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
906
|
+
{isMarkdownMode && activeView === 'wysiwyg' && (
|
|
907
|
+
<div className="squisq-editor-with-gutter" key="wysiwyg-shell">
|
|
908
|
+
{outlineVisible && <OutlinePanel key="outline" width={outlineWidth} />}
|
|
909
|
+
{isCardMode ? (
|
|
910
|
+
<BlockCardView
|
|
911
|
+
key="wysiwyg-card"
|
|
912
|
+
blockCount={blockCount}
|
|
913
|
+
activeBlockKey={activeBlockKey}
|
|
914
|
+
onPrev={prevBlock}
|
|
915
|
+
onNext={nextBlock}
|
|
916
|
+
onAdd={addBlock}
|
|
917
|
+
>
|
|
918
|
+
<WysiwygEditor
|
|
919
|
+
key="wysiwyg-editor"
|
|
920
|
+
submitOnEnter={submitOnEnter}
|
|
921
|
+
placeholder={placeholder}
|
|
922
|
+
readOnly={readOnly}
|
|
923
|
+
/>
|
|
924
|
+
</BlockCardView>
|
|
925
|
+
) : (
|
|
926
|
+
<WysiwygEditor
|
|
927
|
+
key="wysiwyg-editor"
|
|
928
|
+
submitOnEnter={submitOnEnter}
|
|
929
|
+
placeholder={placeholder}
|
|
930
|
+
readOnly={readOnly}
|
|
931
|
+
/>
|
|
932
|
+
)}
|
|
933
|
+
{isCardMode && inlinePreviewVisible && (
|
|
934
|
+
<BlockPreviewPanel key="block-preview" basePath={basePath} />
|
|
935
|
+
)}
|
|
936
|
+
{!isCardMode && inlinePreviewVisible && (
|
|
937
|
+
<InlinePreviewGutter
|
|
938
|
+
key="inline"
|
|
939
|
+
width={inlinePreviewWidth}
|
|
940
|
+
basePath={basePath}
|
|
941
|
+
mediaProvider={mediaProvider}
|
|
942
|
+
/>
|
|
943
|
+
)}
|
|
944
|
+
</div>
|
|
945
|
+
)}
|
|
946
|
+
{isMarkdownMode && isPreview && (
|
|
947
|
+
<PreviewPanel basePath={basePath} workspaceContainer={workspaceContainer} />
|
|
948
|
+
)}
|
|
949
|
+
</div>
|
|
950
|
+
|
|
951
|
+
{isMarkdownMode && showFiles && (
|
|
952
|
+
<MediaBin
|
|
953
|
+
mediaProvider={mediaProvider}
|
|
954
|
+
isDark={isDark}
|
|
955
|
+
refreshKey={mediaRefreshKey}
|
|
956
|
+
onMediaUploaded={insertMediaRef}
|
|
957
|
+
/>
|
|
879
958
|
)}
|
|
880
|
-
</div>
|
|
881
959
|
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
refreshKey={mediaRefreshKey}
|
|
887
|
-
onMediaUploaded={insertMediaRef}
|
|
888
|
-
/>
|
|
889
|
-
)}
|
|
960
|
+
{/* Docked custom-theme designer — a flex sibling of the preview, so
|
|
961
|
+
opening it reflows the preview narrower. Renders nothing when the
|
|
962
|
+
designer is closed. */}
|
|
963
|
+
{isMarkdownMode && <ThemeDesignerDock />}
|
|
890
964
|
|
|
891
|
-
|
|
965
|
+
{/* Drop zone overlay — image / text drop UX is markdown-specific.
|
|
892
966
|
In WYSIWYG, image drops are handled directly by Tiptap's
|
|
893
967
|
`handleDrop` (uploads to the MediaProvider and inserts an
|
|
894
968
|
image node at the mouse position). The overlay would sit on
|
|
@@ -896,23 +970,29 @@ function EditorShellInner({
|
|
|
896
970
|
when the dragged content is media-only on the WYSIWYG view —
|
|
897
971
|
the user gets a one-step "drop where you want it" flow
|
|
898
972
|
instead of a two-step "drop in bin, then insert" flow. */}
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
973
|
+
{isMarkdownMode &&
|
|
974
|
+
isDragging &&
|
|
975
|
+
!(activeView === 'wysiwyg' && dragContentType === 'media') && (
|
|
976
|
+
<DropZoneOverlay
|
|
977
|
+
dragContentType={dragContentType}
|
|
978
|
+
zoneProps={zoneProps}
|
|
979
|
+
hasMediaProvider={mediaProvider !== null}
|
|
980
|
+
/>
|
|
981
|
+
)}
|
|
982
|
+
</div>
|
|
983
|
+
|
|
984
|
+
{/* Timeline track — horizontal strip of block + media bars shown
|
|
985
|
+
below the editor in Timeline view. The card editor above (driven by
|
|
986
|
+
the block navigator) shows whichever block is selected here. */}
|
|
987
|
+
{isTimelineMode && <TimelineTrack />}
|
|
909
988
|
|
|
910
|
-
|
|
989
|
+
{/* Status bar — word / char / line / block counts. Host can
|
|
911
990
|
suppress via `showStatusBar={false}` for embedded chat-style
|
|
912
991
|
composers where the stats are noise. The image viewer has its
|
|
913
992
|
own dimension/zoom status row, so suppress here too. */}
|
|
914
|
-
|
|
915
|
-
|
|
993
|
+
{statusBarVisible && !isImageMode && <StatusBar />}
|
|
994
|
+
</PreviewSettingsProvider>
|
|
995
|
+
</CustomThemeProvider>
|
|
916
996
|
<TooltipLayer />
|
|
917
997
|
{imageEditTarget !== null && mediaProvider && (
|
|
918
998
|
<ImageEditModal
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* One entry in a {@link FolderView} — a file or subfolder. Host-defined
|
|
5
|
+
* `path` is opaque to this component: it's used as the React key and
|
|
6
|
+
* handed back verbatim to the open callbacks so the host can resolve it.
|
|
7
|
+
*/
|
|
8
|
+
export interface FolderEntry {
|
|
9
|
+
/** Display name (basename). */
|
|
10
|
+
name: string;
|
|
11
|
+
/** Full host path. Opaque here; passed back on open. */
|
|
12
|
+
path: string;
|
|
13
|
+
isDirectory: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface FolderViewProps {
|
|
17
|
+
/** Folder display name (basename) shown in the header. */
|
|
18
|
+
name: string;
|
|
19
|
+
/** Immediate children of the folder — subfolders and files. */
|
|
20
|
+
entries: FolderEntry[];
|
|
21
|
+
/** Open a file entry. The host decides what "open" means. */
|
|
22
|
+
onOpenFile: (entry: FolderEntry) => void;
|
|
23
|
+
/** Drill into a subfolder. */
|
|
24
|
+
onOpenFolder: (entry: FolderEntry) => void;
|
|
25
|
+
/** Primary action — create a new document in this folder. */
|
|
26
|
+
onNewDocument: () => void;
|
|
27
|
+
/** Secondary action — create a new subfolder. Omit to hide the button. */
|
|
28
|
+
onNewFolder?: () => void;
|
|
29
|
+
/** Color theme (default `'light'`). */
|
|
30
|
+
theme?: 'light' | 'dark';
|
|
31
|
+
/** CSS height for the container (default `'100%'`). */
|
|
32
|
+
height?: string;
|
|
33
|
+
/** Override the per-entry icon. Defaults to FontAwesome file/folder glyphs. */
|
|
34
|
+
iconFor?: (entry: FolderEntry) => ReactNode;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* FolderView — a standalone folder browser surface.
|
|
39
|
+
*
|
|
40
|
+
* Lists a folder's files and subfolders plus a prominent "New document"
|
|
41
|
+
* action (and an optional "New folder"). It's the companion to
|
|
42
|
+
* {@link EditorShell}: where the shell edits a single document, this
|
|
43
|
+
* presents the directory around it.
|
|
44
|
+
*
|
|
45
|
+
* Like the shell, it's host-agnostic — the consumer supplies the
|
|
46
|
+
* `entries` and the open / new callbacks, and FolderView owns only the
|
|
47
|
+
* presentation and theming. It holds no state and reads nothing from
|
|
48
|
+
* disk, so it composes into any storage backend.
|
|
49
|
+
*/
|
|
50
|
+
export function FolderView({
|
|
51
|
+
name,
|
|
52
|
+
entries,
|
|
53
|
+
onOpenFile,
|
|
54
|
+
onOpenFolder,
|
|
55
|
+
onNewDocument,
|
|
56
|
+
onNewFolder,
|
|
57
|
+
theme = 'light',
|
|
58
|
+
height = '100%',
|
|
59
|
+
iconFor = defaultIconFor,
|
|
60
|
+
}: FolderViewProps) {
|
|
61
|
+
// Folders first, then files; each group alphabetical (case-insensitive).
|
|
62
|
+
const sorted = [...entries].sort((a, b) => {
|
|
63
|
+
if (a.isDirectory !== b.isDirectory) return a.isDirectory ? -1 : 1;
|
|
64
|
+
return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div className="squisq-folder-view" data-theme={theme} style={{ height }}>
|
|
69
|
+
<div className="squisq-folder-view-header">
|
|
70
|
+
<i
|
|
71
|
+
className="fa-regular fa-folder-open squisq-folder-view-header-icon"
|
|
72
|
+
aria-hidden="true"
|
|
73
|
+
/>
|
|
74
|
+
<span className="squisq-folder-view-title">{name}</span>
|
|
75
|
+
<span className="squisq-folder-view-count">
|
|
76
|
+
{entries.length} {entries.length === 1 ? 'item' : 'items'}
|
|
77
|
+
</span>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<div className="squisq-folder-view-body">
|
|
81
|
+
{sorted.length === 0 ? (
|
|
82
|
+
<p className="squisq-folder-view-empty">This folder is empty.</p>
|
|
83
|
+
) : (
|
|
84
|
+
<ul className="squisq-folder-view-list">
|
|
85
|
+
{sorted.map((entry) => (
|
|
86
|
+
<li key={entry.path}>
|
|
87
|
+
<button
|
|
88
|
+
type="button"
|
|
89
|
+
className="squisq-folder-view-item"
|
|
90
|
+
onClick={() => (entry.isDirectory ? onOpenFolder(entry) : onOpenFile(entry))}
|
|
91
|
+
>
|
|
92
|
+
<span className="squisq-folder-view-item-icon">{iconFor(entry)}</span>
|
|
93
|
+
<span className="squisq-folder-view-item-name">{entry.name}</span>
|
|
94
|
+
{entry.isDirectory && (
|
|
95
|
+
<i
|
|
96
|
+
className="fa-solid fa-chevron-right squisq-folder-view-item-chevron"
|
|
97
|
+
aria-hidden="true"
|
|
98
|
+
/>
|
|
99
|
+
)}
|
|
100
|
+
</button>
|
|
101
|
+
</li>
|
|
102
|
+
))}
|
|
103
|
+
</ul>
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div className="squisq-folder-view-actions">
|
|
108
|
+
<button type="button" className="squisq-folder-view-new" onClick={onNewDocument}>
|
|
109
|
+
<i className="fa-solid fa-plus" aria-hidden="true" /> New document
|
|
110
|
+
</button>
|
|
111
|
+
{onNewFolder && (
|
|
112
|
+
<button type="button" className="squisq-folder-view-new-folder" onClick={onNewFolder}>
|
|
113
|
+
<i className="fa-regular fa-folder" aria-hidden="true" /> New folder
|
|
114
|
+
</button>
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const IMAGE_RE = /\.(png|jpe?g|gif|svg|webp|bmp|avif)$/i;
|
|
122
|
+
|
|
123
|
+
function defaultIconFor(entry: FolderEntry): ReactNode {
|
|
124
|
+
if (entry.isDirectory) {
|
|
125
|
+
return <i className="fa-regular fa-folder fa-fw" aria-hidden="true" />;
|
|
126
|
+
}
|
|
127
|
+
if (IMAGE_RE.test(entry.name)) {
|
|
128
|
+
return <i className="fa-regular fa-file-image fa-fw" aria-hidden="true" />;
|
|
129
|
+
}
|
|
130
|
+
return <i className="fa-regular fa-file-lines fa-fw" aria-hidden="true" />;
|
|
131
|
+
}
|
package/src/Icon.tsx
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Icon — renders a Font Awesome Free glyph via the webfont CSS that
|
|
3
|
+
* `styles/index.css` imports (`@fortawesome/fontawesome-free`).
|
|
4
|
+
*
|
|
5
|
+
* Icons are decorative: the surrounding control owns the accessible
|
|
6
|
+
* label (`aria-label` / `data-tooltip`), so the glyph is marked
|
|
7
|
+
* `aria-hidden`. Sizing and color flow from the parent's `font-size`
|
|
8
|
+
* and `color`, matching how the inline `{[icon]}` content nodes render
|
|
9
|
+
* (see `InlineIcon.ts`).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { CSSProperties } from 'react';
|
|
13
|
+
|
|
14
|
+
export interface IconProps {
|
|
15
|
+
/** Full Font Awesome class string, e.g. `"fa-solid fa-bold"`. */
|
|
16
|
+
icon: string;
|
|
17
|
+
/** Extra class names appended after the Font Awesome classes. */
|
|
18
|
+
className?: string;
|
|
19
|
+
style?: CSSProperties;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function Icon({ icon, className, style }: IconProps) {
|
|
23
|
+
return (
|
|
24
|
+
<i className={className ? `${icon} ${className}` : icon} style={style} aria-hidden="true" />
|
|
25
|
+
);
|
|
26
|
+
}
|