@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.
- package/README.md +12 -3
- package/dist/index.d.ts +1304 -208
- package/dist/index.js +22838 -14494
- 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 +2384 -408
- package/package.json +9 -5
- package/src/BlockPropertiesPopover.tsx +48 -17
- package/src/DocumentSettingsDialog.tsx +13 -21
- package/src/EditorContext.tsx +199 -42
- package/src/EditorShell.tsx +403 -270
- package/src/ImageEditor.tsx +22 -1
- package/src/InlinePreviewGutter.tsx +16 -8
- package/src/MediaBin.tsx +276 -40
- package/src/OutlinePanel.tsx +202 -7
- package/src/PlainHtmlPreview.tsx +40 -1
- package/src/PreviewControls.tsx +466 -93
- package/src/PreviewPanel.tsx +313 -77
- package/src/RawEditor.tsx +13 -2
- package/src/StatusBar.tsx +5 -2
- package/src/TemplateAnnotation.ts +34 -10
- package/src/TemplateContentPreview.tsx +56 -0
- package/src/TemplatePicker.tsx +355 -134
- package/src/ThemeCustomizerPanel.tsx +30 -15
- package/src/ThemePicker.tsx +10 -5
- package/src/TimelineBlockPreview.tsx +1 -1
- package/src/Toolbar.tsx +719 -238
- package/src/TransitionPicker.tsx +44 -5
- package/src/ViewMenuPanel.tsx +12 -14
- package/src/ViewSwitcher.tsx +4 -4
- package/src/WysiwygEditor.tsx +167 -92
- 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 +22 -3
- 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 +390 -1
- package/src/__tests__/emojiPicker.test.tsx +2 -42
- package/src/__tests__/headingTransition.test.ts +53 -15
- package/src/__tests__/imageEditAffordance.test.tsx +5 -1
- package/src/__tests__/imageEditModalContract.test.tsx +119 -0
- package/src/__tests__/imageEditorShell.test.tsx +38 -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__/mediaReferences.test.ts +82 -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 +312 -6
- 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 +49 -2
- package/src/__tests__/templateContentPreview.test.ts +101 -0
- package/src/__tests__/templatePickerPortal.test.tsx +62 -0
- package/src/__tests__/tiptapBridge.test.ts +70 -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 +85 -25
- 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 +193 -20
- 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/mediaReferences.ts +299 -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 +101 -25
- package/src/scene/SceneBlockExtension.ts +17 -10
- package/src/scene/SceneBlockWidget.tsx +11 -4
- package/src/scene/SceneSelection.tsx +19 -15
- package/src/scene/SceneSideToolbar.tsx +89 -0
- 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 +24 -9
- package/src/scene/layers/edgeGeometry.ts +31 -4
- package/src/scene/layers/nodeCard.tsx +27 -8
- package/src/scene/scene.css +145 -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 +126 -28
- package/src/scene/tools/DrawingConnectTool.ts +86 -16
- package/src/scene/tools/SceneTool.ts +10 -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 +1566 -219
- 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 +360 -0
- 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 +84 -49
- 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 -270
- package/src/diagram/useDiagramData.ts +0 -126
|
@@ -24,9 +24,10 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
27
|
-
import type { Theme, ThemeSeedColors } from '@bendyline/squisq/schemas';
|
|
27
|
+
import type { CustomTemplateDefinition, Theme, ThemeSeedColors } from '@bendyline/squisq/schemas';
|
|
28
28
|
import { resolveTheme } from '@bendyline/squisq/schemas';
|
|
29
29
|
import { ThemePicker } from '../ThemePicker';
|
|
30
|
+
import { ImportThemeSection } from './ImportThemeSection';
|
|
30
31
|
import {
|
|
31
32
|
type Draft,
|
|
32
33
|
type AccentInput,
|
|
@@ -49,13 +50,19 @@ import { Section, SeedColorRow, FontPicker, PresetRow, AccentEditor } from './th
|
|
|
49
50
|
/** Where a saved theme lands — mirrors `DesignerSaveTarget` for templates. */
|
|
50
51
|
export type ThemeSaveTarget = 'doc' | 'library';
|
|
51
52
|
|
|
53
|
+
/** Extra assets riding along with a theme save (file-import byproducts). */
|
|
54
|
+
export interface ThemeSaveExtras {
|
|
55
|
+
/** Custom layout templates inferred from an imported PPTX. */
|
|
56
|
+
templates?: CustomTemplateDefinition[];
|
|
57
|
+
}
|
|
58
|
+
|
|
52
59
|
export interface CustomThemeDialogProps {
|
|
53
60
|
/** Theme being edited, or null to author a new one. */
|
|
54
61
|
value: Theme | null;
|
|
55
62
|
/** Fired on every edit with the compiled theme (host previews via themeOverride). */
|
|
56
63
|
onChange?: (theme: Theme) => void;
|
|
57
64
|
/** Fired when the user saves, with the chosen target. */
|
|
58
|
-
onSave: (theme: Theme, target: ThemeSaveTarget) => void;
|
|
65
|
+
onSave: (theme: Theme, target: ThemeSaveTarget, extras?: ThemeSaveExtras) => void;
|
|
59
66
|
/** Dismiss (Cancel / Esc / close button). */
|
|
60
67
|
onClose: () => void;
|
|
61
68
|
}
|
|
@@ -70,6 +77,8 @@ function initialDraft(value: Theme | null): Draft {
|
|
|
70
77
|
|
|
71
78
|
export function CustomThemeDialog({ value, onChange, onSave, onClose }: CustomThemeDialogProps) {
|
|
72
79
|
const [draft, setDraft] = useState<Draft>(() => initialDraft(value));
|
|
80
|
+
// Layout templates inferred from a file import, saved alongside the theme.
|
|
81
|
+
const [importedLayouts, setImportedLayouts] = useState<CustomTemplateDefinition[] | null>(null);
|
|
73
82
|
|
|
74
83
|
const nameRef = useRef<HTMLInputElement>(null);
|
|
75
84
|
useEffect(() => {
|
|
@@ -117,12 +126,16 @@ export function CustomThemeDialog({ value, onChange, onSave, onClose }: CustomTh
|
|
|
117
126
|
const handleSave = useCallback(
|
|
118
127
|
(target: ThemeSaveTarget) => {
|
|
119
128
|
try {
|
|
120
|
-
|
|
129
|
+
const extras =
|
|
130
|
+
importedLayouts && importedLayouts.length > 0
|
|
131
|
+
? { templates: importedLayouts }
|
|
132
|
+
: undefined;
|
|
133
|
+
onSave(compile(draft), target, extras);
|
|
121
134
|
} catch {
|
|
122
135
|
// A compile failure here means the draft is invalid; leave the dialog open.
|
|
123
136
|
}
|
|
124
137
|
},
|
|
125
|
-
[draft, onSave, compile],
|
|
138
|
+
[draft, onSave, compile, importedLayouts],
|
|
126
139
|
);
|
|
127
140
|
|
|
128
141
|
// Accents show the base's schemes until the user edits them (then the draft's
|
|
@@ -175,6 +188,19 @@ export function CustomThemeDialog({ value, onChange, onSave, onClose }: CustomTh
|
|
|
175
188
|
/>
|
|
176
189
|
</Section>
|
|
177
190
|
|
|
191
|
+
<Section
|
|
192
|
+
title="Import from file"
|
|
193
|
+
hint="Pull colors and fonts from a Word, PowerPoint, or Excel file. PowerPoint slide layouts become custom layouts."
|
|
194
|
+
>
|
|
195
|
+
<ImportThemeSection
|
|
196
|
+
allowLayouts
|
|
197
|
+
onImported={(patch, result) => {
|
|
198
|
+
updateDraft(patch);
|
|
199
|
+
setImportedLayouts(result.layouts ?? null);
|
|
200
|
+
}}
|
|
201
|
+
/>
|
|
202
|
+
</Section>
|
|
203
|
+
|
|
178
204
|
<Section
|
|
179
205
|
title="Colors"
|
|
180
206
|
hint="Primary, secondary, and tertiary (accent). The rest is derived."
|
|
@@ -269,7 +295,11 @@ export function CustomThemeDialog({ value, onChange, onSave, onClose }: CustomTh
|
|
|
269
295
|
type="button"
|
|
270
296
|
className="squisq-doc-settings-btn squisq-doc-settings-btn--secondary"
|
|
271
297
|
onClick={() => handleSave('library')}
|
|
272
|
-
title=
|
|
298
|
+
title={
|
|
299
|
+
importedLayouts && importedLayouts.length > 0
|
|
300
|
+
? 'Save to your browser-local library so other docs can use it (imported slide layouts only save to the document)'
|
|
301
|
+
: 'Save to your browser-local library so other docs can use it'
|
|
302
|
+
}
|
|
273
303
|
>
|
|
274
304
|
Save to library
|
|
275
305
|
</button>
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ImportThemeSection
|
|
3
|
+
*
|
|
4
|
+
* "Infer theme from a file" affordance shared by the CustomThemeDialog pane
|
|
5
|
+
* and the ThemeCustomizerPanel popover: a Choose-file button plus a local
|
|
6
|
+
* dropzone that accepts a Word/PowerPoint/Excel file, runs
|
|
7
|
+
* `inferThemeFromFile` from `@bendyline/squisq-formats/infer` (loaded
|
|
8
|
+
* lazily), and hands back a `Partial<Draft>` patch for the host's
|
|
9
|
+
* `updateDraft` — so the existing live-preview path applies unchanged.
|
|
10
|
+
*
|
|
11
|
+
* The dropzone handles its own drag events with `stopPropagation` so a file
|
|
12
|
+
* dropped here never reaches EditorShell's document-import drop handling.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useCallback, useRef, useState } from 'react';
|
|
16
|
+
import type { DragEvent } from 'react';
|
|
17
|
+
import type { CustomTemplateDefinition, Theme } from '@bendyline/squisq/schemas';
|
|
18
|
+
import { draftPatchFromImportedTheme, type Draft } from './themeDraft';
|
|
19
|
+
|
|
20
|
+
export interface ImportedThemeResult {
|
|
21
|
+
theme: Theme;
|
|
22
|
+
/** PPTX only, when `allowLayouts`: custom templates derived from slide layouts. */
|
|
23
|
+
layouts?: CustomTemplateDefinition[];
|
|
24
|
+
warnings: string[];
|
|
25
|
+
fileName: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ImportThemeSectionProps {
|
|
29
|
+
/** Fired on successful inference with the draft patch + full result. */
|
|
30
|
+
onImported: (patch: Partial<Draft>, result: ImportedThemeResult) => void;
|
|
31
|
+
/** Also derive PPTX slide layouts as custom templates. Default false. */
|
|
32
|
+
allowLayouts?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const ACCEPT = '.docx,.pptx,.xlsx';
|
|
36
|
+
const SUPPORTED_RE = /\.(docx|pptx|xlsx)$/i;
|
|
37
|
+
const MAX_VISIBLE_WARNINGS = 4;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Read a File's bytes. Prefers the modern `arrayBuffer()`; falls back to
|
|
41
|
+
* FileReader for environments (e.g. some jsdom builds) whose Blob lacks it —
|
|
42
|
+
* same posture as the formats registry's byte normalizer.
|
|
43
|
+
*/
|
|
44
|
+
function readFileBytes(file: File): Promise<ArrayBuffer> {
|
|
45
|
+
if (typeof file.arrayBuffer === 'function') return file.arrayBuffer();
|
|
46
|
+
return new Promise<ArrayBuffer>((resolve, reject) => {
|
|
47
|
+
const reader = new FileReader();
|
|
48
|
+
reader.onload = () => resolve(reader.result as ArrayBuffer);
|
|
49
|
+
reader.onerror = () => reject(reader.error);
|
|
50
|
+
reader.readAsArrayBuffer(file);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function ImportThemeSection({ onImported, allowLayouts = false }: ImportThemeSectionProps) {
|
|
55
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
56
|
+
const [busy, setBusy] = useState(false);
|
|
57
|
+
const [error, setError] = useState<string | null>(null);
|
|
58
|
+
const [status, setStatus] = useState<string | null>(null);
|
|
59
|
+
const [warnings, setWarnings] = useState<string[]>([]);
|
|
60
|
+
const [dragOver, setDragOver] = useState(false);
|
|
61
|
+
|
|
62
|
+
const handleFile = useCallback(
|
|
63
|
+
async (file: File) => {
|
|
64
|
+
if (!SUPPORTED_RE.test(file.name)) {
|
|
65
|
+
setError('Only .docx, .pptx, or .xlsx files carry a theme.');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
setBusy(true);
|
|
69
|
+
setError(null);
|
|
70
|
+
setStatus(null);
|
|
71
|
+
setWarnings([]);
|
|
72
|
+
try {
|
|
73
|
+
const buffer = await readFileBytes(file);
|
|
74
|
+
const { inferThemeFromFile } = await import('@bendyline/squisq-formats/infer');
|
|
75
|
+
const nameHint = file.name.replace(/\.[^.]+$/, '');
|
|
76
|
+
const result = await inferThemeFromFile(buffer, {
|
|
77
|
+
inferLayouts: allowLayouts,
|
|
78
|
+
nameHint,
|
|
79
|
+
});
|
|
80
|
+
const layouts = result.layouts ?? [];
|
|
81
|
+
onImported(draftPatchFromImportedTheme(result.theme), {
|
|
82
|
+
theme: result.theme,
|
|
83
|
+
...(layouts.length > 0 ? { layouts } : {}),
|
|
84
|
+
warnings: result.warnings,
|
|
85
|
+
fileName: file.name,
|
|
86
|
+
});
|
|
87
|
+
setStatus(
|
|
88
|
+
layouts.length > 0
|
|
89
|
+
? `Imported colors and fonts from ${file.name} — ${layouts.length} slide layout${
|
|
90
|
+
layouts.length === 1 ? '' : 's'
|
|
91
|
+
} will be saved with the document.`
|
|
92
|
+
: `Imported colors and fonts from ${file.name}.`,
|
|
93
|
+
);
|
|
94
|
+
setWarnings(result.warnings);
|
|
95
|
+
} catch (err: unknown) {
|
|
96
|
+
setError(err instanceof Error ? err.message : 'Could not read a theme from that file.');
|
|
97
|
+
} finally {
|
|
98
|
+
setBusy(false);
|
|
99
|
+
if (inputRef.current) inputRef.current.value = '';
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
[allowLayouts, onImported],
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// Local drag handling only — never let the editor's global drop routing
|
|
106
|
+
// treat a theme-source file as a document import.
|
|
107
|
+
const onDragOver = useCallback((e: DragEvent<HTMLDivElement>) => {
|
|
108
|
+
e.preventDefault();
|
|
109
|
+
e.stopPropagation();
|
|
110
|
+
setDragOver(true);
|
|
111
|
+
}, []);
|
|
112
|
+
const onDragLeave = useCallback((e: DragEvent<HTMLDivElement>) => {
|
|
113
|
+
e.preventDefault();
|
|
114
|
+
e.stopPropagation();
|
|
115
|
+
setDragOver(false);
|
|
116
|
+
}, []);
|
|
117
|
+
const onDrop = useCallback(
|
|
118
|
+
(e: DragEvent<HTMLDivElement>) => {
|
|
119
|
+
e.preventDefault();
|
|
120
|
+
e.stopPropagation();
|
|
121
|
+
setDragOver(false);
|
|
122
|
+
const file = e.dataTransfer.files?.[0];
|
|
123
|
+
if (file) void handleFile(file);
|
|
124
|
+
},
|
|
125
|
+
[handleFile],
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const visibleWarnings = warnings.slice(0, MAX_VISIBLE_WARNINGS);
|
|
129
|
+
const hiddenWarnings = warnings.length - visibleWarnings.length;
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<div className="squisq-theme-import">
|
|
133
|
+
<div
|
|
134
|
+
className={`squisq-theme-import-drop${dragOver ? ' squisq-theme-import-drop--active' : ''}${
|
|
135
|
+
busy ? ' squisq-theme-import-drop--busy' : ''
|
|
136
|
+
}`}
|
|
137
|
+
onDragOver={onDragOver}
|
|
138
|
+
onDragLeave={onDragLeave}
|
|
139
|
+
onDrop={onDrop}
|
|
140
|
+
>
|
|
141
|
+
<button
|
|
142
|
+
type="button"
|
|
143
|
+
className="squisq-theme-customizer-button"
|
|
144
|
+
onClick={() => inputRef.current?.click()}
|
|
145
|
+
disabled={busy}
|
|
146
|
+
>
|
|
147
|
+
{busy ? 'Reading…' : 'Choose file…'}
|
|
148
|
+
</button>
|
|
149
|
+
<span className="squisq-theme-import-hint">or drop a .docx / .pptx / .xlsx here</span>
|
|
150
|
+
<input
|
|
151
|
+
ref={inputRef}
|
|
152
|
+
type="file"
|
|
153
|
+
accept={ACCEPT}
|
|
154
|
+
style={{ display: 'none' }}
|
|
155
|
+
onChange={(e) => {
|
|
156
|
+
const file = e.target.files?.[0];
|
|
157
|
+
if (file) void handleFile(file);
|
|
158
|
+
}}
|
|
159
|
+
aria-label="Import theme from file"
|
|
160
|
+
/>
|
|
161
|
+
</div>
|
|
162
|
+
{error && (
|
|
163
|
+
<div className="squisq-theme-import-error" role="alert">
|
|
164
|
+
{error}
|
|
165
|
+
</div>
|
|
166
|
+
)}
|
|
167
|
+
{status && <div className="squisq-theme-import-status">{status}</div>}
|
|
168
|
+
{visibleWarnings.length > 0 && (
|
|
169
|
+
<ul className="squisq-theme-import-warnings">
|
|
170
|
+
{visibleWarnings.map((w, i) => (
|
|
171
|
+
<li key={i}>{w}</li>
|
|
172
|
+
))}
|
|
173
|
+
{hiddenWarnings > 0 && <li>…and {hiddenWarnings} more</li>}
|
|
174
|
+
</ul>
|
|
175
|
+
)}
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
saveLibraryTheme,
|
|
12
12
|
deleteLibraryTheme,
|
|
13
13
|
clearThemeLibrary,
|
|
14
|
+
THEME_LIBRARY_STORAGE_KEY,
|
|
14
15
|
} from '../customThemeLibrary';
|
|
15
16
|
import { compileTheme } from '@bendyline/squisq/schemas';
|
|
16
17
|
import type { Theme } from '@bendyline/squisq/schemas';
|
|
@@ -48,4 +49,12 @@ describe('customThemeLibrary', () => {
|
|
|
48
49
|
clearThemeLibrary();
|
|
49
50
|
expect(listLibraryThemes()).toEqual([]);
|
|
50
51
|
});
|
|
52
|
+
|
|
53
|
+
it('filters invalid stored entries instead of crashing sort', () => {
|
|
54
|
+
window.localStorage.setItem(
|
|
55
|
+
THEME_LIBRARY_STORAGE_KEY,
|
|
56
|
+
JSON.stringify({ version: 1, themes: [null, {}, theme('valid')] }),
|
|
57
|
+
);
|
|
58
|
+
expect(listLibraryThemes().map((entry) => entry.id)).toEqual(['valid']);
|
|
59
|
+
});
|
|
51
60
|
});
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ImportThemeSection — the "infer theme from a file" affordance. Verifies the
|
|
3
|
+
* draft patch handed to hosts, error handling, drop-type gating, and that a
|
|
4
|
+
* designer save with imported layouts writes theme + selection + templates in
|
|
5
|
+
* a single frontmatter update.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// @vitest-environment jsdom
|
|
9
|
+
|
|
10
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
11
|
+
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
12
|
+
import type { CustomTemplateDefinition, Theme } from '@bendyline/squisq/schemas';
|
|
13
|
+
import { compileTheme } from '@bendyline/squisq/schemas';
|
|
14
|
+
import { ImportThemeSection } from '../ImportThemeSection';
|
|
15
|
+
import { draftPatchFromImportedTheme } from '../themeDraft';
|
|
16
|
+
import { EditorProvider, useEditorContext } from '../../EditorContext';
|
|
17
|
+
import { PreviewSettingsProvider, usePreviewSettings } from '../../PreviewControls';
|
|
18
|
+
|
|
19
|
+
const { inferMock } = vi.hoisted(() => ({ inferMock: vi.fn() }));
|
|
20
|
+
vi.mock('@bendyline/squisq-formats/infer', () => ({ inferThemeFromFile: inferMock }));
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
cleanup();
|
|
24
|
+
inferMock.mockReset();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const importedTheme: Theme = compileTheme({
|
|
28
|
+
id: 'custom-deck',
|
|
29
|
+
name: 'Deck',
|
|
30
|
+
seedColors: { primary: '#4472c4', background: '#fdfdf8', text: '#1a1a2e' },
|
|
31
|
+
typography: {
|
|
32
|
+
titleFont: { stackId: 'playfair' },
|
|
33
|
+
bodyFont: { custom: { name: 'Aptos', fallback: 'sans-serif' } },
|
|
34
|
+
},
|
|
35
|
+
colorSchemes: { accent1: { bg: '#101020', text: '#e2e8f0', accent: '#4472c4' } },
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const importedLayout: CustomTemplateDefinition = {
|
|
39
|
+
name: 'pptx-two-content',
|
|
40
|
+
label: 'Two Content',
|
|
41
|
+
viewport: { width: 1920, height: 1080 },
|
|
42
|
+
layers: [
|
|
43
|
+
{
|
|
44
|
+
id: 'title',
|
|
45
|
+
type: 'text',
|
|
46
|
+
position: { x: '5%', y: '10%', width: '90%', height: '20%' },
|
|
47
|
+
content: { text: '{title}', style: { fontSize: 88, color: '#111111' } },
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function pickFile(name = 'deck.pptx'): void {
|
|
53
|
+
const file = new File([new Uint8Array([0x50, 0x4b, 3, 4])], name);
|
|
54
|
+
fireEvent.change(screen.getByLabelText('Import theme from file'), {
|
|
55
|
+
target: { files: [file] },
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe('ImportThemeSection', () => {
|
|
60
|
+
it('imports a theme and hands the host a draft patch + layouts', async () => {
|
|
61
|
+
inferMock.mockResolvedValue({
|
|
62
|
+
theme: importedTheme,
|
|
63
|
+
extraction: { sourceFormat: 'pptx', colorMap: { bg1: 'lt1', tx1: 'dk1' }, warnings: [] },
|
|
64
|
+
layouts: [importedLayout],
|
|
65
|
+
warnings: ['theme: accent4 color could not be resolved; dropped'],
|
|
66
|
+
});
|
|
67
|
+
const onImported = vi.fn();
|
|
68
|
+
render(<ImportThemeSection allowLayouts onImported={onImported} />);
|
|
69
|
+
|
|
70
|
+
pickFile();
|
|
71
|
+
await waitFor(() => expect(onImported).toHaveBeenCalledTimes(1));
|
|
72
|
+
|
|
73
|
+
expect(inferMock).toHaveBeenCalledWith(expect.anything(), {
|
|
74
|
+
inferLayouts: true,
|
|
75
|
+
nameHint: 'deck',
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const [patch, result] = onImported.mock.calls[0]!;
|
|
79
|
+
expect(patch.name).toBe('Deck');
|
|
80
|
+
expect(patch.seeds.primary).toBe('#4472c4');
|
|
81
|
+
expect(patch.accentsEdited).toBe(true);
|
|
82
|
+
expect(patch.accents).toEqual([{ key: 'accent1', color: '#4472c4' }]);
|
|
83
|
+
expect(patch.titleFont).toEqual({ kind: 'curated', stackId: 'playfair' });
|
|
84
|
+
expect(patch.bodyFont).toEqual({
|
|
85
|
+
kind: 'custom',
|
|
86
|
+
customName: 'Aptos',
|
|
87
|
+
customFallback: 'sans-serif',
|
|
88
|
+
});
|
|
89
|
+
// The patch never touches base or style presets.
|
|
90
|
+
expect('baseId' in patch).toBe(false);
|
|
91
|
+
expect('borderRadius' in patch).toBe(false);
|
|
92
|
+
|
|
93
|
+
expect(result.layouts).toHaveLength(1);
|
|
94
|
+
expect(result.fileName).toBe('deck.pptx');
|
|
95
|
+
expect(screen.getByText(/1 slide layout/)).toBeTruthy();
|
|
96
|
+
expect(screen.getByText(/accent4/)).toBeTruthy();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('shows the inference error and never fires onImported', async () => {
|
|
100
|
+
inferMock.mockRejectedValue(new Error('No theme part found in this file.'));
|
|
101
|
+
const onImported = vi.fn();
|
|
102
|
+
render(<ImportThemeSection onImported={onImported} />);
|
|
103
|
+
|
|
104
|
+
pickFile();
|
|
105
|
+
await waitFor(() =>
|
|
106
|
+
expect(screen.getByRole('alert').textContent).toContain('No theme part found'),
|
|
107
|
+
);
|
|
108
|
+
expect(onImported).not.toHaveBeenCalled();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('rejects unsupported file types dropped on the zone', async () => {
|
|
112
|
+
const onImported = vi.fn();
|
|
113
|
+
const { container } = render(<ImportThemeSection onImported={onImported} />);
|
|
114
|
+
const zone = container.querySelector('.squisq-theme-import-drop')!;
|
|
115
|
+
fireEvent.drop(zone, {
|
|
116
|
+
dataTransfer: { files: [new File(['x'], 'notes.txt')] },
|
|
117
|
+
});
|
|
118
|
+
await waitFor(() =>
|
|
119
|
+
expect(screen.getByRole('alert').textContent).toContain('Only .docx, .pptx, or .xlsx'),
|
|
120
|
+
);
|
|
121
|
+
expect(inferMock).not.toHaveBeenCalled();
|
|
122
|
+
expect(onImported).not.toHaveBeenCalled();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('draftPatchFromImportedTheme', () => {
|
|
127
|
+
it('carries seeds/fonts/accents and nothing else', () => {
|
|
128
|
+
const patch = draftPatchFromImportedTheme(importedTheme);
|
|
129
|
+
expect(Object.keys(patch).sort()).toEqual([
|
|
130
|
+
'accents',
|
|
131
|
+
'accentsEdited',
|
|
132
|
+
'bodyFont',
|
|
133
|
+
'name',
|
|
134
|
+
'seeds',
|
|
135
|
+
'titleFont',
|
|
136
|
+
]);
|
|
137
|
+
expect(patch.seeds).toMatchObject({ primary: '#4472c4', background: '#fdfdf8' });
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// ── Designer save with imported layouts → single frontmatter write ───
|
|
142
|
+
|
|
143
|
+
function DesignerSaveHarness() {
|
|
144
|
+
const settings = usePreviewSettings();
|
|
145
|
+
const { markdownSource } = useEditorContext();
|
|
146
|
+
return (
|
|
147
|
+
<div>
|
|
148
|
+
<button type="button" onClick={() => settings.openThemeDesigner(null)}>
|
|
149
|
+
open designer
|
|
150
|
+
</button>
|
|
151
|
+
<button
|
|
152
|
+
type="button"
|
|
153
|
+
onClick={() =>
|
|
154
|
+
settings.themeDesigner?.onSave(importedTheme, 'doc', { templates: [importedLayout] })
|
|
155
|
+
}
|
|
156
|
+
>
|
|
157
|
+
save with layouts
|
|
158
|
+
</button>
|
|
159
|
+
<pre data-testid="markdown-source">{markdownSource}</pre>
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function HarnessRoot() {
|
|
165
|
+
const { doc } = useEditorContext();
|
|
166
|
+
return (
|
|
167
|
+
<PreviewSettingsProvider doc={doc}>
|
|
168
|
+
<DesignerSaveHarness />
|
|
169
|
+
</PreviewSettingsProvider>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
describe('theme designer save with imported layouts', () => {
|
|
174
|
+
it('writes theme payload, selection, and templates in one frontmatter update', async () => {
|
|
175
|
+
render(
|
|
176
|
+
<EditorProvider initialMarkdown={'# Hello\n\nBody.\n'}>
|
|
177
|
+
<HarnessRoot />
|
|
178
|
+
</EditorProvider>,
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
fireEvent.click(screen.getByText('open designer'));
|
|
182
|
+
fireEvent.click(screen.getByText('save with layouts'));
|
|
183
|
+
|
|
184
|
+
await waitFor(() => {
|
|
185
|
+
const source = screen.getByTestId('markdown-source').textContent ?? '';
|
|
186
|
+
expect(source).toContain('squisq-theme: custom-deck');
|
|
187
|
+
expect(source).toContain('squisq-custom-themes');
|
|
188
|
+
expect(source).toContain('squisq-custom-templates');
|
|
189
|
+
expect(source).toContain('pptx-two-content');
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
});
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* unavailable.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import type
|
|
17
|
+
import { validateTheme, type Theme } from '@bendyline/squisq/schemas';
|
|
18
18
|
|
|
19
19
|
export const THEME_LIBRARY_STORAGE_KEY = 'squisq:custom-theme-library';
|
|
20
20
|
|
|
@@ -38,12 +38,15 @@ function safeStorage(): Storage | null {
|
|
|
38
38
|
function readPayload(): LibraryPayload {
|
|
39
39
|
const storage = safeStorage();
|
|
40
40
|
if (!storage) return { version: 1, themes: [] };
|
|
41
|
-
const raw = storage.getItem(THEME_LIBRARY_STORAGE_KEY);
|
|
42
|
-
if (!raw) return { version: 1, themes: [] };
|
|
43
41
|
try {
|
|
42
|
+
const raw = storage.getItem(THEME_LIBRARY_STORAGE_KEY);
|
|
43
|
+
if (!raw) return { version: 1, themes: [] };
|
|
44
44
|
const parsed = JSON.parse(raw) as Partial<LibraryPayload>;
|
|
45
45
|
if (parsed && Array.isArray(parsed.themes)) {
|
|
46
|
-
return {
|
|
46
|
+
return {
|
|
47
|
+
version: 1,
|
|
48
|
+
themes: parsed.themes.filter((theme): theme is Theme => validateTheme(theme).valid),
|
|
49
|
+
};
|
|
47
50
|
}
|
|
48
51
|
} catch {
|
|
49
52
|
// Corrupt payload — drop it.
|
|
@@ -27,5 +27,11 @@ export {
|
|
|
27
27
|
CustomThemeDialog,
|
|
28
28
|
type CustomThemeDialogProps,
|
|
29
29
|
type ThemeSaveTarget,
|
|
30
|
+
type ThemeSaveExtras,
|
|
30
31
|
} from './CustomThemeDialog';
|
|
31
|
-
export {
|
|
32
|
+
export {
|
|
33
|
+
ImportThemeSection,
|
|
34
|
+
type ImportThemeSectionProps,
|
|
35
|
+
type ImportedThemeResult,
|
|
36
|
+
} from './ImportThemeSection';
|
|
37
|
+
export { compileDraft, themeToDraft, draftPatchFromImportedTheme, type Draft } from './themeDraft';
|
|
@@ -15,7 +15,7 @@ import type {
|
|
|
15
15
|
ThemeSeedColors,
|
|
16
16
|
ThemeColorScheme,
|
|
17
17
|
} from '@bendyline/squisq/schemas';
|
|
18
|
-
import {
|
|
18
|
+
import { accentToColorScheme, compileTheme } from '@bendyline/squisq/schemas';
|
|
19
19
|
|
|
20
20
|
// ── Preset → schema-value tables ────────────────────────────────────
|
|
21
21
|
|
|
@@ -164,14 +164,12 @@ export function accentsFromTheme(theme: Theme | null): AccentInput[] {
|
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
/**
|
|
167
|
-
* Expand one accent color into a full `{bg, text, accent}` color scheme
|
|
168
|
-
*
|
|
169
|
-
*
|
|
167
|
+
* Expand one accent color into a full `{bg, text, accent}` color scheme.
|
|
168
|
+
* Delegates to core's `accentToColorScheme` so file-import theme inference
|
|
169
|
+
* and the editor's accent list derive identical schemes.
|
|
170
170
|
*/
|
|
171
171
|
export function schemeFromAccent(accent: string): ThemeColorScheme {
|
|
172
|
-
|
|
173
|
-
const scale = deriveScale(accent, 0.3);
|
|
174
|
-
return { bg: scale.darker2, text: scale.lighter2, accent: scale.base };
|
|
172
|
+
return accentToColorScheme(accent);
|
|
175
173
|
}
|
|
176
174
|
|
|
177
175
|
export function themeToDraft(theme: Theme | null): Draft {
|
|
@@ -220,6 +218,24 @@ export function slugify(s: string): string {
|
|
|
220
218
|
);
|
|
221
219
|
}
|
|
222
220
|
|
|
221
|
+
/**
|
|
222
|
+
* Draft patch from a theme inferred from an imported office file: name,
|
|
223
|
+
* seeds, accents (marked authored so the file's schemes survive a re-save),
|
|
224
|
+
* and fonts. Deliberately leaves `baseId` and the style presets untouched —
|
|
225
|
+
* the user's in-progress choices there aren't the file's to overwrite.
|
|
226
|
+
*/
|
|
227
|
+
export function draftPatchFromImportedTheme(theme: Theme): Partial<Draft> {
|
|
228
|
+
const d = themeToDraft(theme);
|
|
229
|
+
return {
|
|
230
|
+
name: d.name,
|
|
231
|
+
seeds: d.seeds,
|
|
232
|
+
accents: d.accents,
|
|
233
|
+
accentsEdited: d.accents.length > 0,
|
|
234
|
+
titleFont: d.titleFont,
|
|
235
|
+
bodyFont: d.bodyFont,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
223
239
|
export interface CompileDraftOptions {
|
|
224
240
|
/** Existing id to preserve across edits (keeps `squisq-theme` selection stable). */
|
|
225
241
|
existingId?: string;
|
|
@@ -22,8 +22,9 @@ import {
|
|
|
22
22
|
type SceneCommand,
|
|
23
23
|
} from '../scene';
|
|
24
24
|
import type { SceneTextEditConfig } from '../scene/text/sceneTextConfig';
|
|
25
|
+
import type { SceneTextChannel } from '../scene/text/sceneTextChannel';
|
|
25
26
|
import { markdownToTiptap } from '../tiptapBridge';
|
|
26
|
-
import type {
|
|
27
|
+
import type { DiagramNode, DiagramEdge } from './types';
|
|
27
28
|
import { DIAGRAM_VIEWPORT, DIAGRAM_TOOLS } from './diagramConstants';
|
|
28
29
|
|
|
29
30
|
export type DiagramCommand =
|
|
@@ -36,8 +37,8 @@ export type DiagramCommand =
|
|
|
36
37
|
| { kind: 'removeNode'; nodeId: string };
|
|
37
38
|
|
|
38
39
|
interface DiagramCanvasProps {
|
|
39
|
-
nodes:
|
|
40
|
-
edges:
|
|
40
|
+
nodes: DiagramNode[];
|
|
41
|
+
edges: DiagramEdge[];
|
|
41
42
|
onCommand: (cmd: DiagramCommand) => void;
|
|
42
43
|
/** When true, render the maximize button. Click toggles `onToggleMaximize`. */
|
|
43
44
|
showMaximize?: boolean;
|
|
@@ -54,6 +55,8 @@ interface DiagramCanvasProps {
|
|
|
54
55
|
onActiveToolIdChange?: (id: string) => void;
|
|
55
56
|
/** Forwarded to the Scene so the host can drive a Delete action. */
|
|
56
57
|
onSelectionChange?: (ids: ReadonlySet<string>) => void;
|
|
58
|
+
/** Per-editor toolbar bridge for detached scene roots. */
|
|
59
|
+
textChannel?: SceneTextChannel;
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
const TOOLS = DIAGRAM_TOOLS;
|
|
@@ -68,6 +71,7 @@ export function DiagramCanvas({
|
|
|
68
71
|
activeToolId: controlledToolId,
|
|
69
72
|
onActiveToolIdChange,
|
|
70
73
|
onSelectionChange,
|
|
74
|
+
textChannel,
|
|
71
75
|
}: DiagramCanvasProps) {
|
|
72
76
|
const scene = useMemo(
|
|
73
77
|
() => buildDiagramScene(incomingNodes, incomingEdges),
|
|
@@ -171,6 +175,7 @@ export function DiagramCanvas({
|
|
|
171
175
|
incomingNodesRef.current = incomingNodes;
|
|
172
176
|
const textConfig = useMemo<SceneTextEditConfig>(
|
|
173
177
|
() => ({
|
|
178
|
+
channel: textChannel,
|
|
174
179
|
resolveEditableId: (id) => (nodeIdFromCardLayerId(id) ? id : null),
|
|
175
180
|
getHtml: (id) => {
|
|
176
181
|
const nodeId = nodeIdFromCardLayerId(id);
|
|
@@ -184,7 +189,7 @@ export function DiagramCanvas({
|
|
|
184
189
|
},
|
|
185
190
|
level: 'inline',
|
|
186
191
|
}),
|
|
187
|
-
[handleSceneCommand],
|
|
192
|
+
[handleSceneCommand, textChannel],
|
|
188
193
|
);
|
|
189
194
|
|
|
190
195
|
return (
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Data shapes consumed by `DiagramCanvas` / `buildDiagramScene`. */
|
|
2
|
+
|
|
3
|
+
import type { DiagramEdgeAnchor } from '@bendyline/squisq/schemas';
|
|
4
|
+
import type { ConnectorRouting } from '@bendyline/squisq/doc';
|
|
5
|
+
|
|
6
|
+
export interface DiagramNode {
|
|
7
|
+
id: string;
|
|
8
|
+
position: { x: number; y: number };
|
|
9
|
+
data: { label: string };
|
|
10
|
+
type?: string;
|
|
11
|
+
/** Per-node width override in canvas units. */
|
|
12
|
+
width?: number;
|
|
13
|
+
/** Per-node height override in canvas units. */
|
|
14
|
+
height?: number;
|
|
15
|
+
/** Render as a background container card (top-anchored label, muted fill). */
|
|
16
|
+
kind?: 'container';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface DiagramEdge {
|
|
20
|
+
id: string;
|
|
21
|
+
source: string;
|
|
22
|
+
target: string;
|
|
23
|
+
label?: string;
|
|
24
|
+
/** False → no end arrowhead. Undefined keeps the diagram default (arrow). */
|
|
25
|
+
directed?: boolean;
|
|
26
|
+
sourceAnchor?: DiagramEdgeAnchor;
|
|
27
|
+
targetAnchor?: DiagramEdgeAnchor;
|
|
28
|
+
routing?: ConnectorRouting;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface DiagramData {
|
|
32
|
+
nodes: DiagramNode[];
|
|
33
|
+
edges: DiagramEdge[];
|
|
34
|
+
warnings: string[];
|
|
35
|
+
}
|