@bendyline/squisq-editor-react 1.5.3 → 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.
Files changed (165) hide show
  1. package/dist/index.d.ts +757 -20
  2. package/dist/index.js +16875 -6883
  3. package/dist/index.js.map +1 -1
  4. package/package.json +4 -4
  5. package/src/BlockCardView.tsx +121 -0
  6. package/src/BlockPreviewPanel.tsx +69 -0
  7. package/src/BlockPropertiesPopover.tsx +191 -0
  8. package/src/EditorContext.tsx +143 -0
  9. package/src/EditorShell.tsx +200 -120
  10. package/src/FolderView.tsx +131 -0
  11. package/src/Icon.tsx +26 -0
  12. package/src/ImageEditor.tsx +69 -22
  13. package/src/OutlinePanel.tsx +38 -3
  14. package/src/PlainHtmlPreview.tsx +30 -3
  15. package/src/PreviewControls.tsx +180 -8
  16. package/src/RawEditor.tsx +169 -13
  17. package/src/RecorderEntry.tsx +9 -16
  18. package/src/TemplateAnnotation.ts +44 -0
  19. package/src/TemplatePicker.tsx +329 -54
  20. package/src/ThemeCustomizerPanel.tsx +30 -336
  21. package/src/ThemePicker.tsx +112 -3
  22. package/src/TimelineBlockPreview.tsx +37 -0
  23. package/src/TimelineTrack.tsx +671 -0
  24. package/src/Toolbar.tsx +528 -174
  25. package/src/Tooltip.tsx +22 -4
  26. package/src/TransitionPicker.tsx +351 -0
  27. package/src/VersionHistoryPanel.tsx +2 -14
  28. package/src/ViewMenuPanel.tsx +17 -14
  29. package/src/WysiwygEditor.tsx +161 -65
  30. package/src/__tests__/blockProperties.test.ts +92 -0
  31. package/src/__tests__/blockRange.test.ts +105 -0
  32. package/src/__tests__/buildPreviewDocTransition.test.ts +73 -0
  33. package/src/__tests__/createShapeLayer.test.ts +46 -0
  34. package/src/__tests__/drawingShapeRoundTrip.test.ts +49 -0
  35. package/src/__tests__/embeddedMedia.test.ts +48 -0
  36. package/src/__tests__/headingTransition.test.ts +138 -0
  37. package/src/__tests__/layoutChildRoundTrip.test.ts +71 -0
  38. package/src/__tests__/plainHtmlPreview.test.tsx +10 -8
  39. package/src/__tests__/recorderMediaInsert.test.ts +86 -0
  40. package/src/__tests__/templateAnnotationRoundTrip.test.ts +18 -0
  41. package/src/__tests__/templatePickerMetadata.test.ts +32 -0
  42. package/src/__tests__/timelineSource.test.ts +134 -0
  43. package/src/__tests__/tiptapBridge.test.ts +92 -0
  44. package/src/__tests__/tiptapBridgeConformance.test.ts +47 -0
  45. package/src/__tests__/tooltip.test.tsx +72 -0
  46. package/src/__tests__/transitionCatalog.test.ts +64 -0
  47. package/src/__tests__/useBlockNavigator.test.tsx +67 -0
  48. package/src/__tests__/useMediaRecorder.test.ts +24 -0
  49. package/src/__tests__/useTimelineClock.test.ts +21 -0
  50. package/src/blockProperties.ts +88 -0
  51. package/src/blockRange.ts +132 -0
  52. package/src/buildPreviewDoc.ts +98 -9
  53. package/src/customTemplates/AddBin.tsx +126 -0
  54. package/src/customTemplates/CustomLayoutManager.tsx +233 -0
  55. package/src/customTemplates/CustomTemplateContext.tsx +182 -0
  56. package/src/customTemplates/LayerToolbar.tsx +580 -0
  57. package/src/customTemplates/ShapeGlyph.tsx +47 -0
  58. package/src/customTemplates/TemplateDesigner.tsx +430 -0
  59. package/src/customTemplates/__tests__/library.test.ts +88 -0
  60. package/src/customTemplates/__tests__/normalizePositions.test.ts +109 -0
  61. package/src/customTemplates/__tests__/shapeDefs.test.ts +49 -0
  62. package/src/customTemplates/__tests__/useMemoryLayerAdapter.test.ts +95 -0
  63. package/src/customTemplates/designer.css +673 -0
  64. package/src/customTemplates/index.ts +31 -0
  65. package/src/customTemplates/library.ts +97 -0
  66. package/src/customTemplates/normalizePositions.ts +75 -0
  67. package/src/customTemplates/shapeDefs.ts +131 -0
  68. package/src/customTemplates/thumbnail.tsx +63 -0
  69. package/src/customTemplates/tokenDefs.ts +60 -0
  70. package/src/customTemplates/useDocCustomTemplates.ts +52 -0
  71. package/src/customTemplates/useMemoryLayerAdapter.ts +123 -0
  72. package/src/customThemes/CustomThemeContext.tsx +179 -0
  73. package/src/customThemes/CustomThemeDialog.tsx +286 -0
  74. package/src/customThemes/__tests__/CustomThemeContext.test.tsx +64 -0
  75. package/src/customThemes/__tests__/CustomThemeDialog.test.tsx +47 -0
  76. package/src/customThemes/__tests__/customThemeLibrary.test.ts +51 -0
  77. package/src/customThemes/customThemeLibrary.ts +97 -0
  78. package/src/customThemes/index.ts +31 -0
  79. package/src/customThemes/themeControls.tsx +229 -0
  80. package/src/customThemes/themeDraft.ts +272 -0
  81. package/src/customThemes/useDocCustomThemes.ts +49 -0
  82. package/src/diagram/DiagramCanvas.tsx +240 -0
  83. package/src/diagram/DiagramExtension.ts +209 -0
  84. package/src/diagram/DiagramMaximizedOverlay.tsx +46 -0
  85. package/src/diagram/DiagramWidget.tsx +270 -0
  86. package/src/diagram/diagramCommands.ts +604 -0
  87. package/src/diagram/diagramConstants.ts +17 -0
  88. package/src/diagram/useDiagramData.ts +126 -0
  89. package/src/embeddedMedia.ts +78 -0
  90. package/src/frontmatter.ts +29 -0
  91. package/src/headingTransition.ts +231 -0
  92. package/src/imageEditor/CanvasSurface.tsx +383 -88
  93. package/src/imageEditor/PropertiesPanel.tsx +47 -1
  94. package/src/imageEditor/Toolbar.tsx +229 -16
  95. package/src/imageEditor/createShapeLayer.ts +280 -0
  96. package/src/imageEditor/icons.tsx +34 -114
  97. package/src/imageEditor/image-editor.css +54 -5
  98. package/src/imageEditor/state.ts +23 -3
  99. package/src/index.ts +77 -0
  100. package/src/recorder/RecorderModal.tsx +120 -53
  101. package/src/recorder/RecorderPanel.tsx +2 -26
  102. package/src/recorder/hooks/useMediaRecorder.ts +8 -1
  103. package/src/recorder/insertMediaBlock.ts +30 -0
  104. package/src/resolveBlockVisual.ts +33 -0
  105. package/src/scene/Scene.tsx +540 -0
  106. package/src/scene/SceneBlockExtension.ts +198 -0
  107. package/src/scene/SceneBlockToolbar.tsx +201 -0
  108. package/src/scene/SceneBlockWidget.tsx +434 -0
  109. package/src/scene/ScenePropsBar.tsx +85 -0
  110. package/src/scene/SceneSelection.tsx +107 -0
  111. package/src/scene/SceneViewport.tsx +102 -0
  112. package/src/scene/ShapePalette.tsx +181 -0
  113. package/src/scene/__tests__/DiagramAdapter.test.ts +56 -0
  114. package/src/scene/__tests__/bezierEdit.test.ts +85 -0
  115. package/src/scene/__tests__/blockLayers.test.ts +57 -0
  116. package/src/scene/__tests__/shapeLayers.test.ts +106 -0
  117. package/src/scene/__tests__/useSceneHitTest.test.ts +90 -0
  118. package/src/scene/__tests__/useScenePanZoom.test.ts +103 -0
  119. package/src/scene/adapters/DiagramAdapter.ts +168 -0
  120. package/src/scene/adapters/DrawingAdapter.ts +415 -0
  121. package/src/scene/adapters/LayoutAdapter.ts +310 -0
  122. package/src/scene/adapters/blockLayers.ts +159 -0
  123. package/src/scene/commands/SceneCommand.ts +70 -0
  124. package/src/scene/commands/drawingCommands.ts +318 -0
  125. package/src/scene/commands/layoutCommands.ts +301 -0
  126. package/src/scene/hooks/useSceneHitTest.ts +105 -0
  127. package/src/scene/hooks/useScenePanZoom.ts +147 -0
  128. package/src/scene/hooks/useSceneSelection.ts +62 -0
  129. package/src/scene/index.ts +95 -0
  130. package/src/scene/layers/DiagramEdges.tsx +127 -0
  131. package/src/scene/layers/edgeGeometry.ts +77 -0
  132. package/src/scene/layers/nodeCard.tsx +145 -0
  133. package/src/scene/layers/renderLayer.tsx +70 -0
  134. package/src/scene/layers/shapeLayers.ts +201 -0
  135. package/src/scene/paths/bezierEdit.ts +208 -0
  136. package/src/scene/scene.css +649 -0
  137. package/src/scene/text/SceneTextOverlay.tsx +161 -0
  138. package/src/scene/text/sceneTextChannel.ts +40 -0
  139. package/src/scene/text/sceneTextConfig.ts +27 -0
  140. package/src/scene/text/sceneTiptap.ts +36 -0
  141. package/src/scene/text/useSceneTextEditing.ts +39 -0
  142. package/src/scene/tools/ConnectTool.ts +111 -0
  143. package/src/scene/tools/DrawingConnectTool.ts +161 -0
  144. package/src/scene/tools/PathTool.ts +158 -0
  145. package/src/scene/tools/PlaceTool.ts +47 -0
  146. package/src/scene/tools/SceneTool.ts +75 -0
  147. package/src/scene/tools/SelectTool.ts +284 -0
  148. package/src/scene/tools/ShapeTool.ts +144 -0
  149. package/src/scene/tools/TextTool.ts +72 -0
  150. package/src/scene/tools/TokenTool.ts +95 -0
  151. package/src/scene/tools/createDrawShapeTool.ts +82 -0
  152. package/src/styles/diagram.css +183 -0
  153. package/src/styles/editor.css +1615 -203
  154. package/src/styles/folder-view.css +210 -0
  155. package/src/styles/image-edit-affordance.css +2 -2
  156. package/src/styles/index.css +4 -0
  157. package/src/timelineSource.ts +244 -0
  158. package/src/tiptapBridge.ts +115 -34
  159. package/src/tooltipPlacement.ts +13 -0
  160. package/src/transitionCatalog.ts +159 -0
  161. package/src/types/monaco-shims.d.ts +10 -0
  162. package/src/useBlockNavigator.ts +153 -0
  163. package/src/useMonacoLoader.ts +23 -1
  164. package/src/useTimelineClock.ts +76 -0
  165. package/src/utils/dropUtils.ts +1 -1
@@ -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 { PreviewSettingsProvider, PreviewToolbarControls } from './PreviewControls';
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
- <PreviewSettingsProvider doc={doc} themeOverride={themeOverride}>
764
- {/* Header. In image mode the full markdown/code Toolbar is replaced
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
- {isImageMode ? (
768
- (toolbarSlotLeft || toolbarSlotRight) && (
769
- <div className="squisq-editor-header squisq-editor-header--image">
770
- {toolbarSlotLeft}
771
- <div style={{ flex: 1 }} />
772
- {toolbarSlotRight}
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
- {/* Main content area */}
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
- overflow: autoGrow ? 'visible' : 'hidden',
824
+ overflowY: autoGrow ? 'auto' : 'hidden',
825
+ overflowX: 'hidden',
809
826
  minHeight: 0,
810
827
  position: 'relative',
828
+ display: 'flex',
811
829
  }}
812
830
  >
813
- {isImageMode &&
814
- imageSrc &&
815
- (imageMode === 'edit' && imageEditorContainer ? (
816
- <ImageEditor
817
- filesContainer={imageEditorContainer}
818
- initialSrc={imageSrc}
819
- allowVersioning={allowVersioning}
820
- versioningAutoSaveIdleMs={versioningAutoSaveIdleMs}
821
- onExport={onImageExport}
822
- />
823
- ) : (
824
- <ImageViewer src={imageSrc} alt={imageAlt} theme={theme} />
825
- ))}
826
- {/* Raw (Monaco) view. Always wrapped in `.squisq-editor-with-gutter`
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
- {!isImageMode && activeView === 'raw' && (
832
- <div className="squisq-editor-with-gutter" key="raw-shell">
833
- {isMarkdownMode && outlineVisible && (
834
- <OutlinePanel key="outline" width={outlineWidth} />
835
- )}
836
- <div key="raw-editor" className="squisq-raw-editor-container">
837
- <RawEditor
838
- theme={theme === 'dark' ? 'vs-dark' : 'vs'}
839
- submitOnEnter={submitOnEnter}
840
- readOnly={readOnly}
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
- {isMarkdownMode && inlinePreviewVisible && (
844
- <InlinePreviewGutter
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
- {isMarkdownMode && activeView === 'wysiwyg' && (
859
- <div className="squisq-editor-with-gutter" key="wysiwyg-shell">
860
- {outlineVisible && <OutlinePanel key="outline" width={outlineWidth} />}
861
- <WysiwygEditor
862
- key="wysiwyg-editor"
863
- submitOnEnter={submitOnEnter}
864
- placeholder={placeholder}
865
- readOnly={readOnly}
866
- />
867
- {inlinePreviewVisible && (
868
- <InlinePreviewGutter
869
- key="inline"
870
- width={inlinePreviewWidth}
871
- basePath={basePath}
872
- mediaProvider={mediaProvider}
873
- />
874
- )}
875
- </div>
876
- )}
877
- {isMarkdownMode && isPreview && (
878
- <PreviewPanel basePath={basePath} workspaceContainer={workspaceContainer} />
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
- {isMarkdownMode && showFiles && (
883
- <MediaBin
884
- mediaProvider={mediaProvider}
885
- isDark={isDark}
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
- {/* Drop zone overlay — image / text drop UX is markdown-specific.
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
- {isMarkdownMode &&
900
- isDragging &&
901
- !(activeView === 'wysiwyg' && dragContentType === 'media') && (
902
- <DropZoneOverlay
903
- dragContentType={dragContentType}
904
- zoneProps={zoneProps}
905
- hasMediaProvider={mediaProvider !== null}
906
- />
907
- )}
908
- </div>
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
- {/* Status bar — word / char / line / block counts. Host can
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
- {statusBarVisible && !isImageMode && <StatusBar />}
915
- </PreviewSettingsProvider>
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
+ }