@bendyline/squisq-editor-react 1.6.2 → 2.0.1

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 (218) hide show
  1. package/README.md +12 -3
  2. package/dist/index.d.ts +1275 -183
  3. package/dist/index.js +20345 -12777
  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 +1607 -241
  9. package/package.json +9 -5
  10. package/src/BlockPropertiesPopover.tsx +30 -15
  11. package/src/DocumentSettingsDialog.tsx +39 -33
  12. package/src/EditorContext.tsx +199 -42
  13. package/src/EditorShell.tsx +349 -261
  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 +362 -89
  20. package/src/PreviewPanel.tsx +312 -78
  21. package/src/RawEditor.tsx +13 -2
  22. package/src/RecorderEntry.tsx +2 -0
  23. package/src/StatusBar.tsx +5 -2
  24. package/src/TemplateAnnotation.ts +12 -3
  25. package/src/TemplatePicker.tsx +75 -21
  26. package/src/ThemeCustomizerPanel.tsx +8 -0
  27. package/src/ThemePicker.tsx +10 -5
  28. package/src/TimelineBlockPreview.tsx +1 -1
  29. package/src/Toolbar.tsx +357 -51
  30. package/src/TransitionPicker.tsx +36 -4
  31. package/src/ViewMenuPanel.tsx +12 -14
  32. package/src/WysiwygEditor.tsx +124 -91
  33. package/src/__tests__/blockPropertiesPopoverTheme.test.tsx +33 -0
  34. package/src/__tests__/blockTagActivity.test.ts +183 -0
  35. package/src/__tests__/buildDocumentPreviewMarkdown.test.ts +75 -0
  36. package/src/__tests__/buildPreviewDocContent.test.ts +48 -0
  37. package/src/__tests__/buildPreviewDocTransition.test.ts +21 -1
  38. package/src/__tests__/canvasSurfaceTextEditing.test.tsx +60 -0
  39. package/src/__tests__/codeContextSectionView.test.tsx +8 -6
  40. package/src/__tests__/customLayoutManagerTheme.test.tsx +34 -0
  41. package/src/__tests__/documentSettingsDialog.test.tsx +51 -1
  42. package/src/__tests__/editorScrollHandoff.test.tsx +102 -0
  43. package/src/__tests__/editorScrollSync.test.ts +65 -0
  44. package/src/__tests__/editorShellProps.test.tsx +123 -1
  45. package/src/__tests__/emojiPicker.test.tsx +2 -42
  46. package/src/__tests__/headingTransition.test.ts +0 -12
  47. package/src/__tests__/imageEditAffordance.test.tsx +5 -1
  48. package/src/__tests__/imageEditModalContract.test.tsx +119 -0
  49. package/src/__tests__/imageEditorShell.test.tsx +15 -1
  50. package/src/__tests__/jsonEditor.test.tsx +56 -1
  51. package/src/__tests__/layersPanel.test.tsx +75 -5
  52. package/src/__tests__/majorApiTypes.test.ts +26 -0
  53. package/src/__tests__/mediaAttachmentFlow.test.ts +2 -2
  54. package/src/__tests__/mediaBinDrop.test.tsx +90 -0
  55. package/src/__tests__/narrationSave.test.ts +148 -0
  56. package/src/__tests__/outlinePanel.test.tsx +93 -3
  57. package/src/__tests__/outlineSource.test.ts +217 -0
  58. package/src/__tests__/plainHtmlPreview.test.tsx +17 -1
  59. package/src/__tests__/presentationMode.test.tsx +290 -0
  60. package/src/__tests__/previewControls.test.tsx +382 -5
  61. package/src/__tests__/previewPanelAudioFailure.test.tsx +46 -0
  62. package/src/__tests__/previewPanelPresentation.test.tsx +126 -0
  63. package/src/__tests__/previewPanelTransforms.test.tsx +117 -0
  64. package/src/__tests__/rawEditorModelIsolation.test.ts +19 -0
  65. package/src/__tests__/recorderTheme.test.tsx +42 -0
  66. package/src/__tests__/resolveBlockVisual.test.ts +43 -0
  67. package/src/__tests__/selectionConversions.test.ts +80 -0
  68. package/src/__tests__/statusBar.test.tsx +27 -0
  69. package/src/__tests__/teleprompter.test.tsx +190 -0
  70. package/src/__tests__/templateAnnotationRoundTrip.test.ts +26 -0
  71. package/src/__tests__/templatePickerPortal.test.tsx +62 -0
  72. package/src/__tests__/tiptapBridge.test.ts +71 -7
  73. package/src/__tests__/tiptapCodeBlockRoundTrip.test.ts +109 -0
  74. package/src/__tests__/tiptapImageRoundTrip.test.ts +1 -1
  75. package/src/__tests__/toolbarSelectionConversion.test.tsx +164 -0
  76. package/src/__tests__/transformStyleId.test.ts +13 -0
  77. package/src/__tests__/useImageEditor.test.tsx +67 -0
  78. package/src/__tests__/useMediaRecorder.test.ts +75 -0
  79. package/src/__tests__/viewMenuPanel.test.tsx +130 -0
  80. package/src/__tests__/wysiwygImageUpload.test.ts +69 -0
  81. package/src/__tests__/wysiwygTemplateBadgeFocus.test.tsx +56 -0
  82. package/src/asciiDiagram/AsciiDiagramExtension.ts +343 -0
  83. package/src/asciiDiagram/AsciiDiagramWidget.tsx +254 -0
  84. package/src/asciiDiagram/RepairableDiagramExtension.ts +200 -0
  85. package/src/asciiDiagram/__tests__/AsciiDiagramExtension.test.ts +145 -0
  86. package/src/asciiDiagram/__tests__/RepairableDiagramExtension.test.ts +159 -0
  87. package/src/asciiDiagram/__tests__/asciiDiagramCommands.test.ts +284 -0
  88. package/src/asciiDiagram/__tests__/asciiDiagramOps.test.ts +114 -0
  89. package/src/asciiDiagram/__tests__/asciiPaste.test.ts +43 -0
  90. package/src/asciiDiagram/asciiDiagramCommands.ts +174 -0
  91. package/src/asciiDiagram/asciiDiagramData.ts +149 -0
  92. package/src/asciiDiagram/asciiDiagramOps.ts +238 -0
  93. package/src/asciiDiagram/asciiPaste.ts +19 -0
  94. package/src/blockTagActivity.ts +233 -0
  95. package/src/buildDocumentPreviewMarkdown.ts +168 -0
  96. package/src/buildPreviewDoc.ts +19 -12
  97. package/src/codeContext/types.ts +1 -1
  98. package/src/customTemplates/CustomLayoutManager.tsx +3 -2
  99. package/src/customTemplates/CustomTemplateContext.tsx +6 -0
  100. package/src/customTemplates/TemplateDesigner.tsx +7 -4
  101. package/src/customTemplates/__tests__/library.test.ts +8 -0
  102. package/src/customTemplates/__tests__/useMemoryLayerAdapter.test.ts +13 -0
  103. package/src/customTemplates/designer.css +158 -99
  104. package/src/customTemplates/library.ts +16 -3
  105. package/src/customTemplates/useMemoryLayerAdapter.ts +7 -1
  106. package/src/customThemes/CustomThemeContext.tsx +6 -0
  107. package/src/customThemes/CustomThemeDialog.tsx +35 -5
  108. package/src/customThemes/ImportThemeSection.tsx +178 -0
  109. package/src/customThemes/__tests__/customThemeLibrary.test.ts +9 -0
  110. package/src/customThemes/__tests__/importThemeSection.test.tsx +192 -0
  111. package/src/customThemes/customThemeLibrary.ts +7 -4
  112. package/src/customThemes/index.ts +7 -1
  113. package/src/customThemes/themeDraft.ts +23 -7
  114. package/src/diagram/DiagramCanvas.tsx +25 -6
  115. package/src/diagram/types.ts +35 -0
  116. package/src/editorScrollSync.ts +73 -0
  117. package/src/emojiData.ts +3 -23
  118. package/src/frontmatterSettings.ts +23 -0
  119. package/src/headingTransition.ts +0 -15
  120. package/src/imageEditor/CanvasSurface.tsx +29 -23
  121. package/src/imageEditor/LayersPanel.tsx +78 -3
  122. package/src/imageEditor/Toolbar.tsx +5 -1
  123. package/src/imageEditor/icons.tsx +4 -0
  124. package/src/imageEditor/image-editor.css +75 -0
  125. package/src/imageEditor/layers/SelectionHandles.tsx +1 -1
  126. package/src/imageEditor/useImageEditor.ts +71 -14
  127. package/src/index.ts +198 -20
  128. package/src/jsonEditor/JsonEditorContext.tsx +12 -6
  129. package/src/jsonEditor/RenderNode.tsx +24 -3
  130. package/src/jsonEditor/editors.tsx +86 -19
  131. package/src/mediaEntries.ts +12 -0
  132. package/src/monaco.ts +35 -0
  133. package/src/monacoWorkers.ts +89 -0
  134. package/src/outlineSource.ts +171 -0
  135. package/src/presentation/PresentationMode.tsx +596 -0
  136. package/src/rawEditorIsolation.ts +18 -0
  137. package/src/recorder/RecorderButton.tsx +9 -1
  138. package/src/recorder/RecorderModal.tsx +84 -41
  139. package/src/recorder/RecorderPanel.tsx +9 -1
  140. package/src/recorder/hooks/useMediaRecorder.ts +97 -53
  141. package/src/resolveBlockVisual.ts +10 -4
  142. package/src/scene/Scene.tsx +48 -18
  143. package/src/scene/SceneBlockExtension.ts +17 -10
  144. package/src/scene/SceneBlockWidget.tsx +5 -1
  145. package/src/scene/SceneViewport.tsx +7 -3
  146. package/src/scene/ShapePalette.tsx +17 -2
  147. package/src/scene/__tests__/DiagramAdapter.test.ts +86 -0
  148. package/src/scene/__tests__/SceneBlockExtension.test.ts +33 -0
  149. package/src/scene/__tests__/sceneIsolation.test.tsx +119 -0
  150. package/src/scene/adapters/DiagramAdapter.ts +12 -101
  151. package/src/scene/adapters/DrawingAdapter.ts +6 -1
  152. package/src/scene/adapters/LayoutAdapter.ts +3 -0
  153. package/src/scene/commands/SceneCommand.ts +17 -3
  154. package/src/scene/index.ts +1 -6
  155. package/src/scene/layers/DiagramEdges.tsx +21 -7
  156. package/src/scene/layers/edgeGeometry.ts +9 -1
  157. package/src/scene/layers/nodeCard.tsx +27 -8
  158. package/src/scene/scene.css +5 -2
  159. package/src/scene/text/SceneTextOverlay.tsx +5 -5
  160. package/src/scene/text/sceneTextChannel.ts +26 -20
  161. package/src/scene/text/sceneTextConfig.ts +4 -0
  162. package/src/scene/tools/ConnectTool.ts +13 -4
  163. package/src/scene/tools/SceneTool.ts +8 -0
  164. package/src/scene/tools/SelectTool.ts +39 -23
  165. package/src/scene/tools/ShapeTool.ts +2 -3
  166. package/src/selectionConversions.ts +155 -0
  167. package/src/styles/ascii-diagram.css +79 -0
  168. package/src/styles/ascii-timeline.css +596 -0
  169. package/src/styles/editor.css +736 -120
  170. package/src/styles/index.css +3 -0
  171. package/src/styles/tree-view.css +189 -0
  172. package/src/teleprompter/TeleprompterControls.tsx +218 -0
  173. package/src/teleprompter/TeleprompterSelfView.tsx +22 -0
  174. package/src/teleprompter/TeleprompterSurface.tsx +267 -0
  175. package/src/teleprompter/TeleprompterView.tsx +338 -0
  176. package/src/teleprompter/canvasRenderer.ts +161 -0
  177. package/src/teleprompter/floatingWindow.ts +352 -0
  178. package/src/teleprompter/index.ts +61 -0
  179. package/src/teleprompter/pcmWorklet.ts +62 -0
  180. package/src/teleprompter/recording/insertPreamble.ts +80 -0
  181. package/src/teleprompter/recording/narrationSave.ts +134 -0
  182. package/src/teleprompter/recording/useNarrationRecorder.ts +367 -0
  183. package/src/teleprompter/scrollModel.ts +85 -0
  184. package/src/teleprompter/teleprompterTheme.ts +303 -0
  185. package/src/teleprompter/types.ts +38 -0
  186. package/src/teleprompter/useFloatingWindow.ts +74 -0
  187. package/src/teleprompter/useMicAnalysis.ts +211 -0
  188. package/src/teleprompter/useTeleprompter.ts +421 -0
  189. package/src/templateContentPreviewResolver.ts +12 -5
  190. package/src/timeline/TimelineEditorWidget.tsx +861 -0
  191. package/src/timeline/TimelineViewExtension.ts +252 -0
  192. package/src/timeline/__tests__/TimelineEditorWidget.test.tsx +414 -0
  193. package/src/timeline/__tests__/TimelineViewExtension.test.ts +163 -0
  194. package/src/timeline/__tests__/timelineCommands.test.ts +359 -0
  195. package/src/timeline/__tests__/timelineOps.test.ts +277 -0
  196. package/src/timeline/__tests__/timelinePaste.test.ts +34 -0
  197. package/src/timeline/timelineCommands.ts +421 -0
  198. package/src/timeline/timelineData.ts +54 -0
  199. package/src/timeline/timelineOps.ts +381 -0
  200. package/src/timeline/timelinePaste.ts +8 -0
  201. package/src/tiptapBridge.ts +47 -21
  202. package/src/transformStyleId.ts +17 -0
  203. package/src/treeview/TreeOutlineWidget.tsx +390 -0
  204. package/src/treeview/TreeViewExtension.ts +250 -0
  205. package/src/treeview/__tests__/TreeOutlineWidget.test.tsx +156 -0
  206. package/src/treeview/__tests__/TreeViewExtension.test.ts +122 -0
  207. package/src/treeview/__tests__/treeOps.test.ts +177 -0
  208. package/src/treeview/__tests__/treePaste.test.ts +40 -0
  209. package/src/treeview/__tests__/treeViewCommands.test.ts +179 -0
  210. package/src/treeview/treeOps.ts +231 -0
  211. package/src/treeview/treePaste.ts +13 -0
  212. package/src/treeview/treeViewCommands.ts +99 -0
  213. package/src/treeview/treeViewData.ts +41 -0
  214. package/src/useMonacoLoader.ts +11 -34
  215. package/src/wysiwygImageUpload.ts +113 -0
  216. package/src/diagram/DiagramExtension.ts +0 -209
  217. package/src/diagram/DiagramWidget.tsx +0 -272
  218. package/src/diagram/useDiagramData.ts +0 -126
@@ -110,6 +110,7 @@ export function useImageEditor(options: UseImageEditorOptions): UseImageEditorRe
110
110
  const seeded = await seedFromSource(container, initialSrc);
111
111
  if (cancelled) return;
112
112
  await writeImageEditDoc(container, seeded, stateFilename);
113
+ if (cancelled) return;
113
114
  dispatch({ type: 'load', doc: seeded });
114
115
  setReady(true);
115
116
  // Capture an initial snapshot of the freshly-seeded state so the
@@ -131,27 +132,53 @@ export function useImageEditor(options: UseImageEditorOptions): UseImageEditorRe
131
132
  // ── Debounced persistence of state.json ────────────────────────────────
132
133
  const persistTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
133
134
  const docRef = useRef<ImageEditDoc | null>(null);
134
- docRef.current = state?.doc ?? null;
135
+ const dirtyRef = useRef(false);
136
+ const revisionRef = useRef(0);
137
+ const previousDocRef = useRef<ImageEditDoc | null>(null);
138
+ const nextDoc = state?.doc ?? null;
139
+ if (nextDoc !== previousDocRef.current) {
140
+ previousDocRef.current = nextDoc;
141
+ revisionRef.current += 1;
142
+ }
143
+ docRef.current = nextDoc;
144
+ dirtyRef.current = state?.dirty ?? false;
145
+ const writeQueueRef = useRef<Promise<void>>(Promise.resolve());
146
+ const persistTargetRef = useRef({ container, stateFilename });
147
+ persistTargetRef.current = { container, stateFilename };
148
+
149
+ const enqueueWrite = useCallback(
150
+ (doc: ImageEditDoc, revision: number, markClean: boolean): Promise<void> => {
151
+ const write = writeQueueRef.current
152
+ .catch(() => undefined)
153
+ .then(() => writeImageEditDoc(container, doc, stateFilename));
154
+ writeQueueRef.current = write.catch(() => undefined);
155
+ return write.then(() => {
156
+ if (markClean && revision === revisionRef.current && docRef.current === doc) {
157
+ dispatch({ type: 'mark-clean' });
158
+ }
159
+ });
160
+ },
161
+ [container, stateFilename],
162
+ );
135
163
 
136
164
  useEffect(() => {
137
165
  if (!state?.dirty) return;
138
166
  if (persistTimerRef.current) clearTimeout(persistTimerRef.current);
139
167
  persistTimerRef.current = setTimeout(() => {
140
- const doc = docRef.current;
168
+ const doc = state.doc;
141
169
  if (!doc) return;
142
- writeImageEditDoc(container, doc, stateFilename)
143
- .then(() => dispatch({ type: 'mark-clean' }))
144
- .catch((err: unknown) => {
145
- console.warn(
146
- '[squisq-editor] image-edit state persist failed:',
147
- err instanceof Error ? err.message : err,
148
- );
149
- });
170
+ const revision = revisionRef.current;
171
+ enqueueWrite(doc, revision, true).catch((err: unknown) => {
172
+ console.warn(
173
+ '[squisq-editor] image-edit state persist failed:',
174
+ err instanceof Error ? err.message : err,
175
+ );
176
+ });
150
177
  }, persistDebounceMs);
151
178
  return () => {
152
179
  if (persistTimerRef.current) clearTimeout(persistTimerRef.current);
153
180
  };
154
- }, [state?.dirty, state?.doc, container, stateFilename, persistDebounceMs]);
181
+ }, [state?.dirty, state?.doc, persistDebounceMs, enqueueWrite]);
155
182
 
156
183
  const flush = useCallback(async () => {
157
184
  const doc = docRef.current;
@@ -160,9 +187,29 @@ export function useImageEditor(options: UseImageEditorOptions): UseImageEditorRe
160
187
  clearTimeout(persistTimerRef.current);
161
188
  persistTimerRef.current = null;
162
189
  }
163
- await writeImageEditDoc(container, doc, stateFilename);
164
- dispatch({ type: 'mark-clean' });
165
- }, [container, stateFilename]);
190
+ await enqueueWrite(doc, revisionRef.current, true);
191
+ }, [enqueueWrite]);
192
+
193
+ // A quick modal close must not discard the final edit that is still inside
194
+ // the debounce window. Queue it behind any in-flight write on unmount.
195
+ useEffect(
196
+ () => () => {
197
+ if (persistTimerRef.current) clearTimeout(persistTimerRef.current);
198
+ const doc = docRef.current;
199
+ if (!dirtyRef.current || !doc) return;
200
+ const target = persistTargetRef.current;
201
+ const write = writeQueueRef.current
202
+ .catch(() => undefined)
203
+ .then(() => writeImageEditDoc(target.container, doc, target.stateFilename));
204
+ writeQueueRef.current = write.catch((err: unknown) => {
205
+ console.warn(
206
+ '[squisq-editor] image-edit final persist failed:',
207
+ err instanceof Error ? err.message : err,
208
+ );
209
+ });
210
+ },
211
+ [],
212
+ );
166
213
 
167
214
  // ── Versioning ─────────────────────────────────────────────────────────
168
215
  const versioning = useMemo(
@@ -204,17 +251,26 @@ export function useImageEditor(options: UseImageEditorOptions): UseImageEditorRe
204
251
 
205
252
  // ── Asset URL cache ────────────────────────────────────────────────────
206
253
  const urlCacheRef = useRef<Map<string, string>>(new Map());
254
+ const urlCacheGenerationRef = useRef(0);
207
255
 
208
256
  const resolveAssetUrl = useCallback(
209
257
  async (path: string): Promise<string> => {
210
258
  const cache = urlCacheRef.current;
259
+ const generation = urlCacheGenerationRef.current;
211
260
  const cached = cache.get(path);
212
261
  if (cached) return cached;
213
262
  const data = await container.readFile(path);
263
+ if (generation !== urlCacheGenerationRef.current) {
264
+ throw new Error('useImageEditor: asset resolution cancelled');
265
+ }
214
266
  if (!data) throw new Error(`useImageEditor: missing asset "${path}"`);
215
267
  const list = await container.listFiles(path);
216
268
  const mime = list.find((e) => e.path === path)?.mimeType ?? 'application/octet-stream';
217
269
  const url = URL.createObjectURL(new Blob([data], { type: mime }));
270
+ if (generation !== urlCacheGenerationRef.current) {
271
+ URL.revokeObjectURL(url);
272
+ throw new Error('useImageEditor: asset resolution cancelled');
273
+ }
218
274
  cache.set(path, url);
219
275
  return url;
220
276
  },
@@ -225,6 +281,7 @@ export function useImageEditor(options: UseImageEditorOptions): UseImageEditorRe
225
281
  useEffect(() => {
226
282
  const cache = urlCacheRef.current;
227
283
  return () => {
284
+ urlCacheGenerationRef.current += 1;
228
285
  for (const url of cache.values()) URL.revokeObjectURL(url);
229
286
  cache.clear();
230
287
  };
package/src/index.ts CHANGED
@@ -45,6 +45,7 @@ export type {
45
45
  DocumentLinkProvider,
46
46
  ViewPreferences,
47
47
  ThemeInheritance,
48
+ BlockTagVisibility,
48
49
  LayoutMode,
49
50
  } from './EditorContext.js';
50
51
 
@@ -99,11 +100,12 @@ export { DocumentSettingsDialog } from './DocumentSettingsDialog.js';
99
100
  export type { DocumentSettingsDialogProps } from './DocumentSettingsDialog.js';
100
101
  export { ThemePicker } from './ThemePicker.js';
101
102
  export type { ThemePickerProps } from './ThemePicker.js';
102
- export { EMOJI_CATEGORIES, ALL_EMOJIS, searchEmojis } from './emojiData.js';
103
- export type { EmojiEntry, EmojiCategory } from './emojiData.js';
103
+ export { PICKER_CATEGORIES, ALL_PICKER_ENTRIES, searchPickerEntries } from './emojiData.js';
104
+ export type { PickerEntry, PickerCategory } from './emojiData.js';
104
105
  export {
105
106
  PreviewSettingsProvider,
106
107
  PreviewToolbarControls,
108
+ PreviewModeMenu,
107
109
  PreviewModeSwitch,
108
110
  PreviewFormatSwitch,
109
111
  usePreviewSettings,
@@ -142,7 +144,6 @@ export {
142
144
  setHeadingLineTransition,
143
145
  readBlockAttrsTransition,
144
146
  setHeadingAttrsTransition,
145
- setBlockAttrsTransition,
146
147
  EMPTY_TRANSITION,
147
148
  } from './headingTransition.js';
148
149
  export type { HeadingTransitionAttrs, TransitionFields } from './headingTransition.js';
@@ -190,12 +191,26 @@ export {
190
191
  export { useMonacoLoader } from './useMonacoLoader.js';
191
192
  export type { UseMonacoLoaderResult } from './useMonacoLoader.js';
192
193
 
194
+ // Monaco language-service worker wiring — the host supplies the `?worker`
195
+ // constructors, this owns the label→worker mapping. See monacoWorkers.ts.
196
+ export { configureMonacoWorkers } from './monacoWorkers.js';
197
+ export type { MonacoWorkerConstructor, MonacoWorkerConstructors } from './monacoWorkers.js';
198
+
193
199
  // Custom themes — provider stack (doc frontmatter + browser-local library)
194
- export { CustomThemeProvider, useCustomThemes, useDocCustomThemes } from './customThemes/index.js';
200
+ export {
201
+ CustomThemeProvider,
202
+ useCustomThemes,
203
+ useDocCustomThemes,
204
+ ImportThemeSection,
205
+ draftPatchFromImportedTheme,
206
+ } from './customThemes/index.js';
195
207
  export type {
196
208
  CustomThemeContextValue,
197
209
  CustomThemeProviderProps,
198
210
  DocCustomThemes,
211
+ ImportThemeSectionProps,
212
+ ImportedThemeResult,
213
+ ThemeSaveExtras,
199
214
  } from './customThemes/index.js';
200
215
 
201
216
  // Custom templates — provider stack (doc frontmatter + browser-local library)
@@ -219,24 +234,130 @@ export { buildPreviewDoc } from './buildPreviewDoc.js';
219
234
  // Tiptap extension: Heading with template annotation support
220
235
  export { HeadingWithTemplate } from './TemplateAnnotation.js';
221
236
 
222
- // Diagram editor — Tiptap extension that mounts a React-Flow canvas
223
- // below any `### Title {[diagram]}` heading and hides the section's
224
- // child headings (they're rendered as nodes in the canvas).
225
- export { DiagramExtension } from './diagram/DiagramExtension.js';
237
+ // Diagram editor — ASCII fences are the authored diagram format. The
238
+ // AsciiDiagramExtension turns code fences containing box-and-line art
239
+ // into interactive canvases; edits re-render the art (the fence stays the
240
+ // source of truth). Legacy `{[diagram]}` heading sections still RENDER in
241
+ // preview/player via core, but heading-based canvas editing was removed
242
+ // (BREAKING: DiagramExtension / DiagramWidget / useDiagramData / the
243
+ // heading command helpers are gone).
226
244
  export { DiagramCanvas } from './diagram/DiagramCanvas.js';
227
245
  export type { DiagramCommand } from './diagram/DiagramCanvas.js';
228
- export { DiagramWidget } from './diagram/DiagramWidget.js';
229
- export { useDiagramData } from './diagram/useDiagramData.js';
230
- export type { DiagramData, DiagramRFNode, DiagramRFEdge } from './diagram/useDiagramData.js';
246
+ export type { DiagramData, DiagramNode, DiagramEdge } from './diagram/types.js';
247
+ export {
248
+ AsciiDiagramExtension,
249
+ findAsciiDiagramBlockPos,
250
+ isAsciiSourceVisible,
251
+ toggleAsciiSource,
252
+ } from './asciiDiagram/AsciiDiagramExtension.js';
253
+ export type {
254
+ AsciiDiagramBlockEntry,
255
+ AsciiDiagramExtensionOptions,
256
+ AsciiDiagramPluginState,
257
+ } from './asciiDiagram/AsciiDiagramExtension.js';
258
+ export { AsciiDiagramWidget } from './asciiDiagram/AsciiDiagramWidget.js';
259
+ export { useAsciiDiagramData, asciiDiagramToCanvas } from './asciiDiagram/asciiDiagramData.js';
260
+ export type { AsciiDiagramView } from './asciiDiagram/asciiDiagramData.js';
261
+ export {
262
+ applyAsciiDiagramCommand,
263
+ applyRepairCommand,
264
+ replaceAsciiFenceText,
265
+ } from './asciiDiagram/asciiDiagramCommands.js';
266
+ export type { ApplyAsciiDiagramCommandOptions } from './asciiDiagram/asciiDiagramCommands.js';
267
+ // RepairableDiagramExtension mounts an inline "Repair as diagram" button on
268
+ // code fences holding BROKEN box art — art too misaligned for clean detection
269
+ // (so it renders as a faithful code block). One click reconstructs it into
270
+ // clean `diagram`-tagged art that the AsciiDiagramExtension turns into a
271
+ // canvas. Peer to the diagram/tree editors; claims the fences neither does.
272
+ export {
273
+ RepairableDiagramExtension,
274
+ findRepairableBlockPos,
275
+ isRepairableFence,
276
+ REPAIRABLE_KEY,
277
+ } from './asciiDiagram/RepairableDiagramExtension.js';
278
+ export type {
279
+ RepairableBlockEntry,
280
+ RepairableDiagramExtensionOptions,
281
+ RepairablePluginState,
282
+ } from './asciiDiagram/RepairableDiagramExtension.js';
283
+ export {
284
+ addEdgeOp,
285
+ addNodeOp,
286
+ moveNodeOp,
287
+ removeEdgeOp,
288
+ removeNodeOp,
289
+ renameNodeOp,
290
+ resizeNodeOp,
291
+ sanitizeAsciiLabel,
292
+ translateDiagramOp,
293
+ } from './asciiDiagram/asciiDiagramOps.js';
294
+ export { shouldPasteAsAsciiFence } from './asciiDiagram/asciiPaste.js';
295
+
296
+ // Treeview editor — ASCII file-tree / outline fences get an interactive
297
+ // outline widget; edits re-render the tree art (the fence stays the source
298
+ // of truth). Peer to the ASCII diagram editor; mutually exclusive with it
299
+ // (a fence with boxes is a diagram, not a tree).
300
+ export { TreeViewExtension, findTreeBlockPos } from './treeview/TreeViewExtension.js';
301
+ export type {
302
+ TreeBlockEntry,
303
+ TreeViewExtensionOptions,
304
+ TreeViewPluginState,
305
+ } from './treeview/TreeViewExtension.js';
306
+ export { TreeOutlineWidget } from './treeview/TreeOutlineWidget.js';
307
+ export { useTreeViewData } from './treeview/treeViewData.js';
308
+ export type { TreeViewData } from './treeview/treeViewData.js';
309
+ export { applyTreeCommand, replaceTreeFenceText } from './treeview/treeViewCommands.js';
310
+ export type { TreeCommand } from './treeview/treeViewCommands.js';
231
311
  export {
232
- moveNode,
233
- addConnection,
234
- removeConnection,
235
- renameNode,
236
- addNode,
237
- removeNode,
238
- listDiagramChildren,
239
- } from './diagram/diagramCommands.js';
312
+ addItemOp,
313
+ indentItemOp,
314
+ moveItemDownOp,
315
+ moveItemUpOp,
316
+ outdentItemOp,
317
+ removeItemOp,
318
+ renameItemOp,
319
+ sanitizeTreeLabel,
320
+ toggleDirOp,
321
+ } from './treeview/treeOps.js';
322
+ export { shouldPasteAsTreeFence } from './treeview/treePaste.js';
323
+
324
+ // Timeline editor — authored marker-rail fences become an accessible canvas
325
+ // with add-point affordances and a selected-point inspector. Semantic edits
326
+ // re-render the canonical `timeline` fence in one undoable transaction.
327
+ export {
328
+ TimelineViewExtension,
329
+ TIMELINE_VIEW_KEY,
330
+ findTimelineBlockPos,
331
+ getTimelineForNode,
332
+ parseTimelineForNode,
333
+ } from './timeline/TimelineViewExtension.js';
334
+ export type {
335
+ TimelineBlockEntry,
336
+ TimelineViewExtensionOptions,
337
+ TimelineViewPluginState,
338
+ } from './timeline/TimelineViewExtension.js';
339
+ export { TimelineEditorWidget } from './timeline/TimelineEditorWidget.js';
340
+ export type { TimelineEditorWidgetProps } from './timeline/TimelineEditorWidget.js';
341
+ export { useTimelineData } from './timeline/timelineData.js';
342
+ export type { TimelineViewData } from './timeline/timelineData.js';
343
+ export {
344
+ applyTimelineCommand,
345
+ isTimelineSourceSafeForSemanticEdit,
346
+ } from './timeline/timelineCommands.js';
347
+ export type { TimelineCommand, TimelineCommandResult } from './timeline/timelineCommands.js';
348
+ export {
349
+ addTimelineEventOp,
350
+ nextTimelineEventId,
351
+ removeTimelineEventOp,
352
+ sanitizeTimelineText,
353
+ updateTimelineEventOp,
354
+ } from './timeline/timelineOps.js';
355
+ export type {
356
+ AddTimelineEventOptions,
357
+ AddTimelineEventResult,
358
+ TimelineEventPatch,
359
+ } from './timeline/timelineOps.js';
360
+ export { shouldPasteAsTimelineFence } from './timeline/timelinePaste.js';
240
361
 
241
362
  // JSON Form — editable component
242
363
  export { JsonEditor } from './jsonEditor/index.js';
@@ -248,7 +369,11 @@ export type { JsonEditorProps } from './jsonEditor/index.js';
248
369
  // `MediaProvider`. Previously published as `@bendyline/squisq-recorder-react`;
249
370
  // folded into editor-react so it ships with the editor it's wired into.
250
371
  export { RecorderModal } from './recorder/RecorderModal.js';
251
- export type { RecorderModalProps, RecorderSaveResult } from './recorder/RecorderModal.js';
372
+ export type {
373
+ RecorderColorScheme,
374
+ RecorderModalProps,
375
+ RecorderSaveResult,
376
+ } from './recorder/RecorderModal.js';
252
377
  export { RecorderButton } from './recorder/RecorderButton.js';
253
378
  export type { RecorderButtonProps } from './recorder/RecorderButton.js';
254
379
  export { RecorderPanel } from './recorder/RecorderPanel.js';
@@ -277,6 +402,59 @@ export type { CaptureKind, ResolvedFormat } from './recorder/formats.js';
277
402
  export { buildTimingJson, encodeTimingJson, timingPathFor } from './recorder/timingJson.js';
278
403
  export type { TimingJson, RecordedBookmark } from './recorder/timingJson.js';
279
404
 
405
+ // Teleprompter — the Narrate display mode under the Use tab: voice-paced
406
+ // prompter surface, floating-window tiers, and the mic analysis pipeline.
407
+ export {
408
+ TeleprompterView,
409
+ TeleprompterSurface,
410
+ TeleprompterControls,
411
+ useTeleprompter,
412
+ useNarrationRecorder,
413
+ buildNarrationSavePlan,
414
+ executeNarrationSave,
415
+ insertNarrationPreamble,
416
+ narrationAnnotationLine,
417
+ cameraVideoLine,
418
+ vadConfigForSensitivity,
419
+ useMicAnalysis,
420
+ PCM_WORKLET_SOURCE,
421
+ PCM_WORKLET_NAME,
422
+ registerPcmWorklet,
423
+ createFloatingWindowManager,
424
+ detectFloatTiers,
425
+ useFloatingWindow,
426
+ measureTokenLines,
427
+ targetOffsetFor,
428
+ stepScroll,
429
+ EYE_LINE_FRACTION,
430
+ TELEPROMPTER_CSS,
431
+ ensureTeleprompterStyles,
432
+ prompterVarsFromTheme,
433
+ DEFAULT_TELEPROMPTER_PREFS,
434
+ } from './teleprompter/index.js';
435
+ export type {
436
+ TeleprompterViewProps,
437
+ TeleprompterSurfaceProps,
438
+ TeleprompterControlsProps,
439
+ TeleprompterController,
440
+ TeleprompterRecordingDeps,
441
+ NarrationRecorderController,
442
+ NarrationRecorderState,
443
+ NarrationTake,
444
+ NarrationSavePlan,
445
+ NarrationSaveResult,
446
+ MicAnalysisHandle,
447
+ MicAnalysisStatus,
448
+ FloatingWindowManager,
449
+ FloatOpenOptions,
450
+ CanvasSink,
451
+ FloatingWindowHandle,
452
+ TokenLineMap,
453
+ TeleprompterPrefs,
454
+ PrompterTransport,
455
+ FloatTier,
456
+ } from './teleprompter/index.js';
457
+
280
458
  // Image editor — layered, sidecar-persisted raster authoring surface.
281
459
  // Pairs with `ImageViewer` and the `<basename>_files/` sidecar convention.
282
460
  export { ImageEditor } from './ImageEditor.js';
@@ -5,7 +5,7 @@
5
5
  * to the host's `onChange`.
6
6
  */
7
7
 
8
- import { createContext, useContext, useMemo, type ReactNode } from 'react';
8
+ import { createContext, useContext, useEffect, useMemo, type ReactNode } from 'react';
9
9
  import type {
10
10
  JsonFormValidationError,
11
11
  JsonFormValidator,
@@ -38,18 +38,24 @@ export interface JsonEditorProviderProps {
38
38
  export function JsonEditorProvider(props: JsonEditorProviderProps) {
39
39
  const { rootSchema, rootData, onRootChange, density, validate, onValidate, children } = props;
40
40
 
41
+ const validationErrors = useMemo(
42
+ () => (validate ? validate(rootData, rootSchema) : []),
43
+ [validate, rootData, rootSchema],
44
+ );
41
45
  const errors = useMemo(() => {
42
- if (!validate) return new Map<string, JsonFormValidationError[]>();
43
- const list = validate(rootData, rootSchema);
44
- if (onValidate) onValidate(list);
45
46
  const grouped = new Map<string, JsonFormValidationError[]>();
46
- for (const e of list) {
47
+ for (const e of validationErrors) {
47
48
  const arr = grouped.get(e.path) ?? [];
48
49
  arr.push(e);
49
50
  grouped.set(e.path, arr);
50
51
  }
51
52
  return grouped;
52
- }, [validate, rootData, rootSchema, onValidate]);
53
+ }, [validationErrors]);
54
+
55
+ useEffect(() => {
56
+ if (!onValidate) return;
57
+ onValidate(validationErrors);
58
+ }, [validationErrors, onValidate]);
53
59
 
54
60
  const setAtPath = useMemo(
55
61
  () => (pointer: string, value: unknown) => {
@@ -11,6 +11,7 @@ import {
11
11
  resolveRef,
12
12
  type SquisqAnnotatedSchema,
13
13
  } from '@bendyline/squisq/jsonForm';
14
+ import { useId } from 'react';
14
15
  import { useJsonEditor } from './JsonEditorContext';
15
16
  import { EDITORS, isCompositeControl } from './editors';
16
17
 
@@ -23,6 +24,7 @@ export interface RenderNodeProps {
23
24
  }
24
25
 
25
26
  export function RenderNode(props: RenderNodeProps): React.ReactElement | null {
27
+ const generatedId = useId().replace(/:/g, '');
26
28
  const { rootSchema, rootData, errors } = useJsonEditor();
27
29
  const resolved = resolveRef(props.schema, rootSchema) ?? props.schema;
28
30
 
@@ -35,6 +37,11 @@ export function RenderNode(props: RenderNodeProps): React.ReactElement | null {
35
37
  const label = resolved.squisq?.label ?? resolved.title;
36
38
  const help = resolved.squisq?.help ?? resolved.description;
37
39
  const fieldErrors = errors.get(props.pointer) ?? [];
40
+ const controlId = `squisq-json-control-${generatedId}`;
41
+ const labelId = label ? `${controlId}-label` : undefined;
42
+ const helpId = help ? `${controlId}-help` : undefined;
43
+ const errorId = fieldErrors.length > 0 ? `${controlId}-error` : undefined;
44
+ const describedBy = [helpId, errorId].filter(Boolean).join(' ') || undefined;
38
45
 
39
46
  const editorEl = (
40
47
  <Editor
@@ -42,6 +49,10 @@ export function RenderNode(props: RenderNodeProps): React.ReactElement | null {
42
49
  schema={resolved}
43
50
  pointer={props.pointer}
44
51
  disabled={disabled}
52
+ controlId={controlId}
53
+ labelledBy={labelId}
54
+ describedBy={describedBy}
55
+ invalid={fieldErrors.length > 0}
45
56
  // GroupEditor honors this — other editors ignore the extra prop.
46
57
  // @ts-expect-error optional renderer-specific prop
47
58
  suppressTitle={props.suppressTopGroupTitle}
@@ -55,11 +66,21 @@ export function RenderNode(props: RenderNodeProps): React.ReactElement | null {
55
66
 
56
67
  return (
57
68
  <div className={`squisq-jf-field${disabled ? ' squisq-jf-field--disabled' : ''}`}>
58
- {label ? <label className="squisq-jf-field__label">{label}</label> : null}
69
+ {label ? (
70
+ <label id={labelId} htmlFor={controlId} className="squisq-jf-field__label">
71
+ {label}
72
+ </label>
73
+ ) : null}
59
74
  {editorEl}
60
- {help ? <p className="squisq-jf-field__help">{help}</p> : null}
75
+ {help ? (
76
+ <p id={helpId} className="squisq-jf-field__help">
77
+ {help}
78
+ </p>
79
+ ) : null}
61
80
  {fieldErrors.length > 0 ? (
62
- <p className="squisq-jf-field__error">{fieldErrors[0].message}</p>
81
+ <p id={errorId} className="squisq-jf-field__error" role="alert">
82
+ {fieldErrors[0].message}
83
+ </p>
63
84
  ) : null}
64
85
  </div>
65
86
  );