@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,19 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { calculateFitScale } from '../fitScale';
3
+
4
+ describe('calculateFitScale', () => {
5
+ it('does not enlarge content that already fits', () => {
6
+ expect(calculateFitScale({ width: 200, height: 100 }, { width: 800, height: 600 })).toBe(1);
7
+ });
8
+
9
+ it('uses the smaller axis scale so every shape remains visible', () => {
10
+ expect(calculateFitScale({ width: 1_000, height: 200 }, { width: 500, height: 300 })).toBe(0.5);
11
+ expect(calculateFitScale({ width: 200, height: 1_000 }, { width: 500, height: 250 })).toBe(
12
+ 0.25,
13
+ );
14
+ });
15
+
16
+ it('falls back to 100% until dimensions are measurable', () => {
17
+ expect(calculateFitScale({ width: 0, height: 100 }, { width: 500, height: 300 })).toBe(1);
18
+ });
19
+ });
@@ -44,7 +44,7 @@ describe('useScenePanZoom', () => {
44
44
  act(() => result.current.zoomAt(100, 0, 0));
45
45
  expect(result.current.transform.scale).toBeLessThanOrEqual(8);
46
46
  act(() => result.current.zoomAt(0.0001, 0, 0));
47
- expect(result.current.transform.scale).toBeGreaterThanOrEqual(0.1);
47
+ expect(result.current.transform.scale).toBeGreaterThanOrEqual(0.02);
48
48
  });
49
49
 
50
50
  it('reset returns to identity', () => {
@@ -88,6 +88,33 @@ describe('useScenePanZoom', () => {
88
88
  expect(result.current.transform.scale).toBeCloseTo(1.6, 5);
89
89
  });
90
90
 
91
+ it('fitBox can cap fit at 100% without enlarging small content', () => {
92
+ const { result } = renderHook(() => useScenePanZoom());
93
+ act(() =>
94
+ result.current.fitBox(
95
+ { x: 25, y: 50, width: 100, height: 80 },
96
+ { width: 800, height: 600 },
97
+ 20,
98
+ 1,
99
+ ),
100
+ );
101
+ expect(result.current.transform.scale).toBe(1);
102
+ expect(result.current.viewportToScreen(75, 90)).toEqual({ x: 400, y: 300 });
103
+ });
104
+
105
+ it('fitBox may shrink below the manual zoom floor to keep all content visible', () => {
106
+ const { result } = renderHook(() => useScenePanZoom());
107
+ act(() =>
108
+ result.current.fitBox(
109
+ { x: 0, y: 0, width: 10_000, height: 10_000 },
110
+ { width: 400, height: 300 },
111
+ 20,
112
+ 1,
113
+ ),
114
+ );
115
+ expect(result.current.transform.scale).toBeCloseTo(0.026, 5);
116
+ });
117
+
91
118
  it('screenToViewport and viewportToScreen are inverses', () => {
92
119
  const { result } = renderHook(() => useScenePanZoom());
93
120
  act(() => {
@@ -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;
@@ -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
+ }
@@ -359,6 +359,119 @@
359
359
  min-width: 0;
360
360
  }
361
361
 
362
+ /* Host-triggered Find mode. It occupies the toolbar's flexible middle lane,
363
+ leaving the view tabs on its left and the normal right-side host/settings
364
+ controls on its right. */
365
+ .squisq-find-toolbar {
366
+ display: flex;
367
+ align-items: center;
368
+ flex: 1 1 auto;
369
+ gap: 3px;
370
+ min-width: 180px;
371
+ padding: 4px 6px;
372
+ }
373
+
374
+ .squisq-find-field {
375
+ display: flex;
376
+ align-items: center;
377
+ flex: 0 1 420px;
378
+ min-width: 130px;
379
+ height: 28px;
380
+ border: 1px solid #9ca3af;
381
+ border-radius: 5px;
382
+ background: #ffffff;
383
+ color: #6b7280;
384
+ box-sizing: border-box;
385
+ }
386
+
387
+ .squisq-find-field:focus-within {
388
+ border-color: #2563eb;
389
+ box-shadow: 0 0 0 1px #2563eb;
390
+ }
391
+
392
+ .squisq-find-field > i {
393
+ margin-left: 8px;
394
+ color: #6b7280;
395
+ font-size: 11px;
396
+ }
397
+
398
+ .squisq-find-input {
399
+ flex: 1 1 auto;
400
+ min-width: 32px;
401
+ height: 100%;
402
+ padding: 0 7px;
403
+ border: 0;
404
+ outline: 0;
405
+ background: transparent;
406
+ color: #111827;
407
+ font: inherit;
408
+ font-size: 12.5px;
409
+ }
410
+
411
+ .squisq-find-input::-webkit-search-cancel-button {
412
+ display: none;
413
+ }
414
+
415
+ .squisq-find-count {
416
+ flex: 0 0 auto;
417
+ padding-right: 7px;
418
+ color: #6b7280;
419
+ font-size: 11px;
420
+ white-space: nowrap;
421
+ }
422
+
423
+ .squisq-find-button {
424
+ display: inline-flex;
425
+ align-items: center;
426
+ justify-content: center;
427
+ width: 28px;
428
+ height: 28px;
429
+ padding: 0;
430
+ border: 0;
431
+ border-radius: 4px;
432
+ background: transparent;
433
+ color: var(--squisq-toolbar-icon);
434
+ cursor: pointer;
435
+ }
436
+
437
+ .squisq-find-button:hover:not(:disabled) {
438
+ background: rgba(0, 0, 0, 0.08);
439
+ color: #111827;
440
+ }
441
+
442
+ .squisq-find-button:disabled {
443
+ opacity: 0.35;
444
+ cursor: default;
445
+ }
446
+
447
+ .squisq-find-close {
448
+ margin-left: 2px;
449
+ }
450
+
451
+ /* Shared by Tiptap decorations, Monaco inline decorations, and the fallback
452
+ used when a preview webview lacks the CSS Custom Highlight API. */
453
+ .squisq-find-match {
454
+ border-radius: 2px;
455
+ background: #fde68a !important;
456
+ color: #111827 !important;
457
+ }
458
+
459
+ .squisq-find-match--selected {
460
+ background: #f59e0b !important;
461
+ box-shadow: 0 0 0 1px #b45309;
462
+ }
463
+
464
+ @container squisq-toolbar (max-width: 680px) {
465
+ .squisq-find-toolbar {
466
+ min-width: 120px;
467
+ padding-right: 2px;
468
+ }
469
+
470
+ .squisq-find-count {
471
+ display: none;
472
+ }
473
+ }
474
+
362
475
  /* Contextual groups (template/transition pickers, table controls) sit at the
363
476
  end of the actions row. When measurement marks one as clipped it moves into
364
477
  the ··· overflow menu; hide the cropped inline original rather than painting
@@ -703,8 +816,178 @@
703
816
  border-top-color: #374151;
704
817
  }
705
818
 
819
+ .squisq-code-snippet-menu {
820
+ width: 220px;
821
+ max-height: min(440px, calc(100vh - 16px));
822
+ overflow-y: auto;
823
+ }
824
+
825
+ .squisq-code-snippet-language {
826
+ justify-content: space-between;
827
+ gap: 16px;
828
+ }
829
+
830
+ .squisq-code-snippet-language code {
831
+ overflow: hidden;
832
+ color: #9ca3af;
833
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
834
+ font-size: 11px;
835
+ text-overflow: ellipsis;
836
+ white-space: nowrap;
837
+ }
838
+
706
839
  /* ─── Template Picker (toolbar) ──────────────────────── */
707
840
 
841
+ .squisq-mermaid-type-menu {
842
+ width: min(520px, calc(100vw - 16px));
843
+ max-height: min(600px, calc(100vh - 16px));
844
+ padding: 0 8px 10px;
845
+ overflow-y: auto;
846
+ }
847
+
848
+ .squisq-mermaid-type-menu-heading {
849
+ position: sticky;
850
+ z-index: 1;
851
+ top: 0;
852
+ display: flex;
853
+ flex-direction: column;
854
+ gap: 2px;
855
+ margin: 0 -8px 4px;
856
+ padding: 12px 14px 10px;
857
+ color: #111827;
858
+ background: #fff;
859
+ border-bottom: 1px solid #e5e7eb;
860
+ }
861
+
862
+ .squisq-mermaid-type-menu-heading strong {
863
+ font-size: 14px;
864
+ }
865
+
866
+ .squisq-mermaid-type-menu-heading span {
867
+ color: #6b7280;
868
+ font-size: 11px;
869
+ line-height: 1.35;
870
+ }
871
+
872
+ .squisq-mermaid-type-section + .squisq-mermaid-type-section {
873
+ margin-top: 4px;
874
+ }
875
+
876
+ .squisq-mermaid-type-grid {
877
+ display: grid;
878
+ grid-template-columns: repeat(2, minmax(0, 1fr));
879
+ gap: 6px;
880
+ }
881
+
882
+ .squisq-mermaid-type-card {
883
+ display: grid;
884
+ grid-template-columns: 82px minmax(0, 1fr);
885
+ align-items: center;
886
+ min-width: 0;
887
+ padding: 8px;
888
+ color: #1f2937;
889
+ font: inherit;
890
+ text-align: left;
891
+ background: #f9fafb;
892
+ border: 1px solid #e5e7eb;
893
+ border-radius: 7px;
894
+ cursor: pointer;
895
+ }
896
+
897
+ .squisq-mermaid-type-card:hover,
898
+ .squisq-mermaid-type-card:focus-visible {
899
+ color: #1d4ed8;
900
+ background: #eff6ff;
901
+ border-color: #93c5fd;
902
+ outline: none;
903
+ }
904
+
905
+ .squisq-mermaid-type-thumbnail {
906
+ display: flex;
907
+ height: 48px;
908
+ align-items: center;
909
+ justify-content: center;
910
+ padding: 3px;
911
+ color: currentcolor;
912
+ }
913
+
914
+ .squisq-mermaid-type-thumbnail svg {
915
+ display: block;
916
+ width: 100%;
917
+ height: 100%;
918
+ }
919
+
920
+ .squisq-mermaid-type-copy,
921
+ .squisq-mermaid-type-title {
922
+ min-width: 0;
923
+ }
924
+
925
+ .squisq-mermaid-type-copy {
926
+ display: flex;
927
+ flex-direction: column;
928
+ gap: 3px;
929
+ }
930
+
931
+ .squisq-mermaid-type-title {
932
+ display: flex;
933
+ align-items: center;
934
+ gap: 5px;
935
+ font-size: 12px;
936
+ font-weight: 650;
937
+ }
938
+
939
+ .squisq-mermaid-type-description {
940
+ display: -webkit-box;
941
+ overflow: hidden;
942
+ color: #6b7280;
943
+ font-size: 10px;
944
+ line-height: 1.3;
945
+ -webkit-box-orient: vertical;
946
+ -webkit-line-clamp: 2;
947
+ }
948
+
949
+ .squisq-mermaid-type-badge {
950
+ flex: none;
951
+ padding: 1px 4px;
952
+ color: #92400e;
953
+ font-size: 8px;
954
+ font-weight: 700;
955
+ letter-spacing: 0.03em;
956
+ text-transform: uppercase;
957
+ background: #fef3c7;
958
+ border-radius: 999px;
959
+ }
960
+
961
+ .squisq-mermaid-type-menu[data-theme='dark'] .squisq-mermaid-type-menu-heading {
962
+ color: #f9fafb;
963
+ background: #1f2937;
964
+ border-bottom-color: #374151;
965
+ }
966
+
967
+ .squisq-mermaid-type-menu[data-theme='dark'] .squisq-mermaid-type-menu-heading span,
968
+ .squisq-mermaid-type-menu[data-theme='dark'] .squisq-mermaid-type-description {
969
+ color: #9ca3af;
970
+ }
971
+
972
+ .squisq-mermaid-type-menu[data-theme='dark'] .squisq-mermaid-type-card {
973
+ color: #e5e7eb;
974
+ background: #111827;
975
+ border-color: #374151;
976
+ }
977
+
978
+ .squisq-mermaid-type-menu[data-theme='dark'] .squisq-mermaid-type-card:hover,
979
+ .squisq-mermaid-type-menu[data-theme='dark'] .squisq-mermaid-type-card:focus-visible {
980
+ color: #bfdbfe;
981
+ background: #1e3a5f;
982
+ border-color: #3b82f6;
983
+ }
984
+
985
+ @media (max-width: 620px) {
986
+ .squisq-mermaid-type-grid {
987
+ grid-template-columns: 1fr;
988
+ }
989
+ }
990
+
708
991
  .squisq-template-picker {
709
992
  display: flex;
710
993
  align-items: center;
@@ -1990,6 +2273,7 @@
1990
2273
  /* Body font follows the active squisq theme when one is selected; the
1991
2274
  fallback inherits from the host page when it isn't. */
1992
2275
  font-family: var(--squisq-theme-body-font, inherit);
2276
+ font-size: var(--squisq-write-text-size, inherit);
1993
2277
  /* Explicit caret color so the blinking insertion point stays visible
1994
2278
  even when an ancestor (e.g. a DocBlocks shell with the OS in dark
1995
2279
  mode) inherits a near-white color into the editor. Without this
@@ -2069,6 +2353,15 @@
2069
2353
  margin: 1.6em 0 0.3em;
2070
2354
  }
2071
2355
 
2356
+ /* ProseMirror gives an empty text block a trailing <br> so its caret remains
2357
+ visible. Heading controls are siblings of the editable content span, so an
2358
+ uncontained break would temporarily push those controls onto a second line
2359
+ after a Markdown heading shortcut. Keep the break's caret line inside a
2360
+ zero-width inline box until the first heading character replaces it. */
2361
+ .squisq-heading-content:has(> br.ProseMirror-trailingBreak:only-child) {
2362
+ display: inline-block;
2363
+ }
2364
+
2072
2365
  /* The first block in the document has nothing above it — don't push the top of
2073
2366
  the editor down with the section-break margin. (The editable ProseMirror node
2074
2367
  itself carries the `squisq-wysiwyg-editor` class, so its blocks are direct
@@ -2078,8 +2371,10 @@
2078
2371
  }
2079
2372
 
2080
2373
  .squisq-wysiwyg-editor p {
2081
- margin: 0.5em 0;
2082
- line-height: 1.7;
2374
+ /* Slightly roomier paragraph separators make authored blank lines easier
2375
+ to scan without turning each paragraph into a separate card. */
2376
+ margin: 0.625em 0;
2377
+ line-height: var(--squisq-write-line-spacing, 1.7);
2083
2378
  }
2084
2379
 
2085
2380
  .squisq-wysiwyg-editor blockquote {
@@ -2745,6 +3040,31 @@
2745
3040
  --squisq-toolbar-icon: #9ca3af;
2746
3041
  }
2747
3042
 
3043
+ .squisq-editor-shell[data-theme='dark'] .squisq-find-field {
3044
+ border-color: #4b5563;
3045
+ background: #111827;
3046
+ color: #9ca3af;
3047
+ }
3048
+
3049
+ .squisq-editor-shell[data-theme='dark'] .squisq-find-field:focus-within {
3050
+ border-color: #60a5fa;
3051
+ box-shadow: 0 0 0 1px #60a5fa;
3052
+ }
3053
+
3054
+ .squisq-editor-shell[data-theme='dark'] .squisq-find-input {
3055
+ color: #f9fafb;
3056
+ }
3057
+
3058
+ .squisq-editor-shell[data-theme='dark'] .squisq-find-input::placeholder,
3059
+ .squisq-editor-shell[data-theme='dark'] .squisq-find-count {
3060
+ color: #9ca3af;
3061
+ }
3062
+
3063
+ .squisq-editor-shell[data-theme='dark'] .squisq-find-button:hover:not(:disabled) {
3064
+ background: rgba(255, 255, 255, 0.1);
3065
+ color: #f9fafb;
3066
+ }
3067
+
2748
3068
  .squisq-editor-shell[data-theme='dark'] .squisq-toolbar-view-tabs {
2749
3069
  background: #111827;
2750
3070
  border-right-color: rgba(255, 255, 255, 0.15);
@@ -12,6 +12,8 @@
12
12
  @import './image-edit-affordance.css';
13
13
  @import './diagram.css';
14
14
  @import './ascii-diagram.css';
15
+ @import './mermaid-diagram.css';
16
+ @import './code-snippet.css';
15
17
  @import './tree-view.css';
16
18
  @import './ascii-timeline.css';
17
19
  @import '../scene/scene.css';