@bendyline/squisq-editor-react 1.6.1 → 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 (208) hide show
  1. package/README.md +12 -3
  2. package/dist/index.d.ts +1304 -208
  3. package/dist/index.js +22838 -14494
  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 +2384 -408
  9. package/package.json +9 -5
  10. package/src/BlockPropertiesPopover.tsx +48 -17
  11. package/src/DocumentSettingsDialog.tsx +13 -21
  12. package/src/EditorContext.tsx +199 -42
  13. package/src/EditorShell.tsx +403 -270
  14. package/src/ImageEditor.tsx +22 -1
  15. package/src/InlinePreviewGutter.tsx +16 -8
  16. package/src/MediaBin.tsx +276 -40
  17. package/src/OutlinePanel.tsx +202 -7
  18. package/src/PlainHtmlPreview.tsx +40 -1
  19. package/src/PreviewControls.tsx +466 -93
  20. package/src/PreviewPanel.tsx +313 -77
  21. package/src/RawEditor.tsx +13 -2
  22. package/src/StatusBar.tsx +5 -2
  23. package/src/TemplateAnnotation.ts +34 -10
  24. package/src/TemplateContentPreview.tsx +56 -0
  25. package/src/TemplatePicker.tsx +355 -134
  26. package/src/ThemeCustomizerPanel.tsx +30 -15
  27. package/src/ThemePicker.tsx +10 -5
  28. package/src/TimelineBlockPreview.tsx +1 -1
  29. package/src/Toolbar.tsx +719 -238
  30. package/src/TransitionPicker.tsx +44 -5
  31. package/src/ViewMenuPanel.tsx +12 -14
  32. package/src/ViewSwitcher.tsx +4 -4
  33. package/src/WysiwygEditor.tsx +167 -92
  34. package/src/__tests__/blockPropertiesPopoverTheme.test.tsx +33 -0
  35. package/src/__tests__/blockTagActivity.test.ts +183 -0
  36. package/src/__tests__/buildDocumentPreviewMarkdown.test.ts +75 -0
  37. package/src/__tests__/buildPreviewDocContent.test.ts +48 -0
  38. package/src/__tests__/buildPreviewDocTransition.test.ts +22 -3
  39. package/src/__tests__/canvasSurfaceTextEditing.test.tsx +60 -0
  40. package/src/__tests__/customLayoutManagerTheme.test.tsx +34 -0
  41. package/src/__tests__/documentSettingsDialog.test.tsx +29 -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 +390 -1
  45. package/src/__tests__/emojiPicker.test.tsx +2 -42
  46. package/src/__tests__/headingTransition.test.ts +53 -15
  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 +38 -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__/mediaBinDrop.test.tsx +90 -0
  54. package/src/__tests__/mediaReferences.test.ts +82 -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 +312 -6
  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__/resolveBlockVisual.test.ts +43 -0
  66. package/src/__tests__/statusBar.test.tsx +27 -0
  67. package/src/__tests__/teleprompter.test.tsx +190 -0
  68. package/src/__tests__/templateAnnotationRoundTrip.test.ts +49 -2
  69. package/src/__tests__/templateContentPreview.test.ts +101 -0
  70. package/src/__tests__/templatePickerPortal.test.tsx +62 -0
  71. package/src/__tests__/tiptapBridge.test.ts +70 -0
  72. package/src/__tests__/tiptapCodeBlockRoundTrip.test.ts +109 -0
  73. package/src/__tests__/transformStyleId.test.ts +13 -0
  74. package/src/__tests__/useImageEditor.test.tsx +67 -0
  75. package/src/__tests__/useMediaRecorder.test.ts +75 -0
  76. package/src/__tests__/viewMenuPanel.test.tsx +130 -0
  77. package/src/__tests__/wysiwygImageUpload.test.ts +69 -0
  78. package/src/__tests__/wysiwygTemplateBadgeFocus.test.tsx +56 -0
  79. package/src/asciiDiagram/AsciiDiagramExtension.ts +343 -0
  80. package/src/asciiDiagram/AsciiDiagramWidget.tsx +224 -0
  81. package/src/asciiDiagram/RepairableDiagramExtension.ts +200 -0
  82. package/src/asciiDiagram/__tests__/AsciiDiagramExtension.test.ts +145 -0
  83. package/src/asciiDiagram/__tests__/RepairableDiagramExtension.test.ts +159 -0
  84. package/src/asciiDiagram/__tests__/asciiDiagramCommands.test.ts +227 -0
  85. package/src/asciiDiagram/__tests__/asciiDiagramOps.test.ts +114 -0
  86. package/src/asciiDiagram/__tests__/asciiPaste.test.ts +43 -0
  87. package/src/asciiDiagram/asciiDiagramCommands.ts +148 -0
  88. package/src/asciiDiagram/asciiDiagramData.ts +116 -0
  89. package/src/asciiDiagram/asciiDiagramOps.ts +219 -0
  90. package/src/asciiDiagram/asciiPaste.ts +19 -0
  91. package/src/blockTagActivity.ts +233 -0
  92. package/src/buildDocumentPreviewMarkdown.ts +168 -0
  93. package/src/buildPreviewDoc.ts +19 -12
  94. package/src/customTemplates/CustomLayoutManager.tsx +3 -2
  95. package/src/customTemplates/CustomTemplateContext.tsx +6 -0
  96. package/src/customTemplates/TemplateDesigner.tsx +7 -4
  97. package/src/customTemplates/__tests__/library.test.ts +8 -0
  98. package/src/customTemplates/designer.css +158 -99
  99. package/src/customTemplates/library.ts +16 -3
  100. package/src/customThemes/CustomThemeContext.tsx +6 -0
  101. package/src/customThemes/CustomThemeDialog.tsx +35 -5
  102. package/src/customThemes/ImportThemeSection.tsx +178 -0
  103. package/src/customThemes/__tests__/customThemeLibrary.test.ts +9 -0
  104. package/src/customThemes/__tests__/importThemeSection.test.tsx +192 -0
  105. package/src/customThemes/customThemeLibrary.ts +7 -4
  106. package/src/customThemes/index.ts +7 -1
  107. package/src/customThemes/themeDraft.ts +23 -7
  108. package/src/diagram/DiagramCanvas.tsx +9 -4
  109. package/src/diagram/types.ts +35 -0
  110. package/src/editorScrollSync.ts +73 -0
  111. package/src/emojiData.ts +3 -23
  112. package/src/headingTransition.ts +85 -25
  113. package/src/imageEditor/CanvasSurface.tsx +29 -23
  114. package/src/imageEditor/LayersPanel.tsx +78 -3
  115. package/src/imageEditor/Toolbar.tsx +5 -1
  116. package/src/imageEditor/icons.tsx +4 -0
  117. package/src/imageEditor/image-editor.css +75 -0
  118. package/src/imageEditor/layers/SelectionHandles.tsx +1 -1
  119. package/src/imageEditor/useImageEditor.ts +71 -14
  120. package/src/index.ts +193 -20
  121. package/src/jsonEditor/JsonEditorContext.tsx +12 -6
  122. package/src/jsonEditor/RenderNode.tsx +24 -3
  123. package/src/jsonEditor/editors.tsx +86 -19
  124. package/src/mediaEntries.ts +12 -0
  125. package/src/mediaReferences.ts +299 -0
  126. package/src/monaco.ts +35 -0
  127. package/src/monacoWorkers.ts +89 -0
  128. package/src/outlineSource.ts +171 -0
  129. package/src/presentation/PresentationMode.tsx +596 -0
  130. package/src/rawEditorIsolation.ts +18 -0
  131. package/src/recorder/hooks/useMediaRecorder.ts +97 -53
  132. package/src/resolveBlockVisual.ts +10 -4
  133. package/src/scene/Scene.tsx +101 -25
  134. package/src/scene/SceneBlockExtension.ts +17 -10
  135. package/src/scene/SceneBlockWidget.tsx +11 -4
  136. package/src/scene/SceneSelection.tsx +19 -15
  137. package/src/scene/SceneSideToolbar.tsx +89 -0
  138. package/src/scene/SceneViewport.tsx +7 -3
  139. package/src/scene/ShapePalette.tsx +17 -2
  140. package/src/scene/__tests__/DiagramAdapter.test.ts +86 -0
  141. package/src/scene/__tests__/SceneBlockExtension.test.ts +33 -0
  142. package/src/scene/__tests__/sceneIsolation.test.tsx +93 -0
  143. package/src/scene/adapters/DiagramAdapter.ts +12 -101
  144. package/src/scene/commands/SceneCommand.ts +4 -1
  145. package/src/scene/index.ts +1 -6
  146. package/src/scene/layers/DiagramEdges.tsx +24 -9
  147. package/src/scene/layers/edgeGeometry.ts +31 -4
  148. package/src/scene/layers/nodeCard.tsx +27 -8
  149. package/src/scene/scene.css +145 -2
  150. package/src/scene/text/SceneTextOverlay.tsx +5 -5
  151. package/src/scene/text/sceneTextChannel.ts +26 -20
  152. package/src/scene/text/sceneTextConfig.ts +4 -0
  153. package/src/scene/tools/ConnectTool.ts +126 -28
  154. package/src/scene/tools/DrawingConnectTool.ts +86 -16
  155. package/src/scene/tools/SceneTool.ts +10 -0
  156. package/src/scene/tools/SelectTool.ts +34 -18
  157. package/src/scene/tools/ShapeTool.ts +2 -3
  158. package/src/styles/ascii-diagram.css +79 -0
  159. package/src/styles/ascii-timeline.css +499 -0
  160. package/src/styles/editor.css +1566 -219
  161. package/src/styles/index.css +3 -0
  162. package/src/styles/tree-view.css +139 -0
  163. package/src/teleprompter/TeleprompterControls.tsx +218 -0
  164. package/src/teleprompter/TeleprompterSelfView.tsx +22 -0
  165. package/src/teleprompter/TeleprompterSurface.tsx +267 -0
  166. package/src/teleprompter/TeleprompterView.tsx +338 -0
  167. package/src/teleprompter/canvasRenderer.ts +161 -0
  168. package/src/teleprompter/floatingWindow.ts +352 -0
  169. package/src/teleprompter/index.ts +61 -0
  170. package/src/teleprompter/pcmWorklet.ts +62 -0
  171. package/src/teleprompter/recording/insertPreamble.ts +80 -0
  172. package/src/teleprompter/recording/narrationSave.ts +134 -0
  173. package/src/teleprompter/recording/useNarrationRecorder.ts +367 -0
  174. package/src/teleprompter/scrollModel.ts +85 -0
  175. package/src/teleprompter/teleprompterTheme.ts +303 -0
  176. package/src/teleprompter/types.ts +38 -0
  177. package/src/teleprompter/useFloatingWindow.ts +74 -0
  178. package/src/teleprompter/useMicAnalysis.ts +211 -0
  179. package/src/teleprompter/useTeleprompter.ts +421 -0
  180. package/src/templateContentPreviewResolver.ts +360 -0
  181. package/src/timeline/TimelineEditorWidget.tsx +702 -0
  182. package/src/timeline/TimelineViewExtension.ts +252 -0
  183. package/src/timeline/__tests__/TimelineEditorWidget.test.tsx +355 -0
  184. package/src/timeline/__tests__/TimelineViewExtension.test.ts +163 -0
  185. package/src/timeline/__tests__/timelineCommands.test.ts +327 -0
  186. package/src/timeline/__tests__/timelineOps.test.ts +222 -0
  187. package/src/timeline/__tests__/timelinePaste.test.ts +34 -0
  188. package/src/timeline/timelineCommands.ts +367 -0
  189. package/src/timeline/timelineData.ts +54 -0
  190. package/src/timeline/timelineOps.ts +277 -0
  191. package/src/timeline/timelinePaste.ts +8 -0
  192. package/src/tiptapBridge.ts +84 -49
  193. package/src/transformStyleId.ts +17 -0
  194. package/src/treeview/TreeOutlineWidget.tsx +240 -0
  195. package/src/treeview/TreeViewExtension.ts +250 -0
  196. package/src/treeview/__tests__/TreeViewExtension.test.ts +122 -0
  197. package/src/treeview/__tests__/treeOps.test.ts +125 -0
  198. package/src/treeview/__tests__/treePaste.test.ts +40 -0
  199. package/src/treeview/__tests__/treeViewCommands.test.ts +163 -0
  200. package/src/treeview/treeOps.ts +172 -0
  201. package/src/treeview/treePaste.ts +13 -0
  202. package/src/treeview/treeViewCommands.ts +94 -0
  203. package/src/treeview/treeViewData.ts +41 -0
  204. package/src/useMonacoLoader.ts +11 -34
  205. package/src/wysiwygImageUpload.ts +113 -0
  206. package/src/diagram/DiagramExtension.ts +0 -209
  207. package/src/diagram/DiagramWidget.tsx +0 -270
  208. package/src/diagram/useDiagramData.ts +0 -126
@@ -193,6 +193,73 @@
193
193
  .squisq-image-editor-layers {
194
194
  border-bottom: 1px solid var(--squisq-image-editor-panel-border);
195
195
  }
196
+ .squisq-image-editor-layers-header {
197
+ display: flex;
198
+ align-items: center;
199
+ justify-content: space-between;
200
+ min-height: 32px;
201
+ }
202
+ .squisq-image-editor-layer-add {
203
+ position: relative;
204
+ display: inline-flex;
205
+ }
206
+ .squisq-image-editor-layer-add-button {
207
+ display: inline-flex;
208
+ align-items: center;
209
+ justify-content: center;
210
+ width: 22px;
211
+ height: 22px;
212
+ padding: 0;
213
+ color: inherit;
214
+ background: transparent;
215
+ border: 1px solid transparent;
216
+ border-radius: 3px;
217
+ cursor: pointer;
218
+ font: inherit;
219
+ }
220
+ .squisq-image-editor-layer-add-button:hover,
221
+ .squisq-image-editor-layer-add-button[aria-expanded='true'] {
222
+ background: var(--squisq-image-editor-control-bg);
223
+ border-color: var(--squisq-image-editor-control-border);
224
+ }
225
+ .squisq-image-editor-layer-add-menu {
226
+ position: absolute;
227
+ top: calc(100% + 4px);
228
+ right: 0;
229
+ z-index: 20;
230
+ min-width: 150px;
231
+ padding: 4px;
232
+ background: var(--squisq-image-editor-panel-bg);
233
+ border: 1px solid var(--squisq-image-editor-panel-border);
234
+ border-radius: 5px;
235
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
236
+ text-transform: none;
237
+ letter-spacing: normal;
238
+ }
239
+ .squisq-image-editor-layer-add-menu-item {
240
+ display: flex;
241
+ align-items: center;
242
+ gap: 8px;
243
+ width: 100%;
244
+ padding: 6px 8px;
245
+ color: inherit;
246
+ background: transparent;
247
+ border: 0;
248
+ border-radius: 3px;
249
+ cursor: pointer;
250
+ font: inherit;
251
+ font-size: 12px;
252
+ text-align: left;
253
+ }
254
+ .squisq-image-editor-layer-add-menu-item:hover,
255
+ .squisq-image-editor-layer-add-menu-item:focus-visible {
256
+ background: var(--squisq-image-editor-control-bg);
257
+ outline: none;
258
+ }
259
+ .squisq-image-editor-layer-add-menu-item svg {
260
+ flex: 0 0 auto;
261
+ width: 14px;
262
+ }
196
263
  .squisq-image-editor-layer-list {
197
264
  list-style: none;
198
265
  margin: 0;
@@ -239,6 +306,7 @@
239
306
  cursor: not-allowed;
240
307
  }
241
308
  .squisq-image-editor-layer-name {
309
+ min-width: 0;
242
310
  flex: 1 1 auto;
243
311
  text-align: left;
244
312
  background: transparent;
@@ -254,7 +322,14 @@
254
322
  text-overflow: ellipsis;
255
323
  white-space: nowrap;
256
324
  }
325
+ .squisq-image-editor-layer-name-text {
326
+ min-width: 0;
327
+ overflow: hidden;
328
+ text-overflow: ellipsis;
329
+ white-space: nowrap;
330
+ }
257
331
  .squisq-image-editor-layer-kind {
332
+ flex: 0 0 auto;
258
333
  margin-left: auto;
259
334
  font-size: 10px;
260
335
  color: var(--squisq-image-editor-text-muted);
@@ -35,7 +35,7 @@ export function SelectionHandles({ box, onHandlePointerDown }: Props) {
35
35
  ];
36
36
 
37
37
  return (
38
- <g pointerEvents="none">
38
+ <g className="squisq-image-editor-selection-handles" pointerEvents="none">
39
39
  {/*
40
40
  * Outline: rendered as two stacked rectangles so the selection
41
41
  * stays visible over both light and dark imagery. The white
@@ -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,
@@ -141,10 +143,10 @@ export {
141
143
  readHeadingLineTransition,
142
144
  setHeadingLineTransition,
143
145
  readBlockAttrsTransition,
144
- setBlockAttrsTransition,
146
+ setHeadingAttrsTransition,
145
147
  EMPTY_TRANSITION,
146
148
  } from './headingTransition.js';
147
- export type { TransitionFields } from './headingTransition.js';
149
+ export type { HeadingTransitionAttrs, TransitionFields } from './headingTransition.js';
148
150
  export {
149
151
  readBlockAttrsParams,
150
152
  readBlockAttrsValue,
@@ -189,12 +191,26 @@ export {
189
191
  export { useMonacoLoader } from './useMonacoLoader.js';
190
192
  export type { UseMonacoLoaderResult } from './useMonacoLoader.js';
191
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
+
192
199
  // Custom themes — provider stack (doc frontmatter + browser-local library)
193
- 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';
194
207
  export type {
195
208
  CustomThemeContextValue,
196
209
  CustomThemeProviderProps,
197
210
  DocCustomThemes,
211
+ ImportThemeSectionProps,
212
+ ImportedThemeResult,
213
+ ThemeSaveExtras,
198
214
  } from './customThemes/index.js';
199
215
 
200
216
  // Custom templates — provider stack (doc frontmatter + browser-local library)
@@ -218,24 +234,128 @@ export { buildPreviewDoc } from './buildPreviewDoc.js';
218
234
  // Tiptap extension: Heading with template annotation support
219
235
  export { HeadingWithTemplate } from './TemplateAnnotation.js';
220
236
 
221
- // Diagram editor — Tiptap extension that mounts a React-Flow canvas
222
- // below any `### Title {[diagram]}` heading and hides the section's
223
- // child headings (they're rendered as nodes in the canvas).
224
- 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).
225
244
  export { DiagramCanvas } from './diagram/DiagramCanvas.js';
226
245
  export type { DiagramCommand } from './diagram/DiagramCanvas.js';
227
- export { DiagramWidget } from './diagram/DiagramWidget.js';
228
- export { useDiagramData } from './diagram/useDiagramData.js';
229
- 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
+ // RepairableDiagramExtension mounts an inline "Repair as diagram" button on
267
+ // code fences holding BROKEN box art — art too misaligned for clean detection
268
+ // (so it renders as a faithful code block). One click reconstructs it into
269
+ // clean `diagram`-tagged art that the AsciiDiagramExtension turns into a
270
+ // canvas. Peer to the diagram/tree editors; claims the fences neither does.
271
+ export {
272
+ RepairableDiagramExtension,
273
+ findRepairableBlockPos,
274
+ isRepairableFence,
275
+ REPAIRABLE_KEY,
276
+ } from './asciiDiagram/RepairableDiagramExtension.js';
277
+ export type {
278
+ RepairableBlockEntry,
279
+ RepairableDiagramExtensionOptions,
280
+ RepairablePluginState,
281
+ } from './asciiDiagram/RepairableDiagramExtension.js';
282
+ export {
283
+ addEdgeOp,
284
+ addNodeOp,
285
+ moveNodeOp,
286
+ removeEdgeOp,
287
+ removeNodeOp,
288
+ renameNodeOp,
289
+ resizeNodeOp,
290
+ sanitizeAsciiLabel,
291
+ } from './asciiDiagram/asciiDiagramOps.js';
292
+ export { shouldPasteAsAsciiFence } from './asciiDiagram/asciiPaste.js';
293
+
294
+ // Treeview editor — ASCII file-tree / outline fences get an interactive
295
+ // outline widget; edits re-render the tree art (the fence stays the source
296
+ // of truth). Peer to the ASCII diagram editor; mutually exclusive with it
297
+ // (a fence with boxes is a diagram, not a tree).
298
+ export { TreeViewExtension, findTreeBlockPos } from './treeview/TreeViewExtension.js';
299
+ export type {
300
+ TreeBlockEntry,
301
+ TreeViewExtensionOptions,
302
+ TreeViewPluginState,
303
+ } from './treeview/TreeViewExtension.js';
304
+ export { TreeOutlineWidget } from './treeview/TreeOutlineWidget.js';
305
+ export { useTreeViewData } from './treeview/treeViewData.js';
306
+ export type { TreeViewData } from './treeview/treeViewData.js';
307
+ export { applyTreeCommand, replaceTreeFenceText } from './treeview/treeViewCommands.js';
308
+ export type { TreeCommand } from './treeview/treeViewCommands.js';
309
+ export {
310
+ addItemOp,
311
+ indentItemOp,
312
+ moveItemDownOp,
313
+ moveItemUpOp,
314
+ outdentItemOp,
315
+ removeItemOp,
316
+ renameItemOp,
317
+ sanitizeTreeLabel,
318
+ toggleDirOp,
319
+ } from './treeview/treeOps.js';
320
+ export { shouldPasteAsTreeFence } from './treeview/treePaste.js';
321
+
322
+ // Timeline editor — authored marker-rail fences become an accessible canvas
323
+ // with add-point affordances and a selected-point inspector. Semantic edits
324
+ // re-render the canonical `timeline` fence in one undoable transaction.
325
+ export {
326
+ TimelineViewExtension,
327
+ TIMELINE_VIEW_KEY,
328
+ findTimelineBlockPos,
329
+ getTimelineForNode,
330
+ parseTimelineForNode,
331
+ } from './timeline/TimelineViewExtension.js';
332
+ export type {
333
+ TimelineBlockEntry,
334
+ TimelineViewExtensionOptions,
335
+ TimelineViewPluginState,
336
+ } from './timeline/TimelineViewExtension.js';
337
+ export { TimelineEditorWidget } from './timeline/TimelineEditorWidget.js';
338
+ export type { TimelineEditorWidgetProps } from './timeline/TimelineEditorWidget.js';
339
+ export { useTimelineData } from './timeline/timelineData.js';
340
+ export type { TimelineViewData } from './timeline/timelineData.js';
230
341
  export {
231
- moveNode,
232
- addConnection,
233
- removeConnection,
234
- renameNode,
235
- addNode,
236
- removeNode,
237
- listDiagramChildren,
238
- } from './diagram/diagramCommands.js';
342
+ applyTimelineCommand,
343
+ isTimelineSourceSafeForSemanticEdit,
344
+ } from './timeline/timelineCommands.js';
345
+ export type { TimelineCommand, TimelineCommandResult } from './timeline/timelineCommands.js';
346
+ export {
347
+ addTimelineEventOp,
348
+ nextTimelineEventId,
349
+ removeTimelineEventOp,
350
+ sanitizeTimelineText,
351
+ updateTimelineEventOp,
352
+ } from './timeline/timelineOps.js';
353
+ export type {
354
+ AddTimelineEventOptions,
355
+ AddTimelineEventResult,
356
+ TimelineEventPatch,
357
+ } from './timeline/timelineOps.js';
358
+ export { shouldPasteAsTimelineFence } from './timeline/timelinePaste.js';
239
359
 
240
360
  // JSON Form — editable component
241
361
  export { JsonEditor } from './jsonEditor/index.js';
@@ -276,6 +396,59 @@ export type { CaptureKind, ResolvedFormat } from './recorder/formats.js';
276
396
  export { buildTimingJson, encodeTimingJson, timingPathFor } from './recorder/timingJson.js';
277
397
  export type { TimingJson, RecordedBookmark } from './recorder/timingJson.js';
278
398
 
399
+ // Teleprompter — the Narrate display mode under the Use tab: voice-paced
400
+ // prompter surface, floating-window tiers, and the mic analysis pipeline.
401
+ export {
402
+ TeleprompterView,
403
+ TeleprompterSurface,
404
+ TeleprompterControls,
405
+ useTeleprompter,
406
+ useNarrationRecorder,
407
+ buildNarrationSavePlan,
408
+ executeNarrationSave,
409
+ insertNarrationPreamble,
410
+ narrationAnnotationLine,
411
+ cameraVideoLine,
412
+ vadConfigForSensitivity,
413
+ useMicAnalysis,
414
+ PCM_WORKLET_SOURCE,
415
+ PCM_WORKLET_NAME,
416
+ registerPcmWorklet,
417
+ createFloatingWindowManager,
418
+ detectFloatTiers,
419
+ useFloatingWindow,
420
+ measureTokenLines,
421
+ targetOffsetFor,
422
+ stepScroll,
423
+ EYE_LINE_FRACTION,
424
+ TELEPROMPTER_CSS,
425
+ ensureTeleprompterStyles,
426
+ prompterVarsFromTheme,
427
+ DEFAULT_TELEPROMPTER_PREFS,
428
+ } from './teleprompter/index.js';
429
+ export type {
430
+ TeleprompterViewProps,
431
+ TeleprompterSurfaceProps,
432
+ TeleprompterControlsProps,
433
+ TeleprompterController,
434
+ TeleprompterRecordingDeps,
435
+ NarrationRecorderController,
436
+ NarrationRecorderState,
437
+ NarrationTake,
438
+ NarrationSavePlan,
439
+ NarrationSaveResult,
440
+ MicAnalysisHandle,
441
+ MicAnalysisStatus,
442
+ FloatingWindowManager,
443
+ FloatOpenOptions,
444
+ CanvasSink,
445
+ FloatingWindowHandle,
446
+ TokenLineMap,
447
+ TeleprompterPrefs,
448
+ PrompterTransport,
449
+ FloatTier,
450
+ } from './teleprompter/index.js';
451
+
279
452
  // Image editor — layered, sidecar-persisted raster authoring surface.
280
453
  // Pairs with `ImageViewer` and the `<basename>_files/` sidecar convention.
281
454
  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
  );