@bendyline/squisq-editor-react 1.6.2 → 2.0.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 (200) hide show
  1. package/README.md +12 -3
  2. package/dist/index.d.ts +1229 -179
  3. package/dist/index.js +18017 -11231
  4. package/dist/index.js.map +1 -1
  5. package/dist/monaco.d.ts +1 -0
  6. package/dist/monaco.js +4 -0
  7. package/dist/monaco.js.map +1 -0
  8. package/dist/styles/index.css +1471 -245
  9. package/package.json +9 -5
  10. package/src/BlockPropertiesPopover.tsx +30 -15
  11. package/src/DocumentSettingsDialog.tsx +13 -21
  12. package/src/EditorContext.tsx +199 -42
  13. package/src/EditorShell.tsx +348 -260
  14. package/src/ImageEditor.tsx +22 -1
  15. package/src/InlinePreviewGutter.tsx +16 -8
  16. package/src/MediaBin.tsx +107 -14
  17. package/src/OutlinePanel.tsx +202 -7
  18. package/src/PlainHtmlPreview.tsx +40 -1
  19. package/src/PreviewControls.tsx +309 -69
  20. package/src/PreviewPanel.tsx +312 -78
  21. package/src/RawEditor.tsx +13 -2
  22. package/src/StatusBar.tsx +5 -2
  23. package/src/TemplateAnnotation.ts +12 -3
  24. package/src/TemplatePicker.tsx +75 -21
  25. package/src/ThemeCustomizerPanel.tsx +8 -0
  26. package/src/ThemePicker.tsx +10 -5
  27. package/src/TimelineBlockPreview.tsx +1 -1
  28. package/src/Toolbar.tsx +202 -41
  29. package/src/TransitionPicker.tsx +36 -4
  30. package/src/ViewMenuPanel.tsx +12 -14
  31. package/src/WysiwygEditor.tsx +124 -91
  32. package/src/__tests__/blockPropertiesPopoverTheme.test.tsx +33 -0
  33. package/src/__tests__/blockTagActivity.test.ts +183 -0
  34. package/src/__tests__/buildDocumentPreviewMarkdown.test.ts +75 -0
  35. package/src/__tests__/buildPreviewDocContent.test.ts +48 -0
  36. package/src/__tests__/buildPreviewDocTransition.test.ts +21 -1
  37. package/src/__tests__/canvasSurfaceTextEditing.test.tsx +60 -0
  38. package/src/__tests__/customLayoutManagerTheme.test.tsx +34 -0
  39. package/src/__tests__/documentSettingsDialog.test.tsx +29 -1
  40. package/src/__tests__/editorScrollHandoff.test.tsx +102 -0
  41. package/src/__tests__/editorScrollSync.test.ts +65 -0
  42. package/src/__tests__/editorShellProps.test.tsx +123 -1
  43. package/src/__tests__/emojiPicker.test.tsx +2 -42
  44. package/src/__tests__/headingTransition.test.ts +0 -12
  45. package/src/__tests__/imageEditAffordance.test.tsx +5 -1
  46. package/src/__tests__/imageEditModalContract.test.tsx +119 -0
  47. package/src/__tests__/imageEditorShell.test.tsx +15 -1
  48. package/src/__tests__/jsonEditor.test.tsx +56 -1
  49. package/src/__tests__/layersPanel.test.tsx +75 -5
  50. package/src/__tests__/majorApiTypes.test.ts +26 -0
  51. package/src/__tests__/mediaBinDrop.test.tsx +90 -0
  52. package/src/__tests__/narrationSave.test.ts +148 -0
  53. package/src/__tests__/outlinePanel.test.tsx +93 -3
  54. package/src/__tests__/outlineSource.test.ts +217 -0
  55. package/src/__tests__/plainHtmlPreview.test.tsx +17 -1
  56. package/src/__tests__/presentationMode.test.tsx +290 -0
  57. package/src/__tests__/previewControls.test.tsx +218 -5
  58. package/src/__tests__/previewPanelAudioFailure.test.tsx +46 -0
  59. package/src/__tests__/previewPanelPresentation.test.tsx +126 -0
  60. package/src/__tests__/previewPanelTransforms.test.tsx +117 -0
  61. package/src/__tests__/rawEditorModelIsolation.test.ts +19 -0
  62. package/src/__tests__/resolveBlockVisual.test.ts +43 -0
  63. package/src/__tests__/statusBar.test.tsx +27 -0
  64. package/src/__tests__/teleprompter.test.tsx +190 -0
  65. package/src/__tests__/templateAnnotationRoundTrip.test.ts +26 -0
  66. package/src/__tests__/templatePickerPortal.test.tsx +62 -0
  67. package/src/__tests__/tiptapBridge.test.ts +23 -0
  68. package/src/__tests__/tiptapCodeBlockRoundTrip.test.ts +109 -0
  69. package/src/__tests__/transformStyleId.test.ts +13 -0
  70. package/src/__tests__/useImageEditor.test.tsx +67 -0
  71. package/src/__tests__/useMediaRecorder.test.ts +75 -0
  72. package/src/__tests__/viewMenuPanel.test.tsx +130 -0
  73. package/src/__tests__/wysiwygImageUpload.test.ts +69 -0
  74. package/src/__tests__/wysiwygTemplateBadgeFocus.test.tsx +56 -0
  75. package/src/asciiDiagram/AsciiDiagramExtension.ts +343 -0
  76. package/src/asciiDiagram/AsciiDiagramWidget.tsx +224 -0
  77. package/src/asciiDiagram/RepairableDiagramExtension.ts +200 -0
  78. package/src/asciiDiagram/__tests__/AsciiDiagramExtension.test.ts +145 -0
  79. package/src/asciiDiagram/__tests__/RepairableDiagramExtension.test.ts +159 -0
  80. package/src/asciiDiagram/__tests__/asciiDiagramCommands.test.ts +227 -0
  81. package/src/asciiDiagram/__tests__/asciiDiagramOps.test.ts +114 -0
  82. package/src/asciiDiagram/__tests__/asciiPaste.test.ts +43 -0
  83. package/src/asciiDiagram/asciiDiagramCommands.ts +148 -0
  84. package/src/asciiDiagram/asciiDiagramData.ts +116 -0
  85. package/src/asciiDiagram/asciiDiagramOps.ts +219 -0
  86. package/src/asciiDiagram/asciiPaste.ts +19 -0
  87. package/src/blockTagActivity.ts +233 -0
  88. package/src/buildDocumentPreviewMarkdown.ts +168 -0
  89. package/src/buildPreviewDoc.ts +19 -12
  90. package/src/customTemplates/CustomLayoutManager.tsx +3 -2
  91. package/src/customTemplates/CustomTemplateContext.tsx +6 -0
  92. package/src/customTemplates/TemplateDesigner.tsx +7 -4
  93. package/src/customTemplates/__tests__/library.test.ts +8 -0
  94. package/src/customTemplates/designer.css +158 -99
  95. package/src/customTemplates/library.ts +16 -3
  96. package/src/customThemes/CustomThemeContext.tsx +6 -0
  97. package/src/customThemes/CustomThemeDialog.tsx +35 -5
  98. package/src/customThemes/ImportThemeSection.tsx +178 -0
  99. package/src/customThemes/__tests__/customThemeLibrary.test.ts +9 -0
  100. package/src/customThemes/__tests__/importThemeSection.test.tsx +192 -0
  101. package/src/customThemes/customThemeLibrary.ts +7 -4
  102. package/src/customThemes/index.ts +7 -1
  103. package/src/customThemes/themeDraft.ts +23 -7
  104. package/src/diagram/DiagramCanvas.tsx +9 -4
  105. package/src/diagram/types.ts +35 -0
  106. package/src/editorScrollSync.ts +73 -0
  107. package/src/emojiData.ts +3 -23
  108. package/src/headingTransition.ts +0 -15
  109. package/src/imageEditor/CanvasSurface.tsx +29 -23
  110. package/src/imageEditor/LayersPanel.tsx +78 -3
  111. package/src/imageEditor/Toolbar.tsx +5 -1
  112. package/src/imageEditor/icons.tsx +4 -0
  113. package/src/imageEditor/image-editor.css +75 -0
  114. package/src/imageEditor/layers/SelectionHandles.tsx +1 -1
  115. package/src/imageEditor/useImageEditor.ts +71 -14
  116. package/src/index.ts +191 -19
  117. package/src/jsonEditor/JsonEditorContext.tsx +12 -6
  118. package/src/jsonEditor/RenderNode.tsx +24 -3
  119. package/src/jsonEditor/editors.tsx +86 -19
  120. package/src/mediaEntries.ts +12 -0
  121. package/src/monaco.ts +35 -0
  122. package/src/monacoWorkers.ts +89 -0
  123. package/src/outlineSource.ts +171 -0
  124. package/src/presentation/PresentationMode.tsx +596 -0
  125. package/src/rawEditorIsolation.ts +18 -0
  126. package/src/recorder/hooks/useMediaRecorder.ts +97 -53
  127. package/src/resolveBlockVisual.ts +10 -4
  128. package/src/scene/Scene.tsx +48 -18
  129. package/src/scene/SceneBlockExtension.ts +17 -10
  130. package/src/scene/SceneBlockWidget.tsx +5 -1
  131. package/src/scene/SceneViewport.tsx +7 -3
  132. package/src/scene/ShapePalette.tsx +17 -2
  133. package/src/scene/__tests__/DiagramAdapter.test.ts +86 -0
  134. package/src/scene/__tests__/SceneBlockExtension.test.ts +33 -0
  135. package/src/scene/__tests__/sceneIsolation.test.tsx +93 -0
  136. package/src/scene/adapters/DiagramAdapter.ts +12 -101
  137. package/src/scene/commands/SceneCommand.ts +4 -1
  138. package/src/scene/index.ts +1 -6
  139. package/src/scene/layers/DiagramEdges.tsx +21 -7
  140. package/src/scene/layers/edgeGeometry.ts +9 -1
  141. package/src/scene/layers/nodeCard.tsx +27 -8
  142. package/src/scene/scene.css +5 -2
  143. package/src/scene/text/SceneTextOverlay.tsx +5 -5
  144. package/src/scene/text/sceneTextChannel.ts +26 -20
  145. package/src/scene/text/sceneTextConfig.ts +4 -0
  146. package/src/scene/tools/ConnectTool.ts +13 -4
  147. package/src/scene/tools/SceneTool.ts +8 -0
  148. package/src/scene/tools/SelectTool.ts +34 -18
  149. package/src/scene/tools/ShapeTool.ts +2 -3
  150. package/src/styles/ascii-diagram.css +79 -0
  151. package/src/styles/ascii-timeline.css +499 -0
  152. package/src/styles/editor.css +706 -120
  153. package/src/styles/index.css +3 -0
  154. package/src/styles/tree-view.css +139 -0
  155. package/src/teleprompter/TeleprompterControls.tsx +218 -0
  156. package/src/teleprompter/TeleprompterSelfView.tsx +22 -0
  157. package/src/teleprompter/TeleprompterSurface.tsx +267 -0
  158. package/src/teleprompter/TeleprompterView.tsx +338 -0
  159. package/src/teleprompter/canvasRenderer.ts +161 -0
  160. package/src/teleprompter/floatingWindow.ts +352 -0
  161. package/src/teleprompter/index.ts +61 -0
  162. package/src/teleprompter/pcmWorklet.ts +62 -0
  163. package/src/teleprompter/recording/insertPreamble.ts +80 -0
  164. package/src/teleprompter/recording/narrationSave.ts +134 -0
  165. package/src/teleprompter/recording/useNarrationRecorder.ts +367 -0
  166. package/src/teleprompter/scrollModel.ts +85 -0
  167. package/src/teleprompter/teleprompterTheme.ts +303 -0
  168. package/src/teleprompter/types.ts +38 -0
  169. package/src/teleprompter/useFloatingWindow.ts +74 -0
  170. package/src/teleprompter/useMicAnalysis.ts +211 -0
  171. package/src/teleprompter/useTeleprompter.ts +421 -0
  172. package/src/templateContentPreviewResolver.ts +12 -5
  173. package/src/timeline/TimelineEditorWidget.tsx +702 -0
  174. package/src/timeline/TimelineViewExtension.ts +252 -0
  175. package/src/timeline/__tests__/TimelineEditorWidget.test.tsx +355 -0
  176. package/src/timeline/__tests__/TimelineViewExtension.test.ts +163 -0
  177. package/src/timeline/__tests__/timelineCommands.test.ts +327 -0
  178. package/src/timeline/__tests__/timelineOps.test.ts +222 -0
  179. package/src/timeline/__tests__/timelinePaste.test.ts +34 -0
  180. package/src/timeline/timelineCommands.ts +367 -0
  181. package/src/timeline/timelineData.ts +54 -0
  182. package/src/timeline/timelineOps.ts +277 -0
  183. package/src/timeline/timelinePaste.ts +8 -0
  184. package/src/tiptapBridge.ts +24 -16
  185. package/src/transformStyleId.ts +17 -0
  186. package/src/treeview/TreeOutlineWidget.tsx +240 -0
  187. package/src/treeview/TreeViewExtension.ts +250 -0
  188. package/src/treeview/__tests__/TreeViewExtension.test.ts +122 -0
  189. package/src/treeview/__tests__/treeOps.test.ts +125 -0
  190. package/src/treeview/__tests__/treePaste.test.ts +40 -0
  191. package/src/treeview/__tests__/treeViewCommands.test.ts +163 -0
  192. package/src/treeview/treeOps.ts +172 -0
  193. package/src/treeview/treePaste.ts +13 -0
  194. package/src/treeview/treeViewCommands.ts +94 -0
  195. package/src/treeview/treeViewData.ts +41 -0
  196. package/src/useMonacoLoader.ts +11 -34
  197. package/src/wysiwygImageUpload.ts +113 -0
  198. package/src/diagram/DiagramExtension.ts +0 -209
  199. package/src/diagram/DiagramWidget.tsx +0 -272
  200. package/src/diagram/useDiagramData.ts +0 -126
package/src/Toolbar.tsx CHANGED
@@ -29,7 +29,7 @@ import { ViewMenuPanel } from './ViewMenuPanel';
29
29
  import {
30
30
  TemplatePicker,
31
31
  TEMPLATE_NAMES,
32
- TEMPLATE_GALLERY_PORTAL_ID,
32
+ TEMPLATE_GALLERY_PORTAL_SELECTOR,
33
33
  templateLabel,
34
34
  } from './TemplatePicker';
35
35
  import { TransitionPicker, TRANSITION_FLYOUT_PORTAL_ID } from './TransitionPicker';
@@ -50,7 +50,8 @@ import { CustomLayoutManager } from './customTemplates/CustomLayoutManager';
50
50
  import { Icon } from './Icon';
51
51
  import type { PickerEntry } from './emojiData';
52
52
  import { createPortal } from 'react-dom';
53
- import { usePreviewSettingsOptional } from './PreviewControls';
53
+ import { PreviewModeMenu, displayModeLabel, usePreviewSettingsOptional } from './PreviewControls';
54
+ import { filterVisibleMediaEntries } from './mediaEntries';
54
55
 
55
56
  const VIEWS: { id: EditorView; label: string; shortLabel?: string; shortcut: string }[] = [
56
57
  { id: 'wysiwyg', label: 'Write', shortcut: '⌘1' },
@@ -220,6 +221,22 @@ const BUTTONS: ToolbarButton[] = [
220
221
  group: 'media',
221
222
  faIcon: 'fa-solid fa-diagram-project',
222
223
  },
224
+ {
225
+ id: 'tree',
226
+ label: 'tree',
227
+ icon: '',
228
+ title: 'Insert tree',
229
+ group: 'media',
230
+ faIcon: 'fa-solid fa-folder-tree',
231
+ },
232
+ {
233
+ id: 'timeline',
234
+ label: 'timeline',
235
+ icon: '',
236
+ title: 'Insert timeline',
237
+ group: 'media',
238
+ faIcon: 'fa-solid fa-timeline',
239
+ },
223
240
  {
224
241
  id: 'drawing',
225
242
  label: 'drawing',
@@ -355,6 +372,79 @@ const LAYOUT_STARTER_TEXT_PARAMS =
355
372
  /** Multi-line markdown for a new layout (raw / code views). */
356
373
  const LAYOUT_STARTER_MARKDOWN = `\n## Layout {[layout]}\n\n### {#text-1} {[text ${LAYOUT_STARTER_TEXT_PARAMS}]}\n\nLayout\n`;
357
374
 
375
+ /**
376
+ * Starter art for a new diagram — diagrams are authored as ASCII-art code
377
+ * fences (the fence is the source of truth; `AsciiDiagramExtension` mounts
378
+ * the interactive canvas over it). This exact art is the codec's own
379
+ * canonical rendering of a two-node flow, so it re-parses and re-renders
380
+ * byte-stably.
381
+ */
382
+ const DIAGRAM_STARTER_ART = [
383
+ '┌─────────┐',
384
+ '│ Start │',
385
+ '└────┬────┘',
386
+ ' │',
387
+ ' ▼',
388
+ '┌────┴────┐',
389
+ '│ Next │',
390
+ '└─────────┘',
391
+ ].join('\n');
392
+ /**
393
+ * Fenced form for raw / code views — tagged with the explicit `diagram`
394
+ * language so the block's identity survives markdown ↔ Tiptap round-trips
395
+ * (the language class round-trips; fence meta does not).
396
+ */
397
+ const DIAGRAM_STARTER_MARKDOWN = '\n```diagram\n' + DIAGRAM_STARTER_ART + '\n```\n';
398
+
399
+ /** Insert a starter ASCII-diagram code fence after the current top-level block. */
400
+ function insertAsciiDiagramBlock(editor: TiptapEditor): void {
401
+ insertFenceBlock(editor, DIAGRAM_STARTER_ART, 'diagram');
402
+ }
403
+
404
+ /**
405
+ * Starter art for a new file tree — an ASCII tree fence (the fence is the
406
+ * source of truth; `TreeViewExtension` mounts the interactive outline).
407
+ */
408
+ const TREE_STARTER_ART = ['src/', '├── index.ts', '└── utils/', ' └── helpers.ts'].join('\n');
409
+ const TREE_STARTER_MARKDOWN = '\n```tree\n' + TREE_STARTER_ART + '\n```\n';
410
+
411
+ /** Insert a starter ASCII tree code fence after the current top-level block. */
412
+ function insertTreeBlock(editor: TiptapEditor): void {
413
+ insertFenceBlock(editor, TREE_STARTER_ART, 'tree');
414
+ }
415
+
416
+ /** Starter multi-event rail for the authored timeline view. */
417
+ const TIMELINE_STARTER_ART =
418
+ 'Milestones: ● Start {#start} ─────● Review {#review} ─────● Ship {#ship} ───►';
419
+ const TIMELINE_STARTER_MARKDOWN = '\n```timeline\n' + TIMELINE_STARTER_ART + '\n```\n';
420
+
421
+ /** Insert a starter ASCII timeline code fence after the current top-level block. */
422
+ function insertTimelineBlock(editor: TiptapEditor): void {
423
+ insertFenceBlock(editor, TIMELINE_STARTER_ART, 'timeline');
424
+ }
425
+
426
+ /**
427
+ * Insert a code fence after the current top-level block. `lang` tags the
428
+ * fence's `language` attribute — pass the explicit authored-view tag so its
429
+ * identity round-trips through markdown and Tiptap.
430
+ */
431
+ function insertFenceBlock(editor: TiptapEditor, art: string, lang?: string): void {
432
+ editor
433
+ .chain()
434
+ .focus()
435
+ .command(({ tr, state, dispatch }) => {
436
+ const codeBlockType = state.schema.nodes.codeBlock;
437
+ if (!codeBlockType) return false;
438
+ const attrs = lang ? { language: lang } : null;
439
+ const block = codeBlockType.create(attrs, state.schema.text(art));
440
+ const { $from } = state.selection;
441
+ const insertPos = $from.depth > 0 ? $from.after(1) : state.doc.content.size;
442
+ if (dispatch) tr.insert(insertPos, block);
443
+ return true;
444
+ })
445
+ .run();
446
+ }
447
+
358
448
  /**
359
449
  * Insert a block-level heading carrying a Scene template (`diagram` /
360
450
  * `drawing` / `layout`) at the top level, after the block the caret sits
@@ -506,6 +596,7 @@ export function Toolbar({
506
596
  bumpMediaRevision,
507
597
  } = useEditorContext();
508
598
  const previewSettings = usePreviewSettingsOptional();
599
+ const [useModeMenuRequest, requestUseModeMenu] = useReducer((count: number) => count + 1, 0);
509
600
  // When a canvas textbox is being edited, its Tiptap instance takes over
510
601
  // the formatting buttons; otherwise they drive the document editor. The
511
602
  // `level` gates which buttons apply (inline labels vs. rich textboxes).
@@ -533,7 +624,7 @@ export function Toolbar({
533
624
  let cancelled = false;
534
625
  mediaProvider.listMedia().then(
535
626
  (entries) => {
536
- if (!cancelled) setScannedFileCount(entries.length);
627
+ if (!cancelled) setScannedFileCount(filterVisibleMediaEntries(entries).length);
537
628
  },
538
629
  () => {
539
630
  if (!cancelled) setScannedFileCount(0);
@@ -682,23 +773,25 @@ export function Toolbar({
682
773
  // with the cursor context and change width with their value, all without
683
774
  // resizing the flex:1 container itself — a container-only observer would
684
775
  // go stale.
685
- const ro = new ResizeObserver(measure);
776
+ const ro = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(measure);
686
777
  const observeAll = () => {
687
- ro.disconnect();
688
- ro.observe(container);
778
+ ro?.disconnect();
779
+ ro?.observe(container);
689
780
  container
690
781
  .querySelectorAll<HTMLElement>(':scope > .squisq-toolbar-group')
691
- .forEach((group) => ro.observe(group));
782
+ .forEach((group) => ro?.observe(group));
692
783
  };
693
784
  const mo = new MutationObserver(() => {
694
785
  observeAll();
695
786
  measure();
696
787
  });
697
788
  mo.observe(container, { childList: true });
789
+ if (!ro) window.addEventListener('resize', measure);
698
790
  observeAll();
699
791
  measure();
700
792
  return () => {
701
- ro.disconnect();
793
+ ro?.disconnect();
794
+ if (!ro) window.removeEventListener('resize', measure);
702
795
  mo.disconnect();
703
796
  };
704
797
  }, [activeView]);
@@ -714,7 +807,7 @@ export function Toolbar({
714
807
  if (overflowRef.current?.contains(target)) return;
715
808
  if (
716
809
  target instanceof Element &&
717
- target.closest(`#${TEMPLATE_GALLERY_PORTAL_ID}, #${TRANSITION_FLYOUT_PORTAL_ID}`)
810
+ target.closest(`${TEMPLATE_GALLERY_PORTAL_SELECTOR}, #${TRANSITION_FLYOUT_PORTAL_ID}`)
718
811
  ) {
719
812
  return;
720
813
  }
@@ -856,9 +949,19 @@ export function Toolbar({
856
949
  insertTaskList(tiptapEditor);
857
950
  break;
858
951
  case 'diagram':
859
- // Empty diagram is usable via its "+ Add first node" affordance;
860
- // sub-headings under this block become nodes.
861
- insertTemplateHeading(tiptapEditor, { template: 'diagram', text: 'Diagram' });
952
+ // Diagrams are ASCII-art code fences; the AsciiDiagramExtension
953
+ // mounts the interactive canvas over the inserted starter.
954
+ insertAsciiDiagramBlock(tiptapEditor);
955
+ break;
956
+ case 'tree':
957
+ // File trees are ASCII tree fences; TreeViewExtension mounts the
958
+ // interactive outline over the inserted starter.
959
+ insertTreeBlock(tiptapEditor);
960
+ break;
961
+ case 'timeline':
962
+ // The authored fence is immediately replaced by the WYSIWYG rail
963
+ // canvas; Preview uses the same events as vector layers.
964
+ insertTimelineBlock(tiptapEditor);
862
965
  break;
863
966
  case 'drawing':
864
967
  // Empty drawing is usable via the canvas's "Shapes ▾" palette;
@@ -1015,9 +1118,19 @@ export function Toolbar({
1015
1118
  break;
1016
1119
  }
1017
1120
  case 'diagram': {
1018
- // A diagram is just a heading with the `{[diagram]}` template
1019
- // annotation; the WYSIWYG view renders its editable canvas.
1020
- replacement = '\n## Diagram {[diagram]}\n';
1121
+ // A diagram is an ASCII-art code fence; the WYSIWYG view
1122
+ // renders its editable canvas, preview renders it as a slide.
1123
+ replacement = DIAGRAM_STARTER_MARKDOWN;
1124
+ newCursorOffset = replacement.length;
1125
+ break;
1126
+ }
1127
+ case 'tree': {
1128
+ replacement = TREE_STARTER_MARKDOWN;
1129
+ newCursorOffset = replacement.length;
1130
+ break;
1131
+ }
1132
+ case 'timeline': {
1133
+ replacement = TIMELINE_STARTER_MARKDOWN;
1021
1134
  newCursorOffset = replacement.length;
1022
1135
  break;
1023
1136
  }
@@ -1099,7 +1212,13 @@ export function Toolbar({
1099
1212
  insertion = `\n${TASK_LIST_MARKDOWN}\n`;
1100
1213
  break;
1101
1214
  case 'diagram':
1102
- insertion = '\n## Diagram {[diagram]}\n';
1215
+ insertion = DIAGRAM_STARTER_MARKDOWN;
1216
+ break;
1217
+ case 'tree':
1218
+ insertion = TREE_STARTER_MARKDOWN;
1219
+ break;
1220
+ case 'timeline':
1221
+ insertion = TIMELINE_STARTER_MARKDOWN;
1103
1222
  break;
1104
1223
  case 'drawing':
1105
1224
  insertion = '\n## Drawing {[drawing]}\n';
@@ -1540,10 +1659,12 @@ export function Toolbar({
1540
1659
  viewport: previewSettings?.activeViewport ?? VIEWPORT_PRESETS.landscape,
1541
1660
  basePath: '/',
1542
1661
  mediaProvider,
1662
+ customTemplates: doc?.customTemplates,
1543
1663
  }
1544
1664
  : undefined,
1545
1665
  [
1546
1666
  mediaProvider,
1667
+ doc?.customTemplates,
1547
1668
  previewSettings?.activeTheme,
1548
1669
  previewSettings?.activeViewport,
1549
1670
  templatePreviewBlock,
@@ -1692,35 +1813,65 @@ export function Toolbar({
1692
1813
  {/* View tabs — hidden when only one view is available (e.g. code mode). */}
1693
1814
  {showViewTabs && (
1694
1815
  <div className="squisq-toolbar-view-tabs" role="tablist" aria-label="Editor view">
1695
- {visibleViews.map((view) => (
1696
- <button
1697
- key={view.id}
1698
- role="tab"
1699
- data-view={view.id}
1700
- aria-selected={activeView === view.id}
1701
- className={`squisq-toolbar-view-tab${activeView === view.id ? ' squisq-toolbar-view-tab--active' : ''}`}
1702
- onClick={() => setActiveView(view.id)}
1703
- data-tooltip={`${view.label} (${view.shortcut})`}
1704
- >
1705
- <span
1706
- className="squisq-toolbar-view-tab-label squisq-toolbar-view-tab-label--long"
1707
- data-label={view.label}
1816
+ {visibleViews.map((view) => {
1817
+ const viewLabel =
1818
+ view.id === 'preview' && previewSettings
1819
+ ? displayModeLabel(previewSettings.activeDisplayMode)
1820
+ : view.label;
1821
+ const tab = (
1822
+ <button
1823
+ role="tab"
1824
+ data-view={view.id}
1825
+ aria-selected={activeView === view.id}
1826
+ className={`squisq-toolbar-view-tab${activeView === view.id ? ' squisq-toolbar-view-tab--active' : ''}`}
1827
+ onClick={() => {
1828
+ if (view.id === 'preview' && activeView === 'preview' && previewSettings) {
1829
+ requestUseModeMenu();
1830
+ return;
1831
+ }
1832
+ setActiveView(view.id);
1833
+ }}
1834
+ data-tooltip={`${viewLabel} (${view.shortcut})`}
1708
1835
  >
1709
- {view.label}
1710
- </span>
1711
- {view.shortLabel && view.shortLabel !== view.label && (
1712
1836
  <span
1713
- className="squisq-toolbar-view-tab-label squisq-toolbar-view-tab-label--short"
1714
- data-label={view.shortLabel}
1837
+ className="squisq-toolbar-view-tab-label squisq-toolbar-view-tab-label--long"
1838
+ data-label={viewLabel}
1715
1839
  >
1716
- {view.shortLabel}
1840
+ {viewLabel}
1717
1841
  </span>
1718
- )}
1719
- </button>
1720
- ))}
1842
+ {view.shortLabel && view.shortLabel !== view.label && (
1843
+ <span
1844
+ className="squisq-toolbar-view-tab-label squisq-toolbar-view-tab-label--short"
1845
+ data-label={view.shortLabel}
1846
+ >
1847
+ {view.shortLabel}
1848
+ </span>
1849
+ )}
1850
+ </button>
1851
+ );
1852
+
1853
+ if (view.id !== 'preview' || !previewSettings) {
1854
+ return (
1855
+ <div key={view.id} className="squisq-toolbar-view-tab-wrap" role="presentation">
1856
+ {tab}
1857
+ </div>
1858
+ );
1859
+ }
1860
+
1861
+ return (
1862
+ <div
1863
+ key={view.id}
1864
+ className={`squisq-toolbar-view-tab-wrap squisq-toolbar-use-tab${activeView === 'preview' ? ' squisq-toolbar-use-tab--active' : ''}`}
1865
+ role="presentation"
1866
+ >
1867
+ {tab}
1868
+ <PreviewModeMenu openRequest={useModeMenuRequest} />
1869
+ </div>
1870
+ );
1871
+ })}
1721
1872
  </div>
1722
1873
  )}
1723
- {/* After-tabs slot — left side, before formatting controls (preview mode switch) */}
1874
+ {/* After-tabs slot — left side, before formatting or preview controls. */}
1724
1875
  {slotAfterTabs}
1725
1876
  {/* Formatting buttons — hidden in preview mode and code mode */}
1726
1877
  {!isPreview && !isCodeMode && (
@@ -1803,7 +1954,12 @@ export function Toolbar({
1803
1954
  data-contextual="transition"
1804
1955
  >
1805
1956
  <div className="squisq-toolbar-separator" />
1806
- <TransitionPicker value={currentTransition} onChange={handleTransitionPick} />
1957
+ <TransitionPicker
1958
+ value={currentTransition}
1959
+ onChange={handleTransitionPick}
1960
+ colorScheme={colorScheme}
1961
+ accentColor={previewSettings?.activeTheme.colors.primary}
1962
+ />
1807
1963
  </div>
1808
1964
  )}
1809
1965
 
@@ -2074,7 +2230,12 @@ export function Toolbar({
2074
2230
  the flyout carries follow-up controls (direction/duration). */}
2075
2231
  {showTransitionInOverflow && (
2076
2232
  <div className="squisq-toolbar-overflow-item squisq-toolbar-overflow-template">
2077
- <TransitionPicker value={currentTransition!} onChange={handleTransitionPick} />
2233
+ <TransitionPicker
2234
+ value={currentTransition!}
2235
+ onChange={handleTransitionPick}
2236
+ colorScheme={colorScheme}
2237
+ accentColor={previewSettings?.activeTheme.colors.primary}
2238
+ />
2078
2239
  </div>
2079
2240
  )}
2080
2241
 
@@ -30,6 +30,10 @@ import {
30
30
  export interface TransitionPickerProps {
31
31
  value: TransitionFields;
32
32
  onChange: (next: TransitionFields) => void;
33
+ /** Surface scheme copied onto the portaled flyout so it can style independently of the shell. */
34
+ colorScheme?: 'light' | 'dark';
35
+ /** Active document-theme accent used for open and selected states. */
36
+ accentColor?: string;
33
37
  }
34
38
 
35
39
  /**
@@ -41,7 +45,12 @@ export const TRANSITION_FLYOUT_PORTAL_ID = 'squisq-transition-flyout-portal';
41
45
 
42
46
  const FLYOUT_ID = TRANSITION_FLYOUT_PORTAL_ID;
43
47
 
44
- export function TransitionPicker({ value, onChange }: TransitionPickerProps) {
48
+ export function TransitionPicker({
49
+ value,
50
+ onChange,
51
+ colorScheme = 'light',
52
+ accentColor,
53
+ }: TransitionPickerProps) {
45
54
  const [open, setOpen] = useState(false);
46
55
  const [style, setStyle] = useState<React.CSSProperties>({});
47
56
  const triggerRef = useRef<HTMLButtonElement>(null);
@@ -119,13 +128,22 @@ export function TransitionPicker({ value, onChange }: TransitionPickerProps) {
119
128
 
120
129
  const flyout = open
121
130
  ? createPortal(
122
- <TransitionFlyout style={style} value={value} onChange={onChange} />,
131
+ <TransitionFlyout
132
+ style={{ ...style, ...blockAccentStyle(accentColor) }}
133
+ value={value}
134
+ onChange={onChange}
135
+ colorScheme={colorScheme}
136
+ />,
123
137
  document.body,
124
138
  )
125
139
  : null;
126
140
 
127
141
  return (
128
- <div className="squisq-transition-picker">
142
+ <div
143
+ className="squisq-transition-picker"
144
+ data-theme={colorScheme}
145
+ style={blockAccentStyle(accentColor)}
146
+ >
129
147
  <button
130
148
  ref={triggerRef}
131
149
  type="button"
@@ -158,10 +176,12 @@ function TransitionFlyout({
158
176
  style,
159
177
  value,
160
178
  onChange,
179
+ colorScheme,
161
180
  }: {
162
181
  style: React.CSSProperties;
163
182
  value: TransitionFields;
164
183
  onChange: (next: TransitionFields) => void;
184
+ colorScheme: 'light' | 'dark';
165
185
  }) {
166
186
  const [query, setQuery] = useState('');
167
187
  const searchRef = useRef<HTMLInputElement>(null);
@@ -211,7 +231,13 @@ function TransitionFlyout({
211
231
  };
212
232
 
213
233
  return (
214
- <div id={FLYOUT_ID} className="squisq-transition-flyout" role="menu" style={style}>
234
+ <div
235
+ id={FLYOUT_ID}
236
+ className="squisq-transition-flyout"
237
+ data-theme={colorScheme}
238
+ role="menu"
239
+ style={style}
240
+ >
215
241
  <div className="squisq-transition-flyout-search">
216
242
  <input
217
243
  ref={searchRef}
@@ -313,6 +339,12 @@ function TransitionFlyout({
313
339
  );
314
340
  }
315
341
 
342
+ function blockAccentStyle(accentColor: string | undefined): React.CSSProperties {
343
+ return accentColor
344
+ ? ({ ['--squisq-block-props-accent' as string]: accentColor } as React.CSSProperties)
345
+ : {};
346
+ }
347
+
316
348
  function TransitionOption({
317
349
  entry,
318
350
  selected,
@@ -10,7 +10,7 @@
10
10
  * EditorShell `inlinePreview` prop.
11
11
  */
12
12
 
13
- import { useCallback, useEffect, useRef, useState } from 'react';
13
+ import { useCallback, useEffect, useId, useRef, useState } from 'react';
14
14
  import { useEditorContext, type ThemeInheritance, type LayoutMode } from './EditorContext';
15
15
  import { Icon } from './Icon';
16
16
 
@@ -22,8 +22,8 @@ export function ViewMenuPanel() {
22
22
  setStatusBarVisible,
23
23
  outlineVisible,
24
24
  setOutlineVisible,
25
- blockTagsVisible,
26
- setBlockTagsVisible,
25
+ blockTagVisibility,
26
+ setBlockTagVisibility,
27
27
  themeInheritance,
28
28
  setThemeInheritance,
29
29
  layoutMode,
@@ -56,10 +56,6 @@ export function ViewMenuPanel() {
56
56
  setOutlineVisible(!outlineVisible);
57
57
  }, [outlineVisible, setOutlineVisible]);
58
58
 
59
- const toggleBlockTags = useCallback(() => {
60
- setBlockTagsVisible(!blockTagsVisible);
61
- }, [blockTagsVisible, setBlockTagsVisible]);
62
-
63
59
  return (
64
60
  <div className="squisq-view-menu" ref={containerRef}>
65
61
  <button
@@ -79,7 +75,6 @@ export function ViewMenuPanel() {
79
75
  <div className="squisq-view-menu-popover" role="menu" aria-label="View options">
80
76
  <MenuRadioGroup
81
77
  label="View"
82
- name="layout-mode"
83
78
  value={layoutMode}
84
79
  onChange={(v) => setLayoutMode(v as LayoutMode)}
85
80
  options={[
@@ -95,10 +90,15 @@ export function ViewMenuPanel() {
95
90
  checked={inlinePreviewVisible}
96
91
  onChange={toggleInlinePreview}
97
92
  />
98
- <MenuToggle
93
+ <MenuRadioGroup
99
94
  label="Show block tags"
100
- checked={blockTagsVisible}
101
- onChange={toggleBlockTags}
95
+ value={blockTagVisibility}
96
+ onChange={(v) => setBlockTagVisibility(v as typeof blockTagVisibility)}
97
+ options={[
98
+ { value: 'none', label: 'No inline block tags' },
99
+ { value: 'active', label: 'Selected/hovered block' },
100
+ { value: 'always', label: 'Always show' },
101
+ ]}
102
102
  />
103
103
  <MenuToggle
104
104
  label="Show status bar"
@@ -108,7 +108,6 @@ export function ViewMenuPanel() {
108
108
  <div className="squisq-view-menu-separator" role="separator" />
109
109
  <MenuRadioGroup
110
110
  label="Editor styling from theme"
111
- name="theme-inheritance"
112
111
  value={themeInheritance}
113
112
  onChange={(v) => setThemeInheritance(v as ThemeInheritance)}
114
113
  options={[
@@ -147,17 +146,16 @@ function MenuToggle({
147
146
 
148
147
  function MenuRadioGroup({
149
148
  label,
150
- name,
151
149
  value,
152
150
  onChange,
153
151
  options,
154
152
  }: {
155
153
  label: string;
156
- name: string;
157
154
  value: string;
158
155
  onChange: (next: string) => void;
159
156
  options: readonly { value: string; label: string }[];
160
157
  }) {
158
+ const name = useId();
161
159
  return (
162
160
  <div
163
161
  className="squisq-view-menu-row squisq-view-menu-row--radios"