@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
|
@@ -13,10 +13,11 @@
|
|
|
13
13
|
* drag).
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import { useId } from 'react';
|
|
16
17
|
import type { MarkerStyle } from '@bendyline/squisq/schemas';
|
|
17
18
|
import { connectorPath, markerPath } from '@bendyline/squisq/doc';
|
|
18
19
|
import type { SceneEdge } from '../commands/SceneCommand';
|
|
19
|
-
import { boxesOf,
|
|
20
|
+
import { boxesOf, edgeEndpoints } from './edgeGeometry';
|
|
20
21
|
import type { EdgeNodeBox } from './edgeGeometry';
|
|
21
22
|
|
|
22
23
|
interface DiagramEdgesProps {
|
|
@@ -33,8 +34,8 @@ interface DiagramEdgesProps {
|
|
|
33
34
|
/** Marker styles that carry a glyph (everything except 'none'). */
|
|
34
35
|
const MARKER_STYLES: MarkerStyle[] = ['arrow', 'open', 'diamond', 'circle', 'square'];
|
|
35
36
|
|
|
36
|
-
function markerId(style: MarkerStyle, dir: 'start' | 'end'): string {
|
|
37
|
-
return
|
|
37
|
+
function markerId(prefix: string, style: MarkerStyle, dir: 'start' | 'end'): string {
|
|
38
|
+
return `${prefix}-squisq-edge-${style}-${dir}`;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
export function DiagramEdges({
|
|
@@ -45,6 +46,7 @@ export function DiagramEdges({
|
|
|
45
46
|
onEdgeClick,
|
|
46
47
|
}: DiagramEdgesProps) {
|
|
47
48
|
const boxById = boxesOf(nodes);
|
|
49
|
+
const markerPrefix = useId().replace(/:/g, '');
|
|
48
50
|
const defaultRouting = variant === 'straight' ? 'straight' : 'curved';
|
|
49
51
|
|
|
50
52
|
return (
|
|
@@ -56,8 +58,8 @@ export function DiagramEdges({
|
|
|
56
58
|
if (!mp) return null;
|
|
57
59
|
return (
|
|
58
60
|
<marker
|
|
59
|
-
key={markerId(style, dir)}
|
|
60
|
-
id={markerId(style, dir)}
|
|
61
|
+
key={markerId(markerPrefix, style, dir)}
|
|
62
|
+
id={markerId(markerPrefix, style, dir)}
|
|
61
63
|
viewBox="0 0 10 10"
|
|
62
64
|
refX={dir === 'end' ? 9 : 1}
|
|
63
65
|
refY={5}
|
|
@@ -81,8 +83,15 @@ export function DiagramEdges({
|
|
|
81
83
|
const a = boxById.get(edge.source);
|
|
82
84
|
const b = boxById.get(edge.target);
|
|
83
85
|
if (!a || !b) return null;
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
+
const snapped = edgeEndpoints(
|
|
87
|
+
nodes,
|
|
88
|
+
edge.source,
|
|
89
|
+
edge.target,
|
|
90
|
+
edge.sourceAnchor,
|
|
91
|
+
edge.targetAnchor,
|
|
92
|
+
);
|
|
93
|
+
if (!snapped) return null;
|
|
94
|
+
const { start, end } = snapped;
|
|
86
95
|
const d = connectorPath(edge.routing ?? defaultRouting, start, end);
|
|
87
96
|
const startMarker = edge.startMarker ?? 'none';
|
|
88
97
|
const endMarker = edge.endMarker ?? 'arrow';
|
|
@@ -98,9 +107,15 @@ export function DiagramEdges({
|
|
|
98
107
|
className="squisq-scene-edge-path"
|
|
99
108
|
strokeDasharray={edge.dasharray}
|
|
100
109
|
markerStart={
|
|
101
|
-
startMarker !== 'none'
|
|
110
|
+
startMarker !== 'none'
|
|
111
|
+
? `url(#${markerId(markerPrefix, startMarker, 'start')})`
|
|
112
|
+
: undefined
|
|
113
|
+
}
|
|
114
|
+
markerEnd={
|
|
115
|
+
endMarker !== 'none'
|
|
116
|
+
? `url(#${markerId(markerPrefix, endMarker, 'end')})`
|
|
117
|
+
: undefined
|
|
102
118
|
}
|
|
103
|
-
markerEnd={endMarker !== 'none' ? `url(#${markerId(endMarker, 'end')})` : undefined}
|
|
104
119
|
/>
|
|
105
120
|
{edge.label ? (
|
|
106
121
|
<text
|
|
@@ -8,6 +8,13 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { NODE_WIDTH, NODE_HEIGHT } from './nodeCard';
|
|
11
|
+
import {
|
|
12
|
+
anchorPoint,
|
|
13
|
+
nearestSnapPoint as nearestCoreSnapPoint,
|
|
14
|
+
snapEndpoints as coreSnapEndpoints,
|
|
15
|
+
type ConnectorAnchor,
|
|
16
|
+
type ConnectorSnapPoint,
|
|
17
|
+
} from '@bendyline/squisq/doc';
|
|
11
18
|
|
|
12
19
|
/** Minimal box an edge needs from a node/shape (id + bounds). */
|
|
13
20
|
export interface EdgeNodeBox {
|
|
@@ -49,21 +56,41 @@ export function edgePoint(from: NodeBox, to: NodeBox): { x: number; y: number }
|
|
|
49
56
|
return { x: from.cx + dx * s, y: from.cy + dy * s };
|
|
50
57
|
}
|
|
51
58
|
|
|
59
|
+
function boxFor(nodes: readonly EdgeNodeBox[], id: string): NodeBox | null {
|
|
60
|
+
return boxesOf(nodes).get(id) ?? null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Nearest stable connector port on the named node/shape to `point`. */
|
|
64
|
+
export function snapPointToward(
|
|
65
|
+
nodes: readonly EdgeNodeBox[],
|
|
66
|
+
id: string,
|
|
67
|
+
point: { x: number; y: number },
|
|
68
|
+
): ConnectorSnapPoint | null {
|
|
69
|
+
const box = boxFor(nodes, id);
|
|
70
|
+
return box ? nearestCoreSnapPoint(box, point) : null;
|
|
71
|
+
}
|
|
72
|
+
|
|
52
73
|
/**
|
|
53
|
-
*
|
|
54
|
-
* endpoint shape is missing. Used by the renderer and by
|
|
74
|
+
* Snapped start/end points of the edge `source`→`target`, or null when an
|
|
75
|
+
* endpoint shape is missing. Used by the renderer and by tool hit-testing
|
|
55
76
|
* endpoint handles against the same geometry the renderer draws.
|
|
56
77
|
*/
|
|
57
78
|
export function edgeEndpoints(
|
|
58
79
|
nodes: readonly EdgeNodeBox[],
|
|
59
80
|
source: string,
|
|
60
81
|
target: string,
|
|
61
|
-
|
|
82
|
+
sourceAnchor?: ConnectorAnchor,
|
|
83
|
+
targetAnchor?: ConnectorAnchor,
|
|
84
|
+
): { start: ConnectorSnapPoint; end: ConnectorSnapPoint } | null {
|
|
62
85
|
const boxes = boxesOf(nodes);
|
|
63
86
|
const a = boxes.get(source);
|
|
64
87
|
const b = boxes.get(target);
|
|
65
88
|
if (!a || !b) return null;
|
|
66
|
-
|
|
89
|
+
const snapped = coreSnapEndpoints(a, b);
|
|
90
|
+
return {
|
|
91
|
+
start: sourceAnchor ? anchorPoint(a, sourceAnchor) : snapped.start,
|
|
92
|
+
end: targetAnchor ? anchorPoint(b, targetAnchor) : snapped.end,
|
|
93
|
+
};
|
|
67
94
|
}
|
|
68
95
|
|
|
69
96
|
export function straightPath(a: { x: number; y: number }, b: { x: number; y: number }): string {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type { ShapeLayer, TextLayer, Layer } from '@bendyline/squisq/schemas';
|
|
14
|
+
import { DIAGRAM_LABEL_LINE_HEIGHT, fitDiagramLabel } from '@bendyline/squisq/doc';
|
|
14
15
|
|
|
15
16
|
export const NODE_WIDTH = 180;
|
|
16
17
|
export const NODE_HEIGHT = 64;
|
|
@@ -34,6 +35,12 @@ export interface DiagramNodeDescriptor {
|
|
|
34
35
|
stroke?: string;
|
|
35
36
|
/** Optional shape: 'rect' (default) or 'pill' (fully rounded). */
|
|
36
37
|
shape?: 'rect' | 'pill';
|
|
38
|
+
/**
|
|
39
|
+
* 'container' renders as a background grouping card: translucent fill,
|
|
40
|
+
* muted stroke, label anchored near the top edge instead of centered
|
|
41
|
+
* (children paint over the interior).
|
|
42
|
+
*/
|
|
43
|
+
kind?: 'container';
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
/**
|
|
@@ -45,43 +52,55 @@ export interface DiagramNodeDescriptor {
|
|
|
45
52
|
export function nodeCardLayers(node: DiagramNodeDescriptor): [ShapeLayer, TextLayer] {
|
|
46
53
|
const width = node.width ?? NODE_WIDTH;
|
|
47
54
|
const height = node.height ?? NODE_HEIGHT;
|
|
55
|
+
const isContainer = node.kind === 'container';
|
|
48
56
|
const card: ShapeLayer = {
|
|
49
57
|
id: `node-card-${node.id}`,
|
|
50
58
|
type: 'shape',
|
|
51
59
|
position: { x: node.x, y: node.y, width, height },
|
|
52
60
|
content: {
|
|
53
61
|
shape: 'rect',
|
|
54
|
-
fill: node.fill ?? '#ffffff',
|
|
55
|
-
|
|
62
|
+
fill: node.fill ?? (isContainer ? '#94a3b8' : '#ffffff'),
|
|
63
|
+
...(isContainer ? { fillOpacity: 0.12 } : {}),
|
|
64
|
+
stroke: node.stroke ?? (isContainer ? '#94a3b8' : '#1e293b'),
|
|
56
65
|
strokeWidth: 2,
|
|
57
66
|
borderRadius: node.shape === 'pill' ? height / 2 : 10,
|
|
58
67
|
},
|
|
59
68
|
};
|
|
60
69
|
|
|
61
70
|
// Scale the label font with the node height so resized cards stay
|
|
62
|
-
// legible without the text overflowing.
|
|
63
|
-
|
|
71
|
+
// legible without the text overflowing. Containers keep a fixed, small
|
|
72
|
+
// title size — their height tracks their children, not their label.
|
|
73
|
+
const labelText = isContainer ? (node.label.split('\n')[0] ?? '') : node.label;
|
|
74
|
+
const preferredFontSize = Math.max(12, Math.min(48, Math.round(height * 0.34)));
|
|
75
|
+
const fit = isContainer
|
|
76
|
+
? { fontSize: 16, firstLineOffset: 0, textWidth: width }
|
|
77
|
+
: fitDiagramLabel(labelText, width, height, preferredFontSize);
|
|
78
|
+
const { fontSize } = fit;
|
|
64
79
|
|
|
65
80
|
// Label has no explicit height — that keeps its bounding box
|
|
66
81
|
// unresolvable in `layerBounds`, which in turn keeps it out of the
|
|
67
82
|
// Scene's hit-test. The label rides along visually via `layerFollows`
|
|
68
83
|
// but never wins a pointer-down (the card underneath does), so a
|
|
69
84
|
// drag-on-text moves the card as expected.
|
|
85
|
+
//
|
|
86
|
+
// Container labels anchor near the top edge — the interior belongs to
|
|
87
|
+
// the children painting over it.
|
|
70
88
|
const label: TextLayer = {
|
|
71
89
|
id: `node-label-${node.id}`,
|
|
72
90
|
type: 'text',
|
|
73
91
|
position: {
|
|
74
92
|
x: node.x + width / 2,
|
|
75
|
-
y: node.y + height / 2,
|
|
76
|
-
width,
|
|
93
|
+
y: isContainer ? node.y + fontSize + 8 : node.y + height / 2 + fit.firstLineOffset,
|
|
94
|
+
width: fit.textWidth,
|
|
77
95
|
anchor: 'center',
|
|
78
96
|
},
|
|
79
97
|
content: {
|
|
80
|
-
text:
|
|
98
|
+
text: labelText,
|
|
81
99
|
style: {
|
|
82
100
|
fontSize,
|
|
101
|
+
...(!isContainer ? { lineHeight: DIAGRAM_LABEL_LINE_HEIGHT } : {}),
|
|
83
102
|
fontWeight: 'bold',
|
|
84
|
-
color: node.stroke ?? '#1e293b',
|
|
103
|
+
color: node.stroke ?? (isContainer ? '#64748b' : '#1e293b'),
|
|
85
104
|
textAlign: 'center',
|
|
86
105
|
},
|
|
87
106
|
},
|
package/src/scene/scene.css
CHANGED
|
@@ -20,6 +20,31 @@
|
|
|
20
20
|
overflow: hidden;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-shell {
|
|
24
|
+
--squisq-surface: #0f172a;
|
|
25
|
+
--squisq-surface-hover: #1e293b;
|
|
26
|
+
--squisq-border: #475569;
|
|
27
|
+
--squisq-text: #f8fafc;
|
|
28
|
+
--squisq-text-muted: #94a3b8;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-root,
|
|
32
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-inline,
|
|
33
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-diagram-inline {
|
|
34
|
+
background: #0f172a;
|
|
35
|
+
border-color: #475569;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-layer--shape,
|
|
39
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-layer--path,
|
|
40
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-layer--text {
|
|
41
|
+
filter: invert(1) hue-rotate(180deg) saturate(0.9);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* Scene extras (diagram/drawing connectors) already use the dark-aware
|
|
45
|
+
--squisq-text tokens below. Inverting this group a second time turns the
|
|
46
|
+
intended light strokes and arrowheads nearly black against the canvas. */
|
|
47
|
+
|
|
23
48
|
.squisq-scene-viewport {
|
|
24
49
|
display: block;
|
|
25
50
|
width: 100%;
|
|
@@ -111,6 +136,18 @@
|
|
|
111
136
|
pointer-events: none;
|
|
112
137
|
}
|
|
113
138
|
|
|
139
|
+
.squisq-scene-connection-point {
|
|
140
|
+
fill: #ffffff;
|
|
141
|
+
stroke: #6366f1;
|
|
142
|
+
stroke-width: 2;
|
|
143
|
+
vector-effect: non-scaling-stroke;
|
|
144
|
+
pointer-events: none;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.squisq-scene-connection-point--active {
|
|
148
|
+
fill: #6366f1;
|
|
149
|
+
}
|
|
150
|
+
|
|
114
151
|
/* ── Toolbar ──────────────────────────────────────────────── */
|
|
115
152
|
|
|
116
153
|
.squisq-scene-toolbar {
|
|
@@ -261,6 +298,46 @@
|
|
|
261
298
|
color: #b91c1c;
|
|
262
299
|
}
|
|
263
300
|
|
|
301
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-toolbar,
|
|
302
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-block-tools,
|
|
303
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-block-actions,
|
|
304
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-block-props {
|
|
305
|
+
background: #111827;
|
|
306
|
+
border-color: #475569;
|
|
307
|
+
color: #e5e7eb;
|
|
308
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.32);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-tool,
|
|
312
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-action {
|
|
313
|
+
background: #111827;
|
|
314
|
+
color: #e5e7eb;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-tool:hover,
|
|
318
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-action:hover:not(:disabled) {
|
|
319
|
+
background: #1f2937;
|
|
320
|
+
border-color: #64748b;
|
|
321
|
+
color: #f8fafc;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-tool--active,
|
|
325
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-tool--active:hover {
|
|
326
|
+
background: #312e81;
|
|
327
|
+
border-color: #818cf8;
|
|
328
|
+
color: #f8fafc;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-action--danger {
|
|
332
|
+
color: #fca5a5;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-action--danger:hover:not(:disabled) {
|
|
336
|
+
background: #450a0a;
|
|
337
|
+
border-color: #f87171;
|
|
338
|
+
color: #fecaca;
|
|
339
|
+
}
|
|
340
|
+
|
|
264
341
|
/* Maximized overlay: a flex-filling canvas with the toolbar as a right-hand
|
|
265
342
|
column (full screen has room to reserve it). */
|
|
266
343
|
.squisq-scene-block-max {
|
|
@@ -303,8 +380,8 @@
|
|
|
303
380
|
overflow-y: auto;
|
|
304
381
|
}
|
|
305
382
|
|
|
306
|
-
/* Re-flow the (normally horizontal, right-aligned) toolbar into a
|
|
307
|
-
vertical column whose button groups wrap within the 180px
|
|
383
|
+
/* Re-flow the (normally horizontal, right-aligned) gutter toolbar into a
|
|
384
|
+
top-aligned vertical column whose button groups wrap within the 180px. */
|
|
308
385
|
.squisq-scene-side-toolbar .squisq-scene-block-toolbar {
|
|
309
386
|
flex-direction: column;
|
|
310
387
|
align-items: stretch;
|
|
@@ -326,6 +403,23 @@
|
|
|
326
403
|
right: calc(100% + 6px);
|
|
327
404
|
}
|
|
328
405
|
|
|
406
|
+
/* ── Narrow-width fallback bar ─────────────────────────────────
|
|
407
|
+
When SceneSideToolbar measures that the right gutter can't hold the 180px
|
|
408
|
+
column, it renders the same toolbar as an in-flow horizontal bar above the
|
|
409
|
+
canvas instead. Normal flow (not an overlay) so it always paints and is
|
|
410
|
+
never clipped; it just keeps the toolbar's natural horizontal layout and
|
|
411
|
+
wraps on very narrow widths. */
|
|
412
|
+
.squisq-scene-inline-toolbar {
|
|
413
|
+
display: flex;
|
|
414
|
+
margin-bottom: 8px;
|
|
415
|
+
}
|
|
416
|
+
.squisq-scene-inline-toolbar .squisq-scene-block-toolbar {
|
|
417
|
+
flex-wrap: wrap;
|
|
418
|
+
justify-content: flex-start;
|
|
419
|
+
width: 100%;
|
|
420
|
+
margin: 0;
|
|
421
|
+
}
|
|
422
|
+
|
|
329
423
|
/* ── Maximize button ──────────────────────────────────────── */
|
|
330
424
|
|
|
331
425
|
.squisq-scene-maximize-btn {
|
|
@@ -354,6 +448,19 @@
|
|
|
354
448
|
color: #0f172a;
|
|
355
449
|
}
|
|
356
450
|
|
|
451
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-maximize-btn {
|
|
452
|
+
background: #111827;
|
|
453
|
+
border-color: #475569;
|
|
454
|
+
color: #e5e7eb;
|
|
455
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.32);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-maximize-btn:hover {
|
|
459
|
+
background: #1f2937;
|
|
460
|
+
border-color: #64748b;
|
|
461
|
+
color: #f8fafc;
|
|
462
|
+
}
|
|
463
|
+
|
|
357
464
|
/* ── Diagram node card (used by nodeCard.tsx) ─────────────── */
|
|
358
465
|
|
|
359
466
|
.squisq-scene-node-card {
|
|
@@ -502,6 +609,35 @@
|
|
|
502
609
|
font-weight: 700;
|
|
503
610
|
}
|
|
504
611
|
|
|
612
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-shape-palette {
|
|
613
|
+
background: #111827;
|
|
614
|
+
color: #e5e7eb;
|
|
615
|
+
border-color: #475569;
|
|
616
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.42);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-shape-palette-search,
|
|
620
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-prop-select {
|
|
621
|
+
background: #0f172a;
|
|
622
|
+
color: #e5e7eb;
|
|
623
|
+
border-color: #475569;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-shape-palette-item:hover,
|
|
627
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-prop-clear:hover {
|
|
628
|
+
background: #1f2937;
|
|
629
|
+
border-color: #64748b;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-shape-thumb,
|
|
633
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-shape-thumb-line {
|
|
634
|
+
stroke: #e5e7eb;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-shape-thumb-text {
|
|
638
|
+
fill: #e5e7eb;
|
|
639
|
+
}
|
|
640
|
+
|
|
505
641
|
/* Per-selection style controls, inline in the contextual toolbar (Fill /
|
|
506
642
|
Stroke / Color swatches + connector dropdowns) — styled as a pill that
|
|
507
643
|
matches the tools/actions groups. */
|
|
@@ -566,6 +702,13 @@
|
|
|
566
702
|
background: #f1f5f9;
|
|
567
703
|
}
|
|
568
704
|
|
|
705
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-prop-color,
|
|
706
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-prop-clear {
|
|
707
|
+
background: #0f172a;
|
|
708
|
+
color: #e5e7eb;
|
|
709
|
+
border-color: #475569;
|
|
710
|
+
}
|
|
711
|
+
|
|
569
712
|
/* ── Inline widget host (mirrors squisq-diagram-inline) ──── */
|
|
570
713
|
|
|
571
714
|
.squisq-scene-widget-host {
|
|
@@ -19,7 +19,6 @@ import { layerBounds } from '../hooks/useSceneHitTest';
|
|
|
19
19
|
import type { SceneTransform } from '../hooks/useScenePanZoom';
|
|
20
20
|
import type { SceneTextEditConfig } from './sceneTextConfig';
|
|
21
21
|
import { buildSceneTextExtensions } from './sceneTiptap';
|
|
22
|
-
import { sceneTextChannel } from './sceneTextChannel';
|
|
23
22
|
|
|
24
23
|
interface SceneTextOverlayProps {
|
|
25
24
|
config: SceneTextEditConfig;
|
|
@@ -90,12 +89,13 @@ export function SceneTextOverlay({
|
|
|
90
89
|
|
|
91
90
|
// Publish to the toolbar channel while mounted; clear on unmount.
|
|
92
91
|
useEffect(() => {
|
|
93
|
-
if (!editor) return;
|
|
94
|
-
|
|
92
|
+
if (!editor || !config.channel) return;
|
|
93
|
+
const channel = config.channel;
|
|
94
|
+
channel.set({ editor, level: config.level });
|
|
95
95
|
return () => {
|
|
96
|
-
if (
|
|
96
|
+
if (channel.get()?.editor === editor) channel.set(null);
|
|
97
97
|
};
|
|
98
|
-
}, [editor, config.level]);
|
|
98
|
+
}, [editor, config.level, config.channel]);
|
|
99
99
|
|
|
100
100
|
// Commit when focus leaves the editor — unless it went to the toolbar
|
|
101
101
|
// (the user clicking Bold) or stayed within the overlay.
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* sceneTextChannel —
|
|
2
|
+
* sceneTextChannel — an editor-owned channel that bridges the canvas's inline
|
|
3
3
|
* text editor (which renders in a **detached React root** created by the
|
|
4
4
|
* Diagram/SceneBlock ProseMirror extensions, outside `<EditorProvider>`)
|
|
5
5
|
* to the provider, so the top formatting toolbar can target it.
|
|
6
6
|
*
|
|
7
7
|
* The active textbox's `SceneTextOverlay` publishes its Tiptap editor here
|
|
8
8
|
* on focus and clears it on blur/unmount; `EditorProvider` subscribes and
|
|
9
|
-
* mirrors the handle into `activeSceneText`.
|
|
10
|
-
*
|
|
9
|
+
* mirrors the handle into `activeSceneText`. Each EditorProvider creates its
|
|
10
|
+
* own channel, preventing focus in one editor from mutating another editor.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type { Editor as TiptapEditor } from '@tiptap/core';
|
|
@@ -20,21 +20,27 @@ export interface SceneTextHandle {
|
|
|
20
20
|
|
|
21
21
|
type Listener = (handle: SceneTextHandle | null) => void;
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
export interface SceneTextChannel {
|
|
24
|
+
set(handle: SceneTextHandle | null): void;
|
|
25
|
+
get(): SceneTextHandle | null;
|
|
26
|
+
subscribe(listener: Listener): () => void;
|
|
27
|
+
}
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
29
|
+
/** Create a channel owned by one EditorProvider; no cross-editor singleton. */
|
|
30
|
+
export function createSceneTextChannel(): SceneTextChannel {
|
|
31
|
+
let current: SceneTextHandle | null = null;
|
|
32
|
+
const listeners = new Set<Listener>();
|
|
33
|
+
return {
|
|
34
|
+
set(handle) {
|
|
35
|
+
current = handle;
|
|
36
|
+
for (const listener of listeners) listener(handle);
|
|
37
|
+
},
|
|
38
|
+
get() {
|
|
39
|
+
return current;
|
|
40
|
+
},
|
|
41
|
+
subscribe(listener) {
|
|
42
|
+
listeners.add(listener);
|
|
43
|
+
return () => listeners.delete(listener);
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -5,9 +5,13 @@
|
|
|
5
5
|
* the host owns persistence (markdown heading vs. layout JSON blob).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import type { SceneTextChannel } from './sceneTextChannel';
|
|
9
|
+
|
|
8
10
|
export type SceneTextLevel = 'inline' | 'block' | 'rich';
|
|
9
11
|
|
|
10
12
|
export interface SceneTextEditConfig {
|
|
13
|
+
/** Per-editor bridge to the formatting toolbar. */
|
|
14
|
+
channel?: SceneTextChannel;
|
|
11
15
|
/**
|
|
12
16
|
* Map a hit layer id (what the pointer landed on) to the layer id whose
|
|
13
17
|
* on-screen box the editor should cover, or `null` when that layer isn't
|