@bendyline/squisq-editor-react 1.6.2 → 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 +1229 -179
- package/dist/index.js +18017 -11231
- 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 +1471 -245
- package/package.json +9 -5
- package/src/BlockPropertiesPopover.tsx +30 -15
- package/src/DocumentSettingsDialog.tsx +13 -21
- package/src/EditorContext.tsx +199 -42
- package/src/EditorShell.tsx +348 -260
- package/src/ImageEditor.tsx +22 -1
- package/src/InlinePreviewGutter.tsx +16 -8
- package/src/MediaBin.tsx +107 -14
- package/src/OutlinePanel.tsx +202 -7
- package/src/PlainHtmlPreview.tsx +40 -1
- package/src/PreviewControls.tsx +309 -69
- package/src/PreviewPanel.tsx +312 -78
- package/src/RawEditor.tsx +13 -2
- package/src/StatusBar.tsx +5 -2
- package/src/TemplateAnnotation.ts +12 -3
- package/src/TemplatePicker.tsx +75 -21
- package/src/ThemeCustomizerPanel.tsx +8 -0
- package/src/ThemePicker.tsx +10 -5
- package/src/TimelineBlockPreview.tsx +1 -1
- package/src/Toolbar.tsx +202 -41
- package/src/TransitionPicker.tsx +36 -4
- package/src/ViewMenuPanel.tsx +12 -14
- package/src/WysiwygEditor.tsx +124 -91
- 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 +21 -1
- 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 +123 -1
- package/src/__tests__/emojiPicker.test.tsx +2 -42
- package/src/__tests__/headingTransition.test.ts +0 -12
- package/src/__tests__/imageEditAffordance.test.tsx +5 -1
- package/src/__tests__/imageEditModalContract.test.tsx +119 -0
- package/src/__tests__/imageEditorShell.test.tsx +15 -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__/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 +218 -5
- 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 +26 -0
- package/src/__tests__/templatePickerPortal.test.tsx +62 -0
- package/src/__tests__/tiptapBridge.test.ts +23 -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 +0 -15
- 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 +191 -19
- 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/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 +48 -18
- package/src/scene/SceneBlockExtension.ts +17 -10
- package/src/scene/SceneBlockWidget.tsx +5 -1
- 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 +21 -7
- package/src/scene/layers/edgeGeometry.ts +9 -1
- package/src/scene/layers/nodeCard.tsx +27 -8
- package/src/scene/scene.css +5 -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 +13 -4
- package/src/scene/tools/SceneTool.ts +8 -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 +706 -120
- 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 +12 -5
- 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 +24 -16
- 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 -272
- 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';
|
|
@@ -36,6 +37,7 @@ import {
|
|
|
36
37
|
PreviewToolbarControls,
|
|
37
38
|
ThemeDesignerDock,
|
|
38
39
|
} from './PreviewControls';
|
|
40
|
+
import { PresentationModeControl, PresentationModeProvider } from './presentation/PresentationMode';
|
|
39
41
|
import { CustomThemeProvider, useDocCustomThemes } from './customThemes';
|
|
40
42
|
import { MediaBin } from './MediaBin';
|
|
41
43
|
import { DropZoneOverlay } from './DropZoneOverlay';
|
|
@@ -51,6 +53,7 @@ import {
|
|
|
51
53
|
collectMediaReferencesFromMarkdown,
|
|
52
54
|
removeMediaReferencesFromMarkdown,
|
|
53
55
|
} from './mediaReferences';
|
|
56
|
+
import { filterVisibleMediaEntries } from './mediaEntries';
|
|
54
57
|
import type { MediaProvider, Theme } from '@bendyline/squisq/schemas';
|
|
55
58
|
import { DARK_SURFACE, LIGHT_SURFACE } from '@bendyline/squisq/schemas';
|
|
56
59
|
import type { ContentContainer } from '@bendyline/squisq/storage';
|
|
@@ -60,7 +63,8 @@ import {
|
|
|
60
63
|
createMediaProviderFromContainer,
|
|
61
64
|
} from '@bendyline/squisq/storage';
|
|
62
65
|
import type { PrunePolicy, SaveVersionResult } from '@bendyline/squisq/versions';
|
|
63
|
-
import type { CSSProperties, ReactNode } from 'react';
|
|
66
|
+
import type { CSSProperties, KeyboardEvent as ReactKeyboardEvent, ReactNode } from 'react';
|
|
67
|
+
import { MediaContext } from '@bendyline/squisq-react';
|
|
64
68
|
|
|
65
69
|
export type { EditorColorScheme } from './EditorContext';
|
|
66
70
|
|
|
@@ -114,12 +118,6 @@ export interface EditorShellProps {
|
|
|
114
118
|
* this container via `createMediaProviderFromContainer`.
|
|
115
119
|
*/
|
|
116
120
|
workspaceContainer?: ContentContainer | null;
|
|
117
|
-
/**
|
|
118
|
-
* @deprecated Renamed to `workspaceContainer` to make the workspace-
|
|
119
|
-
* vs. doc-scoped distinction explicit. Still accepted as a fallback
|
|
120
|
-
* for now; remove in the next breaking release.
|
|
121
|
-
*/
|
|
122
|
-
container?: ContentContainer | null;
|
|
123
121
|
/**
|
|
124
122
|
* Enable version history. Snapshots are stored at
|
|
125
123
|
* `.versions/<basename>.<timestamp>.md` inside the same
|
|
@@ -271,6 +269,14 @@ export interface EditorShellProps {
|
|
|
271
269
|
* the button is hidden regardless of this prop.
|
|
272
270
|
*/
|
|
273
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;
|
|
274
280
|
/**
|
|
275
281
|
* Placeholder text shown in the WYSIWYG editor while the document is
|
|
276
282
|
* empty. When omitted, the editor rotates through its own generic
|
|
@@ -350,12 +356,16 @@ export interface EditorShellProps {
|
|
|
350
356
|
*/
|
|
351
357
|
outlineWidth?: number;
|
|
352
358
|
/**
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
* 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'`.
|
|
357
362
|
*/
|
|
358
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;
|
|
359
369
|
/**
|
|
360
370
|
* How much of the active Squisq theme the WYSIWYG editing surface
|
|
361
371
|
* mirrors. Defaults to `'fonts'` — the historical behavior of
|
|
@@ -367,7 +377,8 @@ export interface EditorShellProps {
|
|
|
367
377
|
* Bundled view preferences — a serializable JSON blob covering the
|
|
368
378
|
* runtime-toggleable view options surfaced in the View menu. When
|
|
369
379
|
* provided, fields here override the corresponding individual props
|
|
370
|
-
* (`outline`, `inlinePreview`, `showStatusBar`
|
|
380
|
+
* (`outline`, `inlinePreview`, `showStatusBar`, `blockTagVisibility`,
|
|
381
|
+
* `blockTags`). Pair with
|
|
371
382
|
* {@link onViewPreferencesChange} to externalize storage of these
|
|
372
383
|
* preferences in the host.
|
|
373
384
|
*/
|
|
@@ -405,7 +416,6 @@ export function EditorShell({
|
|
|
405
416
|
maxHeight,
|
|
406
417
|
mediaProvider,
|
|
407
418
|
workspaceContainer,
|
|
408
|
-
container,
|
|
409
419
|
allowVersioning = false,
|
|
410
420
|
versionBasename,
|
|
411
421
|
versioningPrunePolicy,
|
|
@@ -428,6 +438,7 @@ export function EditorShell({
|
|
|
428
438
|
mentionProvider,
|
|
429
439
|
documentLinkProvider,
|
|
430
440
|
allowRecording = true,
|
|
441
|
+
allowNarrate = true,
|
|
431
442
|
placeholder,
|
|
432
443
|
readOnly = false,
|
|
433
444
|
imageSrc,
|
|
@@ -439,13 +450,14 @@ export function EditorShell({
|
|
|
439
450
|
inlinePreviewWidth = 320,
|
|
440
451
|
outline = false,
|
|
441
452
|
outlineWidth,
|
|
442
|
-
blockTags
|
|
453
|
+
blockTags,
|
|
454
|
+
blockTagVisibility,
|
|
443
455
|
themeInheritance = 'fonts',
|
|
444
456
|
viewPreferences,
|
|
445
457
|
onViewPreferencesChange,
|
|
446
458
|
themeOverride = null,
|
|
447
459
|
}: EditorShellProps) {
|
|
448
|
-
const effectiveContainer = workspaceContainer ??
|
|
460
|
+
const effectiveContainer = workspaceContainer ?? null;
|
|
449
461
|
|
|
450
462
|
// If the host gave us a `workspaceContainer` but no explicit `mediaProvider`,
|
|
451
463
|
// derive one automatically. Without this, drag-and-drop of an image
|
|
@@ -466,65 +478,69 @@ export function EditorShell({
|
|
|
466
478
|
!showPlayTab && initialView === 'preview' ? 'wysiwyg' : initialView;
|
|
467
479
|
|
|
468
480
|
return (
|
|
469
|
-
<
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
allowVersioning={allowVersioning}
|
|
476
|
-
versionBasename={versionBasename}
|
|
477
|
-
versioningPrunePolicy={versioningPrunePolicy}
|
|
478
|
-
versioningAutoSaveIdleMs={versioningAutoSaveIdleMs}
|
|
479
|
-
onSaveVersion={onSaveVersion}
|
|
480
|
-
mediaProvider={effectiveMediaProvider}
|
|
481
|
-
imageDisplayMode={imageDisplayMode}
|
|
482
|
-
mentionProvider={mentionProvider}
|
|
483
|
-
documentLinkProvider={documentLinkProvider}
|
|
484
|
-
allowRecording={allowRecording}
|
|
485
|
-
fileName={fileName}
|
|
486
|
-
language={language}
|
|
487
|
-
inlinePreview={inlinePreview}
|
|
488
|
-
showStatusBar={showStatusBar}
|
|
489
|
-
outline={outline}
|
|
490
|
-
blockTags={blockTags}
|
|
491
|
-
themeInheritance={themeInheritance}
|
|
492
|
-
viewPreferences={viewPreferences}
|
|
493
|
-
onViewPreferencesChange={onViewPreferencesChange}
|
|
494
|
-
>
|
|
495
|
-
<EditorShellInner
|
|
496
|
-
basePath={basePath}
|
|
497
|
-
onChange={onChange}
|
|
498
|
-
className={className}
|
|
499
|
-
height={height}
|
|
500
|
-
minHeight={minHeight}
|
|
501
|
-
maxHeight={maxHeight}
|
|
502
|
-
placeholder={placeholder}
|
|
503
|
-
mediaProvider={effectiveMediaProvider ?? null}
|
|
481
|
+
<MediaContext.Provider value={effectiveMediaProvider ?? null}>
|
|
482
|
+
<EditorProvider
|
|
483
|
+
initialMarkdown={initialMarkdown}
|
|
484
|
+
initialView={effectiveInitialView}
|
|
485
|
+
articleId={articleId}
|
|
486
|
+
colorScheme={colorScheme}
|
|
504
487
|
workspaceContainer={effectiveContainer}
|
|
505
|
-
filesToggleEnabled={filesToggleEnabled}
|
|
506
|
-
toolbarSlotLeft={toolbarSlotLeft}
|
|
507
|
-
toolbarSlotAfterActions={toolbarSlotAfterActions}
|
|
508
|
-
toolbarSlotRight={toolbarSlotRight}
|
|
509
|
-
showPlayTab={showPlayTab}
|
|
510
|
-
submitOnEnter={submitOnEnter}
|
|
511
|
-
codeContext={codeContext}
|
|
512
|
-
fullWidth={fullWidth}
|
|
513
|
-
uxFont={uxFont}
|
|
514
|
-
thinMargins={thinMargins}
|
|
515
|
-
readOnly={readOnly}
|
|
516
|
-
imageSrc={imageSrc}
|
|
517
|
-
imageAlt={imageAlt}
|
|
518
|
-
imageMode={imageMode}
|
|
519
|
-
imageEditorContainer={imageEditorContainer}
|
|
520
|
-
onImageExport={onImageExport}
|
|
521
488
|
allowVersioning={allowVersioning}
|
|
489
|
+
versionBasename={versionBasename}
|
|
490
|
+
versioningPrunePolicy={versioningPrunePolicy}
|
|
522
491
|
versioningAutoSaveIdleMs={versioningAutoSaveIdleMs}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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>
|
|
528
544
|
);
|
|
529
545
|
}
|
|
530
546
|
|
|
@@ -593,6 +609,7 @@ function EditorShellInner({
|
|
|
593
609
|
outlineWidth,
|
|
594
610
|
themeOverride,
|
|
595
611
|
}: EditorShellInnerProps) {
|
|
612
|
+
const shellRef = useRef<HTMLDivElement>(null);
|
|
596
613
|
const {
|
|
597
614
|
activeView,
|
|
598
615
|
markdownSource,
|
|
@@ -662,7 +679,7 @@ function EditorShellInner({
|
|
|
662
679
|
let cancelled = false;
|
|
663
680
|
mediaProvider.listMedia().then(
|
|
664
681
|
(entries) => {
|
|
665
|
-
if (!cancelled) setMediaCount(entries.length);
|
|
682
|
+
if (!cancelled) setMediaCount(filterVisibleMediaEntries(entries).length);
|
|
666
683
|
},
|
|
667
684
|
(err: unknown) => {
|
|
668
685
|
if (!cancelled) {
|
|
@@ -800,6 +817,7 @@ function EditorShellInner({
|
|
|
800
817
|
|
|
801
818
|
const { isDragging, dragContentType, containerProps, zoneProps } = useFileDrop({
|
|
802
819
|
onDrop: handleFileDrop,
|
|
820
|
+
enabled: !readOnly,
|
|
803
821
|
});
|
|
804
822
|
|
|
805
823
|
// Notify parent of changes
|
|
@@ -807,35 +825,28 @@ function EditorShellInner({
|
|
|
807
825
|
onChange?.(markdownSource);
|
|
808
826
|
}, [markdownSource, onChange]);
|
|
809
827
|
|
|
810
|
-
//
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
if (e.ctrlKey
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
e.preventDefault();
|
|
826
|
-
document.querySelector<HTMLButtonElement>('[data-view="preview"]')?.click();
|
|
827
|
-
break;
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
};
|
|
831
|
-
window.addEventListener('keydown', handler);
|
|
832
|
-
return () => window.removeEventListener('keydown', handler);
|
|
833
|
-
}, [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
|
+
);
|
|
834
843
|
|
|
835
844
|
const autoGrow = minHeight !== undefined || maxHeight !== undefined;
|
|
836
845
|
|
|
837
846
|
return (
|
|
838
847
|
<div
|
|
848
|
+
ref={shellRef}
|
|
849
|
+
onKeyDown={handleShellKeyDown}
|
|
839
850
|
className={`squisq-editor-shell ${className || ''}`}
|
|
840
851
|
data-theme={colorScheme}
|
|
841
852
|
data-full-width={fullWidth ? 'true' : undefined}
|
|
@@ -866,85 +877,103 @@ function EditorShellInner({
|
|
|
866
877
|
>
|
|
867
878
|
<CustomThemeProvider docThemes={docThemes} onDocThemesChange={onDocThemesChange}>
|
|
868
879
|
<PreviewSettingsProvider doc={doc} themeOverride={themeOverride}>
|
|
869
|
-
{
|
|
880
|
+
<PresentationModeProvider rootRef={shellRef}>
|
|
881
|
+
{/* Header. In image mode the full markdown/code Toolbar is replaced
|
|
870
882
|
with a minimal slot bar — view tabs, formatting, and preview
|
|
871
883
|
controls don't apply to a binary asset. */}
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
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
|
+
/>
|
|
878
912
|
</div>
|
|
879
|
-
)
|
|
880
|
-
) : (
|
|
881
|
-
<div className="squisq-editor-header">
|
|
882
|
-
<Toolbar
|
|
883
|
-
showFiles={showFiles}
|
|
884
|
-
fileCount={mediaCount}
|
|
885
|
-
onToggleFiles={!isCodeMode && filesToggleEnabled ? handleToggleFiles : undefined}
|
|
886
|
-
slotLeft={toolbarSlotLeft}
|
|
887
|
-
slotAfterTabs={!isCodeMode && isPreview && <PreviewToolbarControls />}
|
|
888
|
-
slotAfterActions={toolbarSlotAfterActions}
|
|
889
|
-
slotRight={toolbarSlotRight}
|
|
890
|
-
showPlayTab={showPlayTab}
|
|
891
|
-
/>
|
|
892
|
-
</div>
|
|
893
|
-
)}
|
|
913
|
+
)}
|
|
894
914
|
|
|
895
|
-
|
|
896
|
-
<div
|
|
897
|
-
className="squisq-editor-content"
|
|
898
|
-
style={{
|
|
899
|
-
flex: autoGrow ? '1 1 auto' : 1,
|
|
900
|
-
overflowY: autoGrow ? 'auto' : 'hidden',
|
|
901
|
-
overflowX: 'hidden',
|
|
902
|
-
minHeight: 0,
|
|
903
|
-
position: 'relative',
|
|
904
|
-
display: 'flex',
|
|
905
|
-
}}
|
|
906
|
-
>
|
|
915
|
+
{/* Main content area */}
|
|
907
916
|
<div
|
|
917
|
+
className="squisq-editor-content"
|
|
908
918
|
style={{
|
|
909
919
|
flex: autoGrow ? '1 1 auto' : 1,
|
|
910
|
-
|
|
920
|
+
overflowY: autoGrow ? 'auto' : 'hidden',
|
|
921
|
+
overflowX: 'hidden',
|
|
911
922
|
minHeight: 0,
|
|
912
923
|
position: 'relative',
|
|
924
|
+
display: 'flex',
|
|
913
925
|
}}
|
|
914
926
|
>
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
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`
|
|
930
950
|
so toggling a pane on/off doesn't change the editor's tree
|
|
931
951
|
position — Monaco stays mounted and `monacoEditor` in
|
|
932
952
|
context stays stable, which is what `useHeadingLayout` needs
|
|
933
953
|
to compute positions. */}
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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
|
+
) : (
|
|
948
977
|
<div key="raw-editor" className="squisq-raw-editor-container">
|
|
949
978
|
<RawEditor
|
|
950
979
|
monacoTheme={colorScheme === 'dark' ? 'vs-dark' : 'vs'}
|
|
@@ -952,103 +981,96 @@ function EditorShellInner({
|
|
|
952
981
|
readOnly={readOnly}
|
|
953
982
|
/>
|
|
954
983
|
</div>
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
<div key="raw-editor" className="squisq-raw-editor-container">
|
|
958
|
-
<RawEditor
|
|
959
|
-
monacoTheme={colorScheme === 'dark' ? 'vs-dark' : 'vs'}
|
|
960
|
-
submitOnEnter={submitOnEnter}
|
|
961
|
-
readOnly={readOnly}
|
|
962
|
-
/>
|
|
963
|
-
</div>
|
|
964
|
-
)}
|
|
965
|
-
{/* Renders nothing in normal flow — portals context
|
|
984
|
+
)}
|
|
985
|
+
{/* Renders nothing in normal flow — portals context
|
|
966
986
|
sections into Monaco view zones via the context's
|
|
967
987
|
monacoEditor. Code mode only. */}
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
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
|
|
985
1005
|
entirely in code or image mode so Tiptap never initializes
|
|
986
1006
|
and the preview pipeline stays idle. Same always-wrapped
|
|
987
1007
|
pattern as the Raw branch above so pane toggles don't
|
|
988
1008
|
remount Tiptap. */}
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
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
|
+
) : (
|
|
1001
1031
|
<WysiwygEditor
|
|
1002
1032
|
key="wysiwyg-editor"
|
|
1003
1033
|
submitOnEnter={submitOnEnter}
|
|
1004
1034
|
placeholder={placeholder}
|
|
1005
1035
|
readOnly={readOnly}
|
|
1006
1036
|
/>
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
/>
|
|
1026
|
-
)}
|
|
1027
|
-
</div>
|
|
1028
|
-
)}
|
|
1029
|
-
{isMarkdownMode && isPreview && (
|
|
1030
|
-
<PreviewPanel basePath={basePath} workspaceContainer={workspaceContainer} />
|
|
1031
|
-
)}
|
|
1032
|
-
</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>
|
|
1033
1055
|
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
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
|
+
)}
|
|
1045
1067
|
|
|
1046
|
-
|
|
1068
|
+
{/* Docked custom-theme designer — a flex sibling of the preview, so
|
|
1047
1069
|
opening it reflows the preview narrower. Renders nothing when the
|
|
1048
1070
|
designer is closed. */}
|
|
1049
|
-
|
|
1071
|
+
{isMarkdownMode && <ThemeDesignerDock />}
|
|
1050
1072
|
|
|
1051
|
-
|
|
1073
|
+
{/* Drop zone overlay — image / text drop UX is markdown-specific.
|
|
1052
1074
|
In WYSIWYG, image drops are handled directly by Tiptap's
|
|
1053
1075
|
`handleDrop` (uploads to the MediaProvider and inserts an
|
|
1054
1076
|
image node at the mouse position). The overlay would sit on
|
|
@@ -1056,27 +1078,28 @@ function EditorShellInner({
|
|
|
1056
1078
|
when the dragged content is media-only on the WYSIWYG view —
|
|
1057
1079
|
the user gets a one-step "drop where you want it" flow
|
|
1058
1080
|
instead of a two-step "drop in bin, then insert" flow. */}
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
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>
|
|
1069
1091
|
|
|
1070
|
-
|
|
1092
|
+
{/* Timeline track — horizontal strip of block + media bars shown
|
|
1071
1093
|
below the editor in Timeline view. The card editor above (driven by
|
|
1072
1094
|
the block navigator) shows whichever block is selected here. */}
|
|
1073
|
-
|
|
1095
|
+
{isTimelineMode && <TimelineTrack />}
|
|
1074
1096
|
|
|
1075
|
-
|
|
1097
|
+
{/* Status bar — word / char / line / block counts. Host can
|
|
1076
1098
|
suppress via `showStatusBar={false}` for embedded chat-style
|
|
1077
1099
|
composers where the stats are noise. The image viewer has its
|
|
1078
1100
|
own dimension/zoom status row, so suppress here too. */}
|
|
1079
|
-
|
|
1101
|
+
{statusBarVisible && !isImageMode && <StatusBar />}
|
|
1102
|
+
</PresentationModeProvider>
|
|
1080
1103
|
</PreviewSettingsProvider>
|
|
1081
1104
|
</CustomThemeProvider>
|
|
1082
1105
|
<TooltipLayer />
|
|
@@ -1140,6 +1163,19 @@ function ImageEditModal({
|
|
|
1140
1163
|
versioningAutoSaveIdleMs,
|
|
1141
1164
|
shellTheme,
|
|
1142
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;
|
|
1143
1179
|
// Each unique image path gets its own sidecar so multiple images in the
|
|
1144
1180
|
// same doc can be edited independently without colliding state. When the
|
|
1145
1181
|
// host didn't supply a `container`, fall back to a fresh in-memory one
|
|
@@ -1147,14 +1183,22 @@ function ImageEditModal({
|
|
|
1147
1183
|
// gets written back through `mediaProvider`.
|
|
1148
1184
|
const sidecar = useMemo(() => {
|
|
1149
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)}`;
|
|
1150
1192
|
const parent: ContentContainer = container ?? new MemoryContentContainer();
|
|
1151
|
-
return scopeContainer(parent, `.imageEdits/${
|
|
1193
|
+
return scopeContainer(parent, `.imageEdits/${scopedName}`);
|
|
1152
1194
|
}, [container, relativePath]);
|
|
1153
1195
|
|
|
1154
1196
|
const [initialSrc, setInitialSrc] = useState<string | null>(null);
|
|
1155
1197
|
const [resolveError, setResolveError] = useState<string | null>(null);
|
|
1156
1198
|
useEffect(() => {
|
|
1157
1199
|
let cancelled = false;
|
|
1200
|
+
setInitialSrc(null);
|
|
1201
|
+
setResolveError(null);
|
|
1158
1202
|
mediaProvider.resolveUrl(relativePath).then(
|
|
1159
1203
|
(url) => {
|
|
1160
1204
|
if (!cancelled) setInitialSrc(url);
|
|
@@ -1185,15 +1229,55 @@ function ImageEditModal({
|
|
|
1185
1229
|
|
|
1186
1230
|
// Close on Escape — global listener so it works regardless of focus.
|
|
1187
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();
|
|
1188
1245
|
const handler = (e: KeyboardEvent) => {
|
|
1189
|
-
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
|
+
}
|
|
1190
1267
|
};
|
|
1191
1268
|
window.addEventListener('keydown', handler);
|
|
1192
|
-
return () =>
|
|
1193
|
-
|
|
1269
|
+
return () => {
|
|
1270
|
+
window.removeEventListener('keydown', handler);
|
|
1271
|
+
inertSiblings.forEach((element) => {
|
|
1272
|
+
(element as HTMLElement).inert = false;
|
|
1273
|
+
});
|
|
1274
|
+
previousFocus?.focus();
|
|
1275
|
+
};
|
|
1276
|
+
}, []);
|
|
1194
1277
|
|
|
1195
1278
|
return (
|
|
1196
1279
|
<div
|
|
1280
|
+
ref={modalRef}
|
|
1197
1281
|
className="squisq-image-edit-modal"
|
|
1198
1282
|
data-testid="image-edit-modal"
|
|
1199
1283
|
role="dialog"
|
|
@@ -1204,7 +1288,7 @@ function ImageEditModal({
|
|
|
1204
1288
|
if (e.target === e.currentTarget) onClose();
|
|
1205
1289
|
}}
|
|
1206
1290
|
>
|
|
1207
|
-
<div className="squisq-image-edit-modal__surface">
|
|
1291
|
+
<div ref={surfaceRef} className="squisq-image-edit-modal__surface">
|
|
1208
1292
|
<header className="squisq-image-edit-modal__header">
|
|
1209
1293
|
<span className="squisq-image-edit-modal__title">Edit image</span>
|
|
1210
1294
|
<span className="squisq-image-edit-modal__path">{relativePath}</span>
|
|
@@ -1223,6 +1307,10 @@ function ImageEditModal({
|
|
|
1223
1307
|
<div className="squisq-image-edit-modal__error">
|
|
1224
1308
|
Failed to load image: {resolveError}
|
|
1225
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>
|
|
1226
1314
|
) : !initialSrc ? (
|
|
1227
1315
|
<div className="squisq-image-edit-modal__loading">Loading image…</div>
|
|
1228
1316
|
) : (
|
|
@@ -1233,7 +1321,7 @@ function ImageEditModal({
|
|
|
1233
1321
|
versioningAutoSaveIdleMs={versioningAutoSaveIdleMs}
|
|
1234
1322
|
onExport={handleExport}
|
|
1235
1323
|
saveBehavior="export"
|
|
1236
|
-
saveFormat=
|
|
1324
|
+
saveFormat={saveFormat}
|
|
1237
1325
|
saveLabel="Save and close"
|
|
1238
1326
|
saveTitle="Save changes back to the image and close"
|
|
1239
1327
|
surface={shellTheme === 'dark' ? DARK_SURFACE : LIGHT_SURFACE}
|