@bendyline/squisq-editor-react 1.6.1 → 2.0.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/README.md +12 -3
- package/dist/index.d.ts +1304 -208
- package/dist/index.js +22838 -14494
- package/dist/index.js.map +1 -1
- package/dist/monaco.d.ts +1 -0
- package/dist/monaco.js +4 -0
- package/dist/monaco.js.map +1 -0
- package/dist/styles/index.css +2384 -408
- package/package.json +9 -5
- package/src/BlockPropertiesPopover.tsx +48 -17
- package/src/DocumentSettingsDialog.tsx +13 -21
- package/src/EditorContext.tsx +199 -42
- package/src/EditorShell.tsx +403 -270
- package/src/ImageEditor.tsx +22 -1
- package/src/InlinePreviewGutter.tsx +16 -8
- package/src/MediaBin.tsx +276 -40
- package/src/OutlinePanel.tsx +202 -7
- package/src/PlainHtmlPreview.tsx +40 -1
- package/src/PreviewControls.tsx +466 -93
- package/src/PreviewPanel.tsx +313 -77
- package/src/RawEditor.tsx +13 -2
- package/src/StatusBar.tsx +5 -2
- package/src/TemplateAnnotation.ts +34 -10
- package/src/TemplateContentPreview.tsx +56 -0
- package/src/TemplatePicker.tsx +355 -134
- package/src/ThemeCustomizerPanel.tsx +30 -15
- package/src/ThemePicker.tsx +10 -5
- package/src/TimelineBlockPreview.tsx +1 -1
- package/src/Toolbar.tsx +719 -238
- package/src/TransitionPicker.tsx +44 -5
- package/src/ViewMenuPanel.tsx +12 -14
- package/src/ViewSwitcher.tsx +4 -4
- package/src/WysiwygEditor.tsx +167 -92
- package/src/__tests__/blockPropertiesPopoverTheme.test.tsx +33 -0
- package/src/__tests__/blockTagActivity.test.ts +183 -0
- package/src/__tests__/buildDocumentPreviewMarkdown.test.ts +75 -0
- package/src/__tests__/buildPreviewDocContent.test.ts +48 -0
- package/src/__tests__/buildPreviewDocTransition.test.ts +22 -3
- package/src/__tests__/canvasSurfaceTextEditing.test.tsx +60 -0
- package/src/__tests__/customLayoutManagerTheme.test.tsx +34 -0
- package/src/__tests__/documentSettingsDialog.test.tsx +29 -1
- package/src/__tests__/editorScrollHandoff.test.tsx +102 -0
- package/src/__tests__/editorScrollSync.test.ts +65 -0
- package/src/__tests__/editorShellProps.test.tsx +390 -1
- package/src/__tests__/emojiPicker.test.tsx +2 -42
- package/src/__tests__/headingTransition.test.ts +53 -15
- package/src/__tests__/imageEditAffordance.test.tsx +5 -1
- package/src/__tests__/imageEditModalContract.test.tsx +119 -0
- package/src/__tests__/imageEditorShell.test.tsx +38 -1
- package/src/__tests__/jsonEditor.test.tsx +56 -1
- package/src/__tests__/layersPanel.test.tsx +75 -5
- package/src/__tests__/majorApiTypes.test.ts +26 -0
- package/src/__tests__/mediaBinDrop.test.tsx +90 -0
- package/src/__tests__/mediaReferences.test.ts +82 -0
- package/src/__tests__/narrationSave.test.ts +148 -0
- package/src/__tests__/outlinePanel.test.tsx +93 -3
- package/src/__tests__/outlineSource.test.ts +217 -0
- package/src/__tests__/plainHtmlPreview.test.tsx +17 -1
- package/src/__tests__/presentationMode.test.tsx +290 -0
- package/src/__tests__/previewControls.test.tsx +312 -6
- package/src/__tests__/previewPanelAudioFailure.test.tsx +46 -0
- package/src/__tests__/previewPanelPresentation.test.tsx +126 -0
- package/src/__tests__/previewPanelTransforms.test.tsx +117 -0
- package/src/__tests__/rawEditorModelIsolation.test.ts +19 -0
- package/src/__tests__/resolveBlockVisual.test.ts +43 -0
- package/src/__tests__/statusBar.test.tsx +27 -0
- package/src/__tests__/teleprompter.test.tsx +190 -0
- package/src/__tests__/templateAnnotationRoundTrip.test.ts +49 -2
- package/src/__tests__/templateContentPreview.test.ts +101 -0
- package/src/__tests__/templatePickerPortal.test.tsx +62 -0
- package/src/__tests__/tiptapBridge.test.ts +70 -0
- package/src/__tests__/tiptapCodeBlockRoundTrip.test.ts +109 -0
- package/src/__tests__/transformStyleId.test.ts +13 -0
- package/src/__tests__/useImageEditor.test.tsx +67 -0
- package/src/__tests__/useMediaRecorder.test.ts +75 -0
- package/src/__tests__/viewMenuPanel.test.tsx +130 -0
- package/src/__tests__/wysiwygImageUpload.test.ts +69 -0
- package/src/__tests__/wysiwygTemplateBadgeFocus.test.tsx +56 -0
- package/src/asciiDiagram/AsciiDiagramExtension.ts +343 -0
- package/src/asciiDiagram/AsciiDiagramWidget.tsx +224 -0
- package/src/asciiDiagram/RepairableDiagramExtension.ts +200 -0
- package/src/asciiDiagram/__tests__/AsciiDiagramExtension.test.ts +145 -0
- package/src/asciiDiagram/__tests__/RepairableDiagramExtension.test.ts +159 -0
- package/src/asciiDiagram/__tests__/asciiDiagramCommands.test.ts +227 -0
- package/src/asciiDiagram/__tests__/asciiDiagramOps.test.ts +114 -0
- package/src/asciiDiagram/__tests__/asciiPaste.test.ts +43 -0
- package/src/asciiDiagram/asciiDiagramCommands.ts +148 -0
- package/src/asciiDiagram/asciiDiagramData.ts +116 -0
- package/src/asciiDiagram/asciiDiagramOps.ts +219 -0
- package/src/asciiDiagram/asciiPaste.ts +19 -0
- package/src/blockTagActivity.ts +233 -0
- package/src/buildDocumentPreviewMarkdown.ts +168 -0
- package/src/buildPreviewDoc.ts +19 -12
- package/src/customTemplates/CustomLayoutManager.tsx +3 -2
- package/src/customTemplates/CustomTemplateContext.tsx +6 -0
- package/src/customTemplates/TemplateDesigner.tsx +7 -4
- package/src/customTemplates/__tests__/library.test.ts +8 -0
- package/src/customTemplates/designer.css +158 -99
- package/src/customTemplates/library.ts +16 -3
- package/src/customThemes/CustomThemeContext.tsx +6 -0
- package/src/customThemes/CustomThemeDialog.tsx +35 -5
- package/src/customThemes/ImportThemeSection.tsx +178 -0
- package/src/customThemes/__tests__/customThemeLibrary.test.ts +9 -0
- package/src/customThemes/__tests__/importThemeSection.test.tsx +192 -0
- package/src/customThemes/customThemeLibrary.ts +7 -4
- package/src/customThemes/index.ts +7 -1
- package/src/customThemes/themeDraft.ts +23 -7
- package/src/diagram/DiagramCanvas.tsx +9 -4
- package/src/diagram/types.ts +35 -0
- package/src/editorScrollSync.ts +73 -0
- package/src/emojiData.ts +3 -23
- package/src/headingTransition.ts +85 -25
- package/src/imageEditor/CanvasSurface.tsx +29 -23
- package/src/imageEditor/LayersPanel.tsx +78 -3
- package/src/imageEditor/Toolbar.tsx +5 -1
- package/src/imageEditor/icons.tsx +4 -0
- package/src/imageEditor/image-editor.css +75 -0
- package/src/imageEditor/layers/SelectionHandles.tsx +1 -1
- package/src/imageEditor/useImageEditor.ts +71 -14
- package/src/index.ts +193 -20
- package/src/jsonEditor/JsonEditorContext.tsx +12 -6
- package/src/jsonEditor/RenderNode.tsx +24 -3
- package/src/jsonEditor/editors.tsx +86 -19
- package/src/mediaEntries.ts +12 -0
- package/src/mediaReferences.ts +299 -0
- package/src/monaco.ts +35 -0
- package/src/monacoWorkers.ts +89 -0
- package/src/outlineSource.ts +171 -0
- package/src/presentation/PresentationMode.tsx +596 -0
- package/src/rawEditorIsolation.ts +18 -0
- package/src/recorder/hooks/useMediaRecorder.ts +97 -53
- package/src/resolveBlockVisual.ts +10 -4
- package/src/scene/Scene.tsx +101 -25
- package/src/scene/SceneBlockExtension.ts +17 -10
- package/src/scene/SceneBlockWidget.tsx +11 -4
- package/src/scene/SceneSelection.tsx +19 -15
- package/src/scene/SceneSideToolbar.tsx +89 -0
- package/src/scene/SceneViewport.tsx +7 -3
- package/src/scene/ShapePalette.tsx +17 -2
- package/src/scene/__tests__/DiagramAdapter.test.ts +86 -0
- package/src/scene/__tests__/SceneBlockExtension.test.ts +33 -0
- package/src/scene/__tests__/sceneIsolation.test.tsx +93 -0
- package/src/scene/adapters/DiagramAdapter.ts +12 -101
- package/src/scene/commands/SceneCommand.ts +4 -1
- package/src/scene/index.ts +1 -6
- package/src/scene/layers/DiagramEdges.tsx +24 -9
- package/src/scene/layers/edgeGeometry.ts +31 -4
- package/src/scene/layers/nodeCard.tsx +27 -8
- package/src/scene/scene.css +145 -2
- package/src/scene/text/SceneTextOverlay.tsx +5 -5
- package/src/scene/text/sceneTextChannel.ts +26 -20
- package/src/scene/text/sceneTextConfig.ts +4 -0
- package/src/scene/tools/ConnectTool.ts +126 -28
- package/src/scene/tools/DrawingConnectTool.ts +86 -16
- package/src/scene/tools/SceneTool.ts +10 -0
- package/src/scene/tools/SelectTool.ts +34 -18
- package/src/scene/tools/ShapeTool.ts +2 -3
- package/src/styles/ascii-diagram.css +79 -0
- package/src/styles/ascii-timeline.css +499 -0
- package/src/styles/editor.css +1566 -219
- package/src/styles/index.css +3 -0
- package/src/styles/tree-view.css +139 -0
- package/src/teleprompter/TeleprompterControls.tsx +218 -0
- package/src/teleprompter/TeleprompterSelfView.tsx +22 -0
- package/src/teleprompter/TeleprompterSurface.tsx +267 -0
- package/src/teleprompter/TeleprompterView.tsx +338 -0
- package/src/teleprompter/canvasRenderer.ts +161 -0
- package/src/teleprompter/floatingWindow.ts +352 -0
- package/src/teleprompter/index.ts +61 -0
- package/src/teleprompter/pcmWorklet.ts +62 -0
- package/src/teleprompter/recording/insertPreamble.ts +80 -0
- package/src/teleprompter/recording/narrationSave.ts +134 -0
- package/src/teleprompter/recording/useNarrationRecorder.ts +367 -0
- package/src/teleprompter/scrollModel.ts +85 -0
- package/src/teleprompter/teleprompterTheme.ts +303 -0
- package/src/teleprompter/types.ts +38 -0
- package/src/teleprompter/useFloatingWindow.ts +74 -0
- package/src/teleprompter/useMicAnalysis.ts +211 -0
- package/src/teleprompter/useTeleprompter.ts +421 -0
- package/src/templateContentPreviewResolver.ts +360 -0
- package/src/timeline/TimelineEditorWidget.tsx +702 -0
- package/src/timeline/TimelineViewExtension.ts +252 -0
- package/src/timeline/__tests__/TimelineEditorWidget.test.tsx +355 -0
- package/src/timeline/__tests__/TimelineViewExtension.test.ts +163 -0
- package/src/timeline/__tests__/timelineCommands.test.ts +327 -0
- package/src/timeline/__tests__/timelineOps.test.ts +222 -0
- package/src/timeline/__tests__/timelinePaste.test.ts +34 -0
- package/src/timeline/timelineCommands.ts +367 -0
- package/src/timeline/timelineData.ts +54 -0
- package/src/timeline/timelineOps.ts +277 -0
- package/src/timeline/timelinePaste.ts +8 -0
- package/src/tiptapBridge.ts +84 -49
- package/src/transformStyleId.ts +17 -0
- package/src/treeview/TreeOutlineWidget.tsx +240 -0
- package/src/treeview/TreeViewExtension.ts +250 -0
- package/src/treeview/__tests__/TreeViewExtension.test.ts +122 -0
- package/src/treeview/__tests__/treeOps.test.ts +125 -0
- package/src/treeview/__tests__/treePaste.test.ts +40 -0
- package/src/treeview/__tests__/treeViewCommands.test.ts +163 -0
- package/src/treeview/treeOps.ts +172 -0
- package/src/treeview/treePaste.ts +13 -0
- package/src/treeview/treeViewCommands.ts +94 -0
- package/src/treeview/treeViewData.ts +41 -0
- package/src/useMonacoLoader.ts +11 -34
- package/src/wysiwygImageUpload.ts +113 -0
- package/src/diagram/DiagramExtension.ts +0 -209
- package/src/diagram/DiagramWidget.tsx +0 -270
- package/src/diagram/useDiagramData.ts +0 -126
package/src/EditorShell.tsx
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
type DocumentLinkProvider,
|
|
17
17
|
type ViewPreferences,
|
|
18
18
|
type ThemeInheritance,
|
|
19
|
+
type BlockTagVisibility,
|
|
19
20
|
} from './EditorContext';
|
|
20
21
|
import { Toolbar } from './Toolbar';
|
|
21
22
|
import { StatusBar } from './StatusBar';
|
|
@@ -34,10 +35,9 @@ import { ImageEditor } from './ImageEditor';
|
|
|
34
35
|
import {
|
|
35
36
|
PreviewSettingsProvider,
|
|
36
37
|
PreviewToolbarControls,
|
|
37
|
-
PreviewModeSwitch,
|
|
38
|
-
PreviewFormatSwitch,
|
|
39
38
|
ThemeDesignerDock,
|
|
40
39
|
} from './PreviewControls';
|
|
40
|
+
import { PresentationModeControl, PresentationModeProvider } from './presentation/PresentationMode';
|
|
41
41
|
import { CustomThemeProvider, useDocCustomThemes } from './customThemes';
|
|
42
42
|
import { MediaBin } from './MediaBin';
|
|
43
43
|
import { DropZoneOverlay } from './DropZoneOverlay';
|
|
@@ -49,6 +49,11 @@ import {
|
|
|
49
49
|
processTextFile,
|
|
50
50
|
processTextFiles,
|
|
51
51
|
} from './utils/dropUtils';
|
|
52
|
+
import {
|
|
53
|
+
collectMediaReferencesFromMarkdown,
|
|
54
|
+
removeMediaReferencesFromMarkdown,
|
|
55
|
+
} from './mediaReferences';
|
|
56
|
+
import { filterVisibleMediaEntries } from './mediaEntries';
|
|
52
57
|
import type { MediaProvider, Theme } from '@bendyline/squisq/schemas';
|
|
53
58
|
import { DARK_SURFACE, LIGHT_SURFACE } from '@bendyline/squisq/schemas';
|
|
54
59
|
import type { ContentContainer } from '@bendyline/squisq/storage';
|
|
@@ -58,7 +63,8 @@ import {
|
|
|
58
63
|
createMediaProviderFromContainer,
|
|
59
64
|
} from '@bendyline/squisq/storage';
|
|
60
65
|
import type { PrunePolicy, SaveVersionResult } from '@bendyline/squisq/versions';
|
|
61
|
-
import type { CSSProperties, ReactNode } from 'react';
|
|
66
|
+
import type { CSSProperties, KeyboardEvent as ReactKeyboardEvent, ReactNode } from 'react';
|
|
67
|
+
import { MediaContext } from '@bendyline/squisq-react';
|
|
62
68
|
|
|
63
69
|
export type { EditorColorScheme } from './EditorContext';
|
|
64
70
|
|
|
@@ -112,12 +118,6 @@ export interface EditorShellProps {
|
|
|
112
118
|
* this container via `createMediaProviderFromContainer`.
|
|
113
119
|
*/
|
|
114
120
|
workspaceContainer?: ContentContainer | null;
|
|
115
|
-
/**
|
|
116
|
-
* @deprecated Renamed to `workspaceContainer` to make the workspace-
|
|
117
|
-
* vs. doc-scoped distinction explicit. Still accepted as a fallback
|
|
118
|
-
* for now; remove in the next breaking release.
|
|
119
|
-
*/
|
|
120
|
-
container?: ContentContainer | null;
|
|
121
121
|
/**
|
|
122
122
|
* Enable version history. Snapshots are stored at
|
|
123
123
|
* `.versions/<basename>.<timestamp>.md` inside the same
|
|
@@ -269,6 +269,14 @@ export interface EditorShellProps {
|
|
|
269
269
|
* the button is hidden regardless of this prop.
|
|
270
270
|
*/
|
|
271
271
|
allowRecording?: boolean;
|
|
272
|
+
/**
|
|
273
|
+
* Whether the Narrate (teleprompter) display mode is offered under the
|
|
274
|
+
* Use tab. Defaults to true. Orthogonal to `allowRecording` — the
|
|
275
|
+
* prompter is useful without capture (reading for external recording
|
|
276
|
+
* software), and the in-mode Record affordance additionally requires
|
|
277
|
+
* `allowRecording` + a `mediaProvider`.
|
|
278
|
+
*/
|
|
279
|
+
allowNarrate?: boolean;
|
|
272
280
|
/**
|
|
273
281
|
* Placeholder text shown in the WYSIWYG editor while the document is
|
|
274
282
|
* empty. When omitted, the editor rotates through its own generic
|
|
@@ -348,12 +356,16 @@ export interface EditorShellProps {
|
|
|
348
356
|
*/
|
|
349
357
|
outlineWidth?: number;
|
|
350
358
|
/**
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
* toggle it at runtime regardless of the initial value.
|
|
359
|
+
* Legacy initial visibility of inline block-template tags on headings.
|
|
360
|
+
* `true` maps to always visible and `false` maps to hidden. When omitted,
|
|
361
|
+
* {@link blockTagVisibility} defaults to `'active'`.
|
|
355
362
|
*/
|
|
356
363
|
blockTags?: boolean;
|
|
364
|
+
/**
|
|
365
|
+
* Initial block-tag visibility mode. Takes precedence over `blockTags`.
|
|
366
|
+
* Defaults to `'active'` (selected/hovered block only).
|
|
367
|
+
*/
|
|
368
|
+
blockTagVisibility?: BlockTagVisibility;
|
|
357
369
|
/**
|
|
358
370
|
* How much of the active Squisq theme the WYSIWYG editing surface
|
|
359
371
|
* mirrors. Defaults to `'fonts'` — the historical behavior of
|
|
@@ -365,7 +377,8 @@ export interface EditorShellProps {
|
|
|
365
377
|
* Bundled view preferences — a serializable JSON blob covering the
|
|
366
378
|
* runtime-toggleable view options surfaced in the View menu. When
|
|
367
379
|
* provided, fields here override the corresponding individual props
|
|
368
|
-
* (`outline`, `inlinePreview`, `showStatusBar`
|
|
380
|
+
* (`outline`, `inlinePreview`, `showStatusBar`, `blockTagVisibility`,
|
|
381
|
+
* `blockTags`). Pair with
|
|
369
382
|
* {@link onViewPreferencesChange} to externalize storage of these
|
|
370
383
|
* preferences in the host.
|
|
371
384
|
*/
|
|
@@ -403,7 +416,6 @@ export function EditorShell({
|
|
|
403
416
|
maxHeight,
|
|
404
417
|
mediaProvider,
|
|
405
418
|
workspaceContainer,
|
|
406
|
-
container,
|
|
407
419
|
allowVersioning = false,
|
|
408
420
|
versionBasename,
|
|
409
421
|
versioningPrunePolicy,
|
|
@@ -426,6 +438,7 @@ export function EditorShell({
|
|
|
426
438
|
mentionProvider,
|
|
427
439
|
documentLinkProvider,
|
|
428
440
|
allowRecording = true,
|
|
441
|
+
allowNarrate = true,
|
|
429
442
|
placeholder,
|
|
430
443
|
readOnly = false,
|
|
431
444
|
imageSrc,
|
|
@@ -437,13 +450,14 @@ export function EditorShell({
|
|
|
437
450
|
inlinePreviewWidth = 320,
|
|
438
451
|
outline = false,
|
|
439
452
|
outlineWidth,
|
|
440
|
-
blockTags
|
|
453
|
+
blockTags,
|
|
454
|
+
blockTagVisibility,
|
|
441
455
|
themeInheritance = 'fonts',
|
|
442
456
|
viewPreferences,
|
|
443
457
|
onViewPreferencesChange,
|
|
444
458
|
themeOverride = null,
|
|
445
459
|
}: EditorShellProps) {
|
|
446
|
-
const effectiveContainer = workspaceContainer ??
|
|
460
|
+
const effectiveContainer = workspaceContainer ?? null;
|
|
447
461
|
|
|
448
462
|
// If the host gave us a `workspaceContainer` but no explicit `mediaProvider`,
|
|
449
463
|
// derive one automatically. Without this, drag-and-drop of an image
|
|
@@ -464,65 +478,69 @@ export function EditorShell({
|
|
|
464
478
|
!showPlayTab && initialView === 'preview' ? 'wysiwyg' : initialView;
|
|
465
479
|
|
|
466
480
|
return (
|
|
467
|
-
<
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
allowVersioning={allowVersioning}
|
|
474
|
-
versionBasename={versionBasename}
|
|
475
|
-
versioningPrunePolicy={versioningPrunePolicy}
|
|
476
|
-
versioningAutoSaveIdleMs={versioningAutoSaveIdleMs}
|
|
477
|
-
onSaveVersion={onSaveVersion}
|
|
478
|
-
mediaProvider={effectiveMediaProvider}
|
|
479
|
-
imageDisplayMode={imageDisplayMode}
|
|
480
|
-
mentionProvider={mentionProvider}
|
|
481
|
-
documentLinkProvider={documentLinkProvider}
|
|
482
|
-
allowRecording={allowRecording}
|
|
483
|
-
fileName={fileName}
|
|
484
|
-
language={language}
|
|
485
|
-
inlinePreview={inlinePreview}
|
|
486
|
-
showStatusBar={showStatusBar}
|
|
487
|
-
outline={outline}
|
|
488
|
-
blockTags={blockTags}
|
|
489
|
-
themeInheritance={themeInheritance}
|
|
490
|
-
viewPreferences={viewPreferences}
|
|
491
|
-
onViewPreferencesChange={onViewPreferencesChange}
|
|
492
|
-
>
|
|
493
|
-
<EditorShellInner
|
|
494
|
-
basePath={basePath}
|
|
495
|
-
onChange={onChange}
|
|
496
|
-
className={className}
|
|
497
|
-
height={height}
|
|
498
|
-
minHeight={minHeight}
|
|
499
|
-
maxHeight={maxHeight}
|
|
500
|
-
placeholder={placeholder}
|
|
501
|
-
mediaProvider={effectiveMediaProvider ?? null}
|
|
481
|
+
<MediaContext.Provider value={effectiveMediaProvider ?? null}>
|
|
482
|
+
<EditorProvider
|
|
483
|
+
initialMarkdown={initialMarkdown}
|
|
484
|
+
initialView={effectiveInitialView}
|
|
485
|
+
articleId={articleId}
|
|
486
|
+
colorScheme={colorScheme}
|
|
502
487
|
workspaceContainer={effectiveContainer}
|
|
503
|
-
filesToggleEnabled={filesToggleEnabled}
|
|
504
|
-
toolbarSlotLeft={toolbarSlotLeft}
|
|
505
|
-
toolbarSlotAfterActions={toolbarSlotAfterActions}
|
|
506
|
-
toolbarSlotRight={toolbarSlotRight}
|
|
507
|
-
showPlayTab={showPlayTab}
|
|
508
|
-
submitOnEnter={submitOnEnter}
|
|
509
|
-
codeContext={codeContext}
|
|
510
|
-
fullWidth={fullWidth}
|
|
511
|
-
uxFont={uxFont}
|
|
512
|
-
thinMargins={thinMargins}
|
|
513
|
-
readOnly={readOnly}
|
|
514
|
-
imageSrc={imageSrc}
|
|
515
|
-
imageAlt={imageAlt}
|
|
516
|
-
imageMode={imageMode}
|
|
517
|
-
imageEditorContainer={imageEditorContainer}
|
|
518
|
-
onImageExport={onImageExport}
|
|
519
488
|
allowVersioning={allowVersioning}
|
|
489
|
+
versionBasename={versionBasename}
|
|
490
|
+
versioningPrunePolicy={versioningPrunePolicy}
|
|
520
491
|
versioningAutoSaveIdleMs={versioningAutoSaveIdleMs}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
492
|
+
onSaveVersion={onSaveVersion}
|
|
493
|
+
mediaProvider={effectiveMediaProvider}
|
|
494
|
+
imageDisplayMode={imageDisplayMode}
|
|
495
|
+
mentionProvider={mentionProvider}
|
|
496
|
+
documentLinkProvider={documentLinkProvider}
|
|
497
|
+
allowRecording={allowRecording}
|
|
498
|
+
allowNarrate={allowNarrate}
|
|
499
|
+
fileName={fileName}
|
|
500
|
+
language={language}
|
|
501
|
+
inlinePreview={inlinePreview}
|
|
502
|
+
showStatusBar={showStatusBar}
|
|
503
|
+
outline={outline}
|
|
504
|
+
blockTags={blockTags}
|
|
505
|
+
blockTagVisibility={blockTagVisibility}
|
|
506
|
+
themeInheritance={themeInheritance}
|
|
507
|
+
viewPreferences={viewPreferences}
|
|
508
|
+
onViewPreferencesChange={onViewPreferencesChange}
|
|
509
|
+
>
|
|
510
|
+
<EditorShellInner
|
|
511
|
+
basePath={basePath}
|
|
512
|
+
onChange={onChange}
|
|
513
|
+
className={className}
|
|
514
|
+
height={height}
|
|
515
|
+
minHeight={minHeight}
|
|
516
|
+
maxHeight={maxHeight}
|
|
517
|
+
placeholder={placeholder}
|
|
518
|
+
mediaProvider={effectiveMediaProvider ?? null}
|
|
519
|
+
workspaceContainer={effectiveContainer}
|
|
520
|
+
filesToggleEnabled={filesToggleEnabled}
|
|
521
|
+
toolbarSlotLeft={toolbarSlotLeft}
|
|
522
|
+
toolbarSlotAfterActions={toolbarSlotAfterActions}
|
|
523
|
+
toolbarSlotRight={toolbarSlotRight}
|
|
524
|
+
showPlayTab={showPlayTab}
|
|
525
|
+
submitOnEnter={submitOnEnter}
|
|
526
|
+
codeContext={codeContext}
|
|
527
|
+
fullWidth={fullWidth}
|
|
528
|
+
uxFont={uxFont}
|
|
529
|
+
thinMargins={thinMargins}
|
|
530
|
+
readOnly={readOnly}
|
|
531
|
+
imageSrc={imageSrc}
|
|
532
|
+
imageAlt={imageAlt}
|
|
533
|
+
imageMode={imageMode}
|
|
534
|
+
imageEditorContainer={imageEditorContainer}
|
|
535
|
+
onImageExport={onImageExport}
|
|
536
|
+
allowVersioning={allowVersioning}
|
|
537
|
+
versioningAutoSaveIdleMs={versioningAutoSaveIdleMs}
|
|
538
|
+
inlinePreviewWidth={inlinePreviewWidth}
|
|
539
|
+
outlineWidth={outlineWidth}
|
|
540
|
+
themeOverride={themeOverride}
|
|
541
|
+
/>
|
|
542
|
+
</EditorProvider>
|
|
543
|
+
</MediaContext.Provider>
|
|
526
544
|
);
|
|
527
545
|
}
|
|
528
546
|
|
|
@@ -591,6 +609,7 @@ function EditorShellInner({
|
|
|
591
609
|
outlineWidth,
|
|
592
610
|
themeOverride,
|
|
593
611
|
}: EditorShellInnerProps) {
|
|
612
|
+
const shellRef = useRef<HTMLDivElement>(null);
|
|
594
613
|
const {
|
|
595
614
|
activeView,
|
|
596
615
|
markdownSource,
|
|
@@ -614,6 +633,7 @@ function EditorShellInner({
|
|
|
614
633
|
imageEditTarget,
|
|
615
634
|
closeImageEdit,
|
|
616
635
|
bumpMediaRevision,
|
|
636
|
+
mediaRevision,
|
|
617
637
|
} = useEditorContext();
|
|
618
638
|
// Dual-catalog custom themes (doc + browser library), mirroring how
|
|
619
639
|
// CustomTemplateProvider is fed. Wraps the preview subtree so the theme
|
|
@@ -632,6 +652,12 @@ function EditorShellInner({
|
|
|
632
652
|
const isTimelineMode = isMarkdownMode && layoutMode === 'timeline' && !isPreview;
|
|
633
653
|
const [showFiles, setShowFiles] = useState(false);
|
|
634
654
|
const [mediaRefreshKey, setMediaRefreshKey] = useState(0);
|
|
655
|
+
const [mediaCount, setMediaCount] = useState(0);
|
|
656
|
+
const mediaListRefreshKey = mediaRefreshKey + mediaRevision;
|
|
657
|
+
const usedMediaPaths = useMemo(
|
|
658
|
+
() => collectMediaReferencesFromMarkdown(markdownSource),
|
|
659
|
+
[markdownSource],
|
|
660
|
+
);
|
|
635
661
|
// Persistent fallback container for image-edit sidecars when the host
|
|
636
662
|
// didn't supply one. Lifted to shell scope so opening the same image
|
|
637
663
|
// multiple times sees the same `.imageEdits/<sanitized>/.versions/...`
|
|
@@ -644,6 +670,33 @@ function EditorShellInner({
|
|
|
644
670
|
const imageEditFallbackContainer = imageEditFallbackContainerRef.current;
|
|
645
671
|
const isDark = colorScheme === 'dark';
|
|
646
672
|
|
|
673
|
+
useEffect(() => {
|
|
674
|
+
if (!mediaProvider) {
|
|
675
|
+
setMediaCount(0);
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
let cancelled = false;
|
|
680
|
+
mediaProvider.listMedia().then(
|
|
681
|
+
(entries) => {
|
|
682
|
+
if (!cancelled) setMediaCount(filterVisibleMediaEntries(entries).length);
|
|
683
|
+
},
|
|
684
|
+
(err: unknown) => {
|
|
685
|
+
if (!cancelled) {
|
|
686
|
+
setMediaCount(0);
|
|
687
|
+
console.warn(
|
|
688
|
+
'[squisq-editor] Failed to read media list:',
|
|
689
|
+
err instanceof Error ? err.message : err,
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
},
|
|
693
|
+
);
|
|
694
|
+
|
|
695
|
+
return () => {
|
|
696
|
+
cancelled = true;
|
|
697
|
+
};
|
|
698
|
+
}, [mediaProvider, mediaListRefreshKey]);
|
|
699
|
+
|
|
647
700
|
const handleToggleFiles = useCallback(() => {
|
|
648
701
|
setShowFiles((prev) => !prev);
|
|
649
702
|
}, []);
|
|
@@ -701,6 +754,24 @@ function EditorShellInner({
|
|
|
701
754
|
[activeView, tiptapEditor, monacoEditor, insertAtCursor, markdownSource, setMarkdownSource],
|
|
702
755
|
);
|
|
703
756
|
|
|
757
|
+
const handleMediaUploaded = useCallback(
|
|
758
|
+
(relativePath: string, name: string, mimeType: string) => {
|
|
759
|
+
insertMediaRef(relativePath, name, mimeType);
|
|
760
|
+
setMediaRefreshKey((k) => k + 1);
|
|
761
|
+
},
|
|
762
|
+
[insertMediaRef],
|
|
763
|
+
);
|
|
764
|
+
|
|
765
|
+
const handleMediaRemoved = useCallback(
|
|
766
|
+
(relativePath: string) => {
|
|
767
|
+
const nextSource = removeMediaReferencesFromMarkdown(markdownSource, relativePath);
|
|
768
|
+
if (nextSource !== markdownSource) replaceAll(nextSource);
|
|
769
|
+
setMediaRefreshKey((k) => k + 1);
|
|
770
|
+
bumpMediaRevision();
|
|
771
|
+
},
|
|
772
|
+
[bumpMediaRevision, markdownSource, replaceAll],
|
|
773
|
+
);
|
|
774
|
+
|
|
704
775
|
const handleFileDrop = useCallback(
|
|
705
776
|
async (files: File[], target: DropTarget) => {
|
|
706
777
|
try {
|
|
@@ -746,6 +817,7 @@ function EditorShellInner({
|
|
|
746
817
|
|
|
747
818
|
const { isDragging, dragContentType, containerProps, zoneProps } = useFileDrop({
|
|
748
819
|
onDrop: handleFileDrop,
|
|
820
|
+
enabled: !readOnly,
|
|
749
821
|
});
|
|
750
822
|
|
|
751
823
|
// Notify parent of changes
|
|
@@ -753,35 +825,28 @@ function EditorShellInner({
|
|
|
753
825
|
onChange?.(markdownSource);
|
|
754
826
|
}, [markdownSource, onChange]);
|
|
755
827
|
|
|
756
|
-
//
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
if (e.ctrlKey
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
e.preventDefault();
|
|
772
|
-
document.querySelector<HTMLButtonElement>('[data-view="preview"]')?.click();
|
|
773
|
-
break;
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
};
|
|
777
|
-
window.addEventListener('keydown', handler);
|
|
778
|
-
return () => window.removeEventListener('keydown', handler);
|
|
779
|
-
}, [showPlayTab]);
|
|
828
|
+
// View shortcuts bubble only through the editor that currently owns focus.
|
|
829
|
+
const handleShellKeyDown = useCallback(
|
|
830
|
+
(e: ReactKeyboardEvent<HTMLDivElement>) => {
|
|
831
|
+
if (!e.ctrlKey && !e.metaKey) return;
|
|
832
|
+
const view =
|
|
833
|
+
e.key === '1' ? 'wysiwyg' : e.key === '2' ? 'raw' : e.key === '3' ? 'preview' : null;
|
|
834
|
+
if (!view || (view === 'preview' && !showPlayTab)) return;
|
|
835
|
+
const button = shellRef.current?.querySelector<HTMLButtonElement>(`[data-view="${view}"]`);
|
|
836
|
+
if (!button) return;
|
|
837
|
+
e.preventDefault();
|
|
838
|
+
e.stopPropagation();
|
|
839
|
+
button.click();
|
|
840
|
+
},
|
|
841
|
+
[showPlayTab],
|
|
842
|
+
);
|
|
780
843
|
|
|
781
844
|
const autoGrow = minHeight !== undefined || maxHeight !== undefined;
|
|
782
845
|
|
|
783
846
|
return (
|
|
784
847
|
<div
|
|
848
|
+
ref={shellRef}
|
|
849
|
+
onKeyDown={handleShellKeyDown}
|
|
785
850
|
className={`squisq-editor-shell ${className || ''}`}
|
|
786
851
|
data-theme={colorScheme}
|
|
787
852
|
data-full-width={fullWidth ? 'true' : undefined}
|
|
@@ -812,97 +877,103 @@ function EditorShellInner({
|
|
|
812
877
|
>
|
|
813
878
|
<CustomThemeProvider docThemes={docThemes} onDocThemesChange={onDocThemesChange}>
|
|
814
879
|
<PreviewSettingsProvider doc={doc} themeOverride={themeOverride}>
|
|
815
|
-
{
|
|
880
|
+
<PresentationModeProvider rootRef={shellRef}>
|
|
881
|
+
{/* Header. In image mode the full markdown/code Toolbar is replaced
|
|
816
882
|
with a minimal slot bar — view tabs, formatting, and preview
|
|
817
883
|
controls don't apply to a binary asset. */}
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
884
|
+
{isImageMode ? (
|
|
885
|
+
(toolbarSlotLeft || toolbarSlotRight) && (
|
|
886
|
+
<div className="squisq-editor-header squisq-editor-header--image">
|
|
887
|
+
{toolbarSlotLeft}
|
|
888
|
+
<div style={{ flex: 1 }} />
|
|
889
|
+
{toolbarSlotRight}
|
|
890
|
+
</div>
|
|
891
|
+
)
|
|
892
|
+
) : (
|
|
893
|
+
<div className="squisq-editor-header">
|
|
894
|
+
<Toolbar
|
|
895
|
+
showFiles={showFiles}
|
|
896
|
+
fileCount={mediaCount}
|
|
897
|
+
onToggleFiles={!isCodeMode && filesToggleEnabled ? handleToggleFiles : undefined}
|
|
898
|
+
slotLeft={toolbarSlotLeft}
|
|
899
|
+
slotAfterTabs={
|
|
900
|
+
!isCodeMode &&
|
|
901
|
+
isPreview && (
|
|
902
|
+
<>
|
|
903
|
+
<PreviewToolbarControls />
|
|
904
|
+
<PresentationModeControl />
|
|
905
|
+
</>
|
|
906
|
+
)
|
|
907
|
+
}
|
|
908
|
+
slotAfterActions={toolbarSlotAfterActions}
|
|
909
|
+
slotRight={toolbarSlotRight}
|
|
910
|
+
showPlayTab={showPlayTab}
|
|
911
|
+
/>
|
|
824
912
|
</div>
|
|
825
|
-
)
|
|
826
|
-
) : (
|
|
827
|
-
<div className="squisq-editor-header">
|
|
828
|
-
<Toolbar
|
|
829
|
-
showFiles={showFiles}
|
|
830
|
-
onToggleFiles={!isCodeMode && filesToggleEnabled ? handleToggleFiles : undefined}
|
|
831
|
-
slotLeft={toolbarSlotLeft}
|
|
832
|
-
slotAfterTabs={
|
|
833
|
-
!isCodeMode &&
|
|
834
|
-
isPreview && (
|
|
835
|
-
<div className="squisq-preview-left-controls">
|
|
836
|
-
<PreviewModeSwitch />
|
|
837
|
-
<span className="squisq-preview-seg-divider" aria-hidden="true" />
|
|
838
|
-
<PreviewFormatSwitch />
|
|
839
|
-
</div>
|
|
840
|
-
)
|
|
841
|
-
}
|
|
842
|
-
slotAfterActions={
|
|
843
|
-
<>
|
|
844
|
-
{toolbarSlotAfterActions}
|
|
845
|
-
{!isCodeMode && isPreview && <PreviewToolbarControls />}
|
|
846
|
-
</>
|
|
847
|
-
}
|
|
848
|
-
slotRight={toolbarSlotRight}
|
|
849
|
-
showPlayTab={showPlayTab}
|
|
850
|
-
/>
|
|
851
|
-
</div>
|
|
852
|
-
)}
|
|
913
|
+
)}
|
|
853
914
|
|
|
854
|
-
|
|
855
|
-
<div
|
|
856
|
-
className="squisq-editor-content"
|
|
857
|
-
style={{
|
|
858
|
-
flex: autoGrow ? '1 1 auto' : 1,
|
|
859
|
-
overflowY: autoGrow ? 'auto' : 'hidden',
|
|
860
|
-
overflowX: 'hidden',
|
|
861
|
-
minHeight: 0,
|
|
862
|
-
position: 'relative',
|
|
863
|
-
display: 'flex',
|
|
864
|
-
}}
|
|
865
|
-
>
|
|
915
|
+
{/* Main content area */}
|
|
866
916
|
<div
|
|
917
|
+
className="squisq-editor-content"
|
|
867
918
|
style={{
|
|
868
919
|
flex: autoGrow ? '1 1 auto' : 1,
|
|
869
|
-
|
|
920
|
+
overflowY: autoGrow ? 'auto' : 'hidden',
|
|
921
|
+
overflowX: 'hidden',
|
|
870
922
|
minHeight: 0,
|
|
871
923
|
position: 'relative',
|
|
924
|
+
display: 'flex',
|
|
872
925
|
}}
|
|
873
926
|
>
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
927
|
+
<div
|
|
928
|
+
style={{
|
|
929
|
+
flex: autoGrow ? '1 1 auto' : 1,
|
|
930
|
+
overflow: autoGrow ? 'visible' : 'hidden',
|
|
931
|
+
minHeight: 0,
|
|
932
|
+
position: 'relative',
|
|
933
|
+
}}
|
|
934
|
+
>
|
|
935
|
+
{isImageMode &&
|
|
936
|
+
imageSrc &&
|
|
937
|
+
(imageMode === 'edit' && imageEditorContainer ? (
|
|
938
|
+
<ImageEditor
|
|
939
|
+
filesContainer={imageEditorContainer}
|
|
940
|
+
initialSrc={imageSrc}
|
|
941
|
+
allowVersioning={allowVersioning}
|
|
942
|
+
versioningAutoSaveIdleMs={versioningAutoSaveIdleMs}
|
|
943
|
+
onExport={onImageExport}
|
|
944
|
+
surface={colorScheme === 'dark' ? DARK_SURFACE : LIGHT_SURFACE}
|
|
945
|
+
/>
|
|
946
|
+
) : (
|
|
947
|
+
<ImageViewer src={imageSrc} alt={imageAlt} theme={colorScheme} />
|
|
948
|
+
))}
|
|
949
|
+
{/* Raw (Monaco) view. Always wrapped in `.squisq-editor-with-gutter`
|
|
888
950
|
so toggling a pane on/off doesn't change the editor's tree
|
|
889
951
|
position — Monaco stays mounted and `monacoEditor` in
|
|
890
952
|
context stays stable, which is what `useHeadingLayout` needs
|
|
891
953
|
to compute positions. */}
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
954
|
+
{!isImageMode && activeView === 'raw' && (
|
|
955
|
+
<div className="squisq-editor-with-gutter" key="raw-shell">
|
|
956
|
+
{isMarkdownMode && outlineVisible && (
|
|
957
|
+
<OutlinePanel key="outline" width={outlineWidth} readOnly={readOnly} />
|
|
958
|
+
)}
|
|
959
|
+
{isCardMode ? (
|
|
960
|
+
<BlockCardView
|
|
961
|
+
key="raw-card"
|
|
962
|
+
blockCount={blockCount}
|
|
963
|
+
activeBlockKey={activeBlockKey}
|
|
964
|
+
onPrev={prevBlock}
|
|
965
|
+
onNext={nextBlock}
|
|
966
|
+
onAdd={addBlock}
|
|
967
|
+
>
|
|
968
|
+
<div key="raw-editor" className="squisq-raw-editor-container">
|
|
969
|
+
<RawEditor
|
|
970
|
+
monacoTheme={colorScheme === 'dark' ? 'vs-dark' : 'vs'}
|
|
971
|
+
submitOnEnter={submitOnEnter}
|
|
972
|
+
readOnly={readOnly}
|
|
973
|
+
/>
|
|
974
|
+
</div>
|
|
975
|
+
</BlockCardView>
|
|
976
|
+
) : (
|
|
906
977
|
<div key="raw-editor" className="squisq-raw-editor-container">
|
|
907
978
|
<RawEditor
|
|
908
979
|
monacoTheme={colorScheme === 'dark' ? 'vs-dark' : 'vs'}
|
|
@@ -910,100 +981,96 @@ function EditorShellInner({
|
|
|
910
981
|
readOnly={readOnly}
|
|
911
982
|
/>
|
|
912
983
|
</div>
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
<div key="raw-editor" className="squisq-raw-editor-container">
|
|
916
|
-
<RawEditor
|
|
917
|
-
monacoTheme={colorScheme === 'dark' ? 'vs-dark' : 'vs'}
|
|
918
|
-
submitOnEnter={submitOnEnter}
|
|
919
|
-
readOnly={readOnly}
|
|
920
|
-
/>
|
|
921
|
-
</div>
|
|
922
|
-
)}
|
|
923
|
-
{/* Renders nothing in normal flow — portals context
|
|
984
|
+
)}
|
|
985
|
+
{/* Renders nothing in normal flow — portals context
|
|
924
986
|
sections into Monaco view zones via the context's
|
|
925
987
|
monacoEditor. Code mode only. */}
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
988
|
+
{isCodeMode && codeContext && (
|
|
989
|
+
<CodeContextZones key="code-context" options={codeContext} />
|
|
990
|
+
)}
|
|
991
|
+
{isMarkdownMode && isCardMode && inlinePreviewVisible && (
|
|
992
|
+
<BlockPreviewPanel key="block-preview" basePath={basePath} />
|
|
993
|
+
)}
|
|
994
|
+
{isMarkdownMode && !isCardMode && inlinePreviewVisible && (
|
|
995
|
+
<InlinePreviewGutter
|
|
996
|
+
key="inline"
|
|
997
|
+
width={inlinePreviewWidth}
|
|
998
|
+
basePath={basePath}
|
|
999
|
+
mediaProvider={mediaProvider}
|
|
1000
|
+
/>
|
|
1001
|
+
)}
|
|
1002
|
+
</div>
|
|
1003
|
+
)}
|
|
1004
|
+
{/* WYSIWYG + Preview are markdown-only surfaces — skip them
|
|
943
1005
|
entirely in code or image mode so Tiptap never initializes
|
|
944
1006
|
and the preview pipeline stays idle. Same always-wrapped
|
|
945
1007
|
pattern as the Raw branch above so pane toggles don't
|
|
946
1008
|
remount Tiptap. */}
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1009
|
+
{isMarkdownMode && activeView === 'wysiwyg' && (
|
|
1010
|
+
<div className="squisq-editor-with-gutter" key="wysiwyg-shell">
|
|
1011
|
+
{outlineVisible && (
|
|
1012
|
+
<OutlinePanel key="outline" width={outlineWidth} readOnly={readOnly} />
|
|
1013
|
+
)}
|
|
1014
|
+
{isCardMode ? (
|
|
1015
|
+
<BlockCardView
|
|
1016
|
+
key="wysiwyg-card"
|
|
1017
|
+
blockCount={blockCount}
|
|
1018
|
+
activeBlockKey={activeBlockKey}
|
|
1019
|
+
onPrev={prevBlock}
|
|
1020
|
+
onNext={nextBlock}
|
|
1021
|
+
onAdd={addBlock}
|
|
1022
|
+
>
|
|
1023
|
+
<WysiwygEditor
|
|
1024
|
+
key="wysiwyg-editor"
|
|
1025
|
+
submitOnEnter={submitOnEnter}
|
|
1026
|
+
placeholder={placeholder}
|
|
1027
|
+
readOnly={readOnly}
|
|
1028
|
+
/>
|
|
1029
|
+
</BlockCardView>
|
|
1030
|
+
) : (
|
|
959
1031
|
<WysiwygEditor
|
|
960
1032
|
key="wysiwyg-editor"
|
|
961
1033
|
submitOnEnter={submitOnEnter}
|
|
962
1034
|
placeholder={placeholder}
|
|
963
1035
|
readOnly={readOnly}
|
|
964
1036
|
/>
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
/>
|
|
984
|
-
)}
|
|
985
|
-
</div>
|
|
986
|
-
)}
|
|
987
|
-
{isMarkdownMode && isPreview && (
|
|
988
|
-
<PreviewPanel basePath={basePath} workspaceContainer={workspaceContainer} />
|
|
989
|
-
)}
|
|
990
|
-
</div>
|
|
1037
|
+
)}
|
|
1038
|
+
{isCardMode && inlinePreviewVisible && (
|
|
1039
|
+
<BlockPreviewPanel key="block-preview" basePath={basePath} />
|
|
1040
|
+
)}
|
|
1041
|
+
{!isCardMode && inlinePreviewVisible && (
|
|
1042
|
+
<InlinePreviewGutter
|
|
1043
|
+
key="inline"
|
|
1044
|
+
width={inlinePreviewWidth}
|
|
1045
|
+
basePath={basePath}
|
|
1046
|
+
mediaProvider={mediaProvider}
|
|
1047
|
+
/>
|
|
1048
|
+
)}
|
|
1049
|
+
</div>
|
|
1050
|
+
)}
|
|
1051
|
+
{isMarkdownMode && isPreview && (
|
|
1052
|
+
<PreviewPanel basePath={basePath} workspaceContainer={workspaceContainer} />
|
|
1053
|
+
)}
|
|
1054
|
+
</div>
|
|
991
1055
|
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1056
|
+
{isMarkdownMode && showFiles && (
|
|
1057
|
+
<MediaBin
|
|
1058
|
+
mediaProvider={mediaProvider}
|
|
1059
|
+
isDark={isDark}
|
|
1060
|
+
refreshKey={mediaListRefreshKey}
|
|
1061
|
+
usedMediaPaths={usedMediaPaths}
|
|
1062
|
+
onMediaUploaded={handleMediaUploaded}
|
|
1063
|
+
onMediaRemoved={handleMediaRemoved}
|
|
1064
|
+
onCountChange={setMediaCount}
|
|
1065
|
+
/>
|
|
1066
|
+
)}
|
|
1000
1067
|
|
|
1001
|
-
|
|
1068
|
+
{/* Docked custom-theme designer — a flex sibling of the preview, so
|
|
1002
1069
|
opening it reflows the preview narrower. Renders nothing when the
|
|
1003
1070
|
designer is closed. */}
|
|
1004
|
-
|
|
1071
|
+
{isMarkdownMode && <ThemeDesignerDock />}
|
|
1005
1072
|
|
|
1006
|
-
|
|
1073
|
+
{/* Drop zone overlay — image / text drop UX is markdown-specific.
|
|
1007
1074
|
In WYSIWYG, image drops are handled directly by Tiptap's
|
|
1008
1075
|
`handleDrop` (uploads to the MediaProvider and inserts an
|
|
1009
1076
|
image node at the mouse position). The overlay would sit on
|
|
@@ -1011,27 +1078,28 @@ function EditorShellInner({
|
|
|
1011
1078
|
when the dragged content is media-only on the WYSIWYG view —
|
|
1012
1079
|
the user gets a one-step "drop where you want it" flow
|
|
1013
1080
|
instead of a two-step "drop in bin, then insert" flow. */}
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1081
|
+
{isMarkdownMode &&
|
|
1082
|
+
isDragging &&
|
|
1083
|
+
!(activeView === 'wysiwyg' && dragContentType === 'media') && (
|
|
1084
|
+
<DropZoneOverlay
|
|
1085
|
+
dragContentType={dragContentType}
|
|
1086
|
+
zoneProps={zoneProps}
|
|
1087
|
+
hasMediaProvider={mediaProvider !== null}
|
|
1088
|
+
/>
|
|
1089
|
+
)}
|
|
1090
|
+
</div>
|
|
1024
1091
|
|
|
1025
|
-
|
|
1092
|
+
{/* Timeline track — horizontal strip of block + media bars shown
|
|
1026
1093
|
below the editor in Timeline view. The card editor above (driven by
|
|
1027
1094
|
the block navigator) shows whichever block is selected here. */}
|
|
1028
|
-
|
|
1095
|
+
{isTimelineMode && <TimelineTrack />}
|
|
1029
1096
|
|
|
1030
|
-
|
|
1097
|
+
{/* Status bar — word / char / line / block counts. Host can
|
|
1031
1098
|
suppress via `showStatusBar={false}` for embedded chat-style
|
|
1032
1099
|
composers where the stats are noise. The image viewer has its
|
|
1033
1100
|
own dimension/zoom status row, so suppress here too. */}
|
|
1034
|
-
|
|
1101
|
+
{statusBarVisible && !isImageMode && <StatusBar />}
|
|
1102
|
+
</PresentationModeProvider>
|
|
1035
1103
|
</PreviewSettingsProvider>
|
|
1036
1104
|
</CustomThemeProvider>
|
|
1037
1105
|
<TooltipLayer />
|
|
@@ -1095,6 +1163,19 @@ function ImageEditModal({
|
|
|
1095
1163
|
versioningAutoSaveIdleMs,
|
|
1096
1164
|
shellTheme,
|
|
1097
1165
|
}: ImageEditModalProps) {
|
|
1166
|
+
const modalRef = useRef<HTMLDivElement>(null);
|
|
1167
|
+
const surfaceRef = useRef<HTMLDivElement>(null);
|
|
1168
|
+
const onCloseRef = useRef(onClose);
|
|
1169
|
+
onCloseRef.current = onClose;
|
|
1170
|
+
const extension = relativePath.split(/[?#]/, 1)[0]?.split('.').pop()?.toLowerCase();
|
|
1171
|
+
const saveFormat: 'png' | 'jpeg' | 'webp' | null =
|
|
1172
|
+
extension === 'png'
|
|
1173
|
+
? 'png'
|
|
1174
|
+
: extension === 'jpg' || extension === 'jpeg'
|
|
1175
|
+
? 'jpeg'
|
|
1176
|
+
: extension === 'webp'
|
|
1177
|
+
? 'webp'
|
|
1178
|
+
: null;
|
|
1098
1179
|
// Each unique image path gets its own sidecar so multiple images in the
|
|
1099
1180
|
// same doc can be edited independently without colliding state. When the
|
|
1100
1181
|
// host didn't supply a `container`, fall back to a fresh in-memory one
|
|
@@ -1102,14 +1183,22 @@ function ImageEditModal({
|
|
|
1102
1183
|
// gets written back through `mediaProvider`.
|
|
1103
1184
|
const sidecar = useMemo(() => {
|
|
1104
1185
|
const sanitized = relativePath.replace(/[^a-zA-Z0-9._-]+/g, '_');
|
|
1186
|
+
let hash = 2166136261;
|
|
1187
|
+
for (let i = 0; i < relativePath.length; i++) {
|
|
1188
|
+
hash ^= relativePath.charCodeAt(i);
|
|
1189
|
+
hash = Math.imul(hash, 16777619);
|
|
1190
|
+
}
|
|
1191
|
+
const scopedName = `${sanitized}-${(hash >>> 0).toString(36)}`;
|
|
1105
1192
|
const parent: ContentContainer = container ?? new MemoryContentContainer();
|
|
1106
|
-
return scopeContainer(parent, `.imageEdits/${
|
|
1193
|
+
return scopeContainer(parent, `.imageEdits/${scopedName}`);
|
|
1107
1194
|
}, [container, relativePath]);
|
|
1108
1195
|
|
|
1109
1196
|
const [initialSrc, setInitialSrc] = useState<string | null>(null);
|
|
1110
1197
|
const [resolveError, setResolveError] = useState<string | null>(null);
|
|
1111
1198
|
useEffect(() => {
|
|
1112
1199
|
let cancelled = false;
|
|
1200
|
+
setInitialSrc(null);
|
|
1201
|
+
setResolveError(null);
|
|
1113
1202
|
mediaProvider.resolveUrl(relativePath).then(
|
|
1114
1203
|
(url) => {
|
|
1115
1204
|
if (!cancelled) setInitialSrc(url);
|
|
@@ -1140,15 +1229,55 @@ function ImageEditModal({
|
|
|
1140
1229
|
|
|
1141
1230
|
// Close on Escape — global listener so it works regardless of focus.
|
|
1142
1231
|
useEffect(() => {
|
|
1232
|
+
const previousFocus = document.activeElement as HTMLElement | null;
|
|
1233
|
+
const shell = modalRef.current?.closest('.squisq-editor-shell');
|
|
1234
|
+
const inertSiblings = shell
|
|
1235
|
+
? Array.from(shell.children).filter((element) => !element.contains(modalRef.current))
|
|
1236
|
+
: [];
|
|
1237
|
+
inertSiblings.forEach((element) => {
|
|
1238
|
+
(element as HTMLElement).inert = true;
|
|
1239
|
+
});
|
|
1240
|
+
surfaceRef.current
|
|
1241
|
+
?.querySelector<HTMLElement>(
|
|
1242
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
|
|
1243
|
+
)
|
|
1244
|
+
?.focus();
|
|
1143
1245
|
const handler = (e: KeyboardEvent) => {
|
|
1144
|
-
if (e.key === 'Escape')
|
|
1246
|
+
if (e.key === 'Escape') {
|
|
1247
|
+
e.preventDefault();
|
|
1248
|
+
onCloseRef.current();
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
if (e.key !== 'Tab' || !surfaceRef.current) return;
|
|
1252
|
+
const focusable = Array.from(
|
|
1253
|
+
surfaceRef.current.querySelectorAll<HTMLElement>(
|
|
1254
|
+
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])',
|
|
1255
|
+
),
|
|
1256
|
+
);
|
|
1257
|
+
if (focusable.length === 0) return;
|
|
1258
|
+
const first = focusable[0];
|
|
1259
|
+
const last = focusable[focusable.length - 1];
|
|
1260
|
+
if (e.shiftKey && document.activeElement === first) {
|
|
1261
|
+
e.preventDefault();
|
|
1262
|
+
last.focus();
|
|
1263
|
+
} else if (!e.shiftKey && document.activeElement === last) {
|
|
1264
|
+
e.preventDefault();
|
|
1265
|
+
first.focus();
|
|
1266
|
+
}
|
|
1145
1267
|
};
|
|
1146
1268
|
window.addEventListener('keydown', handler);
|
|
1147
|
-
return () =>
|
|
1148
|
-
|
|
1269
|
+
return () => {
|
|
1270
|
+
window.removeEventListener('keydown', handler);
|
|
1271
|
+
inertSiblings.forEach((element) => {
|
|
1272
|
+
(element as HTMLElement).inert = false;
|
|
1273
|
+
});
|
|
1274
|
+
previousFocus?.focus();
|
|
1275
|
+
};
|
|
1276
|
+
}, []);
|
|
1149
1277
|
|
|
1150
1278
|
return (
|
|
1151
1279
|
<div
|
|
1280
|
+
ref={modalRef}
|
|
1152
1281
|
className="squisq-image-edit-modal"
|
|
1153
1282
|
data-testid="image-edit-modal"
|
|
1154
1283
|
role="dialog"
|
|
@@ -1159,7 +1288,7 @@ function ImageEditModal({
|
|
|
1159
1288
|
if (e.target === e.currentTarget) onClose();
|
|
1160
1289
|
}}
|
|
1161
1290
|
>
|
|
1162
|
-
<div className="squisq-image-edit-modal__surface">
|
|
1291
|
+
<div ref={surfaceRef} className="squisq-image-edit-modal__surface">
|
|
1163
1292
|
<header className="squisq-image-edit-modal__header">
|
|
1164
1293
|
<span className="squisq-image-edit-modal__title">Edit image</span>
|
|
1165
1294
|
<span className="squisq-image-edit-modal__path">{relativePath}</span>
|
|
@@ -1178,6 +1307,10 @@ function ImageEditModal({
|
|
|
1178
1307
|
<div className="squisq-image-edit-modal__error">
|
|
1179
1308
|
Failed to load image: {resolveError}
|
|
1180
1309
|
</div>
|
|
1310
|
+
) : !saveFormat ? (
|
|
1311
|
+
<div className="squisq-image-edit-modal__error">
|
|
1312
|
+
Editing supports PNG, JPEG, and WebP images without changing the asset type.
|
|
1313
|
+
</div>
|
|
1181
1314
|
) : !initialSrc ? (
|
|
1182
1315
|
<div className="squisq-image-edit-modal__loading">Loading image…</div>
|
|
1183
1316
|
) : (
|
|
@@ -1188,7 +1321,7 @@ function ImageEditModal({
|
|
|
1188
1321
|
versioningAutoSaveIdleMs={versioningAutoSaveIdleMs}
|
|
1189
1322
|
onExport={handleExport}
|
|
1190
1323
|
saveBehavior="export"
|
|
1191
|
-
saveFormat=
|
|
1324
|
+
saveFormat={saveFormat}
|
|
1192
1325
|
saveLabel="Save and close"
|
|
1193
1326
|
saveTitle="Save changes back to the image and close"
|
|
1194
1327
|
surface={shellTheme === 'dark' ? DARK_SURFACE : LIGHT_SURFACE}
|