@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
|
@@ -13,8 +13,10 @@
|
|
|
13
13
|
* the props it receives so we can assert `monacoTheme` reaches it.
|
|
14
14
|
*/
|
|
15
15
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
16
|
-
import { render, screen } from '@testing-library/react';
|
|
16
|
+
import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
|
17
17
|
import type { RawEditorProps } from '../RawEditor';
|
|
18
|
+
import type { MediaEntry, MediaProvider } from '@bendyline/squisq/schemas';
|
|
19
|
+
import { useMediaProvider } from '@bendyline/squisq-react';
|
|
18
20
|
|
|
19
21
|
// Records the props the shell passes to RawEditor on each render.
|
|
20
22
|
const rawEditorProps: RawEditorProps[] = [];
|
|
@@ -33,6 +35,101 @@ vi.mock('../PreviewPanel', () => ({
|
|
|
33
35
|
}));
|
|
34
36
|
|
|
35
37
|
import { EditorShell } from '../EditorShell';
|
|
38
|
+
import { EditorProvider, useEditorContext } from '../EditorContext';
|
|
39
|
+
import { Toolbar } from '../Toolbar';
|
|
40
|
+
|
|
41
|
+
function MarkdownSourceProbe() {
|
|
42
|
+
const { markdownSource } = useEditorContext();
|
|
43
|
+
return <pre data-testid="markdown-source">{markdownSource}</pre>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function MediaProviderProbe({ expected }: { expected: MediaProvider }) {
|
|
47
|
+
const provider = useMediaProvider();
|
|
48
|
+
return (
|
|
49
|
+
<span data-testid="media-context-probe">{provider === expected ? 'same' : 'missing'}</span>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function mediaProviderWith(count: number): MediaProvider {
|
|
54
|
+
const entries: MediaEntry[] = Array.from({ length: count }, (_, i) => ({
|
|
55
|
+
name: `file-${i + 1}.png`,
|
|
56
|
+
mimeType: 'image/png',
|
|
57
|
+
size: i + 1,
|
|
58
|
+
}));
|
|
59
|
+
return {
|
|
60
|
+
async addMedia(name: string) {
|
|
61
|
+
return name;
|
|
62
|
+
},
|
|
63
|
+
async resolveUrl(relPath: string) {
|
|
64
|
+
return relPath;
|
|
65
|
+
},
|
|
66
|
+
async listMedia() {
|
|
67
|
+
return entries;
|
|
68
|
+
},
|
|
69
|
+
async removeMedia() {
|
|
70
|
+
/* no-op */
|
|
71
|
+
},
|
|
72
|
+
dispose() {
|
|
73
|
+
/* no-op */
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function mediaProviderWithCounts(counts: number[]): MediaProvider {
|
|
79
|
+
let callCount = 0;
|
|
80
|
+
return {
|
|
81
|
+
async addMedia(name: string) {
|
|
82
|
+
return name;
|
|
83
|
+
},
|
|
84
|
+
async resolveUrl(relPath: string) {
|
|
85
|
+
return relPath;
|
|
86
|
+
},
|
|
87
|
+
async listMedia() {
|
|
88
|
+
const count = counts[Math.min(callCount, counts.length - 1)] ?? 0;
|
|
89
|
+
callCount += 1;
|
|
90
|
+
return Array.from({ length: count }, (_, i) => ({
|
|
91
|
+
name: `file-${i + 1}.png`,
|
|
92
|
+
mimeType: 'image/png',
|
|
93
|
+
size: i + 1,
|
|
94
|
+
}));
|
|
95
|
+
},
|
|
96
|
+
async removeMedia() {
|
|
97
|
+
/* no-op */
|
|
98
|
+
},
|
|
99
|
+
dispose() {
|
|
100
|
+
/* no-op */
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function mutableMediaProviderWith(entries: MediaEntry[]): {
|
|
106
|
+
provider: MediaProvider;
|
|
107
|
+
removed: string[];
|
|
108
|
+
} {
|
|
109
|
+
let current = [...entries];
|
|
110
|
+
const removed: string[] = [];
|
|
111
|
+
return {
|
|
112
|
+
removed,
|
|
113
|
+
provider: {
|
|
114
|
+
async addMedia(name: string) {
|
|
115
|
+
return name;
|
|
116
|
+
},
|
|
117
|
+
async resolveUrl(relPath: string) {
|
|
118
|
+
return relPath;
|
|
119
|
+
},
|
|
120
|
+
async listMedia() {
|
|
121
|
+
return current;
|
|
122
|
+
},
|
|
123
|
+
async removeMedia(relPath: string) {
|
|
124
|
+
removed.push(relPath);
|
|
125
|
+
current = current.filter((entry) => entry.name !== relPath);
|
|
126
|
+
},
|
|
127
|
+
dispose() {
|
|
128
|
+
/* no-op */
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
36
133
|
|
|
37
134
|
beforeEach(() => {
|
|
38
135
|
rawEditorProps.length = 0;
|
|
@@ -94,3 +191,295 @@ describe('RawEditor monacoTheme prop', () => {
|
|
|
94
191
|
expect(last?.monacoTheme).toBe('vs');
|
|
95
192
|
});
|
|
96
193
|
});
|
|
194
|
+
|
|
195
|
+
describe('<EditorShell> instance boundaries', () => {
|
|
196
|
+
it('provides its mediaProvider to React preview/layer consumers', async () => {
|
|
197
|
+
const provider = mediaProviderWith(0);
|
|
198
|
+
await act(async () => {
|
|
199
|
+
render(
|
|
200
|
+
<EditorShell
|
|
201
|
+
initialMarkdown="# hi"
|
|
202
|
+
initialView="raw"
|
|
203
|
+
mediaProvider={provider}
|
|
204
|
+
toolbarSlotLeft={<MediaProviderProbe expected={provider} />}
|
|
205
|
+
/>,
|
|
206
|
+
);
|
|
207
|
+
});
|
|
208
|
+
expect(screen.getByTestId('media-context-probe').textContent).toBe('same');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('keeps view shortcuts inside the editor that received the key', async () => {
|
|
212
|
+
const { container } = render(
|
|
213
|
+
<>
|
|
214
|
+
<EditorShell initialMarkdown="# first" initialView="raw" />
|
|
215
|
+
<EditorShell initialMarkdown="# second" initialView="raw" />
|
|
216
|
+
</>,
|
|
217
|
+
);
|
|
218
|
+
const shells = container.querySelectorAll<HTMLElement>('.squisq-editor-shell');
|
|
219
|
+
fireEvent.keyDown(within(shells[1]).getByTestId('raw-editor-stub'), {
|
|
220
|
+
key: '1',
|
|
221
|
+
ctrlKey: true,
|
|
222
|
+
});
|
|
223
|
+
await waitFor(() => expect(within(shells[1]).getByTestId('wysiwyg-editor-stub')).toBeTruthy());
|
|
224
|
+
expect(within(shells[0]).getByTestId('raw-editor-stub')).toBeTruthy();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('ignores file drops in readOnly mode', async () => {
|
|
228
|
+
const { container } = render(
|
|
229
|
+
<EditorShell
|
|
230
|
+
initialMarkdown="# original"
|
|
231
|
+
initialView="raw"
|
|
232
|
+
readOnly
|
|
233
|
+
toolbarSlotRight={<MarkdownSourceProbe />}
|
|
234
|
+
/>,
|
|
235
|
+
);
|
|
236
|
+
const shell = container.querySelector<HTMLElement>('.squisq-editor-shell')!;
|
|
237
|
+
const file = new File(['# replacement'], 'replacement.md', { type: 'text/markdown' });
|
|
238
|
+
fireEvent.drop(shell, {
|
|
239
|
+
dataTransfer: {
|
|
240
|
+
types: ['Files'],
|
|
241
|
+
files: [file],
|
|
242
|
+
items: [{ kind: 'file', type: 'text/markdown', getAsFile: () => file }],
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
await act(async () => Promise.resolve());
|
|
246
|
+
expect(screen.getByTestId('markdown-source').textContent).toBe('# original');
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe('<EditorShell> Files badge', () => {
|
|
251
|
+
it('shows the mediaProvider file count on the paperclip button', async () => {
|
|
252
|
+
const { container } = render(
|
|
253
|
+
<EditorShell initialMarkdown="# hi" initialView="raw" mediaProvider={mediaProviderWith(3)} />,
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
await waitFor(() => {
|
|
257
|
+
expect(screen.getByLabelText('Toggle Files panel, 3 files')).toBeTruthy();
|
|
258
|
+
});
|
|
259
|
+
expect(container.querySelector('.squisq-toolbar-files-badge')?.textContent).toBe('3');
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('compacts large visible counts while keeping the full count in the label', async () => {
|
|
263
|
+
const { container } = render(
|
|
264
|
+
<EditorShell
|
|
265
|
+
initialMarkdown="# hi"
|
|
266
|
+
initialView="raw"
|
|
267
|
+
mediaProvider={mediaProviderWith(120)}
|
|
268
|
+
/>,
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
await waitFor(() => {
|
|
272
|
+
expect(screen.getByLabelText('Toggle Files panel, 120 files')).toBeTruthy();
|
|
273
|
+
});
|
|
274
|
+
expect(container.querySelector('.squisq-toolbar-files-badge')?.textContent).toBe('99+');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('uses the MediaBin scan count when the panel discovers files', async () => {
|
|
278
|
+
const { container } = render(
|
|
279
|
+
<EditorShell
|
|
280
|
+
initialMarkdown="# hi"
|
|
281
|
+
initialView="raw"
|
|
282
|
+
mediaProvider={mediaProviderWithCounts([0, 1])}
|
|
283
|
+
/>,
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
await waitFor(() => {
|
|
287
|
+
expect(screen.getByLabelText('Toggle Files panel')).toBeTruthy();
|
|
288
|
+
});
|
|
289
|
+
fireEvent.click(screen.getByLabelText('Toggle Files panel'));
|
|
290
|
+
|
|
291
|
+
await waitFor(() => {
|
|
292
|
+
expect(screen.getByLabelText('Toggle Files panel, 1 file')).toBeTruthy();
|
|
293
|
+
});
|
|
294
|
+
expect(container.querySelector('.squisq-toolbar-files-badge')?.textContent).toBe('1');
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('hides .gitignore entries from the Files panel and its count', async () => {
|
|
298
|
+
const { provider } = mutableMediaProviderWith([
|
|
299
|
+
{ name: '.gitignore', mimeType: 'text/plain', size: 11 },
|
|
300
|
+
{ name: 'notes_files/.gitignore', mimeType: 'text/plain', size: 11 },
|
|
301
|
+
{ name: 'notes_files/hero.png', mimeType: 'image/png', size: 1024 },
|
|
302
|
+
]);
|
|
303
|
+
|
|
304
|
+
render(<EditorShell initialMarkdown="# hi" initialView="raw" mediaProvider={provider} />);
|
|
305
|
+
|
|
306
|
+
await waitFor(() => {
|
|
307
|
+
expect(screen.getByLabelText('Toggle Files panel, 1 file')).toBeTruthy();
|
|
308
|
+
});
|
|
309
|
+
fireEvent.click(screen.getByLabelText('Toggle Files panel, 1 file'));
|
|
310
|
+
|
|
311
|
+
expect(await screen.findByText('hero.png')).toBeTruthy();
|
|
312
|
+
expect(screen.queryByText('.gitignore')).toBeNull();
|
|
313
|
+
expect(screen.getByText('Files (1)')).toBeTruthy();
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('removes a media file and matching markdown refs from the files context menu', async () => {
|
|
317
|
+
const { provider, removed } = mutableMediaProviderWith([
|
|
318
|
+
{
|
|
319
|
+
name: 'attachments/pasted.png',
|
|
320
|
+
mimeType: 'image/png',
|
|
321
|
+
size: 1024,
|
|
322
|
+
},
|
|
323
|
+
]);
|
|
324
|
+
const changes: string[] = [];
|
|
325
|
+
const initialMarkdown = [
|
|
326
|
+
'Before',
|
|
327
|
+
'',
|
|
328
|
+
'',
|
|
329
|
+
'',
|
|
330
|
+
'After',
|
|
331
|
+
].join('\n');
|
|
332
|
+
|
|
333
|
+
render(
|
|
334
|
+
<EditorShell
|
|
335
|
+
initialMarkdown={initialMarkdown}
|
|
336
|
+
initialView="raw"
|
|
337
|
+
mediaProvider={provider}
|
|
338
|
+
onChange={(source) => changes.push(source)}
|
|
339
|
+
/>,
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
await waitFor(() => {
|
|
343
|
+
expect(screen.getByLabelText('Toggle Files panel, 1 file')).toBeTruthy();
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
fireEvent.click(screen.getByLabelText('Toggle Files panel, 1 file'));
|
|
347
|
+
const item = (await screen.findByText('pasted.png')).closest('.squisq-media-bin-item');
|
|
348
|
+
expect(item).toBeTruthy();
|
|
349
|
+
|
|
350
|
+
fireEvent.contextMenu(item as HTMLElement, { clientX: 24, clientY: 32 });
|
|
351
|
+
fireEvent.click(screen.getByRole('menuitem', { name: 'Remove image' }));
|
|
352
|
+
|
|
353
|
+
await waitFor(() => {
|
|
354
|
+
expect(removed).toEqual(['attachments/pasted.png']);
|
|
355
|
+
});
|
|
356
|
+
await waitFor(() => {
|
|
357
|
+
expect(screen.queryByText('pasted.png')).toBeNull();
|
|
358
|
+
});
|
|
359
|
+
await waitFor(() => {
|
|
360
|
+
expect(changes[changes.length - 1]).toBe(['Before', '', '', 'After'].join('\n'));
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('marks files as unused when the document does not reference them', async () => {
|
|
365
|
+
const { container } = render(
|
|
366
|
+
<EditorShell
|
|
367
|
+
initialMarkdown={'\n\n# Notes'}
|
|
368
|
+
initialView="raw"
|
|
369
|
+
mediaProvider={
|
|
370
|
+
mutableMediaProviderWith([
|
|
371
|
+
{
|
|
372
|
+
name: 'attachments/used.png',
|
|
373
|
+
mimeType: 'image/png',
|
|
374
|
+
size: 100,
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
name: 'attachments/unused.png',
|
|
378
|
+
mimeType: 'image/png',
|
|
379
|
+
size: 200,
|
|
380
|
+
},
|
|
381
|
+
]).provider
|
|
382
|
+
}
|
|
383
|
+
/>,
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
await waitFor(() => {
|
|
387
|
+
expect(screen.getByLabelText('Toggle Files panel, 2 files')).toBeTruthy();
|
|
388
|
+
});
|
|
389
|
+
fireEvent.click(screen.getByLabelText('Toggle Files panel, 2 files'));
|
|
390
|
+
|
|
391
|
+
await screen.findByText('used.png');
|
|
392
|
+
await screen.findByText('unused.png');
|
|
393
|
+
|
|
394
|
+
const items = Array.from(container.querySelectorAll('.squisq-media-bin-item'));
|
|
395
|
+
const itemNamed = (name: string) =>
|
|
396
|
+
items.find((item) => item.querySelector('.squisq-media-bin-name')?.textContent === name);
|
|
397
|
+
const usedItem = itemNamed('used.png');
|
|
398
|
+
const unusedItem = itemNamed('unused.png');
|
|
399
|
+
|
|
400
|
+
expect(usedItem?.querySelector('.squisq-media-bin-unused-badge')).toBeNull();
|
|
401
|
+
expect(unusedItem?.querySelector('.squisq-media-bin-unused-badge')?.textContent).toBe('Unused');
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
describe('<Toolbar> Files badge', () => {
|
|
406
|
+
it('self-scans mediaProvider when fileCount is not controlled by a parent', async () => {
|
|
407
|
+
const { container } = render(
|
|
408
|
+
<EditorProvider
|
|
409
|
+
initialMarkdown="# hi"
|
|
410
|
+
initialView="raw"
|
|
411
|
+
mediaProvider={mediaProviderWith(2)}
|
|
412
|
+
allowRecording={false}
|
|
413
|
+
>
|
|
414
|
+
<Toolbar onToggleFiles={() => {}} />
|
|
415
|
+
</EditorProvider>,
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
await waitFor(() => {
|
|
419
|
+
expect(screen.getByLabelText('Toggle Files panel, 2 files')).toBeTruthy();
|
|
420
|
+
});
|
|
421
|
+
expect(container.querySelector('.squisq-toolbar-files-badge')?.textContent).toBe('2');
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it('excludes .gitignore from a self-scanned count', async () => {
|
|
425
|
+
const { provider } = mutableMediaProviderWith([
|
|
426
|
+
{ name: '.gitignore', mimeType: 'text/plain', size: 11 },
|
|
427
|
+
{ name: 'hero.png', mimeType: 'image/png', size: 1024 },
|
|
428
|
+
]);
|
|
429
|
+
const { container } = render(
|
|
430
|
+
<EditorProvider
|
|
431
|
+
initialMarkdown="# hi"
|
|
432
|
+
initialView="raw"
|
|
433
|
+
mediaProvider={provider}
|
|
434
|
+
allowRecording={false}
|
|
435
|
+
>
|
|
436
|
+
<Toolbar onToggleFiles={() => {}} />
|
|
437
|
+
</EditorProvider>,
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
await waitFor(() => {
|
|
441
|
+
expect(screen.getByLabelText('Toggle Files panel, 1 file')).toBeTruthy();
|
|
442
|
+
});
|
|
443
|
+
expect(container.querySelector('.squisq-toolbar-files-badge')?.textContent).toBe('1');
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
describe('<Toolbar> Insert menu', () => {
|
|
448
|
+
it('adds a default task list from the Insert menu in raw fallback mode', async () => {
|
|
449
|
+
render(
|
|
450
|
+
<EditorProvider initialMarkdown="Intro" initialView="raw" allowRecording={false}>
|
|
451
|
+
<Toolbar />
|
|
452
|
+
<MarkdownSourceProbe />
|
|
453
|
+
</EditorProvider>,
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
fireEvent.click(screen.getByLabelText('Insert'));
|
|
457
|
+
const item = await screen.findByRole('menuitem', { name: /Task List/i });
|
|
458
|
+
|
|
459
|
+
fireEvent.click(item);
|
|
460
|
+
|
|
461
|
+
await waitFor(() => {
|
|
462
|
+
expect(screen.getByTestId('markdown-source').textContent).toBe(
|
|
463
|
+
['Intro', '- [ ] Task 1', '- [ ] Task 2', '- [ ] Task 3', ''].join('\n'),
|
|
464
|
+
);
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it('adds an explicit authored timeline fence from the Insert menu', async () => {
|
|
469
|
+
render(
|
|
470
|
+
<EditorProvider initialMarkdown="Intro" initialView="raw" allowRecording={false}>
|
|
471
|
+
<Toolbar />
|
|
472
|
+
<MarkdownSourceProbe />
|
|
473
|
+
</EditorProvider>,
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
fireEvent.click(screen.getByLabelText('Insert'));
|
|
477
|
+
fireEvent.click(await screen.findByRole('menuitem', { name: /Timeline/i }));
|
|
478
|
+
|
|
479
|
+
await waitFor(() => {
|
|
480
|
+
expect(screen.getByTestId('markdown-source').textContent).toContain(
|
|
481
|
+
'```timeline\nMilestones: ● Start',
|
|
482
|
+
);
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
});
|
|
@@ -1,43 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import { fireEvent, render, screen } from '@testing-library/react';
|
|
3
|
-
import { EmojiPicker } from '../
|
|
4
|
-
import {
|
|
5
|
-
EMOJI_CATEGORIES,
|
|
6
|
-
ALL_EMOJIS,
|
|
7
|
-
searchEmojis,
|
|
8
|
-
PICKER_CATEGORIES,
|
|
9
|
-
ALL_PICKER_ENTRIES,
|
|
10
|
-
searchPickerEntries,
|
|
11
|
-
} from '../emojiData';
|
|
12
|
-
|
|
13
|
-
describe('emojiData — legacy emoji exports', () => {
|
|
14
|
-
it('exposes the standard CLDR category buckets', () => {
|
|
15
|
-
const ids = EMOJI_CATEGORIES.map((c) => c.id);
|
|
16
|
-
expect(ids).toEqual([
|
|
17
|
-
'smileys',
|
|
18
|
-
'people',
|
|
19
|
-
'nature',
|
|
20
|
-
'food',
|
|
21
|
-
'travel',
|
|
22
|
-
'activities',
|
|
23
|
-
'objects',
|
|
24
|
-
'symbols',
|
|
25
|
-
'flags',
|
|
26
|
-
]);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('each entry has a non-empty char and name', () => {
|
|
30
|
-
for (const entry of ALL_EMOJIS) {
|
|
31
|
-
expect(entry.char.length).toBeGreaterThan(0);
|
|
32
|
-
expect(entry.name.length).toBeGreaterThan(0);
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('searchEmojis matches by name (case-insensitive)', () => {
|
|
37
|
-
const results = searchEmojis('HEART');
|
|
38
|
-
expect(results.some((e) => e.char === '❤️')).toBe(true);
|
|
39
|
-
});
|
|
40
|
-
});
|
|
3
|
+
import { EmojiPicker, PICKER_CATEGORIES, ALL_PICKER_ENTRIES, searchPickerEntries } from '../index';
|
|
41
4
|
|
|
42
5
|
describe('emojiData — unified picker entries', () => {
|
|
43
6
|
it('appends FA Brands, Solid, and Regular categories after the emoji buckets', () => {
|
|
@@ -78,12 +41,9 @@ describe('EmojiPicker', () => {
|
|
|
78
41
|
|
|
79
42
|
it('renders all emoji + FA tabs when open', () => {
|
|
80
43
|
render(<EmojiPicker open onSelect={() => {}} onClose={() => {}} />);
|
|
81
|
-
for (const cat of
|
|
44
|
+
for (const cat of PICKER_CATEGORIES) {
|
|
82
45
|
expect(screen.getByRole('tab', { name: cat.label })).toBeTruthy();
|
|
83
46
|
}
|
|
84
|
-
expect(screen.getByRole('tab', { name: 'Brands' })).toBeTruthy();
|
|
85
|
-
expect(screen.getByRole('tab', { name: 'Solid' })).toBeTruthy();
|
|
86
|
-
expect(screen.getByRole('tab', { name: 'Regular' })).toBeTruthy();
|
|
87
47
|
});
|
|
88
48
|
|
|
89
49
|
it('fires onSelect with an emoji PickerEntry when an emoji tile is clicked', () => {
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
readHeadingLineTransition,
|
|
4
4
|
setHeadingLineTransition,
|
|
5
5
|
readBlockAttrsTransition,
|
|
6
|
-
|
|
6
|
+
setHeadingAttrsTransition,
|
|
7
7
|
EMPTY_TRANSITION,
|
|
8
8
|
} from '../headingTransition';
|
|
9
9
|
|
|
@@ -40,6 +40,14 @@ describe('readHeadingLineTransition', () => {
|
|
|
40
40
|
});
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
+
it('reads a transition from a param-only {[…]} annotation', () => {
|
|
44
|
+
expect(readHeadingLineTransition('## Tips {[transition=zoom]}')).toEqual({
|
|
45
|
+
type: 'zoom',
|
|
46
|
+
direction: '',
|
|
47
|
+
duration: '',
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
43
51
|
it('prefers the Pandoc block over the template params when both set it', () => {
|
|
44
52
|
expect(
|
|
45
53
|
readHeadingLineTransition('## Tips {transition=fade} {[title transition=zoom]}'),
|
|
@@ -48,32 +56,42 @@ describe('readHeadingLineTransition', () => {
|
|
|
48
56
|
});
|
|
49
57
|
|
|
50
58
|
describe('setHeadingLineTransition', () => {
|
|
51
|
-
it('adds a
|
|
59
|
+
it('adds a squiggly annotation to a plain heading', () => {
|
|
52
60
|
expect(setHeadingLineTransition('## Tips', { type: 'fade', direction: '', duration: '' })).toBe(
|
|
53
|
-
'## Tips {transition=fade}',
|
|
61
|
+
'## Tips {[transition=fade]}',
|
|
54
62
|
);
|
|
55
63
|
});
|
|
56
64
|
|
|
57
|
-
it('
|
|
65
|
+
it('folds into an existing {[…]} template annotation', () => {
|
|
58
66
|
expect(
|
|
59
67
|
setHeadingLineTransition('## Tips {[title]}', { type: 'fade', direction: '', duration: '' }),
|
|
60
|
-
).toBe('## Tips {transition=fade
|
|
68
|
+
).toBe('## Tips {[title transition=fade]}');
|
|
61
69
|
});
|
|
62
70
|
|
|
63
71
|
it('includes direction and duration when present', () => {
|
|
64
72
|
expect(
|
|
65
73
|
setHeadingLineTransition('## Tips', { type: 'push', direction: 'up', duration: '1.2' }),
|
|
66
|
-
).toBe('## Tips {transition=push transitionDirection=up transitionDuration=1.2}');
|
|
74
|
+
).toBe('## Tips {[transition=push transitionDirection=up transitionDuration=1.2]}');
|
|
67
75
|
});
|
|
68
76
|
|
|
69
|
-
it('
|
|
77
|
+
it('migrates an existing Pandoc transition, preserving other params and the id', () => {
|
|
70
78
|
expect(
|
|
71
79
|
setHeadingLineTransition('## Tips {#intro transition=fade x=10}', {
|
|
72
80
|
type: 'zoom',
|
|
73
81
|
direction: '',
|
|
74
82
|
duration: '',
|
|
75
83
|
}),
|
|
76
|
-
).toBe('## Tips {#intro x=10 transition=zoom}');
|
|
84
|
+
).toBe('## Tips {#intro x=10} {[transition=zoom]}');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('migrates into an existing template annotation without duplicating channels', () => {
|
|
88
|
+
expect(
|
|
89
|
+
setHeadingLineTransition('## Tips {#intro transition=fade} {[title color=blue]}', {
|
|
90
|
+
type: 'zoom',
|
|
91
|
+
direction: '',
|
|
92
|
+
duration: '',
|
|
93
|
+
}),
|
|
94
|
+
).toBe('## Tips {#intro} {[title color=blue transition=zoom]}');
|
|
77
95
|
});
|
|
78
96
|
|
|
79
97
|
it('removes the transition (and an empty block) when set to none', () => {
|
|
@@ -125,14 +143,34 @@ describe('block-attrs (WYSIWYG) helpers', () => {
|
|
|
125
143
|
});
|
|
126
144
|
});
|
|
127
145
|
|
|
128
|
-
it('writes
|
|
129
|
-
expect(
|
|
130
|
-
'
|
|
131
|
-
)
|
|
146
|
+
it('writes transition edits to dataTemplateParams by default', () => {
|
|
147
|
+
expect(
|
|
148
|
+
setHeadingAttrsTransition(null, null, { type: 'fade', direction: '', duration: '' }),
|
|
149
|
+
).toEqual({
|
|
150
|
+
blockAttrsInner: null,
|
|
151
|
+
templateParams: 'transition=fade',
|
|
152
|
+
});
|
|
132
153
|
});
|
|
133
154
|
|
|
134
|
-
it('
|
|
135
|
-
expect(
|
|
136
|
-
|
|
155
|
+
it('migrates legacy dataBlockAttrs transitions to dataTemplateParams', () => {
|
|
156
|
+
expect(
|
|
157
|
+
setHeadingAttrsTransition('#intro transition=fade', 'color=blue', {
|
|
158
|
+
type: 'zoom',
|
|
159
|
+
direction: '',
|
|
160
|
+
duration: '',
|
|
161
|
+
}),
|
|
162
|
+
).toEqual({
|
|
163
|
+
blockAttrsInner: '#intro',
|
|
164
|
+
templateParams: 'color=blue transition=zoom',
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('clears transitions from both WYSIWYG channels', () => {
|
|
169
|
+
expect(
|
|
170
|
+
setHeadingAttrsTransition('#intro transition=fade', 'transition=zoom', EMPTY_TRANSITION),
|
|
171
|
+
).toEqual({
|
|
172
|
+
blockAttrsInner: '#intro',
|
|
173
|
+
templateParams: null,
|
|
174
|
+
});
|
|
137
175
|
});
|
|
138
176
|
});
|
|
@@ -182,8 +182,10 @@ describe('<EditorShell> image-edit modal wiring', () => {
|
|
|
182
182
|
|
|
183
183
|
expect(screen.queryByTestId('image-edit-modal')).toBeNull();
|
|
184
184
|
|
|
185
|
+
const trigger = screen.getByTestId('trigger');
|
|
186
|
+
trigger.focus();
|
|
185
187
|
await act(async () => {
|
|
186
|
-
fireEvent.click(
|
|
188
|
+
fireEvent.click(trigger);
|
|
187
189
|
});
|
|
188
190
|
|
|
189
191
|
await waitFor(() => {
|
|
@@ -191,6 +193,7 @@ describe('<EditorShell> image-edit modal wiring', () => {
|
|
|
191
193
|
});
|
|
192
194
|
// Shows the relative path in the header.
|
|
193
195
|
expect(screen.getByText('hero.png')).toBeTruthy();
|
|
196
|
+
expect(document.activeElement).toBe(screen.getByTestId('image-edit-modal-close'));
|
|
194
197
|
|
|
195
198
|
await act(async () => {
|
|
196
199
|
fireEvent.click(screen.getByTestId('image-edit-modal-close'));
|
|
@@ -198,6 +201,7 @@ describe('<EditorShell> image-edit modal wiring', () => {
|
|
|
198
201
|
await waitFor(() => {
|
|
199
202
|
expect(screen.queryByTestId('image-edit-modal')).toBeNull();
|
|
200
203
|
});
|
|
204
|
+
expect(document.activeElement).toBe(screen.getByTestId('trigger'));
|
|
201
205
|
});
|
|
202
206
|
|
|
203
207
|
it('opens the modal with a transient sidecar when only a mediaProvider is wired (no host container)', async () => {
|