From e1b7efbc808202dccd7efbe6dd7939308e913585 Mon Sep 17 00:00:00 2001 From: Guilhem Date: Tue, 11 Nov 2025 19:54:46 +0100 Subject: [PATCH] Handle z-index for stacked group notes --- .../lib/components/graph/FlowGraphV2.svelte | 8 ++- .../lib/components/graph/groupNoteUtils.ts | 58 ++++++++++++++++--- .../components/graph/noteManager.svelte.ts | 26 +++++++-- 3 files changed, 78 insertions(+), 14 deletions(-) diff --git a/frontend/src/lib/components/graph/FlowGraphV2.svelte b/frontend/src/lib/components/graph/FlowGraphV2.svelte index c205fceadd..16788a4054 100644 --- a/frontend/src/lib/components/graph/FlowGraphV2.svelte +++ b/frontend/src/lib/components/graph/FlowGraphV2.svelte @@ -524,7 +524,12 @@ (noteId: string, height: number) => { noteTextHeights[noteId] = height }, - editMode + editMode, + Object.values(graph.nodes).map((n) => ({ + id: n.id, + parentIds: n.parentIds, + offset: n.data.offset ?? 0 + })) ) ] edges = [ @@ -764,6 +769,7 @@ preventScrolling={scroll} zoomOnDoubleClick={false} elementsSelectable={false} + elevateNodesOnSelect={false} {proOptions} nodesDraggable={false} --background-color={false} diff --git a/frontend/src/lib/components/graph/groupNoteUtils.ts b/frontend/src/lib/components/graph/groupNoteUtils.ts index 6be396b463..20502e3095 100644 --- a/frontend/src/lib/components/graph/groupNoteUtils.ts +++ b/frontend/src/lib/components/graph/groupNoteUtils.ts @@ -156,8 +156,7 @@ export function buildNodeSpacingMap( * Creates a stable hash of the noteTextHeights object for cache comparison */ export function hashNoteTextHeights(noteTextHeights: Record): string { - const entries = Object.entries(noteTextHeights) - .sort(([a], [b]) => a.localeCompare(b)) // Sort for stable hash + const entries = Object.entries(noteTextHeights).sort(([a], [b]) => a.localeCompare(b)) // Sort for stable hash return JSON.stringify(entries) } @@ -165,10 +164,13 @@ export function hashNoteTextHeights(noteTextHeights: Record): st /** * Extracts note state signature for layout cache comparison */ -export function getNoteStateSignature(groupNotes: FlowNote[], noteTextHeights: Record) { +export function getNoteStateSignature( + groupNotes: FlowNote[], + noteTextHeights: Record +) { return { notesCount: groupNotes.length, - noteIds: groupNotes.map(n => n.id).sort(), + noteIds: groupNotes.map((n) => n.id).sort(), textHeightHash: hashNoteTextHeights(noteTextHeights) } } @@ -180,11 +182,11 @@ export function getNoteStateSignature(groupNotes: FlowNote[], noteTextHeights: R export function getLayoutSignature(notes: FlowNote[]) { return { notesCount: notes.length, - noteIds: notes.map(n => n.id).sort(), + noteIds: notes.map((n) => n.id).sort(), // Group memberships affect layout spacing groupMemberships: notes - .filter(note => note.type === 'group') - .map(note => ({ + .filter((note) => note.type === 'group') + .map((note) => ({ id: note.id, containedIds: note.contained_node_ids?.slice().sort() || [] })) @@ -198,7 +200,7 @@ export function getLayoutSignature(notes: FlowNote[]) { */ export function getPropertySignature(notes: FlowNote[]) { return notes - .map(note => ({ + .map((note) => ({ id: note.id, text: note.text, color: note.color, @@ -208,3 +210,43 @@ export function getPropertySignature(notes: FlowNote[]) { })) .sort((a, b) => a.id.localeCompare(b.id)) } + +/** + * Calculates z-index values for all notes in a single graph traversal + * Group notes are ordered by their topmost node's hierarchy position + * Free notes get undefined z-index to use SvelteFlow's native behavior + */ +export function calculateAllNoteZIndexes( + notes: FlowNote[], + nodes: NodeDep[] +): Record { + const zIndexMap: Record = {} + + // Create a mapping from node ID to its hierarchy position (topological order) + const nodeHierarchyMap: Record = {} + nodes.forEach((node, index) => { + nodeHierarchyMap[node.id] = index + }) + + // Process each note + for (const note of notes) { + if (note.type === 'free') { + // Free notes use SvelteFlow's native z-index behavior (last selected on top) + zIndexMap[note.id] = undefined + } else if (note.type === 'group') { + // Group notes get z-index based on topmost contained node's hierarchy + const topmostNode = findTopmostNodeInGroup(note, nodes) + if (topmostNode) { + const hierarchyPosition = nodeHierarchyMap[topmostNode.id] ?? 0 + // Higher hierarchy position = lower z-index (appears behind) + // Use negative values starting from -2000 to stay below other elements + zIndexMap[note.id] = hierarchyPosition - 2000 + } else { + // Fallback for group notes without valid contained nodes + zIndexMap[note.id] = -2000 + } + } + } + + return zIndexMap +} diff --git a/frontend/src/lib/components/graph/noteManager.svelte.ts b/frontend/src/lib/components/graph/noteManager.svelte.ts index 34d088ebbd..f6964abe3b 100644 --- a/frontend/src/lib/components/graph/noteManager.svelte.ts +++ b/frontend/src/lib/components/graph/noteManager.svelte.ts @@ -1,7 +1,11 @@ import type { FlowNote } from '$lib/gen' import type { Node } from '@xyflow/svelte' import { calculateNodesBounds } from './util' -import { getLayoutSignature, getPropertySignature } from './groupNoteUtils' +import { + getLayoutSignature, + getPropertySignature, + calculateAllNoteZIndexes +} from './groupNoteUtils' import { deepEqual } from 'fast-equals' export type NodePosition = { @@ -191,7 +195,8 @@ export class NoteManager { currentNodes: Node[], textHeights: Record, onTextHeightChange: (noteId: string, height: number) => void, - editMode: boolean = false + editMode: boolean = false, + zIndex?: number ): Node { const isGroupNote = note.type === 'group' @@ -208,7 +213,7 @@ export class NoteManager { style: `width: ${size.width}px; height: ${size.height}px;`, width: size.width, height: size.height, - zIndex: -2000, + zIndex: zIndex ?? -2000, // Use provided zIndex or fallback draggable: isGroupNote ? false : editMode && !note.locked, selectable: true } @@ -222,10 +227,21 @@ export class NoteManager { currentNodes: Node[], textHeights: Record, onTextHeightChange: (noteId: string, height: number) => void, - editMode: boolean = false + editMode: boolean = false, + flowNodes?: { id: string; parentIds?: string[]; offset?: number }[] ): Node[] { + // Calculate z-indexes for all notes in a single traversal + const zIndexMap = flowNodes ? calculateAllNoteZIndexes(notes, flowNodes) : {} + return notes.map((note) => - this.convertNoteToNode(note, currentNodes, textHeights, onTextHeightChange, editMode) + this.convertNoteToNode( + note, + currentNodes, + textHeights, + onTextHeightChange, + editMode, + zIndexMap[note.id] + ) ) }