@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
@@ -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
- /** Commit a layer's new size (in viewport units). */
20
- | { kind: 'resizeLayer'; id: string; width: number; height: number }
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.1;
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). When
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 = clamp(Math.min(availW / box.width, availH / box.height), MIN_SCALE, MAX_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;
@@ -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
- /* ── Maximize button ──────────────────────────────────────── */
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
- // Resize translates into a move (if origin shifted) + a size change.
216
- // We dispatch both so a single grab on a corner can change all four
217
- // fields atomically from the host's point of view.
218
- ctx.dispatch({ kind: 'moveLayer', id: dragState.layerId, x, y });
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: 120px;
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-label > span {
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: 88px;
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
+ }