@bendyline/squisq-editor-react 1.6.2 → 2.0.1
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 +1275 -183
- package/dist/index.js +20345 -12777
- 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 +1607 -241
- package/package.json +9 -5
- package/src/BlockPropertiesPopover.tsx +30 -15
- package/src/DocumentSettingsDialog.tsx +39 -33
- package/src/EditorContext.tsx +199 -42
- package/src/EditorShell.tsx +349 -261
- 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 +362 -89
- package/src/PreviewPanel.tsx +312 -78
- package/src/RawEditor.tsx +13 -2
- package/src/RecorderEntry.tsx +2 -0
- 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 +357 -51
- 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__/codeContextSectionView.test.tsx +8 -6
- package/src/__tests__/customLayoutManagerTheme.test.tsx +34 -0
- package/src/__tests__/documentSettingsDialog.test.tsx +51 -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__/mediaAttachmentFlow.test.ts +2 -2
- 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 +382 -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__/recorderTheme.test.tsx +42 -0
- package/src/__tests__/resolveBlockVisual.test.ts +43 -0
- package/src/__tests__/selectionConversions.test.ts +80 -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 +71 -7
- package/src/__tests__/tiptapCodeBlockRoundTrip.test.ts +109 -0
- package/src/__tests__/tiptapImageRoundTrip.test.ts +1 -1
- package/src/__tests__/toolbarSelectionConversion.test.tsx +164 -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 +254 -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 +284 -0
- package/src/asciiDiagram/__tests__/asciiDiagramOps.test.ts +114 -0
- package/src/asciiDiagram/__tests__/asciiPaste.test.ts +43 -0
- package/src/asciiDiagram/asciiDiagramCommands.ts +174 -0
- package/src/asciiDiagram/asciiDiagramData.ts +149 -0
- package/src/asciiDiagram/asciiDiagramOps.ts +238 -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/codeContext/types.ts +1 -1
- 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/__tests__/useMemoryLayerAdapter.test.ts +13 -0
- package/src/customTemplates/designer.css +158 -99
- package/src/customTemplates/library.ts +16 -3
- package/src/customTemplates/useMemoryLayerAdapter.ts +7 -1
- 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 +25 -6
- package/src/diagram/types.ts +35 -0
- package/src/editorScrollSync.ts +73 -0
- package/src/emojiData.ts +3 -23
- package/src/frontmatterSettings.ts +23 -0
- 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 +198 -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/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/RecorderButton.tsx +9 -1
- package/src/recorder/RecorderModal.tsx +84 -41
- package/src/recorder/RecorderPanel.tsx +9 -1
- 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 +119 -0
- package/src/scene/adapters/DiagramAdapter.ts +12 -101
- package/src/scene/adapters/DrawingAdapter.ts +6 -1
- package/src/scene/adapters/LayoutAdapter.ts +3 -0
- package/src/scene/commands/SceneCommand.ts +17 -3
- 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 +39 -23
- package/src/scene/tools/ShapeTool.ts +2 -3
- package/src/selectionConversions.ts +155 -0
- package/src/styles/ascii-diagram.css +79 -0
- package/src/styles/ascii-timeline.css +596 -0
- package/src/styles/editor.css +736 -120
- package/src/styles/index.css +3 -0
- package/src/styles/tree-view.css +189 -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 +861 -0
- package/src/timeline/TimelineViewExtension.ts +252 -0
- package/src/timeline/__tests__/TimelineEditorWidget.test.tsx +414 -0
- package/src/timeline/__tests__/TimelineViewExtension.test.ts +163 -0
- package/src/timeline/__tests__/timelineCommands.test.ts +359 -0
- package/src/timeline/__tests__/timelineOps.test.ts +277 -0
- package/src/timeline/__tests__/timelinePaste.test.ts +34 -0
- package/src/timeline/timelineCommands.ts +421 -0
- package/src/timeline/timelineData.ts +54 -0
- package/src/timeline/timelineOps.ts +381 -0
- package/src/timeline/timelinePaste.ts +8 -0
- package/src/tiptapBridge.ts +47 -21
- package/src/transformStyleId.ts +17 -0
- package/src/treeview/TreeOutlineWidget.tsx +390 -0
- package/src/treeview/TreeViewExtension.ts +250 -0
- package/src/treeview/__tests__/TreeOutlineWidget.test.tsx +156 -0
- package/src/treeview/__tests__/TreeViewExtension.test.ts +122 -0
- package/src/treeview/__tests__/treeOps.test.ts +177 -0
- package/src/treeview/__tests__/treePaste.test.ts +40 -0
- package/src/treeview/__tests__/treeViewCommands.test.ts +179 -0
- package/src/treeview/treeOps.ts +231 -0
- package/src/treeview/treePaste.ts +13 -0
- package/src/treeview/treeViewCommands.ts +99 -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
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { Block, Doc } from '@bendyline/squisq/schemas';
|
|
3
|
+
import { buildDocumentPreviewMarkdown } from '../buildDocumentPreviewMarkdown';
|
|
4
|
+
|
|
5
|
+
function docWith(blocks: Block[]): Doc {
|
|
6
|
+
return {
|
|
7
|
+
articleId: 'summary-doc',
|
|
8
|
+
duration: 10,
|
|
9
|
+
blocks,
|
|
10
|
+
audio: { segments: [] },
|
|
11
|
+
frontmatter: { title: 'Quarterly Report' },
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('buildDocumentPreviewMarkdown', () => {
|
|
16
|
+
it('serializes programmatic transform blocks as readable document content', () => {
|
|
17
|
+
const markdown = buildDocumentPreviewMarkdown(
|
|
18
|
+
docWith([
|
|
19
|
+
{
|
|
20
|
+
id: 'transform-stat',
|
|
21
|
+
template: 'statHighlight',
|
|
22
|
+
startTime: 0,
|
|
23
|
+
duration: 5,
|
|
24
|
+
audioSegment: 0,
|
|
25
|
+
stat: '42%',
|
|
26
|
+
description: 'Year-over-year growth',
|
|
27
|
+
} as Block,
|
|
28
|
+
{
|
|
29
|
+
id: 'transform-list',
|
|
30
|
+
template: 'list',
|
|
31
|
+
startTime: 5,
|
|
32
|
+
duration: 5,
|
|
33
|
+
audioSegment: 0,
|
|
34
|
+
title: 'Priorities',
|
|
35
|
+
items: ['Reliability', 'Speed'],
|
|
36
|
+
} as Block,
|
|
37
|
+
]),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
expect(markdown).toContain('# Quarterly Report');
|
|
41
|
+
expect(markdown).toContain('## 42%');
|
|
42
|
+
expect(markdown).toContain('Year-over-year growth');
|
|
43
|
+
expect(markdown).toContain('## Priorities');
|
|
44
|
+
expect(markdown).toContain('Reliability');
|
|
45
|
+
expect(markdown).toContain('Speed');
|
|
46
|
+
expect(markdown).not.toContain('{[statHighlight');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('preserves authored blocks that a conservative transform kept', () => {
|
|
50
|
+
const markdown = buildDocumentPreviewMarkdown(
|
|
51
|
+
docWith([
|
|
52
|
+
{
|
|
53
|
+
id: 'authored',
|
|
54
|
+
startTime: 0,
|
|
55
|
+
duration: 5,
|
|
56
|
+
audioSegment: 0,
|
|
57
|
+
sourceHeading: {
|
|
58
|
+
type: 'heading',
|
|
59
|
+
depth: 2,
|
|
60
|
+
children: [{ type: 'text', value: 'Context' }],
|
|
61
|
+
},
|
|
62
|
+
contents: [
|
|
63
|
+
{
|
|
64
|
+
type: 'paragraph',
|
|
65
|
+
children: [{ type: 'text', value: 'Original supporting detail.' }],
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
]),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
expect(markdown).toContain('## Context');
|
|
73
|
+
expect(markdown).toContain('Original supporting detail.');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { markdownToDoc, materializeBlockLayers } from '@bendyline/squisq/doc';
|
|
3
|
+
import { parseMarkdown } from '@bendyline/squisq/markdown';
|
|
4
|
+
import type { DocBlock, TextLayer } from '@bendyline/squisq/schemas';
|
|
5
|
+
import { buildPreviewDoc } from '../buildPreviewDoc';
|
|
6
|
+
|
|
7
|
+
function firstPreviewSlide(markdown: string): Record<string, unknown> {
|
|
8
|
+
const source = markdownToDoc(parseMarkdown(markdown), {
|
|
9
|
+
articleId: 'stat-content-test',
|
|
10
|
+
generateCoverBlock: false,
|
|
11
|
+
});
|
|
12
|
+
return buildPreviewDoc(source).blocks[0] as unknown as Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('buildPreviewDoc content mapping', () => {
|
|
16
|
+
it('maps the leading bold metric to the large stat layer', () => {
|
|
17
|
+
const slide = firstPreviewSlide(`### The Big Number {[statHighlight colorScheme=green]}
|
|
18
|
+
|
|
19
|
+
**42%** of teams prefer visual blocks over raw slides.`);
|
|
20
|
+
|
|
21
|
+
expect(slide).toMatchObject({
|
|
22
|
+
title: 'The Big Number',
|
|
23
|
+
stat: '42%',
|
|
24
|
+
description: 'of teams prefer visual blocks over raw slides.',
|
|
25
|
+
colorScheme: 'green',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const { layers } = materializeBlockLayers(slide as unknown as DocBlock);
|
|
29
|
+
const stat = layers.find((layer) => layer.id === 'stat') as TextLayer;
|
|
30
|
+
const description = layers.find((layer) => layer.id === 'description') as TextLayer;
|
|
31
|
+
|
|
32
|
+
expect(stat.content.text).toBe('42%');
|
|
33
|
+
expect(description.content.text).toBe('of teams prefer visual blocks over raw slides.');
|
|
34
|
+
expect(stat.content.style.fontSize).toBeGreaterThan(description.content.style.fontSize);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('lets explicit annotation inputs override Markdown-derived defaults', () => {
|
|
38
|
+
const slide = firstPreviewSlide(
|
|
39
|
+
'### The Big Number {[statHighlight stat="99%" description="Pinned description"]}\n\n' +
|
|
40
|
+
'**42%** derived description.',
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
expect(slide).toMatchObject({
|
|
44
|
+
stat: '99%',
|
|
45
|
+
description: 'Pinned description',
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { parseMarkdown } from '@bendyline/squisq/markdown';
|
|
3
3
|
import { markdownToDoc } from '@bendyline/squisq/doc';
|
|
4
|
-
import type { Transition } from '@bendyline/squisq/schemas';
|
|
4
|
+
import type { Doc, Transition } from '@bendyline/squisq/schemas';
|
|
5
5
|
import { buildPreviewDoc } from '../buildPreviewDoc';
|
|
6
6
|
|
|
7
7
|
function previewSlides(md: string) {
|
|
@@ -69,4 +69,24 @@ describe('buildPreviewDoc transition mapping', () => {
|
|
|
69
69
|
expect(slides[1].transition).toEqual({ type: 'push', direction: 'left', duration: 0.8 });
|
|
70
70
|
});
|
|
71
71
|
});
|
|
72
|
+
|
|
73
|
+
it('preserves document-wide fields and mapped audio', () => {
|
|
74
|
+
const source = markdownToDoc(parseMarkdown('# Intro'), { articleId: 'full' }) as Doc;
|
|
75
|
+
const enriched = {
|
|
76
|
+
...source,
|
|
77
|
+
frontmatter: { owner: 'team' },
|
|
78
|
+
customThemes: [{ id: 'custom-theme' }],
|
|
79
|
+
persistentLayers: { layers: [] },
|
|
80
|
+
documentMedia: [{ id: 'bed', kind: 'audio', src: 'bed.mp3', startTime: 0 }],
|
|
81
|
+
audio: {
|
|
82
|
+
segments: [{ src: 'narration.mp3', name: 'narration', duration: 4, startTime: 0 }],
|
|
83
|
+
},
|
|
84
|
+
} as unknown as Doc;
|
|
85
|
+
const preview = buildPreviewDoc(enriched);
|
|
86
|
+
expect(preview.frontmatter).toEqual({ owner: 'team' });
|
|
87
|
+
expect(preview.customThemes).toBe(enriched.customThemes);
|
|
88
|
+
expect(preview.persistentLayers).toBe(enriched.persistentLayers);
|
|
89
|
+
expect(preview.documentMedia).toBe(enriched.documentMedia);
|
|
90
|
+
expect(preview.audio).toBe(enriched.audio);
|
|
91
|
+
});
|
|
72
92
|
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
6
|
+
import type { ImageEditDoc } from '@bendyline/squisq/schemas';
|
|
7
|
+
import { CanvasSurface } from '../imageEditor/CanvasSurface.js';
|
|
8
|
+
import type { ImageEditorAction } from '../imageEditor/state.js';
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
vi.restoreAllMocks();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('CanvasSurface text editing', () => {
|
|
15
|
+
it('uses one tightly measured edit box and applies SVG zoom only once', async () => {
|
|
16
|
+
const context = {
|
|
17
|
+
font: '',
|
|
18
|
+
measureText: (text: string) => ({ width: text.length * 10 }),
|
|
19
|
+
} as unknown as CanvasRenderingContext2D;
|
|
20
|
+
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockImplementation(() => context);
|
|
21
|
+
|
|
22
|
+
const doc: ImageEditDoc = {
|
|
23
|
+
version: 1,
|
|
24
|
+
canvas: { width: 640, height: 480 },
|
|
25
|
+
layers: [
|
|
26
|
+
{
|
|
27
|
+
id: 'text-1',
|
|
28
|
+
type: 'text',
|
|
29
|
+
name: 'Text',
|
|
30
|
+
position: { x: 100, y: 80, width: 240, height: 48 },
|
|
31
|
+
content: {
|
|
32
|
+
text: 'hello',
|
|
33
|
+
style: { fontSize: 20, lineHeight: 1.4, color: '#111111' },
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const { container } = render(
|
|
40
|
+
<CanvasSurface
|
|
41
|
+
doc={doc}
|
|
42
|
+
selectedLayerId="text-1"
|
|
43
|
+
tool="select"
|
|
44
|
+
resolveAssetUrl={async (path) => path}
|
|
45
|
+
dispatch={vi.fn<(action: ImageEditorAction) => void>()}
|
|
46
|
+
requestEditLayerId="text-1"
|
|
47
|
+
zoom={2}
|
|
48
|
+
/>,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const textarea = (await screen.findByRole('textbox')) as HTMLTextAreaElement;
|
|
52
|
+
const editBox = container.querySelector('foreignObject');
|
|
53
|
+
|
|
54
|
+
expect(editBox?.getAttribute('width')).toBe('50');
|
|
55
|
+
expect(textarea.style.fontSize).toBe('20px');
|
|
56
|
+
await waitFor(() => {
|
|
57
|
+
expect(container.querySelector('.squisq-image-editor-selection-handles')).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -10,7 +10,7 @@ const noop = () => {};
|
|
|
10
10
|
const baseSection = {
|
|
11
11
|
id: 'foo@10',
|
|
12
12
|
summaryMarkdown: '**foo** — does things · ↓2 imported-by',
|
|
13
|
-
markdown: 'Body text with [`a.ts`](
|
|
13
|
+
markdown: 'Body text with [`a.ts`](workspace-nav:src%2Fa.ts) and [line 4](#L4).',
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
function renderView(over: Partial<Parameters<typeof CodeContextSectionView>[0]> = {}) {
|
|
@@ -18,7 +18,7 @@ function renderView(over: Partial<Parameters<typeof CodeContextSectionView>[0]>
|
|
|
18
18
|
section: baseSection,
|
|
19
19
|
expanded: false,
|
|
20
20
|
onToggle: vi.fn(),
|
|
21
|
-
linkSchemes: ['
|
|
21
|
+
linkSchemes: ['workspace-nav'] as const,
|
|
22
22
|
onLinkClick: vi.fn(),
|
|
23
23
|
onRevealLine: vi.fn(),
|
|
24
24
|
onMeasure: noop,
|
|
@@ -47,7 +47,7 @@ describe('<CodeContextSectionView>', () => {
|
|
|
47
47
|
const body = container.querySelector('.squisq-ccx-body')!;
|
|
48
48
|
expect(body.textContent).toContain('Body text');
|
|
49
49
|
const anchors = [...body.querySelectorAll('a')].map((a) => a.getAttribute('href'));
|
|
50
|
-
expect(anchors).toContain('
|
|
50
|
+
expect(anchors).toContain('workspace-nav:src%2Fa.ts');
|
|
51
51
|
expect(anchors).toContain('#L4');
|
|
52
52
|
});
|
|
53
53
|
|
|
@@ -63,10 +63,12 @@ describe('<CodeContextSectionView>', () => {
|
|
|
63
63
|
const onLinkClick = vi.fn(() => undefined);
|
|
64
64
|
const { container } = renderView({ expanded: true, onLinkClick });
|
|
65
65
|
const nav = [...container.querySelectorAll('a')].find(
|
|
66
|
-
(a) => a.getAttribute('href') === '
|
|
66
|
+
(a) => a.getAttribute('href') === 'workspace-nav:src%2Fa.ts',
|
|
67
67
|
)!;
|
|
68
68
|
const first = fireEvent.click(nav);
|
|
69
|
-
expect(onLinkClick).toHaveBeenCalledWith('
|
|
69
|
+
expect(onLinkClick).toHaveBeenCalledWith('workspace-nav:src%2Fa.ts', {
|
|
70
|
+
sectionId: 'foo@10',
|
|
71
|
+
});
|
|
70
72
|
expect(first).toBe(false); // preventDefault was called
|
|
71
73
|
|
|
72
74
|
onLinkClick.mockReturnValue(false as unknown as undefined);
|
|
@@ -89,7 +91,7 @@ describe('<CodeContextSectionView>', () => {
|
|
|
89
91
|
it('without linkSchemes, custom-scheme links render blocked (no anchor)', () => {
|
|
90
92
|
const { container } = renderView({ expanded: true, linkSchemes: undefined });
|
|
91
93
|
const anchors = [...container.querySelectorAll('a')].map((a) => a.getAttribute('href'));
|
|
92
|
-
expect(anchors).not.toContain('
|
|
94
|
+
expect(anchors).not.toContain('workspace-nav:src%2Fa.ts');
|
|
93
95
|
expect(container.querySelector('.squisq-md-link--blocked')).toBeTruthy();
|
|
94
96
|
});
|
|
95
97
|
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import { render, screen } from '@testing-library/react';
|
|
5
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
6
|
+
|
|
7
|
+
vi.mock('../EditorContext', () => ({
|
|
8
|
+
useEditorContext: () => ({
|
|
9
|
+
colorScheme: 'dark',
|
|
10
|
+
mediaProvider: null,
|
|
11
|
+
doc: null,
|
|
12
|
+
markdownSource: '',
|
|
13
|
+
setMarkdownSource: vi.fn(),
|
|
14
|
+
}),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
// The regression is at the manager's portaled theme boundary; the designer's
|
|
18
|
+
// interaction surface is unrelated and expensive to mount for this assertion.
|
|
19
|
+
vi.mock('../customTemplates/TemplateDesigner', () => ({
|
|
20
|
+
TemplateDesigner: () => <div data-testid="template-designer" />,
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
import { CustomLayoutManager } from '../customTemplates/CustomLayoutManager';
|
|
24
|
+
|
|
25
|
+
describe('CustomLayoutManager theme propagation', () => {
|
|
26
|
+
it('copies the editor dark scheme onto its portaled theme scope', () => {
|
|
27
|
+
render(<CustomLayoutManager onClose={vi.fn()} />);
|
|
28
|
+
|
|
29
|
+
const dialog = screen.getByRole('dialog', { name: 'Custom layouts' });
|
|
30
|
+
expect(dialog.getAttribute('data-theme')).toBe('dark');
|
|
31
|
+
expect(dialog.classList.contains('squisq-editor-shell')).toBe(true);
|
|
32
|
+
expect(screen.getByTestId('template-designer')).toBeTruthy();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -26,7 +26,7 @@ function pickTheme(name: string) {
|
|
|
26
26
|
// Scope the option lookup to the popover — the Transform <select> in
|
|
27
27
|
// the same dialog also exposes role="option" entries (one of which is
|
|
28
28
|
// literally "Documentary"), so a global query would collide.
|
|
29
|
-
const popover = document.
|
|
29
|
+
const popover = document.querySelector<HTMLElement>('.squisq-theme-picker-popover')!;
|
|
30
30
|
fireEvent.click(within(popover).getByRole('option', { name }));
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -99,6 +99,20 @@ describe('DocumentSettingsDialog', () => {
|
|
|
99
99
|
expect(next).not.toMatch(/^theme:/m);
|
|
100
100
|
});
|
|
101
101
|
|
|
102
|
+
it('reads themeId and canonicalizes both legacy theme keys on save', () => {
|
|
103
|
+
const onSave = vi.fn();
|
|
104
|
+
const src = '---\nthemeId: bold\ntheme: cinematic\n---\n\n# Doc\n';
|
|
105
|
+
open(src, onSave);
|
|
106
|
+
|
|
107
|
+
expect(screen.getByLabelText('Theme').textContent).toContain('Bold');
|
|
108
|
+
clickSave();
|
|
109
|
+
|
|
110
|
+
const next = onSave.mock.calls[0][0] as string;
|
|
111
|
+
expect(next).toContain('squisq-theme: bold');
|
|
112
|
+
expect(next).not.toMatch(/^themeId:/m);
|
|
113
|
+
expect(next).not.toMatch(/^theme:/m);
|
|
114
|
+
});
|
|
115
|
+
|
|
102
116
|
it('removes squisq-theme when reverting to default', () => {
|
|
103
117
|
const onSave = vi.fn();
|
|
104
118
|
const src = '---\nsquisq-theme: documentary\n---\n\n# Doc\n';
|
|
@@ -109,6 +123,28 @@ describe('DocumentSettingsDialog', () => {
|
|
|
109
123
|
expect(next).not.toContain('squisq-theme');
|
|
110
124
|
});
|
|
111
125
|
|
|
126
|
+
it('removes explicit managed defaults and their legacy aliases on save', () => {
|
|
127
|
+
const onSave = vi.fn();
|
|
128
|
+
const src = `---
|
|
129
|
+
squisq-theme: standard
|
|
130
|
+
themeId: standard
|
|
131
|
+
theme: standard
|
|
132
|
+
squisq-captions: standard
|
|
133
|
+
caption-style: standard
|
|
134
|
+
author: Keep
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
# Doc
|
|
138
|
+
`;
|
|
139
|
+
open(src, onSave);
|
|
140
|
+
clickSave();
|
|
141
|
+
|
|
142
|
+
const next = onSave.mock.calls[0][0] as string;
|
|
143
|
+
expect(next).not.toMatch(/^(?:squisq-theme|themeId|theme):/m);
|
|
144
|
+
expect(next).not.toMatch(/^(?:squisq-captions|caption-style):/m);
|
|
145
|
+
expect(next).toContain('author: Keep');
|
|
146
|
+
});
|
|
147
|
+
|
|
112
148
|
it('writes squisq-transform when a transform is picked', () => {
|
|
113
149
|
const onSave = vi.fn();
|
|
114
150
|
open('# Doc\n', onSave);
|
|
@@ -122,6 +158,20 @@ describe('DocumentSettingsDialog', () => {
|
|
|
122
158
|
expect(next).toContain(`squisq-transform: ${first.value}`);
|
|
123
159
|
});
|
|
124
160
|
|
|
161
|
+
it('canonicalizes a stored dataDriven transform id', () => {
|
|
162
|
+
const onSave = vi.fn();
|
|
163
|
+
open('---\ntransform-style: dataDriven\n---\n\n# Doc\n', onSave);
|
|
164
|
+
|
|
165
|
+
const select = screen.getByLabelText('Transform') as HTMLSelectElement;
|
|
166
|
+
expect(select.value).toBe('data-driven');
|
|
167
|
+
clickSave();
|
|
168
|
+
|
|
169
|
+
const next = onSave.mock.calls[0][0] as string;
|
|
170
|
+
expect(next).toContain('squisq-transform: data-driven');
|
|
171
|
+
expect(next).not.toContain('dataDriven');
|
|
172
|
+
expect(next).not.toContain('transform-style');
|
|
173
|
+
});
|
|
174
|
+
|
|
125
175
|
it('writes squisq-captions when picked', () => {
|
|
126
176
|
const onSave = vi.fn();
|
|
127
177
|
open('# Doc\n', onSave);
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import { act, cleanup, render } from '@testing-library/react';
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import { EditorProvider, useEditorContext, type EditorContextValue } from '../EditorContext';
|
|
7
|
+
|
|
8
|
+
let currentContext: EditorContextValue | null = null;
|
|
9
|
+
let animationFrames: FrameRequestCallback[] = [];
|
|
10
|
+
|
|
11
|
+
function ContextProbe() {
|
|
12
|
+
currentContext = useEditorContext();
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function context(): EditorContextValue {
|
|
17
|
+
if (!currentContext) throw new Error('EditorContext has not mounted');
|
|
18
|
+
return currentContext;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function wysiwygEditor(
|
|
22
|
+
scrollTop: number,
|
|
23
|
+
scrollHeight: number,
|
|
24
|
+
clientHeight: number,
|
|
25
|
+
): { editor: unknown; container: HTMLElement } {
|
|
26
|
+
const container = document.createElement('div');
|
|
27
|
+
container.className = 'squisq-wysiwyg-container';
|
|
28
|
+
Object.defineProperties(container, {
|
|
29
|
+
scrollTop: { value: scrollTop, writable: true },
|
|
30
|
+
scrollHeight: { value: scrollHeight },
|
|
31
|
+
clientHeight: { value: clientHeight },
|
|
32
|
+
});
|
|
33
|
+
const dom = document.createElement('div');
|
|
34
|
+
container.append(dom);
|
|
35
|
+
return { editor: { view: { dom } }, container };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function flushAnimationFrame(): void {
|
|
39
|
+
const callback = animationFrames.shift();
|
|
40
|
+
if (callback) act(() => callback(0));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
currentContext = null;
|
|
45
|
+
animationFrames = [];
|
|
46
|
+
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => {
|
|
47
|
+
animationFrames.push(callback);
|
|
48
|
+
return animationFrames.length;
|
|
49
|
+
});
|
|
50
|
+
vi.stubGlobal('cancelAnimationFrame', vi.fn());
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
cleanup();
|
|
55
|
+
vi.unstubAllGlobals();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('EditorProvider scroll handoff', () => {
|
|
59
|
+
it('restores proportional positions from Write to Source and back', () => {
|
|
60
|
+
render(
|
|
61
|
+
<EditorProvider initialMarkdown="# One\n\nBody" initialView="wysiwyg">
|
|
62
|
+
<ContextProbe />
|
|
63
|
+
</EditorProvider>,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const write = wysiwygEditor(300, 1100, 100);
|
|
67
|
+
act(() => context().setTiptapEditor(write.editor as never));
|
|
68
|
+
|
|
69
|
+
act(() => {
|
|
70
|
+
context().setActiveView('raw');
|
|
71
|
+
context().setTiptapEditor(null);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
let sourceScrollTop = 1500;
|
|
75
|
+
const setSourceScrollTop = vi.fn();
|
|
76
|
+
const source = {
|
|
77
|
+
getScrollTop: () => sourceScrollTop,
|
|
78
|
+
getScrollHeight: () => 2100,
|
|
79
|
+
getLayoutInfo: () => ({ height: 100 }),
|
|
80
|
+
setScrollTop: setSourceScrollTop,
|
|
81
|
+
};
|
|
82
|
+
act(() => context().setMonacoEditor(source as never));
|
|
83
|
+
flushAnimationFrame();
|
|
84
|
+
|
|
85
|
+
// Write was 30% through its scrollable range, so Source lands at 30%.
|
|
86
|
+
expect(setSourceScrollTop).toHaveBeenCalledWith(600);
|
|
87
|
+
|
|
88
|
+
// A real Monaco instance reports the new position after setScrollTop.
|
|
89
|
+
sourceScrollTop = 1500;
|
|
90
|
+
act(() => {
|
|
91
|
+
context().setActiveView('wysiwyg');
|
|
92
|
+
context().setMonacoEditor(null);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const nextWrite = wysiwygEditor(0, 1300, 100);
|
|
96
|
+
act(() => context().setTiptapEditor(nextWrite.editor as never));
|
|
97
|
+
flushAnimationFrame();
|
|
98
|
+
|
|
99
|
+
// Source was 75% through; the newly mounted Write surface lands at 75%.
|
|
100
|
+
expect(nextWrite.container.scrollTop).toBe(900);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
readMonacoScrollRatio,
|
|
4
|
+
readWysiwygScrollRatio,
|
|
5
|
+
restoreMonacoScrollRatio,
|
|
6
|
+
restoreWysiwygScrollRatio,
|
|
7
|
+
scrollRatio,
|
|
8
|
+
scrollTopForRatio,
|
|
9
|
+
} from '../editorScrollSync';
|
|
10
|
+
|
|
11
|
+
function scrollElement(scrollTop: number, scrollHeight: number, clientHeight: number): HTMLElement {
|
|
12
|
+
const container = document.createElement('div');
|
|
13
|
+
container.className = 'squisq-wysiwyg-container';
|
|
14
|
+
Object.defineProperties(container, {
|
|
15
|
+
scrollTop: { value: scrollTop, writable: true },
|
|
16
|
+
scrollHeight: { value: scrollHeight },
|
|
17
|
+
clientHeight: { value: clientHeight },
|
|
18
|
+
});
|
|
19
|
+
return container;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('editor scroll synchronization', () => {
|
|
23
|
+
it('converts pixel offsets to a normalized position and back', () => {
|
|
24
|
+
expect(scrollRatio(450, 1000, 100)).toBe(0.5);
|
|
25
|
+
expect(scrollTopForRatio(0.5, 2100, 100)).toBe(1000);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('clamps positions and handles documents that do not scroll', () => {
|
|
29
|
+
expect(scrollRatio(-20, 1000, 100)).toBe(0);
|
|
30
|
+
expect(scrollRatio(1200, 1000, 100)).toBe(1);
|
|
31
|
+
expect(scrollRatio(20, 100, 100)).toBe(0);
|
|
32
|
+
expect(scrollTopForRatio(Number.NaN, 1000, 100)).toBe(0);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('reads and restores Monaco using its scrollable height', () => {
|
|
36
|
+
const setScrollTop = vi.fn();
|
|
37
|
+
const editor = {
|
|
38
|
+
getScrollTop: () => 450,
|
|
39
|
+
getScrollHeight: () => 1000,
|
|
40
|
+
getLayoutInfo: () => ({ height: 100 }),
|
|
41
|
+
setScrollTop,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
expect(readMonacoScrollRatio(editor)).toBe(0.5);
|
|
45
|
+
restoreMonacoScrollRatio(editor, 0.25);
|
|
46
|
+
expect(setScrollTop).toHaveBeenCalledWith(225);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('reads and restores the WYSIWYG scroll container', () => {
|
|
50
|
+
const container = scrollElement(300, 1100, 100);
|
|
51
|
+
const editorDom = document.createElement('div');
|
|
52
|
+
container.append(editorDom);
|
|
53
|
+
const editor = { view: { dom: editorDom } };
|
|
54
|
+
|
|
55
|
+
expect(readWysiwygScrollRatio(editor)).toBe(0.3);
|
|
56
|
+
expect(restoreWysiwygScrollRatio(editor, 0.8)).toBe(true);
|
|
57
|
+
expect(container.scrollTop).toBe(800);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('declines WYSIWYG restoration when the editor is detached', () => {
|
|
61
|
+
const editor = { view: { dom: document.createElement('div') } };
|
|
62
|
+
expect(readWysiwygScrollRatio(editor)).toBeNull();
|
|
63
|
+
expect(restoreWysiwygScrollRatio(editor, 0.5)).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
});
|