@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/TransitionPicker.tsx
CHANGED
|
@@ -30,11 +30,27 @@ import {
|
|
|
30
30
|
export interface TransitionPickerProps {
|
|
31
31
|
value: TransitionFields;
|
|
32
32
|
onChange: (next: TransitionFields) => void;
|
|
33
|
+
/** Surface scheme copied onto the portaled flyout so it can style independently of the shell. */
|
|
34
|
+
colorScheme?: 'light' | 'dark';
|
|
35
|
+
/** Active document-theme accent used for open and selected states. */
|
|
36
|
+
accentColor?: string;
|
|
33
37
|
}
|
|
34
38
|
|
|
35
|
-
|
|
39
|
+
/**
|
|
40
|
+
* DOM id of the portaled transition flyout. Exported so hosts that embed the
|
|
41
|
+
* picker inside their own popovers (e.g. the toolbar overflow menu) can treat
|
|
42
|
+
* clicks inside the flyout as "inside" in their outside-click handling.
|
|
43
|
+
*/
|
|
44
|
+
export const TRANSITION_FLYOUT_PORTAL_ID = 'squisq-transition-flyout-portal';
|
|
45
|
+
|
|
46
|
+
const FLYOUT_ID = TRANSITION_FLYOUT_PORTAL_ID;
|
|
36
47
|
|
|
37
|
-
export function TransitionPicker({
|
|
48
|
+
export function TransitionPicker({
|
|
49
|
+
value,
|
|
50
|
+
onChange,
|
|
51
|
+
colorScheme = 'light',
|
|
52
|
+
accentColor,
|
|
53
|
+
}: TransitionPickerProps) {
|
|
38
54
|
const [open, setOpen] = useState(false);
|
|
39
55
|
const [style, setStyle] = useState<React.CSSProperties>({});
|
|
40
56
|
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
@@ -112,13 +128,22 @@ export function TransitionPicker({ value, onChange }: TransitionPickerProps) {
|
|
|
112
128
|
|
|
113
129
|
const flyout = open
|
|
114
130
|
? createPortal(
|
|
115
|
-
<TransitionFlyout
|
|
131
|
+
<TransitionFlyout
|
|
132
|
+
style={{ ...style, ...blockAccentStyle(accentColor) }}
|
|
133
|
+
value={value}
|
|
134
|
+
onChange={onChange}
|
|
135
|
+
colorScheme={colorScheme}
|
|
136
|
+
/>,
|
|
116
137
|
document.body,
|
|
117
138
|
)
|
|
118
139
|
: null;
|
|
119
140
|
|
|
120
141
|
return (
|
|
121
|
-
<div
|
|
142
|
+
<div
|
|
143
|
+
className="squisq-transition-picker"
|
|
144
|
+
data-theme={colorScheme}
|
|
145
|
+
style={blockAccentStyle(accentColor)}
|
|
146
|
+
>
|
|
122
147
|
<button
|
|
123
148
|
ref={triggerRef}
|
|
124
149
|
type="button"
|
|
@@ -151,10 +176,12 @@ function TransitionFlyout({
|
|
|
151
176
|
style,
|
|
152
177
|
value,
|
|
153
178
|
onChange,
|
|
179
|
+
colorScheme,
|
|
154
180
|
}: {
|
|
155
181
|
style: React.CSSProperties;
|
|
156
182
|
value: TransitionFields;
|
|
157
183
|
onChange: (next: TransitionFields) => void;
|
|
184
|
+
colorScheme: 'light' | 'dark';
|
|
158
185
|
}) {
|
|
159
186
|
const [query, setQuery] = useState('');
|
|
160
187
|
const searchRef = useRef<HTMLInputElement>(null);
|
|
@@ -204,7 +231,13 @@ function TransitionFlyout({
|
|
|
204
231
|
};
|
|
205
232
|
|
|
206
233
|
return (
|
|
207
|
-
<div
|
|
234
|
+
<div
|
|
235
|
+
id={FLYOUT_ID}
|
|
236
|
+
className="squisq-transition-flyout"
|
|
237
|
+
data-theme={colorScheme}
|
|
238
|
+
role="menu"
|
|
239
|
+
style={style}
|
|
240
|
+
>
|
|
208
241
|
<div className="squisq-transition-flyout-search">
|
|
209
242
|
<input
|
|
210
243
|
ref={searchRef}
|
|
@@ -306,6 +339,12 @@ function TransitionFlyout({
|
|
|
306
339
|
);
|
|
307
340
|
}
|
|
308
341
|
|
|
342
|
+
function blockAccentStyle(accentColor: string | undefined): React.CSSProperties {
|
|
343
|
+
return accentColor
|
|
344
|
+
? ({ ['--squisq-block-props-accent' as string]: accentColor } as React.CSSProperties)
|
|
345
|
+
: {};
|
|
346
|
+
}
|
|
347
|
+
|
|
309
348
|
function TransitionOption({
|
|
310
349
|
entry,
|
|
311
350
|
selected,
|
package/src/ViewMenuPanel.tsx
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* EditorShell `inlinePreview` prop.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
13
|
+
import { useCallback, useEffect, useId, useRef, useState } from 'react';
|
|
14
14
|
import { useEditorContext, type ThemeInheritance, type LayoutMode } from './EditorContext';
|
|
15
15
|
import { Icon } from './Icon';
|
|
16
16
|
|
|
@@ -22,8 +22,8 @@ export function ViewMenuPanel() {
|
|
|
22
22
|
setStatusBarVisible,
|
|
23
23
|
outlineVisible,
|
|
24
24
|
setOutlineVisible,
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
blockTagVisibility,
|
|
26
|
+
setBlockTagVisibility,
|
|
27
27
|
themeInheritance,
|
|
28
28
|
setThemeInheritance,
|
|
29
29
|
layoutMode,
|
|
@@ -56,10 +56,6 @@ export function ViewMenuPanel() {
|
|
|
56
56
|
setOutlineVisible(!outlineVisible);
|
|
57
57
|
}, [outlineVisible, setOutlineVisible]);
|
|
58
58
|
|
|
59
|
-
const toggleBlockTags = useCallback(() => {
|
|
60
|
-
setBlockTagsVisible(!blockTagsVisible);
|
|
61
|
-
}, [blockTagsVisible, setBlockTagsVisible]);
|
|
62
|
-
|
|
63
59
|
return (
|
|
64
60
|
<div className="squisq-view-menu" ref={containerRef}>
|
|
65
61
|
<button
|
|
@@ -79,7 +75,6 @@ export function ViewMenuPanel() {
|
|
|
79
75
|
<div className="squisq-view-menu-popover" role="menu" aria-label="View options">
|
|
80
76
|
<MenuRadioGroup
|
|
81
77
|
label="View"
|
|
82
|
-
name="layout-mode"
|
|
83
78
|
value={layoutMode}
|
|
84
79
|
onChange={(v) => setLayoutMode(v as LayoutMode)}
|
|
85
80
|
options={[
|
|
@@ -95,10 +90,15 @@ export function ViewMenuPanel() {
|
|
|
95
90
|
checked={inlinePreviewVisible}
|
|
96
91
|
onChange={toggleInlinePreview}
|
|
97
92
|
/>
|
|
98
|
-
<
|
|
93
|
+
<MenuRadioGroup
|
|
99
94
|
label="Show block tags"
|
|
100
|
-
|
|
101
|
-
onChange={
|
|
95
|
+
value={blockTagVisibility}
|
|
96
|
+
onChange={(v) => setBlockTagVisibility(v as typeof blockTagVisibility)}
|
|
97
|
+
options={[
|
|
98
|
+
{ value: 'none', label: 'No inline block tags' },
|
|
99
|
+
{ value: 'active', label: 'Selected/hovered block' },
|
|
100
|
+
{ value: 'always', label: 'Always show' },
|
|
101
|
+
]}
|
|
102
102
|
/>
|
|
103
103
|
<MenuToggle
|
|
104
104
|
label="Show status bar"
|
|
@@ -108,7 +108,6 @@ export function ViewMenuPanel() {
|
|
|
108
108
|
<div className="squisq-view-menu-separator" role="separator" />
|
|
109
109
|
<MenuRadioGroup
|
|
110
110
|
label="Editor styling from theme"
|
|
111
|
-
name="theme-inheritance"
|
|
112
111
|
value={themeInheritance}
|
|
113
112
|
onChange={(v) => setThemeInheritance(v as ThemeInheritance)}
|
|
114
113
|
options={[
|
|
@@ -147,17 +146,16 @@ function MenuToggle({
|
|
|
147
146
|
|
|
148
147
|
function MenuRadioGroup({
|
|
149
148
|
label,
|
|
150
|
-
name,
|
|
151
149
|
value,
|
|
152
150
|
onChange,
|
|
153
151
|
options,
|
|
154
152
|
}: {
|
|
155
153
|
label: string;
|
|
156
|
-
name: string;
|
|
157
154
|
value: string;
|
|
158
155
|
onChange: (next: string) => void;
|
|
159
156
|
options: readonly { value: string; label: string }[];
|
|
160
157
|
}) {
|
|
158
|
+
const name = useId();
|
|
161
159
|
return (
|
|
162
160
|
<div
|
|
163
161
|
className="squisq-view-menu-row squisq-view-menu-row--radios"
|
package/src/ViewSwitcher.tsx
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ViewSwitcher
|
|
3
3
|
*
|
|
4
|
-
* Tab bar for switching between
|
|
4
|
+
* Tab bar for switching between Write, Source, and Use editor views.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { useEditorContext, type EditorView } from './EditorContext';
|
|
8
8
|
|
|
9
9
|
const VIEWS: { id: EditorView; label: string; shortLabel?: string; shortcut: string }[] = [
|
|
10
|
-
{ id: '
|
|
11
|
-
{ id: '
|
|
12
|
-
{ id: 'preview', label: '
|
|
10
|
+
{ id: 'wysiwyg', label: 'Write', shortcut: '⌘1' },
|
|
11
|
+
{ id: 'raw', label: 'Source', shortcut: '⌘2' },
|
|
12
|
+
{ id: 'preview', label: 'Use', shortcut: '⌘3' },
|
|
13
13
|
];
|
|
14
14
|
|
|
15
15
|
export interface ViewSwitcherProps {
|
package/src/WysiwygEditor.tsx
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
14
14
|
import type { CSSProperties } from 'react';
|
|
15
15
|
import { useEditor, EditorContent } from '@tiptap/react';
|
|
16
|
+
import { Selection } from '@tiptap/pm/state';
|
|
17
|
+
import type { EditorView as ProseMirrorView } from '@tiptap/pm/view';
|
|
16
18
|
import StarterKit from '@tiptap/starter-kit';
|
|
17
19
|
import Table from '@tiptap/extension-table';
|
|
18
20
|
import TableRow from '@tiptap/extension-table-row';
|
|
@@ -22,9 +24,19 @@ import TaskList from '@tiptap/extension-task-list';
|
|
|
22
24
|
import TaskItem from '@tiptap/extension-task-item';
|
|
23
25
|
import Link from '@tiptap/extension-link';
|
|
24
26
|
import Placeholder from '@tiptap/extension-placeholder';
|
|
25
|
-
import { resolveFontFamily, FONT_FALLBACKS } from '@bendyline/squisq/schemas';
|
|
27
|
+
import { resolveFontFamily, FONT_FALLBACKS, VIEWPORT_PRESETS } from '@bendyline/squisq/schemas';
|
|
28
|
+
import { DEFAULT_THEME, flattenBlocks, markdownToDoc } from '@bendyline/squisq/doc';
|
|
29
|
+
import { parseMarkdown } from '@bendyline/squisq/markdown';
|
|
26
30
|
import { HeadingWithTemplate } from './TemplateAnnotation';
|
|
27
|
-
import {
|
|
31
|
+
import { AsciiDiagramExtension } from './asciiDiagram/AsciiDiagramExtension';
|
|
32
|
+
import { RepairableDiagramExtension } from './asciiDiagram/RepairableDiagramExtension';
|
|
33
|
+
import { applyRepairCommand } from './asciiDiagram/asciiDiagramCommands';
|
|
34
|
+
import { shouldPasteAsAsciiFence } from './asciiDiagram/asciiPaste';
|
|
35
|
+
import { TreeViewExtension } from './treeview/TreeViewExtension';
|
|
36
|
+
import { shouldPasteAsTreeFence } from './treeview/treePaste';
|
|
37
|
+
import { TimelineViewExtension } from './timeline/TimelineViewExtension';
|
|
38
|
+
import { shouldPasteAsTimelineFence } from './timeline/timelinePaste';
|
|
39
|
+
import { BlockTagActivityExtension } from './blockTagActivity';
|
|
28
40
|
import { SceneBlockExtension } from './scene/SceneBlockExtension';
|
|
29
41
|
import { InlineIcon } from './InlineIcon';
|
|
30
42
|
import { ImageWithMediaProvider } from './ImageNodeView';
|
|
@@ -49,6 +61,9 @@ import { markdownToTiptap, tiptapToMarkdown } from './tiptapBridge';
|
|
|
49
61
|
import { looksLikeMarkdown } from './detectMarkdown';
|
|
50
62
|
import { SQUISQ_MEDIA_MIME, parseSquisqMediaPayload } from './mediaDragMime';
|
|
51
63
|
import { usePreviewSettingsOptional } from './PreviewControls';
|
|
64
|
+
import { uploadAndInsertImages } from './wysiwygImageUpload';
|
|
65
|
+
|
|
66
|
+
type ImageMutationView = Pick<ProseMirrorView, 'state' | 'dispatch'>;
|
|
52
67
|
|
|
53
68
|
/**
|
|
54
69
|
* Rotating placeholder prompts shown when the editor is empty. One is
|
|
@@ -68,6 +83,12 @@ const EMPTY_PROMPTS = [
|
|
|
68
83
|
'Begin at the beginning…',
|
|
69
84
|
];
|
|
70
85
|
|
|
86
|
+
const BLOCK_TAG_DATA_VALUES = {
|
|
87
|
+
none: 'hidden',
|
|
88
|
+
active: 'active',
|
|
89
|
+
always: 'visible',
|
|
90
|
+
} as const;
|
|
91
|
+
|
|
71
92
|
function pickEmptyPrompt(): string {
|
|
72
93
|
return EMPTY_PROMPTS[Math.floor(Math.random() * EMPTY_PROMPTS.length)];
|
|
73
94
|
}
|
|
@@ -106,8 +127,11 @@ export function WysiwygEditor({
|
|
|
106
127
|
setTiptapEditor,
|
|
107
128
|
mediaProvider,
|
|
108
129
|
mentionProvider,
|
|
109
|
-
|
|
130
|
+
blockTagVisibility,
|
|
110
131
|
themeInheritance,
|
|
132
|
+
colorScheme,
|
|
133
|
+
bumpMediaRevision,
|
|
134
|
+
sceneTextChannel,
|
|
111
135
|
} = useEditorContext();
|
|
112
136
|
// Custom templates inlined in the active doc's frontmatter + the
|
|
113
137
|
// persist callback that writes a new list back into the source.
|
|
@@ -162,7 +186,6 @@ export function WysiwygEditor({
|
|
|
162
186
|
useEffect(() => {
|
|
163
187
|
submitOnEnterRef.current = submitOnEnter;
|
|
164
188
|
}, [submitOnEnter]);
|
|
165
|
-
|
|
166
189
|
const editor = useEditor({
|
|
167
190
|
editable: !readOnly,
|
|
168
191
|
extensions: [
|
|
@@ -174,8 +197,12 @@ export function WysiwygEditor({
|
|
|
174
197
|
},
|
|
175
198
|
}),
|
|
176
199
|
HeadingWithTemplate.configure({ levels: [1, 2, 3, 4, 5, 6] }),
|
|
177
|
-
|
|
178
|
-
|
|
200
|
+
BlockTagActivityExtension,
|
|
201
|
+
AsciiDiagramExtension.configure({ textChannel: sceneTextChannel }),
|
|
202
|
+
RepairableDiagramExtension.configure({ onRepair: applyRepairCommand }),
|
|
203
|
+
TimelineViewExtension,
|
|
204
|
+
TreeViewExtension,
|
|
205
|
+
SceneBlockExtension.configure({ textChannel: sceneTextChannel }),
|
|
179
206
|
Table.configure({ resizable: true }),
|
|
180
207
|
TableRow,
|
|
181
208
|
TableCell,
|
|
@@ -197,8 +224,13 @@ export function WysiwygEditor({
|
|
|
197
224
|
content: markdownToTiptap(stripFrontmatter(editorSource).body),
|
|
198
225
|
onUpdate: ({ editor: ed }) => {
|
|
199
226
|
if (isExternalUpdate.current) return;
|
|
200
|
-
|
|
201
|
-
|
|
227
|
+
// Keep the source channel synchronous with Tiptap. Structural actions
|
|
228
|
+
// such as block navigation/addition may run in the very next browser
|
|
229
|
+
// event; deferring this write lets a stale callback serialize the newly
|
|
230
|
+
// selected block into the previous block's range. The expensive full-doc
|
|
231
|
+
// parse remains debounced in EditorContext, so correctness here does not
|
|
232
|
+
// reintroduce parse-on-every-keystroke work.
|
|
233
|
+
const bodyMd = tiptapToMarkdown(ed.getHTML());
|
|
202
234
|
const newSource = frontmatterRef.current + bodyMd;
|
|
203
235
|
lastSourceRef.current = newSource;
|
|
204
236
|
setEditorSource(newSource);
|
|
@@ -221,7 +253,9 @@ export function WysiwygEditor({
|
|
|
221
253
|
// its container with `display: block` while items are showing
|
|
222
254
|
// and `display: none` when empty — only short-circuit when
|
|
223
255
|
// there's actually a candidate to pick.
|
|
224
|
-
const popover =
|
|
256
|
+
const popover = view.dom
|
|
257
|
+
.closest('.squisq-editor-shell')
|
|
258
|
+
?.querySelector<HTMLElement>('.squisq-mention-popover');
|
|
225
259
|
if (popover && popover.style.display !== 'none') {
|
|
226
260
|
return false;
|
|
227
261
|
}
|
|
@@ -257,12 +291,67 @@ export function WysiwygEditor({
|
|
|
257
291
|
const imageFiles = filesFromClipboard(clipboard);
|
|
258
292
|
if (imageFiles.length > 0 && mediaProviderRef.current) {
|
|
259
293
|
event.preventDefault();
|
|
260
|
-
uploadAndInsertImages(view, imageFiles, mediaProviderRef.current);
|
|
294
|
+
uploadAndInsertImages(view, imageFiles, mediaProviderRef.current, bumpMediaRevision);
|
|
261
295
|
return true;
|
|
262
296
|
}
|
|
263
297
|
|
|
264
298
|
const text = clipboard.getData('text/plain');
|
|
265
|
-
if (!text
|
|
299
|
+
if (!text) return false;
|
|
300
|
+
|
|
301
|
+
// Bare (unfenced) ASCII diagram art → verbatim into a fresh code
|
|
302
|
+
// block, which AsciiDiagramExtension turns into an interactive
|
|
303
|
+
// canvas. Must run BEFORE the markdown branch: `| x |` art rows
|
|
304
|
+
// false-positive as GFM table rows and would be mangled.
|
|
305
|
+
if (shouldPasteAsAsciiFence(text)) {
|
|
306
|
+
const codeBlockType = view.state.schema.nodes.codeBlock;
|
|
307
|
+
if (codeBlockType) {
|
|
308
|
+
event.preventDefault();
|
|
309
|
+
// Tag the fence `diagram` so its identity survives a later
|
|
310
|
+
// flatten → markdown → re-import round-trip (the language class
|
|
311
|
+
// round-trips; fence meta does not).
|
|
312
|
+
view.dispatch(
|
|
313
|
+
view.state.tr.replaceSelectionWith(
|
|
314
|
+
codeBlockType.create({ language: 'diagram' }, view.state.schema.text(text)),
|
|
315
|
+
),
|
|
316
|
+
);
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Bare (unfenced) marker rails → an explicit timeline code block.
|
|
322
|
+
// Run before the tree gate because timeline callout elbows can look
|
|
323
|
+
// like nested tree connectors to a deliberately lenient parser.
|
|
324
|
+
if (shouldPasteAsTimelineFence(text)) {
|
|
325
|
+
const codeBlockType = view.state.schema.nodes.codeBlock;
|
|
326
|
+
if (codeBlockType) {
|
|
327
|
+
event.preventDefault();
|
|
328
|
+
view.dispatch(
|
|
329
|
+
view.state.tr.replaceSelectionWith(
|
|
330
|
+
codeBlockType.create({ language: 'timeline' }, view.state.schema.text(text)),
|
|
331
|
+
),
|
|
332
|
+
);
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Bare (unfenced) ASCII tree art → a fresh code block that the
|
|
338
|
+
// TreeViewExtension turns into an outline. After the diagram gate,
|
|
339
|
+
// before markdown (the `├──` lines must not be mangled as prose).
|
|
340
|
+
if (shouldPasteAsTreeFence(text)) {
|
|
341
|
+
const codeBlockType = view.state.schema.nodes.codeBlock;
|
|
342
|
+
if (codeBlockType) {
|
|
343
|
+
event.preventDefault();
|
|
344
|
+
// Tag the fence `tree` so its identity survives round-trips.
|
|
345
|
+
view.dispatch(
|
|
346
|
+
view.state.tr.replaceSelectionWith(
|
|
347
|
+
codeBlockType.create({ language: 'tree' }, view.state.schema.text(text)),
|
|
348
|
+
),
|
|
349
|
+
);
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (!looksLikeMarkdown(text)) return false;
|
|
266
355
|
const html = markdownToTiptap(text);
|
|
267
356
|
if (!html) return false;
|
|
268
357
|
event.preventDefault();
|
|
@@ -326,7 +415,7 @@ export function WysiwygEditor({
|
|
|
326
415
|
|
|
327
416
|
event.preventDefault();
|
|
328
417
|
moveSelectionToDropPoint(view, event);
|
|
329
|
-
uploadAndInsertImages(view, imageFiles, mediaProviderRef.current);
|
|
418
|
+
uploadAndInsertImages(view, imageFiles, mediaProviderRef.current, bumpMediaRevision);
|
|
330
419
|
return true;
|
|
331
420
|
},
|
|
332
421
|
},
|
|
@@ -366,6 +455,16 @@ export function WysiwygEditor({
|
|
|
366
455
|
templateParams: string | null;
|
|
367
456
|
} | null>(null);
|
|
368
457
|
|
|
458
|
+
const closeBadgeMenu = useCallback(() => {
|
|
459
|
+
setBadgeMenu(null);
|
|
460
|
+
// The portaled gallery moves focus into its search field. Restore the
|
|
461
|
+
// document's preserved ProseMirror selection after React has unmounted the
|
|
462
|
+
// dialog, so the caret resumes blinking where the author left it.
|
|
463
|
+
requestAnimationFrame(() => {
|
|
464
|
+
if (editor && !editor.isDestroyed) editor.commands.focus();
|
|
465
|
+
});
|
|
466
|
+
}, [editor]);
|
|
467
|
+
|
|
369
468
|
useEffect(() => {
|
|
370
469
|
if (!editor) return;
|
|
371
470
|
const root = containerRef.current;
|
|
@@ -456,9 +555,10 @@ export function WysiwygEditor({
|
|
|
456
555
|
isExternalUpdate.current = false;
|
|
457
556
|
}, [editorSource, editor]);
|
|
458
557
|
|
|
459
|
-
// Match the WYSIWYG editor's appearance to the active Squisq theme
|
|
460
|
-
//
|
|
461
|
-
//
|
|
558
|
+
// Match the WYSIWYG editor's appearance to the active Squisq theme when one
|
|
559
|
+
// is set in frontmatter or picked in the preview dropdown. The block-props
|
|
560
|
+
// affordance always carries the theme's primary as UI accent (like the
|
|
561
|
+
// outline); document-content inheritance is driven by the View menu setting:
|
|
462
562
|
// - 'none' → don't inherit anything
|
|
463
563
|
// - 'fonts' → body + heading fonts only (historical default)
|
|
464
564
|
// - 'fonts-colors' → fonts plus the theme's canvas / text colors
|
|
@@ -467,9 +567,38 @@ export function WysiwygEditor({
|
|
|
467
567
|
// a PreviewSettingsProvider in scope).
|
|
468
568
|
const previewSettings = usePreviewSettingsOptional();
|
|
469
569
|
const activeTheme = previewSettings?.activeTheme;
|
|
570
|
+
const badgePreviewSource = useMemo(() => {
|
|
571
|
+
if (!badgeMenu) return undefined;
|
|
572
|
+
try {
|
|
573
|
+
const previewDoc = markdownToDoc(parseMarkdown(editorSource), { autoTemplates: false });
|
|
574
|
+
const block = flattenBlocks(previewDoc.blocks)[badgeMenu.headingIndex];
|
|
575
|
+
if (!block) return undefined;
|
|
576
|
+
return {
|
|
577
|
+
block,
|
|
578
|
+
theme: previewSettings?.activeTheme ?? DEFAULT_THEME,
|
|
579
|
+
viewport: previewSettings?.activeViewport ?? VIEWPORT_PRESETS.landscape,
|
|
580
|
+
basePath: '/',
|
|
581
|
+
mediaProvider,
|
|
582
|
+
customTemplates: docTemplates,
|
|
583
|
+
};
|
|
584
|
+
} catch {
|
|
585
|
+
return undefined;
|
|
586
|
+
}
|
|
587
|
+
}, [
|
|
588
|
+
badgeMenu,
|
|
589
|
+
docTemplates,
|
|
590
|
+
editorSource,
|
|
591
|
+
mediaProvider,
|
|
592
|
+
previewSettings?.activeTheme,
|
|
593
|
+
previewSettings?.activeViewport,
|
|
594
|
+
]);
|
|
470
595
|
const themeStyle = useMemo<CSSProperties>(() => {
|
|
471
|
-
if (
|
|
596
|
+
if (!activeTheme) return {};
|
|
472
597
|
const out: Record<string, string> = {
|
|
598
|
+
'--squisq-block-props-accent': activeTheme.colors.primary,
|
|
599
|
+
};
|
|
600
|
+
if (themeInheritance === 'none') return out as CSSProperties;
|
|
601
|
+
Object.assign(out, {
|
|
473
602
|
'--squisq-theme-body-font': resolveFontFamily(
|
|
474
603
|
activeTheme.typography.bodyFont,
|
|
475
604
|
FONT_FALLBACKS.sans,
|
|
@@ -478,7 +607,7 @@ export function WysiwygEditor({
|
|
|
478
607
|
activeTheme.typography.titleFont,
|
|
479
608
|
FONT_FALLBACKS.sans,
|
|
480
609
|
),
|
|
481
|
-
};
|
|
610
|
+
});
|
|
482
611
|
if (themeInheritance === 'fonts-colors') {
|
|
483
612
|
const colors = activeTheme.colors;
|
|
484
613
|
out['--squisq-theme-bg'] = colors.background;
|
|
@@ -498,7 +627,7 @@ export function WysiwygEditor({
|
|
|
498
627
|
className={`squisq-wysiwyg-container${className ? ` ${className}` : ''}`}
|
|
499
628
|
style={{ width: '100%', height: '100%', overflow: 'auto', ...themeStyle }}
|
|
500
629
|
data-testid="wysiwyg-container"
|
|
501
|
-
data-block-tags={
|
|
630
|
+
data-block-tags={BLOCK_TAG_DATA_VALUES[blockTagVisibility]}
|
|
502
631
|
data-theme-inheritance={themeInheritance}
|
|
503
632
|
ref={containerRef}
|
|
504
633
|
>
|
|
@@ -507,6 +636,7 @@ export function WysiwygEditor({
|
|
|
507
636
|
<TemplateBadgePopover
|
|
508
637
|
anchorRect={badgeMenu.rect}
|
|
509
638
|
value={badgeMenu.template}
|
|
639
|
+
colorScheme={colorScheme}
|
|
510
640
|
recommended={(() => {
|
|
511
641
|
// `headingIndex` is counted within the mounted Tiptap doc, which
|
|
512
642
|
// reflects `editorSource` — the active block's slice in block
|
|
@@ -516,6 +646,7 @@ export function WysiwygEditor({
|
|
|
516
646
|
const profile = profileBlockContents(slice);
|
|
517
647
|
return recommendTemplatesForBlock(profile, TEMPLATE_NAMES).recommended;
|
|
518
648
|
})()}
|
|
649
|
+
previewSource={badgePreviewSource}
|
|
519
650
|
onOpenDesigner={() => {
|
|
520
651
|
setBadgeMenu(null);
|
|
521
652
|
setDesignerState({});
|
|
@@ -528,7 +659,7 @@ export function WysiwygEditor({
|
|
|
528
659
|
});
|
|
529
660
|
editor.view.dispatch(tr);
|
|
530
661
|
}}
|
|
531
|
-
onClose={
|
|
662
|
+
onClose={closeBadgeMenu}
|
|
532
663
|
/>
|
|
533
664
|
)}
|
|
534
665
|
{propsMenu && (
|
|
@@ -536,6 +667,8 @@ export function WysiwygEditor({
|
|
|
536
667
|
anchorRect={propsMenu.rect}
|
|
537
668
|
blockAttrs={propsMenu.blockAttrs}
|
|
538
669
|
templateParams={propsMenu.templateParams}
|
|
670
|
+
colorScheme={colorScheme}
|
|
671
|
+
accentColor={activeTheme?.colors.primary}
|
|
539
672
|
onChange={(nextInner) => {
|
|
540
673
|
if (!editor) return;
|
|
541
674
|
const current = editor.state.doc.nodeAt(propsMenu.headingPos);
|
|
@@ -546,6 +679,17 @@ export function WysiwygEditor({
|
|
|
546
679
|
});
|
|
547
680
|
editor.view.dispatch(tr);
|
|
548
681
|
}}
|
|
682
|
+
onAnnotationChange={(next) => {
|
|
683
|
+
if (!editor) return;
|
|
684
|
+
const current = editor.state.doc.nodeAt(propsMenu.headingPos);
|
|
685
|
+
if (!current || current.type.name !== 'heading') return;
|
|
686
|
+
const tr = editor.state.tr.setNodeMarkup(propsMenu.headingPos, undefined, {
|
|
687
|
+
...current.attrs,
|
|
688
|
+
dataBlockAttrs: next.blockAttrsInner,
|
|
689
|
+
dataTemplateParams: next.templateParams,
|
|
690
|
+
});
|
|
691
|
+
editor.view.dispatch(tr);
|
|
692
|
+
}}
|
|
549
693
|
onClose={() => setPropsMenu(null)}
|
|
550
694
|
/>
|
|
551
695
|
)}
|
|
@@ -555,6 +699,7 @@ export function WysiwygEditor({
|
|
|
555
699
|
onSave={handleDesignerSave}
|
|
556
700
|
onClose={() => setDesignerState(null)}
|
|
557
701
|
mediaProvider={mediaProvider}
|
|
702
|
+
colorScheme={colorScheme}
|
|
558
703
|
/>
|
|
559
704
|
)}
|
|
560
705
|
</div>
|
|
@@ -621,41 +766,8 @@ function filesFromClipboard(clipboard: DataTransfer): File[] {
|
|
|
621
766
|
return files;
|
|
622
767
|
}
|
|
623
768
|
|
|
624
|
-
/**
|
|
625
|
-
* Upload image files to the MediaProvider and insert <img> nodes at the
|
|
626
|
-
* current selection. Inserts a placeholder name when files lack one
|
|
627
|
-
* (e.g., screenshots from the system clipboard).
|
|
628
|
-
*/
|
|
629
|
-
async function uploadAndInsertImages(
|
|
630
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
631
|
-
view: any,
|
|
632
|
-
files: File[],
|
|
633
|
-
mediaProvider: import('@bendyline/squisq/schemas').MediaProvider,
|
|
634
|
-
): Promise<void> {
|
|
635
|
-
for (const file of files) {
|
|
636
|
-
try {
|
|
637
|
-
const buffer = await file.arrayBuffer();
|
|
638
|
-
const mimeType = file.type || 'image/png';
|
|
639
|
-
const name =
|
|
640
|
-
file.name && file.name !== 'image.png'
|
|
641
|
-
? file.name
|
|
642
|
-
: `pasted-${uniquePasteToken()}.${extFromMime(mimeType)}`;
|
|
643
|
-
const relativePath = await mediaProvider.addMedia(name, buffer, mimeType);
|
|
644
|
-
const altText = name.replace(/\.[^.]+$/, '').replace(/[-_]/g, ' ');
|
|
645
|
-
insertImageNode(view, relativePath, altText);
|
|
646
|
-
} catch (err) {
|
|
647
|
-
console.error('Failed to upload dropped image:', err);
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
|
|
652
769
|
/** Insert an image node at the current selection using the schema image type. */
|
|
653
|
-
function insertImageNode(
|
|
654
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
655
|
-
view: any,
|
|
656
|
-
src: string,
|
|
657
|
-
alt: string,
|
|
658
|
-
): void {
|
|
770
|
+
function insertImageNode(view: ImageMutationView, src: string, alt: string): void {
|
|
659
771
|
const { schema } = view.state;
|
|
660
772
|
const imageType = schema.nodes.image;
|
|
661
773
|
if (!imageType) return;
|
|
@@ -665,50 +777,13 @@ function insertImageNode(
|
|
|
665
777
|
}
|
|
666
778
|
|
|
667
779
|
/** Move the selection to the document position under the drop event's coordinates. */
|
|
668
|
-
function moveSelectionToDropPoint(
|
|
669
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
670
|
-
view: any,
|
|
671
|
-
event: DragEvent,
|
|
672
|
-
): void {
|
|
780
|
+
function moveSelectionToDropPoint(view: ProseMirrorView, event: DragEvent): void {
|
|
673
781
|
const coords = view.posAtCoords({ left: event.clientX, top: event.clientY });
|
|
674
782
|
if (!coords) return;
|
|
675
|
-
const tr = view.state.tr.setSelection(
|
|
676
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
677
|
-
(view.state.selection.constructor as any).near(view.state.doc.resolve(coords.pos)),
|
|
678
|
-
);
|
|
783
|
+
const tr = view.state.tr.setSelection(Selection.near(view.state.doc.resolve(coords.pos)));
|
|
679
784
|
view.dispatch(tr);
|
|
680
785
|
}
|
|
681
786
|
|
|
682
|
-
/**
|
|
683
|
-
* Produce a unique token for a pasted-file name. `Date.now()` alone can
|
|
684
|
-
* collide when a user pastes several clipboard images in the same tick
|
|
685
|
-
* (multi-image paste from a screenshot grid, for example), which would make
|
|
686
|
-
* `MediaProvider.addMedia` overwrite or reject later entries. Prefer
|
|
687
|
-
* `crypto.randomUUID()` when available and fall back to a counter so the
|
|
688
|
-
* helper stays pure-JS-everywhere.
|
|
689
|
-
*/
|
|
690
|
-
let pasteCounter = 0;
|
|
691
|
-
function uniquePasteToken(): string {
|
|
692
|
-
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
693
|
-
return crypto.randomUUID();
|
|
694
|
-
}
|
|
695
|
-
pasteCounter = (pasteCounter + 1) % 1_000_000;
|
|
696
|
-
return `${Date.now()}-${pasteCounter.toString(36)}`;
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
function extFromMime(mime: string): string {
|
|
700
|
-
const map: Record<string, string> = {
|
|
701
|
-
'image/png': 'png',
|
|
702
|
-
'image/jpeg': 'jpg',
|
|
703
|
-
'image/jpg': 'jpg',
|
|
704
|
-
'image/gif': 'gif',
|
|
705
|
-
'image/webp': 'webp',
|
|
706
|
-
'image/svg+xml': 'svg',
|
|
707
|
-
'image/avif': 'avif',
|
|
708
|
-
};
|
|
709
|
-
return map[mime.toLowerCase()] ?? 'png';
|
|
710
|
-
}
|
|
711
|
-
|
|
712
787
|
/**
|
|
713
788
|
* Hook to access the Tiptap editor instance for toolbar commands.
|
|
714
789
|
* The WysiwygEditor must be mounted as a sibling or descendant.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { BlockPropertiesPopover } from '../BlockPropertiesPopover';
|
|
4
|
+
import { TRANSITION_FLYOUT_PORTAL_ID } from '../TransitionPicker';
|
|
5
|
+
|
|
6
|
+
describe('BlockPropertiesPopover theme propagation', () => {
|
|
7
|
+
it('copies dark mode and the document accent onto both portal roots', async () => {
|
|
8
|
+
render(
|
|
9
|
+
<BlockPropertiesPopover
|
|
10
|
+
anchorRect={new DOMRect(20, 20, 20, 20)}
|
|
11
|
+
blockAttrs={null}
|
|
12
|
+
templateParams={null}
|
|
13
|
+
colorScheme="dark"
|
|
14
|
+
accentColor="#c2410c"
|
|
15
|
+
onChange={vi.fn()}
|
|
16
|
+
onAnnotationChange={vi.fn()}
|
|
17
|
+
onClose={vi.fn()}
|
|
18
|
+
/>,
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const panel = screen.getByRole('dialog', { name: 'Block properties' });
|
|
22
|
+
expect(panel.getAttribute('data-theme')).toBe('dark');
|
|
23
|
+
expect(panel.style.getPropertyValue('--squisq-block-props-accent')).toBe('#c2410c');
|
|
24
|
+
|
|
25
|
+
fireEvent.click(screen.getByRole('button', { name: /Transition:/ }));
|
|
26
|
+
|
|
27
|
+
await waitFor(() => {
|
|
28
|
+
const flyout = document.getElementById(TRANSITION_FLYOUT_PORTAL_ID);
|
|
29
|
+
expect(flyout?.getAttribute('data-theme')).toBe('dark');
|
|
30
|
+
expect(flyout?.style.getPropertyValue('--squisq-block-props-accent')).toBe('#c2410c');
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
});
|