@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.
- package/README.md +12 -3
- package/dist/index.d.ts +1229 -179
- package/dist/index.js +18017 -11231
- package/dist/index.js.map +1 -1
- package/dist/monaco.d.ts +1 -0
- package/dist/monaco.js +4 -0
- package/dist/monaco.js.map +1 -0
- package/dist/styles/index.css +1471 -245
- package/package.json +9 -5
- package/src/BlockPropertiesPopover.tsx +30 -15
- package/src/DocumentSettingsDialog.tsx +13 -21
- package/src/EditorContext.tsx +199 -42
- package/src/EditorShell.tsx +348 -260
- package/src/ImageEditor.tsx +22 -1
- package/src/InlinePreviewGutter.tsx +16 -8
- package/src/MediaBin.tsx +107 -14
- package/src/OutlinePanel.tsx +202 -7
- package/src/PlainHtmlPreview.tsx +40 -1
- package/src/PreviewControls.tsx +309 -69
- package/src/PreviewPanel.tsx +312 -78
- package/src/RawEditor.tsx +13 -2
- package/src/StatusBar.tsx +5 -2
- package/src/TemplateAnnotation.ts +12 -3
- package/src/TemplatePicker.tsx +75 -21
- package/src/ThemeCustomizerPanel.tsx +8 -0
- package/src/ThemePicker.tsx +10 -5
- package/src/TimelineBlockPreview.tsx +1 -1
- package/src/Toolbar.tsx +202 -41
- package/src/TransitionPicker.tsx +36 -4
- package/src/ViewMenuPanel.tsx +12 -14
- package/src/WysiwygEditor.tsx +124 -91
- package/src/__tests__/blockPropertiesPopoverTheme.test.tsx +33 -0
- package/src/__tests__/blockTagActivity.test.ts +183 -0
- package/src/__tests__/buildDocumentPreviewMarkdown.test.ts +75 -0
- package/src/__tests__/buildPreviewDocContent.test.ts +48 -0
- package/src/__tests__/buildPreviewDocTransition.test.ts +21 -1
- package/src/__tests__/canvasSurfaceTextEditing.test.tsx +60 -0
- package/src/__tests__/customLayoutManagerTheme.test.tsx +34 -0
- package/src/__tests__/documentSettingsDialog.test.tsx +29 -1
- package/src/__tests__/editorScrollHandoff.test.tsx +102 -0
- package/src/__tests__/editorScrollSync.test.ts +65 -0
- package/src/__tests__/editorShellProps.test.tsx +123 -1
- package/src/__tests__/emojiPicker.test.tsx +2 -42
- package/src/__tests__/headingTransition.test.ts +0 -12
- package/src/__tests__/imageEditAffordance.test.tsx +5 -1
- package/src/__tests__/imageEditModalContract.test.tsx +119 -0
- package/src/__tests__/imageEditorShell.test.tsx +15 -1
- package/src/__tests__/jsonEditor.test.tsx +56 -1
- package/src/__tests__/layersPanel.test.tsx +75 -5
- package/src/__tests__/majorApiTypes.test.ts +26 -0
- package/src/__tests__/mediaBinDrop.test.tsx +90 -0
- package/src/__tests__/narrationSave.test.ts +148 -0
- package/src/__tests__/outlinePanel.test.tsx +93 -3
- package/src/__tests__/outlineSource.test.ts +217 -0
- package/src/__tests__/plainHtmlPreview.test.tsx +17 -1
- package/src/__tests__/presentationMode.test.tsx +290 -0
- package/src/__tests__/previewControls.test.tsx +218 -5
- package/src/__tests__/previewPanelAudioFailure.test.tsx +46 -0
- package/src/__tests__/previewPanelPresentation.test.tsx +126 -0
- package/src/__tests__/previewPanelTransforms.test.tsx +117 -0
- package/src/__tests__/rawEditorModelIsolation.test.ts +19 -0
- package/src/__tests__/resolveBlockVisual.test.ts +43 -0
- package/src/__tests__/statusBar.test.tsx +27 -0
- package/src/__tests__/teleprompter.test.tsx +190 -0
- package/src/__tests__/templateAnnotationRoundTrip.test.ts +26 -0
- package/src/__tests__/templatePickerPortal.test.tsx +62 -0
- package/src/__tests__/tiptapBridge.test.ts +23 -0
- package/src/__tests__/tiptapCodeBlockRoundTrip.test.ts +109 -0
- package/src/__tests__/transformStyleId.test.ts +13 -0
- package/src/__tests__/useImageEditor.test.tsx +67 -0
- package/src/__tests__/useMediaRecorder.test.ts +75 -0
- package/src/__tests__/viewMenuPanel.test.tsx +130 -0
- package/src/__tests__/wysiwygImageUpload.test.ts +69 -0
- package/src/__tests__/wysiwygTemplateBadgeFocus.test.tsx +56 -0
- package/src/asciiDiagram/AsciiDiagramExtension.ts +343 -0
- package/src/asciiDiagram/AsciiDiagramWidget.tsx +224 -0
- package/src/asciiDiagram/RepairableDiagramExtension.ts +200 -0
- package/src/asciiDiagram/__tests__/AsciiDiagramExtension.test.ts +145 -0
- package/src/asciiDiagram/__tests__/RepairableDiagramExtension.test.ts +159 -0
- package/src/asciiDiagram/__tests__/asciiDiagramCommands.test.ts +227 -0
- package/src/asciiDiagram/__tests__/asciiDiagramOps.test.ts +114 -0
- package/src/asciiDiagram/__tests__/asciiPaste.test.ts +43 -0
- package/src/asciiDiagram/asciiDiagramCommands.ts +148 -0
- package/src/asciiDiagram/asciiDiagramData.ts +116 -0
- package/src/asciiDiagram/asciiDiagramOps.ts +219 -0
- package/src/asciiDiagram/asciiPaste.ts +19 -0
- package/src/blockTagActivity.ts +233 -0
- package/src/buildDocumentPreviewMarkdown.ts +168 -0
- package/src/buildPreviewDoc.ts +19 -12
- package/src/customTemplates/CustomLayoutManager.tsx +3 -2
- package/src/customTemplates/CustomTemplateContext.tsx +6 -0
- package/src/customTemplates/TemplateDesigner.tsx +7 -4
- package/src/customTemplates/__tests__/library.test.ts +8 -0
- package/src/customTemplates/designer.css +158 -99
- package/src/customTemplates/library.ts +16 -3
- package/src/customThemes/CustomThemeContext.tsx +6 -0
- package/src/customThemes/CustomThemeDialog.tsx +35 -5
- package/src/customThemes/ImportThemeSection.tsx +178 -0
- package/src/customThemes/__tests__/customThemeLibrary.test.ts +9 -0
- package/src/customThemes/__tests__/importThemeSection.test.tsx +192 -0
- package/src/customThemes/customThemeLibrary.ts +7 -4
- package/src/customThemes/index.ts +7 -1
- package/src/customThemes/themeDraft.ts +23 -7
- package/src/diagram/DiagramCanvas.tsx +9 -4
- package/src/diagram/types.ts +35 -0
- package/src/editorScrollSync.ts +73 -0
- package/src/emojiData.ts +3 -23
- package/src/headingTransition.ts +0 -15
- package/src/imageEditor/CanvasSurface.tsx +29 -23
- package/src/imageEditor/LayersPanel.tsx +78 -3
- package/src/imageEditor/Toolbar.tsx +5 -1
- package/src/imageEditor/icons.tsx +4 -0
- package/src/imageEditor/image-editor.css +75 -0
- package/src/imageEditor/layers/SelectionHandles.tsx +1 -1
- package/src/imageEditor/useImageEditor.ts +71 -14
- package/src/index.ts +191 -19
- package/src/jsonEditor/JsonEditorContext.tsx +12 -6
- package/src/jsonEditor/RenderNode.tsx +24 -3
- package/src/jsonEditor/editors.tsx +86 -19
- package/src/mediaEntries.ts +12 -0
- package/src/monaco.ts +35 -0
- package/src/monacoWorkers.ts +89 -0
- package/src/outlineSource.ts +171 -0
- package/src/presentation/PresentationMode.tsx +596 -0
- package/src/rawEditorIsolation.ts +18 -0
- package/src/recorder/hooks/useMediaRecorder.ts +97 -53
- package/src/resolveBlockVisual.ts +10 -4
- package/src/scene/Scene.tsx +48 -18
- package/src/scene/SceneBlockExtension.ts +17 -10
- package/src/scene/SceneBlockWidget.tsx +5 -1
- package/src/scene/SceneViewport.tsx +7 -3
- package/src/scene/ShapePalette.tsx +17 -2
- package/src/scene/__tests__/DiagramAdapter.test.ts +86 -0
- package/src/scene/__tests__/SceneBlockExtension.test.ts +33 -0
- package/src/scene/__tests__/sceneIsolation.test.tsx +93 -0
- package/src/scene/adapters/DiagramAdapter.ts +12 -101
- package/src/scene/commands/SceneCommand.ts +4 -1
- package/src/scene/index.ts +1 -6
- package/src/scene/layers/DiagramEdges.tsx +21 -7
- package/src/scene/layers/edgeGeometry.ts +9 -1
- package/src/scene/layers/nodeCard.tsx +27 -8
- package/src/scene/scene.css +5 -2
- package/src/scene/text/SceneTextOverlay.tsx +5 -5
- package/src/scene/text/sceneTextChannel.ts +26 -20
- package/src/scene/text/sceneTextConfig.ts +4 -0
- package/src/scene/tools/ConnectTool.ts +13 -4
- package/src/scene/tools/SceneTool.ts +8 -0
- package/src/scene/tools/SelectTool.ts +34 -18
- package/src/scene/tools/ShapeTool.ts +2 -3
- package/src/styles/ascii-diagram.css +79 -0
- package/src/styles/ascii-timeline.css +499 -0
- package/src/styles/editor.css +706 -120
- package/src/styles/index.css +3 -0
- package/src/styles/tree-view.css +139 -0
- package/src/teleprompter/TeleprompterControls.tsx +218 -0
- package/src/teleprompter/TeleprompterSelfView.tsx +22 -0
- package/src/teleprompter/TeleprompterSurface.tsx +267 -0
- package/src/teleprompter/TeleprompterView.tsx +338 -0
- package/src/teleprompter/canvasRenderer.ts +161 -0
- package/src/teleprompter/floatingWindow.ts +352 -0
- package/src/teleprompter/index.ts +61 -0
- package/src/teleprompter/pcmWorklet.ts +62 -0
- package/src/teleprompter/recording/insertPreamble.ts +80 -0
- package/src/teleprompter/recording/narrationSave.ts +134 -0
- package/src/teleprompter/recording/useNarrationRecorder.ts +367 -0
- package/src/teleprompter/scrollModel.ts +85 -0
- package/src/teleprompter/teleprompterTheme.ts +303 -0
- package/src/teleprompter/types.ts +38 -0
- package/src/teleprompter/useFloatingWindow.ts +74 -0
- package/src/teleprompter/useMicAnalysis.ts +211 -0
- package/src/teleprompter/useTeleprompter.ts +421 -0
- package/src/templateContentPreviewResolver.ts +12 -5
- package/src/timeline/TimelineEditorWidget.tsx +702 -0
- package/src/timeline/TimelineViewExtension.ts +252 -0
- package/src/timeline/__tests__/TimelineEditorWidget.test.tsx +355 -0
- package/src/timeline/__tests__/TimelineViewExtension.test.ts +163 -0
- package/src/timeline/__tests__/timelineCommands.test.ts +327 -0
- package/src/timeline/__tests__/timelineOps.test.ts +222 -0
- package/src/timeline/__tests__/timelinePaste.test.ts +34 -0
- package/src/timeline/timelineCommands.ts +367 -0
- package/src/timeline/timelineData.ts +54 -0
- package/src/timeline/timelineOps.ts +277 -0
- package/src/timeline/timelinePaste.ts +8 -0
- package/src/tiptapBridge.ts +24 -16
- package/src/transformStyleId.ts +17 -0
- package/src/treeview/TreeOutlineWidget.tsx +240 -0
- package/src/treeview/TreeViewExtension.ts +250 -0
- package/src/treeview/__tests__/TreeViewExtension.test.ts +122 -0
- package/src/treeview/__tests__/treeOps.test.ts +125 -0
- package/src/treeview/__tests__/treePaste.test.ts +40 -0
- package/src/treeview/__tests__/treeViewCommands.test.ts +163 -0
- package/src/treeview/treeOps.ts +172 -0
- package/src/treeview/treePaste.ts +13 -0
- package/src/treeview/treeViewCommands.ts +94 -0
- package/src/treeview/treeViewData.ts +41 -0
- package/src/useMonacoLoader.ts +11 -34
- package/src/wysiwygImageUpload.ts +113 -0
- package/src/diagram/DiagramExtension.ts +0 -209
- package/src/diagram/DiagramWidget.tsx +0 -272
- package/src/diagram/useDiagramData.ts +0 -126
|
@@ -173,6 +173,9 @@ export function useMediaRecorder(options: UseMediaRecorderOptions = {}): UseMedi
|
|
|
173
173
|
const startTimestampRef = useRef<number | null>(null);
|
|
174
174
|
const tickerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
175
175
|
const stopResolversRef = useRef<Array<(blob: Blob | null) => void>>([]);
|
|
176
|
+
const stopPromiseRef = useRef<Promise<Blob | null> | null>(null);
|
|
177
|
+
const requestPromiseRef = useRef<Promise<void> | null>(null);
|
|
178
|
+
const lifecycleRef = useRef(0);
|
|
176
179
|
|
|
177
180
|
// Stable copy of options for callbacks that read them late. Re-evaluated
|
|
178
181
|
// each render, but each callback closes over the ref so we don't have
|
|
@@ -222,9 +225,13 @@ export function useMediaRecorder(options: UseMediaRecorderOptions = {}): UseMedi
|
|
|
222
225
|
}, [clearTicker]);
|
|
223
226
|
|
|
224
227
|
const cancel = useCallback(() => {
|
|
228
|
+
lifecycleRef.current += 1;
|
|
225
229
|
const rec = recorderRef.current;
|
|
226
230
|
if (rec && rec.state !== 'inactive') {
|
|
227
231
|
try {
|
|
232
|
+
rec.ondataavailable = null;
|
|
233
|
+
rec.onstop = null;
|
|
234
|
+
rec.onerror = null;
|
|
228
235
|
rec.stop();
|
|
229
236
|
} catch {
|
|
230
237
|
// Ignore — we're tearing down anyway.
|
|
@@ -237,6 +244,8 @@ export function useMediaRecorder(options: UseMediaRecorderOptions = {}): UseMedi
|
|
|
237
244
|
startTimestampRef.current = null;
|
|
238
245
|
// Any in-flight stop() promises won't get a blob.
|
|
239
246
|
stopResolversRef.current.splice(0).forEach((resolve) => resolve(null));
|
|
247
|
+
stopPromiseRef.current = null;
|
|
248
|
+
requestPromiseRef.current = null;
|
|
240
249
|
setBlob(null);
|
|
241
250
|
setDurationMs(0);
|
|
242
251
|
setError(null);
|
|
@@ -244,66 +253,89 @@ export function useMediaRecorder(options: UseMediaRecorderOptions = {}): UseMedi
|
|
|
244
253
|
}, [clearTicker, releaseStream]);
|
|
245
254
|
|
|
246
255
|
const request = useCallback(async () => {
|
|
256
|
+
if (requestPromiseRef.current) return requestPromiseRef.current;
|
|
257
|
+
if (recorderRef.current?.stream.active) return;
|
|
247
258
|
if (!supportsMediaRecorder()) {
|
|
248
259
|
const err = new Error('MediaRecorder is not supported in this environment.');
|
|
249
260
|
setError(err);
|
|
250
261
|
setState('error');
|
|
251
262
|
throw err;
|
|
252
263
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
264
|
+
const lifecycle = ++lifecycleRef.current;
|
|
265
|
+
const requestPromise = (async () => {
|
|
266
|
+
setError(null);
|
|
267
|
+
setState('requesting');
|
|
268
|
+
let acquired: { stream: MediaStream; dispose: () => void } | null = null;
|
|
269
|
+
try {
|
|
270
|
+
const source = optionsRef.current.source ?? 'mic';
|
|
271
|
+
acquired = await acquireStream(source, optionsRef.current);
|
|
272
|
+
const { stream: nextStream, dispose } = acquired;
|
|
273
|
+
if (lifecycle !== lifecycleRef.current) {
|
|
274
|
+
nextStream.getTracks().forEach((track) => track.stop());
|
|
275
|
+
dispose();
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const resolved = resolveFormat(captureKindFor(source), optionsRef.current.mimeType);
|
|
279
|
+
const recorderOptions: MediaRecorderOptions = {};
|
|
280
|
+
if (resolved.mimeType) recorderOptions.mimeType = resolved.mimeType;
|
|
281
|
+
if (optionsRef.current.bitsPerSecond) {
|
|
282
|
+
recorderOptions.bitsPerSecond = optionsRef.current.bitsPerSecond;
|
|
283
|
+
}
|
|
284
|
+
const recorder = new MediaRecorder(nextStream, recorderOptions);
|
|
269
285
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
286
|
+
recorder.ondataavailable = (e) => {
|
|
287
|
+
if (e.data && e.data.size > 0) chunksRef.current.push(e.data);
|
|
288
|
+
};
|
|
289
|
+
recorder.onstop = () => {
|
|
290
|
+
if (recorderRef.current !== recorder || lifecycle !== lifecycleRef.current) return;
|
|
291
|
+
// The recorded MIME type is authoritative once data is in hand —
|
|
292
|
+
// some browsers down-negotiate the format (e.g. drop codec hint).
|
|
293
|
+
const recordedType = recorder.mimeType || resolved.mimeType || 'application/octet-stream';
|
|
294
|
+
const finalBlob = new Blob(chunksRef.current, { type: recordedType });
|
|
295
|
+
chunksRef.current = [];
|
|
296
|
+
setBlob(finalBlob);
|
|
297
|
+
setState('stopped');
|
|
298
|
+
clearTicker();
|
|
299
|
+
stopResolversRef.current.splice(0).forEach((resolve) => resolve(finalBlob));
|
|
300
|
+
stopPromiseRef.current = null;
|
|
301
|
+
};
|
|
302
|
+
recorder.onerror = (event) => {
|
|
303
|
+
if (recorderRef.current !== recorder || lifecycle !== lifecycleRef.current) return;
|
|
304
|
+
const detail = (event as unknown as { error?: DOMException }).error;
|
|
305
|
+
const err = detail instanceof Error ? detail : new Error('Recorder error');
|
|
306
|
+
setError(err);
|
|
307
|
+
setState('error');
|
|
308
|
+
clearTicker();
|
|
309
|
+
stopResolversRef.current.splice(0).forEach((resolve) => resolve(null));
|
|
310
|
+
stopPromiseRef.current = null;
|
|
311
|
+
};
|
|
292
312
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
313
|
+
recorderRef.current = recorder;
|
|
314
|
+
disposeStreamRef.current = dispose;
|
|
315
|
+
setStream(nextStream);
|
|
316
|
+
setFormat(resolved);
|
|
317
|
+
setBlob(null);
|
|
318
|
+
setDurationMs(0);
|
|
319
|
+
setState('ready');
|
|
320
|
+
acquired = null;
|
|
321
|
+
} catch (err: unknown) {
|
|
322
|
+
if (acquired) {
|
|
323
|
+
acquired.stream.getTracks().forEach((track) => track.stop());
|
|
324
|
+
acquired.dispose();
|
|
325
|
+
}
|
|
326
|
+
const normalized = err instanceof Error ? err : new Error('Stream acquisition failed');
|
|
327
|
+
if (lifecycle === lifecycleRef.current) {
|
|
328
|
+
setError(normalized);
|
|
329
|
+
setState('error');
|
|
330
|
+
}
|
|
331
|
+
throw normalized;
|
|
332
|
+
} finally {
|
|
333
|
+
if (lifecycle === lifecycleRef.current) requestPromiseRef.current = null;
|
|
334
|
+
}
|
|
335
|
+
})();
|
|
336
|
+
requestPromiseRef.current = requestPromise;
|
|
337
|
+
return requestPromise;
|
|
338
|
+
}, [clearTicker]);
|
|
307
339
|
|
|
308
340
|
const start = useCallback(() => {
|
|
309
341
|
const rec = recorderRef.current;
|
|
@@ -329,12 +361,13 @@ export function useMediaRecorder(options: UseMediaRecorderOptions = {}): UseMedi
|
|
|
329
361
|
}, [clearTicker]);
|
|
330
362
|
|
|
331
363
|
const stop = useCallback((): Promise<Blob | null> => {
|
|
364
|
+
if (stopPromiseRef.current) return stopPromiseRef.current;
|
|
332
365
|
const rec = recorderRef.current;
|
|
333
366
|
if (!rec || rec.state === 'inactive') {
|
|
334
367
|
return Promise.resolve(blob);
|
|
335
368
|
}
|
|
336
369
|
setState('stopping');
|
|
337
|
-
|
|
370
|
+
const stopPromise = new Promise<Blob | null>((resolve) => {
|
|
338
371
|
stopResolversRef.current.push(resolve);
|
|
339
372
|
try {
|
|
340
373
|
rec.stop();
|
|
@@ -346,6 +379,11 @@ export function useMediaRecorder(options: UseMediaRecorderOptions = {}): UseMedi
|
|
|
346
379
|
stopResolversRef.current.splice(0).forEach((r) => r(null));
|
|
347
380
|
}
|
|
348
381
|
});
|
|
382
|
+
stopPromiseRef.current = stopPromise;
|
|
383
|
+
void stopPromise.finally(() => {
|
|
384
|
+
if (stopPromiseRef.current === stopPromise) stopPromiseRef.current = null;
|
|
385
|
+
});
|
|
386
|
+
return stopPromise;
|
|
349
387
|
}, [blob, clearTicker]);
|
|
350
388
|
|
|
351
389
|
// Final unmount cleanup — make sure we don't leak the camera light /
|
|
@@ -360,9 +398,14 @@ export function useMediaRecorder(options: UseMediaRecorderOptions = {}): UseMedi
|
|
|
360
398
|
useEffect(() => {
|
|
361
399
|
const pendingResolvers = stopResolversRef.current;
|
|
362
400
|
return () => {
|
|
401
|
+
lifecycleRef.current += 1;
|
|
402
|
+
requestPromiseRef.current = null;
|
|
363
403
|
const rec = recorderRef.current;
|
|
364
404
|
if (rec && rec.state !== 'inactive') {
|
|
365
405
|
try {
|
|
406
|
+
rec.ondataavailable = null;
|
|
407
|
+
rec.onstop = null;
|
|
408
|
+
rec.onerror = null;
|
|
366
409
|
rec.stop();
|
|
367
410
|
} catch {
|
|
368
411
|
// ignore
|
|
@@ -371,6 +414,7 @@ export function useMediaRecorder(options: UseMediaRecorderOptions = {}): UseMedi
|
|
|
371
414
|
releaseStream();
|
|
372
415
|
clearTicker();
|
|
373
416
|
pendingResolvers.splice(0).forEach((resolve) => resolve(null));
|
|
417
|
+
stopPromiseRef.current = null;
|
|
374
418
|
};
|
|
375
419
|
}, [releaseStream, clearTicker]);
|
|
376
420
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* resolveBlockVisual — resolves a doc block into a renderable card (with
|
|
3
|
-
* `layers`) via the shared preview pipeline
|
|
3
|
+
* `layers`) via the shared preview pipeline and canonical materializer.
|
|
4
4
|
*
|
|
5
5
|
* Kept in its own module (rather than alongside `BlockThumbnail` in
|
|
6
6
|
* `TimelineBlockPreview.tsx`) so that component file exports only components,
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { Block, DocBlock, Doc, ViewportConfig, Theme } from '@bendyline/squisq/schemas';
|
|
11
|
-
import {
|
|
11
|
+
import { materializeBlockLayers, type MaterializeBlockLayersOptions } from '@bendyline/squisq/doc';
|
|
12
12
|
import { buildPreviewDoc } from './buildPreviewDoc';
|
|
13
13
|
|
|
14
14
|
/**
|
|
@@ -24,8 +24,14 @@ export function resolveBlockVisual(
|
|
|
24
24
|
try {
|
|
25
25
|
const slide = buildPreviewDoc({ ...doc, blocks: [block] }).blocks[0] as DocBlock | undefined;
|
|
26
26
|
if (!slide) return null;
|
|
27
|
-
const ctx:
|
|
28
|
-
|
|
27
|
+
const ctx: MaterializeBlockLayersOptions = {
|
|
28
|
+
blockIndex: 0,
|
|
29
|
+
totalBlocks: 1,
|
|
30
|
+
theme,
|
|
31
|
+
viewport,
|
|
32
|
+
customTemplates: doc.customTemplates,
|
|
33
|
+
};
|
|
34
|
+
const { layers } = materializeBlockLayers(slide, ctx);
|
|
29
35
|
return { ...(slide as unknown as Block), layers };
|
|
30
36
|
} catch {
|
|
31
37
|
return null;
|
package/src/scene/Scene.tsx
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
} from 'react';
|
|
24
24
|
import type { Layer } from '@bendyline/squisq/schemas';
|
|
25
25
|
import type { SceneCommand, SceneEdge } from './commands/SceneCommand';
|
|
26
|
-
import type { SceneTool, SceneToolContext } from './tools/SceneTool';
|
|
26
|
+
import type { SceneInteractionState, SceneTool, SceneToolContext } from './tools/SceneTool';
|
|
27
27
|
import { useScenePanZoom } from './hooks/useScenePanZoom';
|
|
28
28
|
import { useSceneSelection } from './hooks/useSceneSelection';
|
|
29
29
|
import { useSceneHitTest, layerBounds, type HitTestable } from './hooks/useSceneHitTest';
|
|
@@ -140,12 +140,21 @@ export function Scene(props: SceneProps) {
|
|
|
140
140
|
// ── Pan/zoom + selection ────────────────────────────────────
|
|
141
141
|
const panZoom = useScenePanZoom();
|
|
142
142
|
const selection = useSceneSelection();
|
|
143
|
+
const setSceneSelection = selection.setSelection;
|
|
143
144
|
const { hit } = useSceneHitTest();
|
|
145
|
+
const interaction = useRef<SceneInteractionState>({}).current;
|
|
144
146
|
|
|
145
|
-
// Surface selection
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
// Surface selection in the originating interaction rather than a passive
|
|
148
|
+
// effect. Detached editor roots can then update their toolbar without
|
|
149
|
+
// forcing a flush from inside another component's lifecycle.
|
|
150
|
+
const setSelection = useCallback(
|
|
151
|
+
(ids: Iterable<string>) => {
|
|
152
|
+
const next = new Set(ids);
|
|
153
|
+
setSceneSelection(next);
|
|
154
|
+
onSelectionChange?.(next);
|
|
155
|
+
},
|
|
156
|
+
[setSceneSelection, onSelectionChange],
|
|
157
|
+
);
|
|
149
158
|
|
|
150
159
|
// ── Hit-test cache ──────────────────────────────────────────
|
|
151
160
|
const hitItems: HitTestable[] = useMemo(() => {
|
|
@@ -161,6 +170,7 @@ export function Scene(props: SceneProps) {
|
|
|
161
170
|
// ── Tool context (rebuilt every render — cheap, no allocations in hot path) ──
|
|
162
171
|
const ctx: SceneToolContext = useMemo(
|
|
163
172
|
() => ({
|
|
173
|
+
interaction,
|
|
164
174
|
viewport,
|
|
165
175
|
transform: panZoom.transform,
|
|
166
176
|
layers,
|
|
@@ -170,7 +180,7 @@ export function Scene(props: SceneProps) {
|
|
|
170
180
|
screenToViewport: panZoom.screenToViewport,
|
|
171
181
|
viewportToScreen: panZoom.viewportToScreen,
|
|
172
182
|
hit: (p) => hit(p, hitItems),
|
|
173
|
-
setSelection
|
|
183
|
+
setSelection,
|
|
174
184
|
dispatch: onCommand,
|
|
175
185
|
setActiveTool,
|
|
176
186
|
beginTextEdit: textEdit.begin,
|
|
@@ -183,16 +193,17 @@ export function Scene(props: SceneProps) {
|
|
|
183
193
|
layers,
|
|
184
194
|
edges,
|
|
185
195
|
selection.selection,
|
|
186
|
-
|
|
196
|
+
setSelection,
|
|
187
197
|
hitItems,
|
|
188
198
|
hit,
|
|
189
199
|
onCommand,
|
|
190
200
|
setActiveTool,
|
|
191
201
|
textEdit.begin,
|
|
202
|
+
interaction,
|
|
192
203
|
],
|
|
193
204
|
);
|
|
194
|
-
// Mirror ctx in a ref so keyboard handlers
|
|
195
|
-
//
|
|
205
|
+
// Mirror ctx in a ref so root keyboard handlers read the latest selection
|
|
206
|
+
// without re-binding on every selection change.
|
|
196
207
|
const ctxRef = useRef(ctx);
|
|
197
208
|
ctxRef.current = ctx;
|
|
198
209
|
|
|
@@ -220,12 +231,9 @@ export function Scene(props: SceneProps) {
|
|
|
220
231
|
[panZoom],
|
|
221
232
|
);
|
|
222
233
|
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
// a gesture is in progress. We bump `dragTick` on pointer-down and on
|
|
227
|
-
// every move thereafter — cheap (one setState per move) and only fires
|
|
228
|
-
// while the user is actively interacting.
|
|
234
|
+
// Gesture state belongs to this Scene instance and lives outside React's
|
|
235
|
+
// render cycle. Bump `dragTick` while a pointer gesture is active so its
|
|
236
|
+
// live drag/resize preview is visible.
|
|
229
237
|
const [, setDragTick] = useState(0);
|
|
230
238
|
const pointerActive = useRef(false);
|
|
231
239
|
const bumpTick = useCallback(() => setDragTick((t) => t + 1), []);
|
|
@@ -280,6 +288,27 @@ export function Scene(props: SceneProps) {
|
|
|
280
288
|
[activeTool],
|
|
281
289
|
);
|
|
282
290
|
|
|
291
|
+
const handlePointerCancel = useCallback(
|
|
292
|
+
(e: React.PointerEvent<SVGSVGElement>) => {
|
|
293
|
+
pointerActive.current = false;
|
|
294
|
+
isPanning.current = false;
|
|
295
|
+
panLast.current = null;
|
|
296
|
+
interaction.selectDrag = undefined;
|
|
297
|
+
interaction.connect = undefined;
|
|
298
|
+
(e.currentTarget as Element).releasePointerCapture?.(e.pointerId);
|
|
299
|
+
bumpTick();
|
|
300
|
+
},
|
|
301
|
+
[interaction, bumpTick],
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
useEffect(
|
|
305
|
+
() => () => {
|
|
306
|
+
interaction.selectDrag = undefined;
|
|
307
|
+
interaction.connect = undefined;
|
|
308
|
+
},
|
|
309
|
+
[interaction],
|
|
310
|
+
);
|
|
311
|
+
|
|
283
312
|
const handleDoubleClick = useCallback(
|
|
284
313
|
(e: React.MouseEvent<SVGSVGElement>) => {
|
|
285
314
|
activeTool?.onDoubleClick?.(e, ctxRef.current);
|
|
@@ -387,8 +416,8 @@ export function Scene(props: SceneProps) {
|
|
|
387
416
|
}, [containerSize, hitItems, panZoom]);
|
|
388
417
|
|
|
389
418
|
// ── Render ──────────────────────────────────────────────────
|
|
390
|
-
const liveOffset = activeId === 'select' ? getActiveMoveOffset() : null;
|
|
391
|
-
const liveResize = activeId === 'select' ? getActiveResize() : null;
|
|
419
|
+
const liveOffset = activeId === 'select' ? getActiveMoveOffset(interaction) : null;
|
|
420
|
+
const liveResize = activeId === 'select' ? getActiveResize(interaction) : null;
|
|
392
421
|
const showSelectionHandles = activeTool?.hideSelectionHandles !== true;
|
|
393
422
|
|
|
394
423
|
// Per-layer transform applied during an in-flight drag or resize so
|
|
@@ -453,7 +482,7 @@ export function Scene(props: SceneProps) {
|
|
|
453
482
|
const sx = e.clientX - rect.left;
|
|
454
483
|
const sy = e.clientY - rect.top;
|
|
455
484
|
const startV = panZoom.screenToViewport(sx, sy);
|
|
456
|
-
beginHandleDrag({ layerId: id, corner, startV, startBounds: bounds });
|
|
485
|
+
beginHandleDrag({ layerId: id, corner, startV, startBounds: bounds }, interaction);
|
|
457
486
|
(e.currentTarget as Element).setPointerCapture?.(e.pointerId);
|
|
458
487
|
// Mark a gesture as active so the Scene's pointer-move handler
|
|
459
488
|
// triggers re-renders for the live resize preview.
|
|
@@ -475,6 +504,7 @@ export function Scene(props: SceneProps) {
|
|
|
475
504
|
onPointerDown={handlePointerDown}
|
|
476
505
|
onPointerMove={handlePointerMove}
|
|
477
506
|
onPointerUp={handlePointerUp}
|
|
507
|
+
onPointerCancel={handlePointerCancel}
|
|
478
508
|
onDoubleClick={handleDoubleClick}
|
|
479
509
|
onDragOver={onDrop ? handleDragOver : undefined}
|
|
480
510
|
onDrop={onDrop ? handleDrop : undefined}
|
|
@@ -20,6 +20,7 @@ import { createElement } from 'react';
|
|
|
20
20
|
import type { Node as PMNode } from '@tiptap/pm/model';
|
|
21
21
|
import { parsePandocAttrTokens } from '@bendyline/squisq/markdown';
|
|
22
22
|
import { SceneBlockWidget, type SceneBlockMode } from './SceneBlockWidget';
|
|
23
|
+
import type { SceneTextChannel } from './text/sceneTextChannel';
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* Stable identifier for a layout/drawing heading — used as part of the
|
|
@@ -27,24 +28,24 @@ import { SceneBlockWidget, type SceneBlockMode } from './SceneBlockWidget';
|
|
|
27
28
|
* changes (drag/resize commits write into the heading's
|
|
28
29
|
* `data-block-attrs`).
|
|
29
30
|
*/
|
|
30
|
-
export function getHeadingKey(node: PMNode): string {
|
|
31
|
+
export function getHeadingKey(node: PMNode, position?: number): string {
|
|
31
32
|
const raw = (node.attrs as Record<string, unknown>).dataBlockAttrs;
|
|
32
33
|
if (typeof raw === 'string' && raw.length > 0) {
|
|
33
34
|
const attrs = parsePandocAttrTokens(raw);
|
|
34
|
-
if (attrs.id) return attrs.id
|
|
35
|
+
if (attrs.id) return position == null ? attrs.id : `${attrs.id}@${position}`;
|
|
35
36
|
}
|
|
36
37
|
let text = '';
|
|
37
38
|
node.content.forEach((child) => {
|
|
38
39
|
if (child.isText) text += child.text ?? '';
|
|
39
40
|
});
|
|
40
|
-
|
|
41
|
+
const slug =
|
|
41
42
|
text
|
|
42
43
|
.toLowerCase()
|
|
43
44
|
.replace(/[^\w\s-]/g, '')
|
|
44
45
|
.replace(/[\s_]+/g, '-')
|
|
45
46
|
.replace(/-+/g, '-')
|
|
46
|
-
.replace(/^-|-$/g, '') || 'block'
|
|
47
|
-
|
|
47
|
+
.replace(/^-|-$/g, '') || 'block';
|
|
48
|
+
return position == null ? slug : `${slug}@${position}`;
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
/**
|
|
@@ -63,7 +64,7 @@ export function findSceneHeadingPos(
|
|
|
63
64
|
if (node.type.name !== 'heading') return;
|
|
64
65
|
const attrs = node.attrs as { dataTemplate?: string };
|
|
65
66
|
if (attrs.dataTemplate !== expectedTemplate) return;
|
|
66
|
-
if (getHeadingKey(node) === headingKey) {
|
|
67
|
+
if (getHeadingKey(node, pos) === headingKey) {
|
|
67
68
|
found = pos;
|
|
68
69
|
return false;
|
|
69
70
|
}
|
|
@@ -82,7 +83,11 @@ interface SceneRoot {
|
|
|
82
83
|
root: Root;
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
function buildDecorations(
|
|
86
|
+
function buildDecorations(
|
|
87
|
+
state: EditorState,
|
|
88
|
+
editor: Editor,
|
|
89
|
+
textChannel?: SceneTextChannel,
|
|
90
|
+
): DecorationSet {
|
|
86
91
|
const decos: Decoration[] = [];
|
|
87
92
|
|
|
88
93
|
state.doc.descendants((node, pos) => {
|
|
@@ -93,7 +98,7 @@ function buildDecorations(state: EditorState, editor: Editor): DecorationSet {
|
|
|
93
98
|
const parentDepth = attrs.level;
|
|
94
99
|
const widgetPos = pos + node.nodeSize;
|
|
95
100
|
const parentPos = pos;
|
|
96
|
-
const headingKey = getHeadingKey(node);
|
|
101
|
+
const headingKey = getHeadingKey(node, pos);
|
|
97
102
|
decos.push(
|
|
98
103
|
Decoration.widget(
|
|
99
104
|
widgetPos,
|
|
@@ -112,6 +117,7 @@ function buildDecorations(state: EditorState, editor: Editor): DecorationSet {
|
|
|
112
117
|
fallbackParentPos: parentPos,
|
|
113
118
|
mode,
|
|
114
119
|
host: view.dom.parentElement ?? view.dom,
|
|
120
|
+
textChannel,
|
|
115
121
|
}),
|
|
116
122
|
);
|
|
117
123
|
|
|
@@ -161,6 +167,7 @@ function buildDecorations(state: EditorState, editor: Editor): DecorationSet {
|
|
|
161
167
|
export interface SceneBlockExtensionOptions {
|
|
162
168
|
/** When false, the extension is inert. */
|
|
163
169
|
enabled?: boolean;
|
|
170
|
+
textChannel?: SceneTextChannel;
|
|
164
171
|
}
|
|
165
172
|
|
|
166
173
|
export const SceneBlockExtension = Extension.create<SceneBlockExtensionOptions>({
|
|
@@ -179,10 +186,10 @@ export const SceneBlockExtension = Extension.create<SceneBlockExtensionOptions>(
|
|
|
179
186
|
new Plugin({
|
|
180
187
|
key: KEY,
|
|
181
188
|
state: {
|
|
182
|
-
init: (_config, state) => buildDecorations(state, editor),
|
|
189
|
+
init: (_config, state) => buildDecorations(state, editor, this.options.textChannel),
|
|
183
190
|
apply: (tr, oldDecos, _oldState, newState) => {
|
|
184
191
|
if (!tr.docChanged) return oldDecos;
|
|
185
|
-
return buildDecorations(newState, editor);
|
|
192
|
+
return buildDecorations(newState, editor, this.options.textChannel);
|
|
186
193
|
},
|
|
187
194
|
},
|
|
188
195
|
props: {
|
|
@@ -30,6 +30,7 @@ import type { SceneTextEditConfig } from './text/sceneTextConfig';
|
|
|
30
30
|
import { markdownToTiptap } from '../tiptapBridge';
|
|
31
31
|
import { Icon } from '../Icon';
|
|
32
32
|
import { DiagramMaximizedOverlay } from '../diagram/DiagramMaximizedOverlay';
|
|
33
|
+
import type { SceneTextChannel } from './text/sceneTextChannel';
|
|
33
34
|
|
|
34
35
|
export type SceneBlockMode = 'layout' | 'drawing';
|
|
35
36
|
|
|
@@ -40,6 +41,7 @@ interface SceneBlockWidgetProps {
|
|
|
40
41
|
mode: SceneBlockMode;
|
|
41
42
|
/** Host element for portal targeting (used by maximize overlay). */
|
|
42
43
|
host?: HTMLElement | null;
|
|
44
|
+
textChannel?: SceneTextChannel;
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
// Default viewport for layout/drawing surfaces. Matches Squisq's 16:9
|
|
@@ -59,6 +61,7 @@ export function SceneBlockWidget({
|
|
|
59
61
|
fallbackParentPos,
|
|
60
62
|
mode,
|
|
61
63
|
host,
|
|
64
|
+
textChannel,
|
|
62
65
|
}: SceneBlockWidgetProps) {
|
|
63
66
|
const [parentPos, setParentPos] = useState<number>(
|
|
64
67
|
() => findSceneHeadingPos(editor, headingKey, mode) ?? fallbackParentPos,
|
|
@@ -89,6 +92,7 @@ export function SceneBlockWidget({
|
|
|
89
92
|
dispatchRef.current = adapter.dispatch;
|
|
90
93
|
const textConfig = useMemo<SceneTextEditConfig>(
|
|
91
94
|
() => ({
|
|
95
|
+
channel: textChannel,
|
|
92
96
|
resolveEditableId: (id) => {
|
|
93
97
|
const l = layersRef.current.find((x) => x.id === id);
|
|
94
98
|
return l && l.type === 'text' ? id : null;
|
|
@@ -114,7 +118,7 @@ export function SceneBlockWidget({
|
|
|
114
118
|
// content is stored as the child sub-block's markdown body.
|
|
115
119
|
level: isDrawing ? 'inline' : 'block',
|
|
116
120
|
}),
|
|
117
|
-
[isDrawing],
|
|
121
|
+
[isDrawing, textChannel],
|
|
118
122
|
);
|
|
119
123
|
|
|
120
124
|
const [maximized, setMaximized] = useState(false);
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* canvas grid would.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { forwardRef, type ReactNode, type WheelEvent } from 'react';
|
|
14
|
+
import { forwardRef, useId, type ReactNode, type WheelEvent } from 'react';
|
|
15
15
|
import type { SceneTransform } from './hooks/useScenePanZoom';
|
|
16
16
|
|
|
17
17
|
interface SceneViewportProps {
|
|
@@ -24,6 +24,7 @@ interface SceneViewportProps {
|
|
|
24
24
|
onPointerDown?: (e: React.PointerEvent<SVGSVGElement>) => void;
|
|
25
25
|
onPointerMove?: (e: React.PointerEvent<SVGSVGElement>) => void;
|
|
26
26
|
onPointerUp?: (e: React.PointerEvent<SVGSVGElement>) => void;
|
|
27
|
+
onPointerCancel?: (e: React.PointerEvent<SVGSVGElement>) => void;
|
|
27
28
|
onDoubleClick?: (e: React.MouseEvent<SVGSVGElement>) => void;
|
|
28
29
|
/** HTML5 drag-and-drop handlers — used for palette drag-to-place. */
|
|
29
30
|
onDragOver?: (e: React.DragEvent<SVGSVGElement>) => void;
|
|
@@ -43,6 +44,7 @@ export const SceneViewport = forwardRef<SVGSVGElement, SceneViewportProps>(funct
|
|
|
43
44
|
onPointerDown,
|
|
44
45
|
onPointerMove,
|
|
45
46
|
onPointerUp,
|
|
47
|
+
onPointerCancel,
|
|
46
48
|
onDoubleClick,
|
|
47
49
|
onDragOver,
|
|
48
50
|
onDrop,
|
|
@@ -52,6 +54,7 @@ export const SceneViewport = forwardRef<SVGSVGElement, SceneViewportProps>(funct
|
|
|
52
54
|
ref,
|
|
53
55
|
) {
|
|
54
56
|
const t = transform;
|
|
57
|
+
const gridId = `squisq-scene-dot-grid-${useId().replace(/:/g, '')}`;
|
|
55
58
|
const groupTransform = `translate(${t.tx} ${t.ty}) scale(${t.scale})`;
|
|
56
59
|
|
|
57
60
|
return (
|
|
@@ -70,6 +73,7 @@ export const SceneViewport = forwardRef<SVGSVGElement, SceneViewportProps>(funct
|
|
|
70
73
|
onPointerDown={onPointerDown}
|
|
71
74
|
onPointerMove={onPointerMove}
|
|
72
75
|
onPointerUp={onPointerUp}
|
|
76
|
+
onPointerCancel={onPointerCancel}
|
|
73
77
|
onDoubleClick={onDoubleClick}
|
|
74
78
|
onDragOver={onDragOver}
|
|
75
79
|
onDrop={onDrop}
|
|
@@ -85,7 +89,7 @@ export const SceneViewport = forwardRef<SVGSVGElement, SceneViewportProps>(funct
|
|
|
85
89
|
>
|
|
86
90
|
<defs>
|
|
87
91
|
<pattern
|
|
88
|
-
id=
|
|
92
|
+
id={gridId}
|
|
89
93
|
x={t.tx}
|
|
90
94
|
y={t.ty}
|
|
91
95
|
width={24 * t.scale}
|
|
@@ -95,7 +99,7 @@ export const SceneViewport = forwardRef<SVGSVGElement, SceneViewportProps>(funct
|
|
|
95
99
|
<circle cx={1} cy={1} r={1} className="squisq-scene-dot" />
|
|
96
100
|
</pattern>
|
|
97
101
|
</defs>
|
|
98
|
-
<rect width="100%" height="100%" fill=
|
|
102
|
+
<rect width="100%" height="100%" fill={`url(#${gridId})`} />
|
|
99
103
|
<g transform={groupTransform}>{children}</g>
|
|
100
104
|
</svg>
|
|
101
105
|
);
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* gets drawn.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
11
|
+
import { useEffect, useId, useMemo, useRef, useState } from 'react';
|
|
12
12
|
import { shapePath } from '@bendyline/squisq/doc';
|
|
13
13
|
|
|
14
14
|
interface ShapePaletteProps {
|
|
@@ -152,8 +152,23 @@ export function ShapePalette({
|
|
|
152
152
|
/** A 40×30 SVG thumbnail of a shape, inside a 48×38 button viewport. */
|
|
153
153
|
function ShapeThumb({ kind }: { kind: string }) {
|
|
154
154
|
const d = shapePath(kind, 4, 4, 40, 30);
|
|
155
|
+
const arrowId = `squisq-shape-thumb-arrow-${useId().replace(/:/g, '')}`;
|
|
155
156
|
return (
|
|
156
157
|
<svg viewBox="0 0 48 38" width={36} height={28} aria-hidden="true">
|
|
158
|
+
{kind === 'arrow' ? (
|
|
159
|
+
<defs>
|
|
160
|
+
<marker
|
|
161
|
+
id={arrowId}
|
|
162
|
+
viewBox="0 0 10 10"
|
|
163
|
+
refX={9}
|
|
164
|
+
refY={5}
|
|
165
|
+
markerWidth={4}
|
|
166
|
+
markerHeight={4}
|
|
167
|
+
>
|
|
168
|
+
<path d="M 0 0 L 10 5 L 0 10 z" className="squisq-shape-thumb-line" />
|
|
169
|
+
</marker>
|
|
170
|
+
</defs>
|
|
171
|
+
) : null}
|
|
157
172
|
{d ? (
|
|
158
173
|
<path d={d} className="squisq-shape-thumb" />
|
|
159
174
|
) : kind === 'circle' ? (
|
|
@@ -167,7 +182,7 @@ function ShapeThumb({ kind }: { kind: string }) {
|
|
|
167
182
|
x2={44}
|
|
168
183
|
y2={4}
|
|
169
184
|
className="squisq-shape-thumb-line"
|
|
170
|
-
markerEnd=
|
|
185
|
+
markerEnd={`url(#${arrowId})`}
|
|
171
186
|
/>
|
|
172
187
|
) : kind === 'text' ? (
|
|
173
188
|
<text x={24} y={26} textAnchor="middle" className="squisq-shape-thumb-text">
|