@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.
Files changed (110) hide show
  1. package/README.md +10 -2
  2. package/dist/index.d.ts +579 -47
  3. package/dist/index.js +7477 -2943
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles/index.css +1008 -38
  6. package/package.json +6 -5
  7. package/src/DocumentSettingsDialog.tsx +32 -18
  8. package/src/EditorContext.tsx +27 -0
  9. package/src/EditorShell.tsx +25 -1
  10. package/src/PreviewControls.tsx +77 -29
  11. package/src/PreviewPanel.tsx +5 -1
  12. package/src/RawEditor.tsx +12 -0
  13. package/src/RecorderEntry.tsx +5 -0
  14. package/src/Toolbar.tsx +479 -36
  15. package/src/WysiwygEditor.tsx +25 -11
  16. package/src/__tests__/buildPreviewDocContent.test.ts +17 -0
  17. package/src/__tests__/codeContextSectionView.test.tsx +8 -6
  18. package/src/__tests__/documentSettingsDialog.test.tsx +22 -0
  19. package/src/__tests__/editorShellProps.test.tsx +65 -0
  20. package/src/__tests__/findMode.test.tsx +104 -0
  21. package/src/__tests__/findModel.test.ts +55 -0
  22. package/src/__tests__/markdownCodeFence.test.ts +45 -0
  23. package/src/__tests__/mediaAttachmentFlow.test.ts +2 -2
  24. package/src/__tests__/mediaReferences.test.ts +15 -0
  25. package/src/__tests__/previewControls.test.tsx +195 -0
  26. package/src/__tests__/recorderTheme.test.tsx +42 -0
  27. package/src/__tests__/selectionConversions.test.ts +80 -0
  28. package/src/__tests__/tiptapBridge.test.ts +57 -7
  29. package/src/__tests__/tiptapImageRoundTrip.test.ts +1 -1
  30. package/src/__tests__/toolbarSelectionConversion.test.tsx +190 -0
  31. package/src/__tests__/writeCanvasSettings.test.ts +15 -0
  32. package/src/asciiDiagram/AsciiDiagramWidget.tsx +34 -4
  33. package/src/asciiDiagram/__tests__/AsciiDiagramExtension.test.ts +20 -1
  34. package/src/asciiDiagram/__tests__/asciiDiagramCommands.test.ts +58 -1
  35. package/src/asciiDiagram/asciiDiagramCommands.ts +36 -10
  36. package/src/asciiDiagram/asciiDiagramData.ts +33 -0
  37. package/src/asciiDiagram/asciiDiagramOps.ts +19 -0
  38. package/src/buildPreviewDoc.ts +9 -7
  39. package/src/codeContext/types.ts +1 -1
  40. package/src/codeSnippet/CodeSnippetExtension.ts +205 -0
  41. package/src/codeSnippet/CodeSnippetWidget.tsx +109 -0
  42. package/src/codeSnippet/__tests__/CodeSnippetExtension.test.ts +95 -0
  43. package/src/codeSnippet/__tests__/codeSnippetLanguages.test.ts +50 -0
  44. package/src/codeSnippet/codeSnippetCommands.ts +21 -0
  45. package/src/codeSnippet/codeSnippetData.ts +43 -0
  46. package/src/codeSnippet/codeSnippetLanguages.ts +216 -0
  47. package/src/customTemplates/__tests__/useMemoryLayerAdapter.test.ts +13 -0
  48. package/src/customTemplates/useMemoryLayerAdapter.ts +7 -1
  49. package/src/diagram/DiagramCanvas.tsx +17 -2
  50. package/src/find/FindHighlightExtension.ts +81 -0
  51. package/src/find/FindToolbar.tsx +314 -0
  52. package/src/find/findModel.ts +77 -0
  53. package/src/frontmatterSettings.ts +23 -0
  54. package/src/index.ts +96 -1
  55. package/src/markdownCodeFence.ts +72 -0
  56. package/src/mediaReferences.ts +5 -3
  57. package/src/mermaid/MermaidDiagramCanvas.tsx +693 -0
  58. package/src/mermaid/MermaidDiagramExtension.ts +243 -0
  59. package/src/mermaid/MermaidDiagramTypeThumbnail.tsx +184 -0
  60. package/src/mermaid/MermaidDiagramWidget.tsx +653 -0
  61. package/src/mermaid/MermaidShapePalette.tsx +245 -0
  62. package/src/mermaid/__tests__/MermaidDiagramExtension.test.ts +361 -0
  63. package/src/mermaid/__tests__/mermaidDiagramTypes.test.ts +35 -0
  64. package/src/mermaid/__tests__/mermaidRenderer.test.ts +49 -0
  65. package/src/mermaid/__tests__/mermaidSourceOps.test.ts +213 -0
  66. package/src/mermaid/__tests__/mermaidSyntax.test.ts +71 -0
  67. package/src/mermaid/mermaidCommands.ts +34 -0
  68. package/src/mermaid/mermaidData.ts +31 -0
  69. package/src/mermaid/mermaidDiagramTypes.ts +325 -0
  70. package/src/mermaid/mermaidModel.ts +31 -0
  71. package/src/mermaid/mermaidRenderer.ts +181 -0
  72. package/src/mermaid/mermaidShapes.ts +113 -0
  73. package/src/mermaid/mermaidSourceOps.ts +454 -0
  74. package/src/recorder/RecorderButton.tsx +9 -1
  75. package/src/recorder/RecorderModal.tsx +84 -41
  76. package/src/recorder/RecorderPanel.tsx +9 -1
  77. package/src/scene/Scene.tsx +46 -15
  78. package/src/scene/SceneBlockToolbar.tsx +29 -14
  79. package/src/scene/SceneViewControls.tsx +43 -0
  80. package/src/scene/__tests__/fitScale.test.ts +19 -0
  81. package/src/scene/__tests__/sceneIsolation.test.tsx +27 -1
  82. package/src/scene/__tests__/useScenePanZoom.test.ts +28 -1
  83. package/src/scene/adapters/DrawingAdapter.ts +6 -1
  84. package/src/scene/adapters/LayoutAdapter.ts +3 -0
  85. package/src/scene/commands/SceneCommand.ts +13 -2
  86. package/src/scene/fitScale.ts +17 -0
  87. package/src/scene/hooks/useScenePanZoom.ts +11 -4
  88. package/src/scene/scene.css +85 -3
  89. package/src/scene/tools/SelectTool.ts +5 -5
  90. package/src/selectionConversions.ts +155 -0
  91. package/src/styles/ascii-timeline.css +101 -4
  92. package/src/styles/code-snippet.css +76 -0
  93. package/src/styles/editor.css +352 -2
  94. package/src/styles/index.css +2 -0
  95. package/src/styles/mermaid-diagram.css +487 -0
  96. package/src/styles/tree-view.css +51 -1
  97. package/src/timeline/TimelineEditorWidget.tsx +200 -41
  98. package/src/timeline/__tests__/TimelineEditorWidget.test.tsx +61 -2
  99. package/src/timeline/__tests__/timelineCommands.test.ts +32 -0
  100. package/src/timeline/__tests__/timelineOps.test.ts +55 -0
  101. package/src/timeline/timelineCommands.ts +54 -0
  102. package/src/timeline/timelineOps.ts +107 -3
  103. package/src/tiptapBridge.ts +23 -5
  104. package/src/treeview/TreeOutlineWidget.tsx +153 -3
  105. package/src/treeview/__tests__/TreeOutlineWidget.test.tsx +156 -0
  106. package/src/treeview/__tests__/treeOps.test.ts +52 -0
  107. package/src/treeview/__tests__/treeViewCommands.test.ts +16 -0
  108. package/src/treeview/treeOps.ts +59 -0
  109. package/src/treeview/treeViewCommands.ts +5 -0
  110. 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);
@@ -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
+ }