@bendyline/squisq-editor-react 2.0.0 → 2.1.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 +10 -2
- package/dist/index.d.ts +579 -47
- package/dist/index.js +7477 -2943
- package/dist/index.js.map +1 -1
- package/dist/styles/index.css +1008 -38
- package/package.json +6 -5
- package/src/DocumentSettingsDialog.tsx +32 -18
- package/src/EditorContext.tsx +27 -0
- package/src/EditorShell.tsx +25 -1
- package/src/PreviewControls.tsx +77 -29
- package/src/PreviewPanel.tsx +5 -1
- package/src/RawEditor.tsx +12 -0
- package/src/RecorderEntry.tsx +5 -0
- package/src/Toolbar.tsx +479 -36
- package/src/WysiwygEditor.tsx +25 -11
- package/src/__tests__/buildPreviewDocContent.test.ts +17 -0
- package/src/__tests__/codeContextSectionView.test.tsx +8 -6
- package/src/__tests__/documentSettingsDialog.test.tsx +22 -0
- package/src/__tests__/editorShellProps.test.tsx +65 -0
- package/src/__tests__/findMode.test.tsx +104 -0
- package/src/__tests__/findModel.test.ts +55 -0
- package/src/__tests__/markdownCodeFence.test.ts +45 -0
- package/src/__tests__/mediaAttachmentFlow.test.ts +2 -2
- package/src/__tests__/mediaReferences.test.ts +15 -0
- package/src/__tests__/previewControls.test.tsx +195 -0
- package/src/__tests__/recorderTheme.test.tsx +42 -0
- package/src/__tests__/selectionConversions.test.ts +80 -0
- package/src/__tests__/tiptapBridge.test.ts +57 -7
- package/src/__tests__/tiptapImageRoundTrip.test.ts +1 -1
- package/src/__tests__/toolbarSelectionConversion.test.tsx +190 -0
- package/src/__tests__/writeCanvasSettings.test.ts +15 -0
- package/src/asciiDiagram/AsciiDiagramWidget.tsx +34 -4
- package/src/asciiDiagram/__tests__/AsciiDiagramExtension.test.ts +20 -1
- package/src/asciiDiagram/__tests__/asciiDiagramCommands.test.ts +58 -1
- package/src/asciiDiagram/asciiDiagramCommands.ts +36 -10
- package/src/asciiDiagram/asciiDiagramData.ts +33 -0
- package/src/asciiDiagram/asciiDiagramOps.ts +19 -0
- package/src/buildPreviewDoc.ts +9 -7
- package/src/codeContext/types.ts +1 -1
- package/src/codeSnippet/CodeSnippetExtension.ts +205 -0
- package/src/codeSnippet/CodeSnippetWidget.tsx +109 -0
- package/src/codeSnippet/__tests__/CodeSnippetExtension.test.ts +95 -0
- package/src/codeSnippet/__tests__/codeSnippetLanguages.test.ts +50 -0
- package/src/codeSnippet/codeSnippetCommands.ts +21 -0
- package/src/codeSnippet/codeSnippetData.ts +43 -0
- package/src/codeSnippet/codeSnippetLanguages.ts +216 -0
- package/src/customTemplates/__tests__/useMemoryLayerAdapter.test.ts +13 -0
- package/src/customTemplates/useMemoryLayerAdapter.ts +7 -1
- package/src/diagram/DiagramCanvas.tsx +17 -2
- package/src/find/FindHighlightExtension.ts +81 -0
- package/src/find/FindToolbar.tsx +314 -0
- package/src/find/findModel.ts +77 -0
- package/src/frontmatterSettings.ts +23 -0
- package/src/index.ts +96 -1
- package/src/markdownCodeFence.ts +72 -0
- package/src/mediaReferences.ts +5 -3
- package/src/mermaid/MermaidDiagramCanvas.tsx +693 -0
- package/src/mermaid/MermaidDiagramExtension.ts +243 -0
- package/src/mermaid/MermaidDiagramTypeThumbnail.tsx +184 -0
- package/src/mermaid/MermaidDiagramWidget.tsx +653 -0
- package/src/mermaid/MermaidShapePalette.tsx +245 -0
- package/src/mermaid/__tests__/MermaidDiagramExtension.test.ts +361 -0
- package/src/mermaid/__tests__/mermaidDiagramTypes.test.ts +35 -0
- package/src/mermaid/__tests__/mermaidRenderer.test.ts +49 -0
- package/src/mermaid/__tests__/mermaidSourceOps.test.ts +213 -0
- package/src/mermaid/__tests__/mermaidSyntax.test.ts +71 -0
- package/src/mermaid/mermaidCommands.ts +34 -0
- package/src/mermaid/mermaidData.ts +31 -0
- package/src/mermaid/mermaidDiagramTypes.ts +325 -0
- package/src/mermaid/mermaidModel.ts +31 -0
- package/src/mermaid/mermaidRenderer.ts +181 -0
- package/src/mermaid/mermaidShapes.ts +113 -0
- package/src/mermaid/mermaidSourceOps.ts +454 -0
- package/src/recorder/RecorderButton.tsx +9 -1
- package/src/recorder/RecorderModal.tsx +84 -41
- package/src/recorder/RecorderPanel.tsx +9 -1
- package/src/scene/Scene.tsx +46 -15
- package/src/scene/SceneBlockToolbar.tsx +29 -14
- package/src/scene/SceneViewControls.tsx +43 -0
- package/src/scene/__tests__/fitScale.test.ts +19 -0
- package/src/scene/__tests__/sceneIsolation.test.tsx +27 -1
- package/src/scene/__tests__/useScenePanZoom.test.ts +28 -1
- package/src/scene/adapters/DrawingAdapter.ts +6 -1
- package/src/scene/adapters/LayoutAdapter.ts +3 -0
- package/src/scene/commands/SceneCommand.ts +13 -2
- package/src/scene/fitScale.ts +17 -0
- package/src/scene/hooks/useScenePanZoom.ts +11 -4
- package/src/scene/scene.css +85 -3
- package/src/scene/tools/SelectTool.ts +5 -5
- package/src/selectionConversions.ts +155 -0
- package/src/styles/ascii-timeline.css +101 -4
- package/src/styles/code-snippet.css +76 -0
- package/src/styles/editor.css +352 -2
- package/src/styles/index.css +2 -0
- package/src/styles/mermaid-diagram.css +487 -0
- package/src/styles/tree-view.css +51 -1
- package/src/timeline/TimelineEditorWidget.tsx +200 -41
- package/src/timeline/__tests__/TimelineEditorWidget.test.tsx +61 -2
- package/src/timeline/__tests__/timelineCommands.test.ts +32 -0
- package/src/timeline/__tests__/timelineOps.test.ts +55 -0
- package/src/timeline/timelineCommands.ts +54 -0
- package/src/timeline/timelineOps.ts +107 -3
- package/src/tiptapBridge.ts +23 -5
- package/src/treeview/TreeOutlineWidget.tsx +153 -3
- package/src/treeview/__tests__/TreeOutlineWidget.test.tsx +156 -0
- package/src/treeview/__tests__/treeOps.test.ts +52 -0
- package/src/treeview/__tests__/treeViewCommands.test.ts +16 -0
- package/src/treeview/treeOps.ts +59 -0
- package/src/treeview/treeViewCommands.ts +5 -0
- package/src/writeCanvasSettings.ts +30 -0
|
@@ -3,6 +3,7 @@ import { parseTree, renderTree, type Tree, type TreeNode } from '@bendyline/squi
|
|
|
3
3
|
import {
|
|
4
4
|
addItemOp,
|
|
5
5
|
indentItemOp,
|
|
6
|
+
moveItemOp,
|
|
6
7
|
moveItemDownOp,
|
|
7
8
|
moveItemUpOp,
|
|
8
9
|
outdentItemOp,
|
|
@@ -90,6 +91,56 @@ describe('treeOps', () => {
|
|
|
90
91
|
expect(outdentItemOp(t, 'root')).toBe(t);
|
|
91
92
|
});
|
|
92
93
|
|
|
94
|
+
it('moveItemOp reorders siblings before or after a target', () => {
|
|
95
|
+
expect(flat(moveItemOp(tree(BASE), 'c', 'a', 'before'))).toEqual([
|
|
96
|
+
'0:root/',
|
|
97
|
+
'1:c',
|
|
98
|
+
'1:a',
|
|
99
|
+
'1:b/',
|
|
100
|
+
'2:b1',
|
|
101
|
+
]);
|
|
102
|
+
expect(flat(moveItemOp(tree(BASE), 'b', 'c', 'after'))).toEqual([
|
|
103
|
+
'0:root/',
|
|
104
|
+
'1:a',
|
|
105
|
+
'1:c',
|
|
106
|
+
'1:b/',
|
|
107
|
+
'2:b1',
|
|
108
|
+
]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('moveItemOp reparents nodes and carries their subtrees', () => {
|
|
112
|
+
expect(flat(moveItemOp(tree(BASE), 'c', 'a', 'child'))).toEqual([
|
|
113
|
+
'0:root/',
|
|
114
|
+
'1:a',
|
|
115
|
+
'2:c',
|
|
116
|
+
'1:b/',
|
|
117
|
+
'2:b1',
|
|
118
|
+
]);
|
|
119
|
+
expect(flat(moveItemOp(tree(BASE), 'b', 'a', 'child'))).toEqual([
|
|
120
|
+
'0:root/',
|
|
121
|
+
'1:a',
|
|
122
|
+
'2:b/',
|
|
123
|
+
'3:b1',
|
|
124
|
+
'1:c',
|
|
125
|
+
]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('moveItemOp can outdent relative to a shallower target', () => {
|
|
129
|
+
expect(flat(moveItemOp(tree(BASE), 'b1', 'b', 'before'))).toEqual([
|
|
130
|
+
'0:root/',
|
|
131
|
+
'1:a',
|
|
132
|
+
'1:b1',
|
|
133
|
+
'1:b/',
|
|
134
|
+
'1:c',
|
|
135
|
+
]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('moveItemOp rejects self-drops and drops into the source subtree', () => {
|
|
139
|
+
const t = tree(BASE);
|
|
140
|
+
expect(moveItemOp(t, 'b', 'b', 'child')).toBe(t);
|
|
141
|
+
expect(moveItemOp(t, 'b', 'b1', 'child')).toBe(t);
|
|
142
|
+
});
|
|
143
|
+
|
|
93
144
|
it('moveItemUp / moveItemDown reorder siblings within bounds', () => {
|
|
94
145
|
expect(flat(moveItemDownOp(tree(BASE), 'a'))).toEqual([
|
|
95
146
|
'0:root/',
|
|
@@ -115,6 +166,7 @@ describe('treeOps', () => {
|
|
|
115
166
|
(t: Tree) => addItemOp(t, 'b', 'child', 'b2'),
|
|
116
167
|
(t: Tree) => indentItemOp(t, 'c'),
|
|
117
168
|
(t: Tree) => outdentItemOp(t, 'b1'),
|
|
169
|
+
(t: Tree) => moveItemOp(t, 'c', 'a', 'child'),
|
|
118
170
|
(t: Tree) => moveItemDownOp(t, 'a'),
|
|
119
171
|
]) {
|
|
120
172
|
const next = op(tree(BASE));
|
|
@@ -111,6 +111,22 @@ describe('applyTreeCommand', () => {
|
|
|
111
111
|
expect(utils?.children.map((c) => c.label)).toContain('config.ts');
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
+
it('moveItem reorders and reparents through one fence rewrite', () => {
|
|
115
|
+
const editor = makeEditor('```\n' + ART + '\n```\n');
|
|
116
|
+
const id = firstId(editor);
|
|
117
|
+
expect(
|
|
118
|
+
applyTreeCommand(editor, id, {
|
|
119
|
+
kind: 'moveItem',
|
|
120
|
+
id: 'config-ts',
|
|
121
|
+
targetId: 'utils',
|
|
122
|
+
position: 'child',
|
|
123
|
+
}),
|
|
124
|
+
).toBe(true);
|
|
125
|
+
const tree = parseTree(fenceOf(editor).text);
|
|
126
|
+
const utils = tree.roots[0].children.find((n) => n.label === 'utils/');
|
|
127
|
+
expect(utils?.children.map((child) => child.label)).toEqual(['math.ts', 'config.ts']);
|
|
128
|
+
});
|
|
129
|
+
|
|
114
130
|
it('removeItem drops a node', () => {
|
|
115
131
|
const editor = makeEditor('```\n' + ART + '\n```\n');
|
|
116
132
|
const id = firstId(editor);
|
package/src/treeview/treeOps.ts
CHANGED
|
@@ -41,6 +41,8 @@ interface Loc {
|
|
|
41
41
|
parent: TreeNode | null;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
export type TreeDropPosition = 'before' | 'child' | 'after';
|
|
45
|
+
|
|
44
46
|
/** Locate a node by id in a (cloned) tree, returning its sibling array + index + parent. */
|
|
45
47
|
function locate(roots: TreeNode[], id: string, parent: TreeNode | null = null): Loc | null {
|
|
46
48
|
for (let i = 0; i < roots.length; i++) {
|
|
@@ -55,6 +57,10 @@ function markDir(n: TreeNode): void {
|
|
|
55
57
|
n.isDir = n.label.endsWith('/') || n.children.length > 0;
|
|
56
58
|
}
|
|
57
59
|
|
|
60
|
+
function containsNode(node: TreeNode, id: string): boolean {
|
|
61
|
+
return node.id === id || node.children.some((child) => containsNode(child, id));
|
|
62
|
+
}
|
|
63
|
+
|
|
58
64
|
export function renameItemOp(tree: Tree, id: string, label: string): Tree {
|
|
59
65
|
const clean = sanitizeTreeLabel(label);
|
|
60
66
|
const next = cloneTree(tree);
|
|
@@ -127,6 +133,59 @@ export function outdentItemOp(tree: Tree, id: string): Tree {
|
|
|
127
133
|
return next;
|
|
128
134
|
}
|
|
129
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Move a node and its complete subtree relative to any other node. Dropping
|
|
138
|
+
* before/after adopts the target's parent; dropping as a child appends to the
|
|
139
|
+
* target. A node can never be moved into its own subtree.
|
|
140
|
+
*/
|
|
141
|
+
export function moveItemOp(
|
|
142
|
+
tree: Tree,
|
|
143
|
+
id: string,
|
|
144
|
+
targetId: string,
|
|
145
|
+
position: TreeDropPosition,
|
|
146
|
+
): Tree {
|
|
147
|
+
const currentSource = locate(tree.roots, id);
|
|
148
|
+
const currentTarget = locate(tree.roots, targetId);
|
|
149
|
+
if (
|
|
150
|
+
!currentSource ||
|
|
151
|
+
!currentTarget ||
|
|
152
|
+
id === targetId ||
|
|
153
|
+
containsNode(currentSource.node, targetId)
|
|
154
|
+
) {
|
|
155
|
+
return tree;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Avoid a fence rewrite when the drop describes the node's current slot.
|
|
159
|
+
if (currentSource.siblings === currentTarget.siblings) {
|
|
160
|
+
if (position === 'before' && currentSource.index === currentTarget.index - 1) return tree;
|
|
161
|
+
if (position === 'after' && currentSource.index === currentTarget.index + 1) return tree;
|
|
162
|
+
}
|
|
163
|
+
if (
|
|
164
|
+
position === 'child' &&
|
|
165
|
+
currentSource.parent?.id === currentTarget.node.id &&
|
|
166
|
+
currentSource.index === currentSource.siblings.length - 1
|
|
167
|
+
) {
|
|
168
|
+
return tree;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const next = cloneTree(tree);
|
|
172
|
+
const source = locate(next.roots, id);
|
|
173
|
+
if (!source) return tree;
|
|
174
|
+
const [moved] = source.siblings.splice(source.index, 1);
|
|
175
|
+
if (source.parent) markDir(source.parent);
|
|
176
|
+
|
|
177
|
+
// Locate again after removal so same-sibling indices are current.
|
|
178
|
+
const target = locate(next.roots, targetId);
|
|
179
|
+
if (!target) return tree;
|
|
180
|
+
if (position === 'child') {
|
|
181
|
+
target.node.children.push(moved);
|
|
182
|
+
markDir(target.node);
|
|
183
|
+
} else {
|
|
184
|
+
target.siblings.splice(target.index + (position === 'after' ? 1 : 0), 0, moved);
|
|
185
|
+
}
|
|
186
|
+
return next;
|
|
187
|
+
}
|
|
188
|
+
|
|
130
189
|
export function moveItemUpOp(tree: Tree, id: string): Tree {
|
|
131
190
|
const next = cloneTree(tree);
|
|
132
191
|
const loc = locate(next.roots, id);
|
|
@@ -13,12 +13,14 @@ import { findTreeBlockPos, parseTreeForNode } from './TreeViewExtension';
|
|
|
13
13
|
import {
|
|
14
14
|
addItemOp,
|
|
15
15
|
indentItemOp,
|
|
16
|
+
moveItemOp,
|
|
16
17
|
moveItemDownOp,
|
|
17
18
|
moveItemUpOp,
|
|
18
19
|
outdentItemOp,
|
|
19
20
|
removeItemOp,
|
|
20
21
|
renameItemOp,
|
|
21
22
|
toggleDirOp,
|
|
23
|
+
type TreeDropPosition,
|
|
22
24
|
} from './treeOps';
|
|
23
25
|
|
|
24
26
|
export type TreeCommand =
|
|
@@ -32,6 +34,7 @@ export type TreeCommand =
|
|
|
32
34
|
| { kind: 'renameItem'; id: string; label: string }
|
|
33
35
|
| { kind: 'indentItem'; id: string }
|
|
34
36
|
| { kind: 'outdentItem'; id: string }
|
|
37
|
+
| { kind: 'moveItem'; id: string; targetId: string; position: TreeDropPosition }
|
|
35
38
|
| { kind: 'moveItemUp'; id: string }
|
|
36
39
|
| { kind: 'moveItemDown'; id: string }
|
|
37
40
|
| { kind: 'removeItem'; id: string }
|
|
@@ -79,6 +82,8 @@ export function applyTreeCommand(editor: Editor, blockId: string, cmd: TreeComma
|
|
|
79
82
|
return applyOp(editor, blockId, (t) => indentItemOp(t, cmd.id));
|
|
80
83
|
case 'outdentItem':
|
|
81
84
|
return applyOp(editor, blockId, (t) => outdentItemOp(t, cmd.id));
|
|
85
|
+
case 'moveItem':
|
|
86
|
+
return applyOp(editor, blockId, (t) => moveItemOp(t, cmd.id, cmd.targetId, cmd.position));
|
|
82
87
|
case 'moveItemUp':
|
|
83
88
|
return applyOp(editor, blockId, (t) => moveItemUpOp(t, cmd.id));
|
|
84
89
|
case 'moveItemDown':
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { CSSProperties } from 'react';
|
|
2
|
+
|
|
3
|
+
/** Host-controlled typography for the markdown Write canvas. */
|
|
4
|
+
export interface WriteCanvasSettings {
|
|
5
|
+
/** Base text size in CSS pixels. Headings continue to scale relative to it. */
|
|
6
|
+
textSize?: number;
|
|
7
|
+
/** Unitless line-height multiplier for body text. */
|
|
8
|
+
lineSpacing?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type WriteCanvasCSSProperties = CSSProperties & {
|
|
12
|
+
'--squisq-write-text-size'?: string;
|
|
13
|
+
'--squisq-write-line-spacing'?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** Translate public Write-canvas settings into the CSS variables consumed by the editor. */
|
|
17
|
+
export function writeCanvasSettingsStyle(settings?: WriteCanvasSettings): WriteCanvasCSSProperties {
|
|
18
|
+
const style: WriteCanvasCSSProperties = {};
|
|
19
|
+
if (isPositiveFinite(settings?.textSize)) {
|
|
20
|
+
style['--squisq-write-text-size'] = `${settings.textSize}px`;
|
|
21
|
+
}
|
|
22
|
+
if (isPositiveFinite(settings?.lineSpacing)) {
|
|
23
|
+
style['--squisq-write-line-spacing'] = String(settings.lineSpacing);
|
|
24
|
+
}
|
|
25
|
+
return style;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isPositiveFinite(value: number | undefined): value is number {
|
|
29
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0;
|
|
30
|
+
}
|