@@ -11,7 +11,10 @@ import { MultiNodePanel } from "./MultiNodePanel";
1111import { EditorPanel } from "./EditorPanel" ;
1212import { EdgePanel } from "./EdgePanel" ;
1313import { SettingsPanel } from "./SettingsPanel" ;
14+ import { LayersPanel } from "./LayersPanel" ;
1415import { NodeData } from "./types" ;
16+ import { getNodesAtPosition } from "./zIndexHelper" ;
17+ import { getReadableTextColor } from "./colorHelper" ;
1518
1619const nodeTypes = {
1720 topic : TopicNode ,
@@ -39,6 +42,7 @@ export const EditorCanvas = memo(() => {
3942 const onConnect = useEditorStore ( state => state . onConnect ) ;
4043 const setSelectedNodeIds = useEditorStore ( state => state . setSelectedNodeIds ) ;
4144 const setSelectedNodeId = useEditorStore ( state => state . setSelectedNodeId ) ;
45+ const selectNode = useEditorStore ( state => state . selectNode ) ;
4246 const setSelectedEdge = useEditorStore ( state => state . setSelectedEdge ) ;
4347 const setDrawerOpen = useEditorStore ( state => state . setDrawerOpen ) ;
4448 const setEdgeDrawerOpen = useEditorStore ( state => state . setEdgeDrawerOpen ) ;
@@ -71,20 +75,42 @@ export const EditorCanvas = memo(() => {
7175 canRedo : state . futureStates . length > 0 ,
7276 } ) ) ;
7377
74- const handleNodeClick = useCallback ( ( _ : any , node : Node < NodeData > ) => {
78+ /**
79+ * Picks the next node underneath the cursor, so nodes covered by another one
80+ * can still be reached. Returns the clicked node when there is nothing to
81+ * cycle through.
82+ */
83+ const cycleStackedNode = useCallback ( ( event : React . MouseEvent , clickedNodeId ?: string ) => {
84+ const { nodes : currentNodes , selectedNodeId : currentSelectedNodeId } = useEditorStore . getState ( ) ;
85+ const point = screenToFlowPosition ( { x : event . clientX , y : event . clientY } ) ;
86+ const stack = getNodesAtPosition ( currentNodes , point ) ;
87+
88+ if ( stack . length === 0 ) return null ;
89+
90+ const activeId = currentSelectedNodeId ?? clickedNodeId ;
91+ const activeIndex = stack . findIndex ( n => n . id === activeId ) ;
92+ return stack [ ( activeIndex + 1 ) % stack . length ] ;
93+ } , [ screenToFlowPosition ] ) ;
94+
95+ const handleNodeClick = useCallback ( ( event : React . MouseEvent , node : Node < NodeData > ) => {
7596 // Execute picker callback when in picker mode
7697 if ( pickerMode ) {
7798 const executePickerCallback = useEditorStore . getState ( ) . executePickerCallback ;
7899 executePickerCallback ( node . id ) ;
79100 return ;
80101 }
81-
82- setSelectedNodeId ( node . id ) ;
83- setDrawerOpen ( true ) ;
84- setSelectedEdge ( null ) ;
85- setEdgeDrawerOpen ( false ) ;
86- setSettingsDrawerOpen ( false ) ;
87- } , [ setSelectedNodeId , setDrawerOpen , setSelectedEdge , setEdgeDrawerOpen , setSettingsDrawerOpen , pickerMode ] ) ;
102+
103+ // Ctrl/Cmd-click adds to the selection and Shift-drag draws a selection
104+ // box. Both are handled by React Flow, and taking over here would clear
105+ // the other selected nodes.
106+ if ( event . ctrlKey || event . metaKey || event . shiftKey ) {
107+ return ;
108+ }
109+
110+ const target = event . altKey ? cycleStackedNode ( event , node . id ) ?? node : node ;
111+
112+ selectNode ( target . id , true ) ;
113+ } , [ selectNode , pickerMode , cycleStackedNode ] ) ;
88114
89115 const handleEdgeClick = useCallback ( ( _ : any , edge : Edge ) => {
90116 setSelectedEdge ( edge ) ;
@@ -99,11 +125,16 @@ export const EditorCanvas = memo(() => {
99125 // Only select nodes, not edges (as per requirement #6)
100126 setSelectedNodeIds ( selectedNodes . map ( n => n . id ) ) ;
101127
102- // Close the node panel if no nodes are selected and it's currently open
103- if ( selectedNodes . length === 0 ) {
104- setDrawerOpen ( false ) ;
105- setSelectedNodeId ( null ) ;
106- }
128+ if ( selectedNodes . length > 0 ) return ;
129+
130+ // Locked nodes are never selected on the canvas but can still be edited
131+ // through the panel, so keep the panel open for them.
132+ const state = useEditorStore . getState ( ) ;
133+ const activeNode = state . nodes . find ( n => n . id === state . selectedNodeId ) ;
134+ if ( activeNode ?. data ?. locked ) return ;
135+
136+ setDrawerOpen ( false ) ;
137+ setSelectedNodeId ( null ) ;
107138 } ,
108139 [ setSelectedNodeIds , setDrawerOpen , setSelectedNodeId ]
109140 ) ;
@@ -115,13 +146,23 @@ export const EditorCanvas = memo(() => {
115146 } , [ screenToFlowPosition , setLastMousePosition ] ) ;
116147
117148 // Close panels when clicking on empty canvas
118- const handlePaneClick = useCallback ( ( ) => {
149+ const handlePaneClick = useCallback ( ( event : React . MouseEvent ) => {
150+ // Locked nodes do not receive clicks, so alt-clicking "through" them lands
151+ // on the pane. Cycle from here as well to keep them reachable.
152+ if ( event . altKey && ! pickerMode ) {
153+ const target = cycleStackedNode ( event ) ;
154+ if ( target ) {
155+ selectNode ( target . id , true ) ;
156+ return ;
157+ }
158+ }
159+
119160 setDrawerOpen ( false ) ;
120161 setSelectedNodeId ( null ) ;
121162 setEdgeDrawerOpen ( false ) ;
122163 setSelectedEdge ( null ) ;
123164 setSettingsDrawerOpen ( false ) ;
124- } , [ setDrawerOpen , setSelectedNodeId , setEdgeDrawerOpen , setSelectedEdge , setSettingsDrawerOpen ] ) ;
165+ } , [ setDrawerOpen , setSelectedNodeId , setEdgeDrawerOpen , setSelectedEdge , setSettingsDrawerOpen , cycleStackedNode , selectNode , pickerMode ] ) ;
125166
126167 const defaultEdgeOptions = {
127168 animated : false ,
@@ -139,6 +180,8 @@ export const EditorCanvas = memo(() => {
139180 style = { {
140181 backgroundColor : settings ?. background ?. color || "#ffffff" ,
141182 cursor : pickerMode ? "crosshair" : "default" ,
183+ // Default text colour for text nodes that have none of their own.
184+ [ "--learningmap-text-default" as any ] : getReadableTextColor ( settings ?. background ?. color ) ,
142185 } }
143186 onMouseMove = { handleMouseMove }
144187 >
@@ -159,7 +202,9 @@ export const EditorCanvas = memo(() => {
159202 proOptions = { { hideAttribution : true } }
160203 defaultEdgeOptions = { defaultEdgeOptions }
161204 nodesDraggable = { ! pickerMode }
162- elevateNodesOnSelect = { false }
205+ // Selected nodes are lifted above the stack so their resize handles
206+ // stay reachable even when the node itself sits in the background.
207+ elevateNodesOnSelect = { true }
163208 nodesConnectable = { ! pickerMode }
164209 selectNodesOnDrag = { false }
165210 elementsSelectable = { ! pickerMode }
@@ -178,6 +223,7 @@ export const EditorCanvas = memo(() => {
178223 </ ControlButton >
179224 </ Controls >
180225 { selectedNodeIds . length > 1 && < MultiNodePanel /> }
226+ < LayersPanel />
181227 < EditorPanel />
182228 < EdgePanel />
183229 < SettingsPanel />
0 commit comments