@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
|
@@ -16,8 +16,19 @@ import type { DiagramEdgeAnchor, Layer } from '@bendyline/squisq/schemas';
|
|
|
16
16
|
export type SceneCommand =
|
|
17
17
|
/** Commit a layer's new top-left position (in viewport units). */
|
|
18
18
|
| { kind: 'moveLayer'; id: string; x: number; y: number }
|
|
19
|
-
/**
|
|
20
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Commit a layer's new size (in viewport units). `x`/`y` carry the final
|
|
21
|
+
* top-left for west/north handle drags so hosts can persist the whole
|
|
22
|
+
* resize atomically instead of reparsing between a move and a size write.
|
|
23
|
+
*/
|
|
24
|
+
| {
|
|
25
|
+
kind: 'resizeLayer';
|
|
26
|
+
id: string;
|
|
27
|
+
width: number;
|
|
28
|
+
height: number;
|
|
29
|
+
x?: number;
|
|
30
|
+
y?: number;
|
|
31
|
+
}
|
|
21
32
|
/** Append a new layer to the scene. */
|
|
22
33
|
| { kind: 'addLayer'; layer: Layer }
|
|
23
34
|
/** Remove a layer by id. */
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface SceneSize {
|
|
2
|
+
width: number;
|
|
3
|
+
height: number;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Scale content so it is wholly visible, without enlarging it past the
|
|
8
|
+
* supplied maximum (100% by default). Invalid or not-yet-measured dimensions
|
|
9
|
+
* fall back to 100%.
|
|
10
|
+
*/
|
|
11
|
+
export function calculateFitScale(content: SceneSize, container: SceneSize, maxScale = 1): number {
|
|
12
|
+
if (content.width <= 0 || content.height <= 0 || container.width <= 0 || container.height <= 0) {
|
|
13
|
+
return Math.min(1, maxScale);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return Math.min(maxScale, container.width / content.width, container.height / content.height);
|
|
17
|
+
}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { useCallback, useRef, useState } from 'react';
|
|
16
|
+
import { calculateFitScale } from '../fitScale';
|
|
16
17
|
|
|
17
18
|
export interface SceneTransform {
|
|
18
19
|
/** Translation in screen pixels. */
|
|
@@ -25,7 +26,7 @@ export interface SceneTransform {
|
|
|
25
26
|
export const IDENTITY_TRANSFORM: SceneTransform = Object.freeze({ tx: 0, ty: 0, scale: 1 });
|
|
26
27
|
|
|
27
28
|
/** Clamp scale to a sensible range so a single accidental wheel doesn't escape. */
|
|
28
|
-
const MIN_SCALE = 0.
|
|
29
|
+
const MIN_SCALE = 0.02;
|
|
29
30
|
const MAX_SCALE = 8;
|
|
30
31
|
|
|
31
32
|
export interface UseScenePanZoomResult {
|
|
@@ -45,13 +46,14 @@ export interface UseScenePanZoomResult {
|
|
|
45
46
|
reset: () => void;
|
|
46
47
|
/**
|
|
47
48
|
* Fit a viewport-unit bounding box into a container of `containerSize`
|
|
48
|
-
* screen pixels, with optional padding (also in screen pixels)
|
|
49
|
-
* the box is empty, no-ops.
|
|
49
|
+
* screen pixels, with optional padding (also in screen pixels) and an
|
|
50
|
+
* optional maximum scale. When the box is empty, no-ops.
|
|
50
51
|
*/
|
|
51
52
|
fitBox: (
|
|
52
53
|
box: { x: number; y: number; width: number; height: number },
|
|
53
54
|
containerSize: { width: number; height: number },
|
|
54
55
|
padding?: number,
|
|
56
|
+
maxScale?: number,
|
|
55
57
|
) => void;
|
|
56
58
|
/** Convert a screen-pixel point (container-relative) → viewport units. */
|
|
57
59
|
screenToViewport: (sx: number, sy: number) => { x: number; y: number };
|
|
@@ -104,12 +106,17 @@ export function useScenePanZoom(
|
|
|
104
106
|
box: { x: number; y: number; width: number; height: number },
|
|
105
107
|
containerSize: { width: number; height: number },
|
|
106
108
|
padding = 24,
|
|
109
|
+
maxScale = MAX_SCALE,
|
|
107
110
|
) => {
|
|
108
111
|
if (box.width <= 0 || box.height <= 0) return;
|
|
109
112
|
if (containerSize.width <= 0 || containerSize.height <= 0) return;
|
|
110
113
|
const availW = Math.max(1, containerSize.width - padding * 2);
|
|
111
114
|
const availH = Math.max(1, containerSize.height - padding * 2);
|
|
112
|
-
const scale =
|
|
115
|
+
const scale = calculateFitScale(
|
|
116
|
+
{ width: box.width, height: box.height },
|
|
117
|
+
{ width: availW, height: availH },
|
|
118
|
+
Math.min(MAX_SCALE, Math.max(Number.EPSILON, maxScale)),
|
|
119
|
+
);
|
|
113
120
|
// Center the box in the container.
|
|
114
121
|
const scaledW = box.width * scale;
|
|
115
122
|
const scaledH = box.height * scale;
|
package/src/scene/scene.css
CHANGED
|
@@ -266,6 +266,18 @@
|
|
|
266
266
|
border-color: #cbd5e1;
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
+
.squisq-scene-action--active,
|
|
270
|
+
.squisq-scene-action--active:hover:not(:disabled) {
|
|
271
|
+
background: #e0e7ff;
|
|
272
|
+
border-color: #4f46e5;
|
|
273
|
+
color: #4338ca;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.squisq-scene-action-popover-anchor {
|
|
277
|
+
position: relative;
|
|
278
|
+
display: inline-flex;
|
|
279
|
+
}
|
|
280
|
+
|
|
269
281
|
.squisq-scene-action:focus-visible {
|
|
270
282
|
outline: 2px solid #6366f1;
|
|
271
283
|
outline-offset: 1px;
|
|
@@ -322,7 +334,9 @@
|
|
|
322
334
|
}
|
|
323
335
|
|
|
324
336
|
.squisq-editor-shell[data-theme='dark'] .squisq-scene-tool--active,
|
|
325
|
-
.squisq-editor-shell[data-theme='dark'] .squisq-scene-tool--active:hover
|
|
337
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-tool--active:hover,
|
|
338
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-action--active,
|
|
339
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-action--active:hover:not(:disabled) {
|
|
326
340
|
background: #312e81;
|
|
327
341
|
border-color: #818cf8;
|
|
328
342
|
color: #f8fafc;
|
|
@@ -350,7 +364,8 @@
|
|
|
350
364
|
padding: 12px;
|
|
351
365
|
}
|
|
352
366
|
.squisq-scene-block-max > .squisq-diagram-canvas,
|
|
353
|
-
.squisq-scene-block-max > .squisq-scene-stage
|
|
367
|
+
.squisq-scene-block-max > .squisq-scene-stage,
|
|
368
|
+
.squisq-scene-block-max > .squisq-mermaid-canvas {
|
|
354
369
|
flex: 1;
|
|
355
370
|
min-width: 0;
|
|
356
371
|
min-height: 0;
|
|
@@ -403,6 +418,12 @@
|
|
|
403
418
|
right: calc(100% + 6px);
|
|
404
419
|
}
|
|
405
420
|
|
|
421
|
+
.squisq-scene-side-toolbar .squisq-scene-action-popover-anchor > [role='dialog'] {
|
|
422
|
+
top: 0;
|
|
423
|
+
left: auto;
|
|
424
|
+
right: calc(100% + 6px);
|
|
425
|
+
}
|
|
426
|
+
|
|
406
427
|
/* ── Narrow-width fallback bar ─────────────────────────────────
|
|
407
428
|
When SceneSideToolbar measures that the right gutter can't hold the 180px
|
|
408
429
|
column, it renders the same toolbar as an in-flow horizontal bar above the
|
|
@@ -420,7 +441,68 @@
|
|
|
420
441
|
margin: 0;
|
|
421
442
|
}
|
|
422
443
|
|
|
423
|
-
/* ──
|
|
444
|
+
/* ── View controls + maximize ─────────────────────────────── */
|
|
445
|
+
|
|
446
|
+
.squisq-scene-view-controls {
|
|
447
|
+
position: absolute;
|
|
448
|
+
top: 8px;
|
|
449
|
+
left: 8px;
|
|
450
|
+
z-index: 10;
|
|
451
|
+
display: inline-flex;
|
|
452
|
+
align-items: center;
|
|
453
|
+
overflow: hidden;
|
|
454
|
+
border: 1px solid var(--squisq-border, #e2e8f0);
|
|
455
|
+
border-radius: 6px;
|
|
456
|
+
background: var(--squisq-surface, #ffffff);
|
|
457
|
+
color: var(--squisq-text, #1e293b);
|
|
458
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
.squisq-scene-view-controls button,
|
|
462
|
+
.squisq-scene-view-scale {
|
|
463
|
+
box-sizing: border-box;
|
|
464
|
+
min-width: 32px;
|
|
465
|
+
height: 32px;
|
|
466
|
+
padding: 0 8px;
|
|
467
|
+
border: 0;
|
|
468
|
+
border-right: 1px solid var(--squisq-border, #e2e8f0);
|
|
469
|
+
background: transparent;
|
|
470
|
+
color: inherit;
|
|
471
|
+
font: inherit;
|
|
472
|
+
font-size: 12px;
|
|
473
|
+
line-height: 32px;
|
|
474
|
+
text-align: center;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
.squisq-scene-view-controls button {
|
|
478
|
+
cursor: pointer;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
.squisq-scene-view-controls button:last-child {
|
|
482
|
+
border-right: 0;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
.squisq-scene-view-controls button:hover,
|
|
486
|
+
.squisq-scene-view-fit[data-active='true'] {
|
|
487
|
+
background: var(--squisq-surface-hover, #f1f5f9);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
.squisq-scene-view-fit[data-active='true'] {
|
|
491
|
+
color: #4338ca;
|
|
492
|
+
font-weight: 600;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-view-controls {
|
|
496
|
+
background: #111827;
|
|
497
|
+
border-color: #475569;
|
|
498
|
+
color: #e5e7eb;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-view-controls button:hover,
|
|
502
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-view-fit[data-active='true'] {
|
|
503
|
+
background: #312e81;
|
|
504
|
+
color: #f8fafc;
|
|
505
|
+
}
|
|
424
506
|
|
|
425
507
|
.squisq-scene-maximize-btn {
|
|
426
508
|
position: absolute;
|
|
@@ -212,11 +212,11 @@ export const SelectTool: SceneTool = {
|
|
|
212
212
|
}
|
|
213
213
|
} else if (dragState.kind === 'resize') {
|
|
214
214
|
const { x, y, width, height } = dragState.currentBounds;
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
|
|
219
|
-
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 });
|
|
220
220
|
}
|
|
221
221
|
(e.currentTarget as Element).releasePointerCapture?.(e.pointerId);
|
|
222
222
|
setDragState(ctx.interaction, null);
|
|
@@ -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
|
+
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
.squisq-ascii-timeline-editor {
|
|
15
15
|
--squisq-ascii-timeline-accent: var(--squisq-theme-primary, #6366f1);
|
|
16
|
-
--squisq-ascii-timeline-label-width:
|
|
16
|
+
--squisq-ascii-timeline-label-width: 160px;
|
|
17
17
|
border: 1px solid var(--squisq-border, #e2e8f0);
|
|
18
18
|
border-radius: 10px;
|
|
19
19
|
background: var(--squisq-input-bg, #fff);
|
|
@@ -44,6 +44,33 @@
|
|
|
44
44
|
color: var(--squisq-text-muted, #64748b);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
.squisq-ascii-timeline-header-actions {
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
justify-content: flex-end;
|
|
51
|
+
gap: 10px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.squisq-ascii-timeline-header-actions button {
|
|
55
|
+
display: inline-flex;
|
|
56
|
+
align-items: center;
|
|
57
|
+
gap: 5px;
|
|
58
|
+
padding: 4px 8px;
|
|
59
|
+
border: 1px solid var(--squisq-border, #cbd5e1);
|
|
60
|
+
border-radius: 5px;
|
|
61
|
+
background: var(--squisq-input-bg, #fff);
|
|
62
|
+
color: var(--squisq-ascii-timeline-accent);
|
|
63
|
+
font: inherit;
|
|
64
|
+
font-size: 12px;
|
|
65
|
+
cursor: pointer;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.squisq-ascii-timeline-header-actions button:hover,
|
|
69
|
+
.squisq-ascii-timeline-header-actions button:focus-visible {
|
|
70
|
+
border-color: var(--squisq-ascii-timeline-accent);
|
|
71
|
+
outline: 2px solid color-mix(in srgb, var(--squisq-ascii-timeline-accent) 22%, transparent);
|
|
72
|
+
}
|
|
73
|
+
|
|
47
74
|
.squisq-ascii-timeline-canvas {
|
|
48
75
|
position: relative;
|
|
49
76
|
min-width: 600px;
|
|
@@ -72,9 +99,70 @@
|
|
|
72
99
|
white-space: nowrap;
|
|
73
100
|
}
|
|
74
101
|
|
|
75
|
-
.squisq-ascii-timeline-track-
|
|
102
|
+
.squisq-ascii-timeline-track-name {
|
|
103
|
+
min-width: 0;
|
|
104
|
+
flex: 1 1 auto;
|
|
105
|
+
padding: 3px 4px;
|
|
76
106
|
overflow: hidden;
|
|
107
|
+
border: 1px solid transparent;
|
|
108
|
+
border-radius: 4px;
|
|
109
|
+
background: transparent;
|
|
110
|
+
color: inherit;
|
|
111
|
+
font: inherit;
|
|
112
|
+
text-align: right;
|
|
77
113
|
text-overflow: ellipsis;
|
|
114
|
+
white-space: nowrap;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
button.squisq-ascii-timeline-track-name {
|
|
118
|
+
cursor: text;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
button.squisq-ascii-timeline-track-name:hover,
|
|
122
|
+
button.squisq-ascii-timeline-track-name:focus-visible {
|
|
123
|
+
border-color: var(--squisq-border, #cbd5e1);
|
|
124
|
+
background: var(--squisq-input-bg, #fff);
|
|
125
|
+
outline: none;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.squisq-ascii-timeline-track-name-input {
|
|
129
|
+
width: 100%;
|
|
130
|
+
min-width: 0;
|
|
131
|
+
flex: 1 1 auto;
|
|
132
|
+
padding: 3px 4px;
|
|
133
|
+
border: 1px solid var(--squisq-ascii-timeline-accent);
|
|
134
|
+
border-radius: 4px;
|
|
135
|
+
background: var(--squisq-input-bg, #fff);
|
|
136
|
+
color: var(--squisq-text, #1e293b);
|
|
137
|
+
font: inherit;
|
|
138
|
+
text-align: right;
|
|
139
|
+
outline: 2px solid color-mix(in srgb, var(--squisq-ascii-timeline-accent) 22%, transparent);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.squisq-ascii-timeline-track-delete {
|
|
143
|
+
display: grid;
|
|
144
|
+
width: 22px;
|
|
145
|
+
height: 22px;
|
|
146
|
+
flex: 0 0 22px;
|
|
147
|
+
place-items: center;
|
|
148
|
+
padding: 0;
|
|
149
|
+
border: 0;
|
|
150
|
+
border-radius: 4px;
|
|
151
|
+
background: transparent;
|
|
152
|
+
color: var(--squisq-text-muted, #64748b);
|
|
153
|
+
cursor: pointer;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.squisq-ascii-timeline-track-delete:hover,
|
|
157
|
+
.squisq-ascii-timeline-track-delete:focus-visible {
|
|
158
|
+
background: color-mix(in srgb, #ef4444 12%, transparent);
|
|
159
|
+
color: #dc2626;
|
|
160
|
+
outline: 2px solid color-mix(in srgb, #ef4444 25%, transparent);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.squisq-ascii-timeline-track-delete:disabled {
|
|
164
|
+
opacity: 0.35;
|
|
165
|
+
cursor: not-allowed;
|
|
78
166
|
}
|
|
79
167
|
|
|
80
168
|
.squisq-ascii-timeline-track-add {
|
|
@@ -95,7 +183,8 @@
|
|
|
95
183
|
}
|
|
96
184
|
|
|
97
185
|
.squisq-ascii-timeline-track-add:hover,
|
|
98
|
-
.squisq-ascii-timeline-track-add:focus-visible
|
|
186
|
+
.squisq-ascii-timeline-track-add:focus-visible,
|
|
187
|
+
.squisq-ascii-timeline-track-add[aria-pressed='true'] {
|
|
99
188
|
border-color: var(--squisq-ascii-timeline-accent);
|
|
100
189
|
outline: 2px solid color-mix(in srgb, var(--squisq-ascii-timeline-accent) 22%, transparent);
|
|
101
190
|
}
|
|
@@ -105,6 +194,10 @@
|
|
|
105
194
|
height: 96px;
|
|
106
195
|
min-width: 420px;
|
|
107
196
|
outline: none;
|
|
197
|
+
cursor: default;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.squisq-ascii-timeline-rail--adding {
|
|
108
201
|
cursor: crosshair;
|
|
109
202
|
}
|
|
110
203
|
|
|
@@ -460,7 +553,7 @@
|
|
|
460
553
|
|
|
461
554
|
@media (max-width: 760px) {
|
|
462
555
|
.squisq-ascii-timeline-editor {
|
|
463
|
-
--squisq-ascii-timeline-label-width:
|
|
556
|
+
--squisq-ascii-timeline-label-width: 132px;
|
|
464
557
|
}
|
|
465
558
|
|
|
466
559
|
.squisq-ascii-timeline-inspector {
|
|
@@ -475,6 +568,10 @@
|
|
|
475
568
|
.squisq-ascii-timeline-delete {
|
|
476
569
|
grid-column: 2 / 3;
|
|
477
570
|
}
|
|
571
|
+
|
|
572
|
+
.squisq-ascii-timeline-header-actions small {
|
|
573
|
+
display: none;
|
|
574
|
+
}
|
|
478
575
|
}
|
|
479
576
|
|
|
480
577
|
@media (prefers-reduced-motion: reduce) {
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/* Monaco inset for ordinary explicit-language Markdown code fences. */
|
|
2
|
+
|
|
3
|
+
.squisq-wysiwyg-editor pre.squisq-code-snippet-fence-hidden {
|
|
4
|
+
display: none;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.squisq-code-snippet-widget-host {
|
|
8
|
+
margin: 0.75rem 0 1.25rem;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.squisq-code-snippet-shell {
|
|
12
|
+
box-sizing: border-box;
|
|
13
|
+
display: flex;
|
|
14
|
+
width: 100%;
|
|
15
|
+
height: 240px;
|
|
16
|
+
min-height: 128px;
|
|
17
|
+
max-height: 70vh;
|
|
18
|
+
flex-direction: column;
|
|
19
|
+
overflow: hidden;
|
|
20
|
+
resize: vertical;
|
|
21
|
+
border: 1px solid var(--squisq-border, #d1d5db);
|
|
22
|
+
border-radius: 8px;
|
|
23
|
+
background: var(--squisq-surface, #ffffff);
|
|
24
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.squisq-code-snippet-header {
|
|
28
|
+
box-sizing: border-box;
|
|
29
|
+
display: flex;
|
|
30
|
+
min-height: 34px;
|
|
31
|
+
align-items: center;
|
|
32
|
+
justify-content: space-between;
|
|
33
|
+
gap: 12px;
|
|
34
|
+
padding: 0 10px;
|
|
35
|
+
border-bottom: 1px solid var(--squisq-border, #d1d5db);
|
|
36
|
+
background: var(--squisq-surface-muted, #f8fafc);
|
|
37
|
+
color: var(--squisq-text-muted, #64748b);
|
|
38
|
+
font-family: var(--squisq-ux-font, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);
|
|
39
|
+
font-size: 12px;
|
|
40
|
+
user-select: none;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.squisq-code-snippet-title {
|
|
44
|
+
display: inline-flex;
|
|
45
|
+
min-width: 0;
|
|
46
|
+
align-items: center;
|
|
47
|
+
gap: 7px;
|
|
48
|
+
color: var(--squisq-text, #334155);
|
|
49
|
+
font-weight: 600;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.squisq-code-snippet-header code {
|
|
53
|
+
overflow: hidden;
|
|
54
|
+
color: inherit;
|
|
55
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
|
|
56
|
+
text-overflow: ellipsis;
|
|
57
|
+
white-space: nowrap;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.squisq-code-snippet-editor {
|
|
61
|
+
position: relative;
|
|
62
|
+
min-height: 0;
|
|
63
|
+
flex: 1;
|
|
64
|
+
overflow: hidden;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.squisq-code-snippet-loading {
|
|
68
|
+
display: flex;
|
|
69
|
+
width: 100%;
|
|
70
|
+
height: 100%;
|
|
71
|
+
align-items: center;
|
|
72
|
+
justify-content: center;
|
|
73
|
+
color: var(--squisq-text-muted, #64748b);
|
|
74
|
+
font-family: var(--squisq-ux-font, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);
|
|
75
|
+
font-size: 12px;
|
|
76
|
+
}
|