@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.
Files changed (218) hide show
  1. package/README.md +12 -3
  2. package/dist/index.d.ts +1275 -183
  3. package/dist/index.js +20345 -12777
  4. package/dist/index.js.map +1 -1
  5. package/dist/monaco.d.ts +1 -0
  6. package/dist/monaco.js +4 -0
  7. package/dist/monaco.js.map +1 -0
  8. package/dist/styles/index.css +1607 -241
  9. package/package.json +9 -5
  10. package/src/BlockPropertiesPopover.tsx +30 -15
  11. package/src/DocumentSettingsDialog.tsx +39 -33
  12. package/src/EditorContext.tsx +199 -42
  13. package/src/EditorShell.tsx +349 -261
  14. package/src/ImageEditor.tsx +22 -1
  15. package/src/InlinePreviewGutter.tsx +16 -8
  16. package/src/MediaBin.tsx +107 -14
  17. package/src/OutlinePanel.tsx +202 -7
  18. package/src/PlainHtmlPreview.tsx +40 -1
  19. package/src/PreviewControls.tsx +362 -89
  20. package/src/PreviewPanel.tsx +312 -78
  21. package/src/RawEditor.tsx +13 -2
  22. package/src/RecorderEntry.tsx +2 -0
  23. package/src/StatusBar.tsx +5 -2
  24. package/src/TemplateAnnotation.ts +12 -3
  25. package/src/TemplatePicker.tsx +75 -21
  26. package/src/ThemeCustomizerPanel.tsx +8 -0
  27. package/src/ThemePicker.tsx +10 -5
  28. package/src/TimelineBlockPreview.tsx +1 -1
  29. package/src/Toolbar.tsx +357 -51
  30. package/src/TransitionPicker.tsx +36 -4
  31. package/src/ViewMenuPanel.tsx +12 -14
  32. package/src/WysiwygEditor.tsx +124 -91
  33. package/src/__tests__/blockPropertiesPopoverTheme.test.tsx +33 -0
  34. package/src/__tests__/blockTagActivity.test.ts +183 -0
  35. package/src/__tests__/buildDocumentPreviewMarkdown.test.ts +75 -0
  36. package/src/__tests__/buildPreviewDocContent.test.ts +48 -0
  37. package/src/__tests__/buildPreviewDocTransition.test.ts +21 -1
  38. package/src/__tests__/canvasSurfaceTextEditing.test.tsx +60 -0
  39. package/src/__tests__/codeContextSectionView.test.tsx +8 -6
  40. package/src/__tests__/customLayoutManagerTheme.test.tsx +34 -0
  41. package/src/__tests__/documentSettingsDialog.test.tsx +51 -1
  42. package/src/__tests__/editorScrollHandoff.test.tsx +102 -0
  43. package/src/__tests__/editorScrollSync.test.ts +65 -0
  44. package/src/__tests__/editorShellProps.test.tsx +123 -1
  45. package/src/__tests__/emojiPicker.test.tsx +2 -42
  46. package/src/__tests__/headingTransition.test.ts +0 -12
  47. package/src/__tests__/imageEditAffordance.test.tsx +5 -1
  48. package/src/__tests__/imageEditModalContract.test.tsx +119 -0
  49. package/src/__tests__/imageEditorShell.test.tsx +15 -1
  50. package/src/__tests__/jsonEditor.test.tsx +56 -1
  51. package/src/__tests__/layersPanel.test.tsx +75 -5
  52. package/src/__tests__/majorApiTypes.test.ts +26 -0
  53. package/src/__tests__/mediaAttachmentFlow.test.ts +2 -2
  54. package/src/__tests__/mediaBinDrop.test.tsx +90 -0
  55. package/src/__tests__/narrationSave.test.ts +148 -0
  56. package/src/__tests__/outlinePanel.test.tsx +93 -3
  57. package/src/__tests__/outlineSource.test.ts +217 -0
  58. package/src/__tests__/plainHtmlPreview.test.tsx +17 -1
  59. package/src/__tests__/presentationMode.test.tsx +290 -0
  60. package/src/__tests__/previewControls.test.tsx +382 -5
  61. package/src/__tests__/previewPanelAudioFailure.test.tsx +46 -0
  62. package/src/__tests__/previewPanelPresentation.test.tsx +126 -0
  63. package/src/__tests__/previewPanelTransforms.test.tsx +117 -0
  64. package/src/__tests__/rawEditorModelIsolation.test.ts +19 -0
  65. package/src/__tests__/recorderTheme.test.tsx +42 -0
  66. package/src/__tests__/resolveBlockVisual.test.ts +43 -0
  67. package/src/__tests__/selectionConversions.test.ts +80 -0
  68. package/src/__tests__/statusBar.test.tsx +27 -0
  69. package/src/__tests__/teleprompter.test.tsx +190 -0
  70. package/src/__tests__/templateAnnotationRoundTrip.test.ts +26 -0
  71. package/src/__tests__/templatePickerPortal.test.tsx +62 -0
  72. package/src/__tests__/tiptapBridge.test.ts +71 -7
  73. package/src/__tests__/tiptapCodeBlockRoundTrip.test.ts +109 -0
  74. package/src/__tests__/tiptapImageRoundTrip.test.ts +1 -1
  75. package/src/__tests__/toolbarSelectionConversion.test.tsx +164 -0
  76. package/src/__tests__/transformStyleId.test.ts +13 -0
  77. package/src/__tests__/useImageEditor.test.tsx +67 -0
  78. package/src/__tests__/useMediaRecorder.test.ts +75 -0
  79. package/src/__tests__/viewMenuPanel.test.tsx +130 -0
  80. package/src/__tests__/wysiwygImageUpload.test.ts +69 -0
  81. package/src/__tests__/wysiwygTemplateBadgeFocus.test.tsx +56 -0
  82. package/src/asciiDiagram/AsciiDiagramExtension.ts +343 -0
  83. package/src/asciiDiagram/AsciiDiagramWidget.tsx +254 -0
  84. package/src/asciiDiagram/RepairableDiagramExtension.ts +200 -0
  85. package/src/asciiDiagram/__tests__/AsciiDiagramExtension.test.ts +145 -0
  86. package/src/asciiDiagram/__tests__/RepairableDiagramExtension.test.ts +159 -0
  87. package/src/asciiDiagram/__tests__/asciiDiagramCommands.test.ts +284 -0
  88. package/src/asciiDiagram/__tests__/asciiDiagramOps.test.ts +114 -0
  89. package/src/asciiDiagram/__tests__/asciiPaste.test.ts +43 -0
  90. package/src/asciiDiagram/asciiDiagramCommands.ts +174 -0
  91. package/src/asciiDiagram/asciiDiagramData.ts +149 -0
  92. package/src/asciiDiagram/asciiDiagramOps.ts +238 -0
  93. package/src/asciiDiagram/asciiPaste.ts +19 -0
  94. package/src/blockTagActivity.ts +233 -0
  95. package/src/buildDocumentPreviewMarkdown.ts +168 -0
  96. package/src/buildPreviewDoc.ts +19 -12
  97. package/src/codeContext/types.ts +1 -1
  98. package/src/customTemplates/CustomLayoutManager.tsx +3 -2
  99. package/src/customTemplates/CustomTemplateContext.tsx +6 -0
  100. package/src/customTemplates/TemplateDesigner.tsx +7 -4
  101. package/src/customTemplates/__tests__/library.test.ts +8 -0
  102. package/src/customTemplates/__tests__/useMemoryLayerAdapter.test.ts +13 -0
  103. package/src/customTemplates/designer.css +158 -99
  104. package/src/customTemplates/library.ts +16 -3
  105. package/src/customTemplates/useMemoryLayerAdapter.ts +7 -1
  106. package/src/customThemes/CustomThemeContext.tsx +6 -0
  107. package/src/customThemes/CustomThemeDialog.tsx +35 -5
  108. package/src/customThemes/ImportThemeSection.tsx +178 -0
  109. package/src/customThemes/__tests__/customThemeLibrary.test.ts +9 -0
  110. package/src/customThemes/__tests__/importThemeSection.test.tsx +192 -0
  111. package/src/customThemes/customThemeLibrary.ts +7 -4
  112. package/src/customThemes/index.ts +7 -1
  113. package/src/customThemes/themeDraft.ts +23 -7
  114. package/src/diagram/DiagramCanvas.tsx +25 -6
  115. package/src/diagram/types.ts +35 -0
  116. package/src/editorScrollSync.ts +73 -0
  117. package/src/emojiData.ts +3 -23
  118. package/src/frontmatterSettings.ts +23 -0
  119. package/src/headingTransition.ts +0 -15
  120. package/src/imageEditor/CanvasSurface.tsx +29 -23
  121. package/src/imageEditor/LayersPanel.tsx +78 -3
  122. package/src/imageEditor/Toolbar.tsx +5 -1
  123. package/src/imageEditor/icons.tsx +4 -0
  124. package/src/imageEditor/image-editor.css +75 -0
  125. package/src/imageEditor/layers/SelectionHandles.tsx +1 -1
  126. package/src/imageEditor/useImageEditor.ts +71 -14
  127. package/src/index.ts +198 -20
  128. package/src/jsonEditor/JsonEditorContext.tsx +12 -6
  129. package/src/jsonEditor/RenderNode.tsx +24 -3
  130. package/src/jsonEditor/editors.tsx +86 -19
  131. package/src/mediaEntries.ts +12 -0
  132. package/src/monaco.ts +35 -0
  133. package/src/monacoWorkers.ts +89 -0
  134. package/src/outlineSource.ts +171 -0
  135. package/src/presentation/PresentationMode.tsx +596 -0
  136. package/src/rawEditorIsolation.ts +18 -0
  137. package/src/recorder/RecorderButton.tsx +9 -1
  138. package/src/recorder/RecorderModal.tsx +84 -41
  139. package/src/recorder/RecorderPanel.tsx +9 -1
  140. package/src/recorder/hooks/useMediaRecorder.ts +97 -53
  141. package/src/resolveBlockVisual.ts +10 -4
  142. package/src/scene/Scene.tsx +48 -18
  143. package/src/scene/SceneBlockExtension.ts +17 -10
  144. package/src/scene/SceneBlockWidget.tsx +5 -1
  145. package/src/scene/SceneViewport.tsx +7 -3
  146. package/src/scene/ShapePalette.tsx +17 -2
  147. package/src/scene/__tests__/DiagramAdapter.test.ts +86 -0
  148. package/src/scene/__tests__/SceneBlockExtension.test.ts +33 -0
  149. package/src/scene/__tests__/sceneIsolation.test.tsx +119 -0
  150. package/src/scene/adapters/DiagramAdapter.ts +12 -101
  151. package/src/scene/adapters/DrawingAdapter.ts +6 -1
  152. package/src/scene/adapters/LayoutAdapter.ts +3 -0
  153. package/src/scene/commands/SceneCommand.ts +17 -3
  154. package/src/scene/index.ts +1 -6
  155. package/src/scene/layers/DiagramEdges.tsx +21 -7
  156. package/src/scene/layers/edgeGeometry.ts +9 -1
  157. package/src/scene/layers/nodeCard.tsx +27 -8
  158. package/src/scene/scene.css +5 -2
  159. package/src/scene/text/SceneTextOverlay.tsx +5 -5
  160. package/src/scene/text/sceneTextChannel.ts +26 -20
  161. package/src/scene/text/sceneTextConfig.ts +4 -0
  162. package/src/scene/tools/ConnectTool.ts +13 -4
  163. package/src/scene/tools/SceneTool.ts +8 -0
  164. package/src/scene/tools/SelectTool.ts +39 -23
  165. package/src/scene/tools/ShapeTool.ts +2 -3
  166. package/src/selectionConversions.ts +155 -0
  167. package/src/styles/ascii-diagram.css +79 -0
  168. package/src/styles/ascii-timeline.css +596 -0
  169. package/src/styles/editor.css +736 -120
  170. package/src/styles/index.css +3 -0
  171. package/src/styles/tree-view.css +189 -0
  172. package/src/teleprompter/TeleprompterControls.tsx +218 -0
  173. package/src/teleprompter/TeleprompterSelfView.tsx +22 -0
  174. package/src/teleprompter/TeleprompterSurface.tsx +267 -0
  175. package/src/teleprompter/TeleprompterView.tsx +338 -0
  176. package/src/teleprompter/canvasRenderer.ts +161 -0
  177. package/src/teleprompter/floatingWindow.ts +352 -0
  178. package/src/teleprompter/index.ts +61 -0
  179. package/src/teleprompter/pcmWorklet.ts +62 -0
  180. package/src/teleprompter/recording/insertPreamble.ts +80 -0
  181. package/src/teleprompter/recording/narrationSave.ts +134 -0
  182. package/src/teleprompter/recording/useNarrationRecorder.ts +367 -0
  183. package/src/teleprompter/scrollModel.ts +85 -0
  184. package/src/teleprompter/teleprompterTheme.ts +303 -0
  185. package/src/teleprompter/types.ts +38 -0
  186. package/src/teleprompter/useFloatingWindow.ts +74 -0
  187. package/src/teleprompter/useMicAnalysis.ts +211 -0
  188. package/src/teleprompter/useTeleprompter.ts +421 -0
  189. package/src/templateContentPreviewResolver.ts +12 -5
  190. package/src/timeline/TimelineEditorWidget.tsx +861 -0
  191. package/src/timeline/TimelineViewExtension.ts +252 -0
  192. package/src/timeline/__tests__/TimelineEditorWidget.test.tsx +414 -0
  193. package/src/timeline/__tests__/TimelineViewExtension.test.ts +163 -0
  194. package/src/timeline/__tests__/timelineCommands.test.ts +359 -0
  195. package/src/timeline/__tests__/timelineOps.test.ts +277 -0
  196. package/src/timeline/__tests__/timelinePaste.test.ts +34 -0
  197. package/src/timeline/timelineCommands.ts +421 -0
  198. package/src/timeline/timelineData.ts +54 -0
  199. package/src/timeline/timelineOps.ts +381 -0
  200. package/src/timeline/timelinePaste.ts +8 -0
  201. package/src/tiptapBridge.ts +47 -21
  202. package/src/transformStyleId.ts +17 -0
  203. package/src/treeview/TreeOutlineWidget.tsx +390 -0
  204. package/src/treeview/TreeViewExtension.ts +250 -0
  205. package/src/treeview/__tests__/TreeOutlineWidget.test.tsx +156 -0
  206. package/src/treeview/__tests__/TreeViewExtension.test.ts +122 -0
  207. package/src/treeview/__tests__/treeOps.test.ts +177 -0
  208. package/src/treeview/__tests__/treePaste.test.ts +40 -0
  209. package/src/treeview/__tests__/treeViewCommands.test.ts +179 -0
  210. package/src/treeview/treeOps.ts +231 -0
  211. package/src/treeview/treePaste.ts +13 -0
  212. package/src/treeview/treeViewCommands.ts +99 -0
  213. package/src/treeview/treeViewData.ts +41 -0
  214. package/src/useMonacoLoader.ts +11 -34
  215. package/src/wysiwygImageUpload.ts +113 -0
  216. package/src/diagram/DiagramExtension.ts +0 -209
  217. package/src/diagram/DiagramWidget.tsx +0 -272
  218. package/src/diagram/useDiagramData.ts +0 -126
@@ -18,7 +18,7 @@
18
18
  */
19
19
 
20
20
  import { createElement, type JSX } from 'react';
21
- import type { SceneTool, SceneToolContext } from './SceneTool';
21
+ import type { SceneInteractionState, SceneTool, SceneToolContext } from './SceneTool';
22
22
  import type { SceneCommand } from '../commands/SceneCommand';
23
23
  import type { ResizeCorner } from '../SceneSelection';
24
24
 
@@ -53,7 +53,13 @@ type DragState = MoveDragState | MarqueeDragState | ResizeDragState;
53
53
  const DRAG_THRESHOLD_PX = 3;
54
54
  const MIN_LAYER_SIZE = 8;
55
55
 
56
- let dragState: DragState | null = null;
56
+ function getDragState(interaction: SceneInteractionState): DragState | null {
57
+ return (interaction.selectDrag as DragState | undefined) ?? null;
58
+ }
59
+
60
+ function setDragState(interaction: SceneInteractionState, state: DragState | null): void {
61
+ interaction.selectDrag = state ?? undefined;
62
+ }
57
63
 
58
64
  function pointerCoords(e: React.PointerEvent): { sx: number; sy: number } {
59
65
  const rect = (e.currentTarget as Element).getBoundingClientRect();
@@ -90,20 +96,23 @@ function applyResize(
90
96
  }
91
97
 
92
98
  /** Begin a handle-initiated resize drag. Called by the Scene when a corner is pressed. */
93
- export function beginHandleDrag(args: {
94
- layerId: string;
95
- corner: ResizeCorner;
96
- startV: { x: number; y: number };
97
- startBounds: { x: number; y: number; width: number; height: number };
98
- }): void {
99
- dragState = {
99
+ export function beginHandleDrag(
100
+ args: {
101
+ layerId: string;
102
+ corner: ResizeCorner;
103
+ startV: { x: number; y: number };
104
+ startBounds: { x: number; y: number; width: number; height: number };
105
+ },
106
+ interaction: SceneInteractionState,
107
+ ): void {
108
+ setDragState(interaction, {
100
109
  kind: 'resize',
101
110
  layerId: args.layerId,
102
111
  corner: args.corner,
103
112
  startV: args.startV,
104
113
  startBounds: args.startBounds,
105
114
  currentBounds: args.startBounds,
106
- };
115
+ });
107
116
  }
108
117
 
109
118
  export const SelectTool: SceneTool = {
@@ -135,29 +144,30 @@ export const SelectTool: SceneTool = {
135
144
  const origin = getLayerOrigin(ctx, id);
136
145
  if (origin) layerStarts.set(id, origin);
137
146
  }
138
- dragState = {
147
+ setDragState(ctx.interaction, {
139
148
  kind: 'move',
140
149
  startV: v,
141
150
  startS: { x: sx, y: sy },
142
151
  layerStarts,
143
152
  currentV: v,
144
153
  movedPastThreshold: false,
145
- };
154
+ });
146
155
  } else {
147
156
  if (!e.shiftKey) ctx.setSelection([]);
148
- dragState = {
157
+ setDragState(ctx.interaction, {
149
158
  kind: 'marquee',
150
159
  startV: v,
151
160
  startS: { x: sx, y: sy },
152
161
  currentV: v,
153
162
  movedPastThreshold: false,
154
- };
163
+ });
155
164
  }
156
165
 
157
166
  (e.currentTarget as Element).setPointerCapture?.(e.pointerId);
158
167
  },
159
168
 
160
169
  onPointerMove(e, ctx) {
170
+ const dragState = getDragState(ctx.interaction);
161
171
  if (!dragState) return;
162
172
  const { sx, sy } = pointerCoords(e);
163
173
  const v = ctx.screenToViewport(sx, sy);
@@ -182,6 +192,7 @@ export const SelectTool: SceneTool = {
182
192
  },
183
193
 
184
194
  onPointerUp(e, ctx) {
195
+ const dragState = getDragState(ctx.interaction);
185
196
  if (!dragState) return;
186
197
  if (
187
198
  dragState.kind === 'move' &&
@@ -201,14 +212,14 @@ export const SelectTool: SceneTool = {
201
212
  }
202
213
  } else if (dragState.kind === 'resize') {
203
214
  const { x, y, width, height } = dragState.currentBounds;
204
- // Resize translates into a move (if origin shifted) + a size change.
205
- // We dispatch both so a single grab on a corner can change all four
206
- // fields atomically from the host's point of view.
207
- ctx.dispatch({ kind: 'moveLayer', id: dragState.layerId, x, y });
208
- ctx.dispatch({ kind: 'resizeLayer', id: dragState.layerId, width, height });
215
+ // Keep the final origin + size in one command. In particular, the ASCII
216
+ // diagram adapter must render/reparse only once: committing a move and
217
+ // then a resize used to normalize the art between the two halves of one
218
+ // pointer gesture and could snap the node to a different grid position.
219
+ ctx.dispatch({ kind: 'resizeLayer', id: dragState.layerId, x, y, width, height });
209
220
  }
210
221
  (e.currentTarget as Element).releasePointerCapture?.(e.pointerId);
211
- dragState = null;
222
+ setDragState(ctx.interaction, null);
212
223
  },
213
224
 
214
225
  onDoubleClick(e, ctx) {
@@ -232,7 +243,8 @@ export const SelectTool: SceneTool = {
232
243
  }
233
244
  },
234
245
 
235
- renderOverlay(): JSX.Element | null {
246
+ renderOverlay(ctx): JSX.Element | null {
247
+ const dragState = getDragState(ctx.interaction);
236
248
  if (!dragState || dragState.kind !== 'marquee' || !dragState.movedPastThreshold) return null;
237
249
  const box = boxFromCorners(dragState.startV, dragState.currentV);
238
250
  return createElement('rect', {
@@ -247,7 +259,10 @@ export const SelectTool: SceneTool = {
247
259
  };
248
260
 
249
261
  /** Return the active move-drag offset (viewport units). Null if no drag, or if the drag is a marquee/resize. */
250
- export function getActiveMoveOffset(): { dx: number; dy: number } | null {
262
+ export function getActiveMoveOffset(
263
+ interaction: SceneInteractionState,
264
+ ): { dx: number; dy: number } | null {
265
+ const dragState = getDragState(interaction);
251
266
  if (!dragState || dragState.kind !== 'move' || !dragState.movedPastThreshold) return null;
252
267
  return {
253
268
  dx: dragState.currentV.x - dragState.startV.x,
@@ -256,10 +271,11 @@ export function getActiveMoveOffset(): { dx: number; dy: number } | null {
256
271
  }
257
272
 
258
273
  /** Return the live bounds for the layer currently being resized. */
259
- export function getActiveResize(): {
274
+ export function getActiveResize(interaction: SceneInteractionState): {
260
275
  layerId: string;
261
276
  bounds: { x: number; y: number; width: number; height: number };
262
277
  } | null {
278
+ const dragState = getDragState(interaction);
263
279
  if (!dragState || dragState.kind !== 'resize') return null;
264
280
  return { layerId: dragState.layerId, bounds: dragState.currentBounds };
265
281
  }
@@ -2,9 +2,8 @@
2
2
  * ShapeTool — drag to draw a rectangle or circle as a new ShapeLayer.
3
3
  *
4
4
  * Exposed as a factory so a host can pre-bind the shape kind and
5
- * stroke/fill defaults. Returns a fresh SceneTool instance the
6
- * factory pattern lets multiple Scene instances coexist without
7
- * sharing module-level drag state.
5
+ * stroke/fill defaults. Returns a fresh SceneTool instance so hosts can bind
6
+ * independent tool identities and defaults; gesture state belongs to Scene.
8
7
  */
9
8
 
10
9
  import { createElement, type JSX } from 'react';
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Plain-text selection conversions used by the toolbar's Convert menu.
3
+ * Keeping delimiter detection separate from the editor integrations makes
4
+ * the behavior identical in Monaco and Tiptap.
5
+ */
6
+
7
+ export type SelectionTableDelimiter = 'pipe' | 'comma' | 'tab' | 'multispace' | null;
8
+
9
+ export interface SelectionTable {
10
+ delimiter: SelectionTableDelimiter;
11
+ rows: string[][];
12
+ }
13
+
14
+ export interface SelectionTaskItem {
15
+ checked: boolean;
16
+ text: string;
17
+ }
18
+
19
+ type CandidateDelimiter = Exclude<SelectionTableDelimiter, null>;
20
+
21
+ /** Non-empty selected lines, trimmed without changing their order. */
22
+ export function selectionLines(text: string): string[] {
23
+ return text
24
+ .replace(/\r\n?/g, '\n')
25
+ .split('\n')
26
+ .map((line) => line.trim())
27
+ .filter((line) => line.length > 0);
28
+ }
29
+
30
+ function splitPipeLine(line: string): string[] {
31
+ let value = line.trim();
32
+ if (value.startsWith('|')) value = value.slice(1);
33
+ if (value.endsWith('|') && !value.endsWith('\\|')) value = value.slice(0, -1);
34
+
35
+ const cells: string[] = [];
36
+ let cell = '';
37
+ for (let i = 0; i < value.length; i++) {
38
+ const char = value[i];
39
+ if (char === '\\' && value[i + 1] === '|') {
40
+ cell += '|';
41
+ i++;
42
+ } else if (char === '|') {
43
+ cells.push(cell.trim());
44
+ cell = '';
45
+ } else {
46
+ cell += char;
47
+ }
48
+ }
49
+ cells.push(cell.trim());
50
+ return cells;
51
+ }
52
+
53
+ /** A small CSV-row parser so quoted commas remain inside their cell. */
54
+ function splitCommaLine(line: string): string[] {
55
+ const cells: string[] = [];
56
+ let cell = '';
57
+ let quoted = false;
58
+
59
+ for (let i = 0; i < line.length; i++) {
60
+ const char = line[i];
61
+ if (char === '"' && !quoted && cell.trim().length === 0) {
62
+ // A quote only opens CSV quoting at the start of a field. Literal
63
+ // quotes elsewhere (for example 5" screen) remain part of the value.
64
+ quoted = true;
65
+ cell = '';
66
+ } else if (char === '"' && quoted) {
67
+ if (line[i + 1] === '"') {
68
+ cell += '"';
69
+ i++;
70
+ } else {
71
+ quoted = false;
72
+ }
73
+ } else if (char === ',' && !quoted) {
74
+ cells.push(cell.trim());
75
+ cell = '';
76
+ } else {
77
+ cell += char;
78
+ }
79
+ }
80
+ if (quoted) return line.split(',').map((value) => value.trim());
81
+ cells.push(cell.trim());
82
+ return cells;
83
+ }
84
+
85
+ function splitLine(line: string, delimiter: CandidateDelimiter): string[] {
86
+ switch (delimiter) {
87
+ case 'pipe':
88
+ return splitPipeLine(line);
89
+ case 'comma':
90
+ return splitCommaLine(line);
91
+ case 'tab':
92
+ return line
93
+ .trim()
94
+ .split('\t')
95
+ .map((cell) => cell.trim());
96
+ case 'multispace':
97
+ return line
98
+ .trim()
99
+ .split(/[ \u00a0]{2,}/)
100
+ .map((cell) => cell.trim());
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Detect a delimiter only when at least two lines use it and every line
106
+ * produces the same number of columns. Inconsistent prose therefore falls
107
+ * back to a useful one-column table instead of losing text.
108
+ */
109
+ export function selectionToTable(text: string): SelectionTable {
110
+ const lines = selectionLines(text);
111
+ const candidates: CandidateDelimiter[] = ['pipe', 'comma', 'tab', 'multispace'];
112
+
113
+ if (lines.length >= 2) {
114
+ for (const delimiter of candidates) {
115
+ const rows = lines.map((line) => splitLine(line, delimiter));
116
+ const columnCount = rows[0]?.length ?? 0;
117
+ if (columnCount >= 2 && rows.every((row) => row.length === columnCount)) {
118
+ return { delimiter, rows };
119
+ }
120
+ }
121
+ }
122
+
123
+ return { delimiter: null, rows: lines.map((line) => [line]) };
124
+ }
125
+
126
+ function escapeTableCell(cell: string): string {
127
+ return cell.replace(/\|/g, '\\|');
128
+ }
129
+
130
+ /** Render a selection as a GFM table, using its first row as the header. */
131
+ export function selectionToTableMarkdown(text: string): string {
132
+ const { rows } = selectionToTable(text);
133
+ if (rows.length === 0) return '';
134
+
135
+ const formatRow = (row: string[]) => `| ${row.map(escapeTableCell).join(' | ')} |`;
136
+ const separator = rows[0].map(() => '---');
137
+ return [formatRow(rows[0]), formatRow(separator), ...rows.slice(1).map(formatRow)].join('\n');
138
+ }
139
+
140
+ /** Convert selected lines to task items, normalizing existing list markers. */
141
+ export function selectionToTaskItems(text: string): SelectionTaskItem[] {
142
+ return selectionLines(text).map((line) => {
143
+ const task = /^[-*+]\s+\[([ xX])\]\s*(.*)$/.exec(line);
144
+ if (task) return { checked: task[1].toLowerCase() === 'x', text: task[2].trim() };
145
+
146
+ const withoutListMarker = line.replace(/^(?:[-*+]\s+|\d+[.)]\s+)/, '');
147
+ return { checked: false, text: withoutListMarker.trim() };
148
+ });
149
+ }
150
+
151
+ export function selectionToTaskListMarkdown(text: string): string {
152
+ return selectionToTaskItems(text)
153
+ .map((item) => `- [${item.checked ? 'x' : ' '}] ${item.text}`)
154
+ .join('\n');
155
+ }
@@ -0,0 +1,79 @@
1
+ /* ASCII diagram fences in the WYSIWYG editor.
2
+ *
3
+ * A qualifying code fence is hidden behind its interactive canvas
4
+ * (AsciiDiagramWidget); toggling "Source" on the widget swaps the hide
5
+ * class for a monospace, non-wrapping view of the raw art. */
6
+
7
+ .squisq-wysiwyg-editor pre.squisq-ascii-fence-hidden {
8
+ display: none;
9
+ }
10
+
11
+ .squisq-wysiwyg-editor pre.squisq-ascii-fence-source {
12
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
13
+ /* prosemirror.css sets `.ProseMirror pre { white-space: pre-wrap }`,
14
+ which soft-wraps and visually scrambles column-aligned art. */
15
+ white-space: pre;
16
+ overflow-x: auto;
17
+ line-height: 1.2;
18
+ tab-size: 4;
19
+ }
20
+
21
+ .squisq-ascii-diagram-widget-host {
22
+ margin: 0.75rem 0 1.25rem;
23
+ }
24
+
25
+ .squisq-ascii-diagram-warnings {
26
+ margin-top: 4px;
27
+ font-size: 12px;
28
+ color: var(--squisq-theme-text-muted, #6b7280);
29
+ }
30
+
31
+ .squisq-ascii-diagram-warnings summary {
32
+ cursor: pointer;
33
+ user-select: none;
34
+ }
35
+
36
+ .squisq-ascii-diagram-warnings ul {
37
+ margin: 4px 0 0;
38
+ padding-left: 18px;
39
+ }
40
+
41
+ /* "Repair as diagram" affordance on broken box-art fences. The fence stays
42
+ * visible above this banner; clicking rewrites it to clean diagram art. */
43
+ .squisq-repair-banner {
44
+ display: flex;
45
+ align-items: center;
46
+ gap: 10px;
47
+ flex-wrap: wrap;
48
+ margin: 4px 0 1rem;
49
+ padding: 8px 12px;
50
+ border: 1px dashed var(--squisq-border, #d1d5db);
51
+ border-radius: 8px;
52
+ background: var(--squisq-input-bg, #f9fafb);
53
+ font-size: 13px;
54
+ }
55
+
56
+ .squisq-repair-banner-text {
57
+ color: var(--squisq-text-muted, #6b7280);
58
+ }
59
+
60
+ .squisq-repair-banner-btn {
61
+ cursor: pointer;
62
+ border: 1px solid var(--squisq-accent, #6366f1);
63
+ border-radius: 6px;
64
+ background: var(--squisq-accent, #6366f1);
65
+ color: #fff;
66
+ padding: 4px 12px;
67
+ font-size: 13px;
68
+ font-weight: 500;
69
+ line-height: 1.4;
70
+ white-space: nowrap;
71
+ }
72
+
73
+ .squisq-repair-banner-btn:hover {
74
+ filter: brightness(1.05);
75
+ }
76
+
77
+ .squisq-repair-banner-btn:active {
78
+ filter: brightness(0.95);
79
+ }