@bendyline/squisq-editor-react 2.0.1 → 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 (70) hide show
  1. package/README.md +10 -2
  2. package/dist/index.d.ts +505 -15
  3. package/dist/index.js +6035 -2283
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles/index.css +870 -40
  6. package/package.json +6 -5
  7. package/src/EditorContext.tsx +27 -0
  8. package/src/EditorShell.tsx +24 -0
  9. package/src/PreviewControls.tsx +20 -5
  10. package/src/PreviewPanel.tsx +5 -1
  11. package/src/RawEditor.tsx +12 -0
  12. package/src/RecorderEntry.tsx +3 -0
  13. package/src/Toolbar.tsx +315 -17
  14. package/src/WysiwygEditor.tsx +25 -11
  15. package/src/__tests__/buildPreviewDocContent.test.ts +17 -0
  16. package/src/__tests__/editorShellProps.test.tsx +65 -0
  17. package/src/__tests__/findMode.test.tsx +104 -0
  18. package/src/__tests__/findModel.test.ts +55 -0
  19. package/src/__tests__/markdownCodeFence.test.ts +45 -0
  20. package/src/__tests__/mediaReferences.test.ts +15 -0
  21. package/src/__tests__/previewControls.test.tsx +31 -0
  22. package/src/__tests__/tiptapBridge.test.ts +9 -0
  23. package/src/__tests__/toolbarSelectionConversion.test.tsx +26 -0
  24. package/src/__tests__/writeCanvasSettings.test.ts +15 -0
  25. package/src/asciiDiagram/__tests__/AsciiDiagramExtension.test.ts +20 -1
  26. package/src/buildPreviewDoc.ts +9 -7
  27. package/src/codeSnippet/CodeSnippetExtension.ts +205 -0
  28. package/src/codeSnippet/CodeSnippetWidget.tsx +109 -0
  29. package/src/codeSnippet/__tests__/CodeSnippetExtension.test.ts +95 -0
  30. package/src/codeSnippet/__tests__/codeSnippetLanguages.test.ts +50 -0
  31. package/src/codeSnippet/codeSnippetCommands.ts +21 -0
  32. package/src/codeSnippet/codeSnippetData.ts +43 -0
  33. package/src/codeSnippet/codeSnippetLanguages.ts +216 -0
  34. package/src/diagram/DiagramCanvas.tsx +1 -0
  35. package/src/find/FindHighlightExtension.ts +81 -0
  36. package/src/find/FindToolbar.tsx +314 -0
  37. package/src/find/findModel.ts +77 -0
  38. package/src/index.ts +89 -0
  39. package/src/markdownCodeFence.ts +72 -0
  40. package/src/mediaReferences.ts +5 -3
  41. package/src/mermaid/MermaidDiagramCanvas.tsx +693 -0
  42. package/src/mermaid/MermaidDiagramExtension.ts +243 -0
  43. package/src/mermaid/MermaidDiagramTypeThumbnail.tsx +184 -0
  44. package/src/mermaid/MermaidDiagramWidget.tsx +653 -0
  45. package/src/mermaid/MermaidShapePalette.tsx +245 -0
  46. package/src/mermaid/__tests__/MermaidDiagramExtension.test.ts +361 -0
  47. package/src/mermaid/__tests__/mermaidDiagramTypes.test.ts +35 -0
  48. package/src/mermaid/__tests__/mermaidRenderer.test.ts +49 -0
  49. package/src/mermaid/__tests__/mermaidSourceOps.test.ts +213 -0
  50. package/src/mermaid/__tests__/mermaidSyntax.test.ts +71 -0
  51. package/src/mermaid/mermaidCommands.ts +34 -0
  52. package/src/mermaid/mermaidData.ts +31 -0
  53. package/src/mermaid/mermaidDiagramTypes.ts +325 -0
  54. package/src/mermaid/mermaidModel.ts +31 -0
  55. package/src/mermaid/mermaidRenderer.ts +181 -0
  56. package/src/mermaid/mermaidShapes.ts +113 -0
  57. package/src/mermaid/mermaidSourceOps.ts +454 -0
  58. package/src/scene/Scene.tsx +46 -15
  59. package/src/scene/SceneBlockToolbar.tsx +29 -14
  60. package/src/scene/SceneViewControls.tsx +43 -0
  61. package/src/scene/__tests__/fitScale.test.ts +19 -0
  62. package/src/scene/__tests__/useScenePanZoom.test.ts +28 -1
  63. package/src/scene/fitScale.ts +17 -0
  64. package/src/scene/hooks/useScenePanZoom.ts +11 -4
  65. package/src/scene/scene.css +85 -3
  66. package/src/styles/code-snippet.css +76 -0
  67. package/src/styles/editor.css +322 -2
  68. package/src/styles/index.css +2 -0
  69. package/src/styles/mermaid-diagram.css +487 -0
  70. package/src/writeCanvasSettings.ts +30 -0
@@ -0,0 +1,487 @@
1
+ /* Lossless Mermaid-fence editor. Mermaid owns syntax/rendering; Squisq owns
2
+ * the same source/maximize/resize chrome used by the ASCII diagram canvas. */
3
+
4
+ .squisq-wysiwyg-editor pre.squisq-mermaid-fence-hidden {
5
+ display: none;
6
+ }
7
+
8
+ .squisq-wysiwyg-editor pre.squisq-mermaid-fence-source {
9
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
10
+ white-space: pre;
11
+ overflow-x: auto;
12
+ line-height: 1.35;
13
+ tab-size: 2;
14
+ }
15
+
16
+ .squisq-mermaid-diagram-widget-host {
17
+ margin: 0.75rem 0 1.25rem;
18
+ }
19
+
20
+ .squisq-mermaid-canvas {
21
+ position: relative;
22
+ width: 100%;
23
+ height: 100%;
24
+ background: var(--squisq-surface, #ffffff);
25
+ }
26
+
27
+ .squisq-mermaid-canvas-scroll {
28
+ box-sizing: border-box;
29
+ width: 100%;
30
+ height: 100%;
31
+ overflow: auto;
32
+ padding: 44px 24px 24px;
33
+ }
34
+
35
+ .squisq-mermaid-canvas-scroll[data-panning='true'] {
36
+ cursor: grabbing;
37
+ user-select: none;
38
+ }
39
+
40
+ .squisq-mermaid-svg {
41
+ position: relative;
42
+ min-height: 100%;
43
+ margin: 0 auto;
44
+ will-change: transform;
45
+ transition: width 120ms ease;
46
+ }
47
+
48
+ .squisq-mermaid-svg > div:first-child > svg {
49
+ display: block;
50
+ width: 100% !important;
51
+ height: auto !important;
52
+ max-width: none !important;
53
+ margin: 0 auto;
54
+ }
55
+
56
+ .squisq-mermaid-svg g.node {
57
+ cursor: pointer;
58
+ }
59
+
60
+ .squisq-mermaid-svg g.node:focus-visible {
61
+ outline: none;
62
+ filter: drop-shadow(0 0 4px #4f46e5);
63
+ }
64
+
65
+ .squisq-mermaid-svg g.squisq-mermaid-node-selected {
66
+ filter: drop-shadow(0 0 5px #4f46e5);
67
+ }
68
+
69
+ .squisq-mermaid-svg g.squisq-mermaid-connect-source {
70
+ filter: drop-shadow(0 0 6px #0d9488);
71
+ }
72
+
73
+ .squisq-mermaid-svg .edgePath,
74
+ .squisq-mermaid-svg path.flowchart-link,
75
+ .squisq-mermaid-svg .edgeLabel {
76
+ cursor: pointer;
77
+ }
78
+
79
+ .squisq-mermaid-svg path.squisq-mermaid-edge-hit-target {
80
+ fill: none;
81
+ stroke: rgba(0, 0, 0, 0.001);
82
+ stroke-width: 14px;
83
+ pointer-events: stroke;
84
+ cursor: pointer;
85
+ }
86
+
87
+ .squisq-mermaid-svg .squisq-mermaid-edge-selected {
88
+ filter: drop-shadow(0 0 3px #4f46e5);
89
+ }
90
+
91
+ /* Mermaid's default lineColor is #333, which is nearly invisible against the
92
+ * editor's dark canvas. Retheme default relationship strokes and arrowheads
93
+ * while leaving explicitly authored edge strokes (linkStyle/style) intact. */
94
+ .squisq-editor-shell[data-theme='dark'] .squisq-mermaid-svg {
95
+ --squisq-mermaid-edge: #cbd5e1;
96
+ --squisq-mermaid-edge-label-bg: rgba(15, 23, 42, 0.88);
97
+ }
98
+
99
+ .squisq-editor-shell[data-theme='dark']
100
+ .squisq-mermaid-svg
101
+ :is(
102
+ path.flowchart-link,
103
+ .edgePath .path,
104
+ .relationshipLine,
105
+ .messageLine0,
106
+ .messageLine1,
107
+ .transition
108
+ ):not([style*='stroke:']) {
109
+ stroke: var(--squisq-mermaid-edge) !important;
110
+ }
111
+
112
+ .squisq-editor-shell[data-theme='dark'] .squisq-mermaid-svg :is(.marker, .arrowheadPath),
113
+ .squisq-editor-shell[data-theme='dark'] .squisq-mermaid-svg marker :is(path, polygon) {
114
+ stroke: var(--squisq-mermaid-edge) !important;
115
+ }
116
+
117
+ .squisq-editor-shell[data-theme='dark']
118
+ .squisq-mermaid-svg
119
+ marker
120
+ :is(path, polygon):not([fill='none']):not([style*='fill:none']):not([style*='fill: none']) {
121
+ fill: var(--squisq-mermaid-edge) !important;
122
+ }
123
+
124
+ .squisq-editor-shell[data-theme='dark'] .squisq-mermaid-svg .edgeLabel,
125
+ .squisq-editor-shell[data-theme='dark'] .squisq-mermaid-svg .edgeLabel :is(span, p) {
126
+ color: var(--squisq-mermaid-edge) !important;
127
+ }
128
+
129
+ .squisq-editor-shell[data-theme='dark'] .squisq-mermaid-svg .edgeLabel p,
130
+ .squisq-editor-shell[data-theme='dark'] .squisq-mermaid-svg .edgeLabel rect {
131
+ background: var(--squisq-mermaid-edge-label-bg) !important;
132
+ fill: var(--squisq-mermaid-edge-label-bg) !important;
133
+ }
134
+
135
+ .squisq-mermaid-selection-actions {
136
+ position: absolute;
137
+ z-index: 12;
138
+ display: inline-flex;
139
+ gap: 2px;
140
+ padding: 3px;
141
+ border: 1px solid var(--squisq-border, #cbd5e1);
142
+ border-radius: 7px;
143
+ background: var(--squisq-surface, #ffffff);
144
+ box-shadow: 0 4px 14px rgba(15, 23, 42, 0.2);
145
+ transform: translate(-50%, calc(-100% - 8px));
146
+ }
147
+
148
+ .squisq-mermaid-selection-actions[data-placement='below'] {
149
+ transform: translate(-50%, 8px);
150
+ }
151
+
152
+ .squisq-mermaid-inline-rename {
153
+ position: absolute;
154
+ z-index: 14;
155
+ margin: 0;
156
+ transform: translate(-50%, -50%);
157
+ }
158
+
159
+ .squisq-mermaid-inline-rename input {
160
+ box-sizing: border-box;
161
+ width: 100%;
162
+ min-height: 36px;
163
+ padding: 6px 10px;
164
+ border: 2px solid var(--squisq-accent, #4f46e5);
165
+ border-radius: 7px;
166
+ outline: none;
167
+ background: var(--squisq-surface, #ffffff);
168
+ color: var(--squisq-text, #1e293b);
169
+ box-shadow:
170
+ 0 0 0 3px color-mix(in srgb, var(--squisq-accent, #4f46e5) 22%, transparent),
171
+ 0 6px 18px rgba(15, 23, 42, 0.2);
172
+ font: inherit;
173
+ font-size: 16px;
174
+ line-height: 1.3;
175
+ text-align: center;
176
+ }
177
+
178
+ .squisq-mermaid-inline-rename input:invalid {
179
+ border-color: #dc2626;
180
+ }
181
+
182
+ .squisq-mermaid-inline-rename span {
183
+ position: absolute;
184
+ top: calc(100% + 6px);
185
+ left: 50%;
186
+ padding: 3px 7px;
187
+ border: 1px solid var(--squisq-border, #cbd5e1);
188
+ border-radius: 5px;
189
+ background: var(--squisq-surface, #ffffff);
190
+ color: var(--squisq-text-muted, #64748b);
191
+ box-shadow: 0 2px 8px rgba(15, 23, 42, 0.14);
192
+ font-size: 11px;
193
+ line-height: 1.2;
194
+ white-space: nowrap;
195
+ transform: translateX(-50%);
196
+ pointer-events: none;
197
+ }
198
+
199
+ .squisq-mermaid-selection-actions button {
200
+ width: 28px;
201
+ height: 28px;
202
+ padding: 0;
203
+ border: 0;
204
+ border-radius: 4px;
205
+ background: transparent;
206
+ color: var(--squisq-text, #1e293b);
207
+ cursor: pointer;
208
+ }
209
+
210
+ .squisq-mermaid-selection-actions button:hover {
211
+ background: var(--squisq-surface-hover, #f1f5f9);
212
+ }
213
+
214
+ .squisq-mermaid-selection-actions button[data-danger='true'] {
215
+ color: #b91c1c;
216
+ }
217
+
218
+ .squisq-mermaid-connect-hint {
219
+ position: absolute;
220
+ top: 10px;
221
+ left: 50%;
222
+ z-index: 11;
223
+ padding: 6px 10px;
224
+ border-radius: 999px;
225
+ background: #312e81;
226
+ color: #fff;
227
+ font-size: 12px;
228
+ transform: translateX(-50%);
229
+ pointer-events: none;
230
+ }
231
+
232
+ .squisq-mermaid-shape-palette,
233
+ .squisq-mermaid-direction-picker {
234
+ position: absolute;
235
+ top: calc(100% + 6px);
236
+ left: 0;
237
+ z-index: 40;
238
+ display: flex;
239
+ box-sizing: border-box;
240
+ width: 360px;
241
+ max-height: min(520px, 70vh);
242
+ flex-direction: column;
243
+ padding: 8px;
244
+ border: 1px solid var(--squisq-border, #e2e8f0);
245
+ border-radius: 8px;
246
+ background: var(--squisq-surface, #ffffff);
247
+ color: var(--squisq-text, #1e293b);
248
+ box-shadow: 0 8px 24px rgba(15, 23, 42, 0.18);
249
+ }
250
+
251
+ .squisq-mermaid-shape-search {
252
+ box-sizing: border-box;
253
+ width: 100%;
254
+ margin-bottom: 6px;
255
+ padding: 7px 8px;
256
+ border: 1px solid var(--squisq-border, #cbd5e1);
257
+ border-radius: 6px;
258
+ background: var(--squisq-surface-subtle, #f8fafc);
259
+ color: inherit;
260
+ }
261
+
262
+ .squisq-mermaid-shape-scroll {
263
+ min-height: 0;
264
+ flex: 1 1 auto;
265
+ overflow-y: auto;
266
+ overscroll-behavior: contain;
267
+ }
268
+
269
+ .squisq-mermaid-shape-heading {
270
+ margin: 9px 3px 4px;
271
+ color: var(--squisq-text-muted, #64748b);
272
+ font-size: 11px;
273
+ font-weight: 700;
274
+ letter-spacing: 0.04em;
275
+ text-transform: uppercase;
276
+ }
277
+
278
+ .squisq-mermaid-shape-grid {
279
+ display: grid;
280
+ grid-template-columns: repeat(2, minmax(0, 1fr));
281
+ gap: 3px;
282
+ }
283
+
284
+ .squisq-mermaid-shape-item {
285
+ display: flex;
286
+ align-items: center;
287
+ gap: 6px;
288
+ min-width: 0;
289
+ padding: 5px;
290
+ border: 1px solid transparent;
291
+ border-radius: 6px;
292
+ background: transparent;
293
+ color: inherit;
294
+ cursor: pointer;
295
+ font-size: 12px;
296
+ text-align: left;
297
+ }
298
+
299
+ .squisq-mermaid-shape-item svg {
300
+ width: 34px;
301
+ height: 24px;
302
+ flex: 0 0 34px;
303
+ }
304
+
305
+ .squisq-mermaid-shape-item span {
306
+ overflow: hidden;
307
+ text-overflow: ellipsis;
308
+ white-space: nowrap;
309
+ }
310
+
311
+ .squisq-mermaid-shape-item:hover,
312
+ .squisq-mermaid-shape-item--selected {
313
+ border-color: #a5b4fc;
314
+ background: #eef2ff;
315
+ color: #3730a3;
316
+ }
317
+
318
+ .squisq-mermaid-direction-picker {
319
+ display: grid;
320
+ grid-template-columns: repeat(2, 1fr);
321
+ gap: 7px;
322
+ width: min(344px, calc(100vw - 24px));
323
+ padding: 10px;
324
+ overflow: auto;
325
+ overscroll-behavior: contain;
326
+ }
327
+
328
+ .squisq-mermaid-direction-heading {
329
+ display: flex;
330
+ grid-column: 1 / -1;
331
+ flex-direction: column;
332
+ gap: 2px;
333
+ padding: 1px 3px 5px;
334
+ }
335
+
336
+ .squisq-mermaid-direction-heading strong {
337
+ font-size: 13px;
338
+ }
339
+
340
+ .squisq-mermaid-direction-heading span {
341
+ color: var(--squisq-text-muted, #64748b);
342
+ font-size: 11px;
343
+ }
344
+
345
+ .squisq-mermaid-direction-card {
346
+ display: flex;
347
+ min-width: 0;
348
+ flex-direction: column;
349
+ align-items: stretch;
350
+ gap: 5px;
351
+ padding: 7px;
352
+ border: 1px solid var(--squisq-border, #cbd5e1);
353
+ border-radius: 7px;
354
+ background: transparent;
355
+ color: inherit;
356
+ cursor: pointer;
357
+ font: inherit;
358
+ text-align: left;
359
+ }
360
+
361
+ .squisq-mermaid-direction-card svg {
362
+ width: 100%;
363
+ height: 64px;
364
+ border-radius: 5px;
365
+ background: var(--squisq-surface-subtle, rgb(148 163 184 / 10%));
366
+ color: var(--squisq-text, #334155);
367
+ }
368
+
369
+ .squisq-mermaid-direction-card svg rect,
370
+ .squisq-mermaid-direction-card svg path {
371
+ fill: none;
372
+ stroke: currentcolor;
373
+ stroke-width: 1.8;
374
+ }
375
+
376
+ .squisq-mermaid-direction-card svg polygon,
377
+ .squisq-mermaid-direction-card svg text {
378
+ fill: currentcolor;
379
+ }
380
+
381
+ .squisq-mermaid-direction-card svg text {
382
+ font-size: 9px;
383
+ text-anchor: middle;
384
+ }
385
+
386
+ .squisq-mermaid-direction-copy {
387
+ display: flex;
388
+ min-width: 0;
389
+ flex-direction: column;
390
+ gap: 1px;
391
+ }
392
+
393
+ .squisq-mermaid-direction-copy strong {
394
+ overflow: hidden;
395
+ font-size: 12px;
396
+ text-overflow: ellipsis;
397
+ white-space: nowrap;
398
+ }
399
+
400
+ .squisq-mermaid-direction-copy small {
401
+ color: var(--squisq-text-muted, #64748b);
402
+ font-size: 10px;
403
+ }
404
+
405
+ .squisq-mermaid-direction-card:hover,
406
+ .squisq-mermaid-direction-card[aria-pressed='true'] {
407
+ border-color: #818cf8;
408
+ background: #eef2ff;
409
+ color: #3730a3;
410
+ }
411
+
412
+ .squisq-mermaid-direction-card:hover svg,
413
+ .squisq-mermaid-direction-card[aria-pressed='true'] svg {
414
+ color: currentcolor;
415
+ }
416
+
417
+ .squisq-editor-shell[data-theme='dark'] .squisq-mermaid-direction-card svg {
418
+ color: #cbd5e1;
419
+ }
420
+
421
+ .squisq-editor-shell[data-theme='dark'] .squisq-mermaid-connect-hint,
422
+ .squisq-editor-shell[data-theme='dark'] .squisq-mermaid-shape-item:hover,
423
+ .squisq-editor-shell[data-theme='dark'] .squisq-mermaid-shape-item--selected,
424
+ .squisq-editor-shell[data-theme='dark'] .squisq-mermaid-direction-card:hover,
425
+ .squisq-editor-shell[data-theme='dark'] .squisq-mermaid-direction-card[aria-pressed='true'] {
426
+ border-color: #6366f1;
427
+ background: #312e81;
428
+ color: #eef2ff;
429
+ }
430
+
431
+ .squisq-mermaid-edit-notice {
432
+ margin-top: 6px;
433
+ padding: 7px 9px;
434
+ border: 1px solid #fbbf24;
435
+ border-radius: 6px;
436
+ background: #fffbeb;
437
+ color: #92400e;
438
+ font-size: 12px;
439
+ }
440
+
441
+ .squisq-editor-shell[data-theme='dark'] .squisq-mermaid-edit-notice {
442
+ border-color: #92400e;
443
+ background: #451a03;
444
+ color: #fde68a;
445
+ }
446
+
447
+ .squisq-mermaid-status,
448
+ .squisq-mermaid-error {
449
+ display: flex;
450
+ min-height: 180px;
451
+ align-items: center;
452
+ justify-content: center;
453
+ color: var(--squisq-text-muted, #64748b);
454
+ text-align: center;
455
+ }
456
+
457
+ .squisq-mermaid-error {
458
+ box-sizing: border-box;
459
+ flex-direction: column;
460
+ gap: 8px;
461
+ align-items: flex-start;
462
+ justify-content: flex-start;
463
+ padding: 16px;
464
+ border: 1px solid #fca5a5;
465
+ border-radius: 8px;
466
+ background: #fef2f2;
467
+ color: #991b1b;
468
+ text-align: left;
469
+ }
470
+
471
+ .squisq-mermaid-error pre {
472
+ width: 100%;
473
+ max-height: 160px;
474
+ overflow: auto;
475
+ margin: 0;
476
+ padding: 8px;
477
+ white-space: pre-wrap;
478
+ background: transparent;
479
+ color: inherit;
480
+ font-size: 12px;
481
+ }
482
+
483
+ .squisq-editor-shell[data-theme='dark'] .squisq-mermaid-error {
484
+ border-color: #7f1d1d;
485
+ background: #450a0a;
486
+ color: #fecaca;
487
+ }
@@ -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
+ }