refactor: hide group header card when expanded, keep only tab and ellipsis menu

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Guilhem
2026-03-04 16:04:44 +00:00
co-authored by Claude Opus 4.6
parent 0b40175339
commit 95de798414
3 changed files with 36 additions and 67 deletions
@@ -718,12 +718,10 @@
// Apply group label spacing (pushes nodes down like group notes do)
const groups = groupEditorContext?.groupEditor.getGroups() ?? []
const noteHeights = showNotes ? (groupEditorContext?.groupEditor.getNoteHeights() ?? {}) : {}
if (groups.length > 0) {
const groupPositions = computeGroupSpacing(
groups,
finalNodes.map((n) => ({ id: n.id, position: n.position })),
noteHeights
finalNodes.map((n) => ({ id: n.id, position: n.position }))
)
finalNodes = finalNodes.map((n) => ({
...n,
@@ -734,10 +732,11 @@
// Apply collapsed group note spacing
const collapsedGroups = groupEditorContext?.groupEditor.getCollapsedGroups() ?? []
if (collapsedGroups.length > 0) {
const collapsedNoteHeights = showNotes ? (groupEditorContext?.groupEditor.getNoteHeights() ?? {}) : {}
const collapsedPositions = computeCollapsedGroupNoteSpacing(
collapsedGroups,
finalNodes.map((n) => ({ id: n.id, position: n.position })),
noteHeights
collapsedNoteHeights
)
finalNodes = finalNodes.map((n) => ({
...n,
@@ -1120,7 +1119,6 @@
{hoveredNodeId}
allNodes={nodesWithOffset as (Node & { type: string })[]}
{editMode}
{showNotes}
/>
<!-- SelectionTool for handling selection changes and filtering -->
@@ -1,11 +1,9 @@
<script lang="ts">
import { ViewportPortal, type Node } from '@xyflow/svelte'
import { calculateNodesBoundsWithOffset } from './util'
import { getGroupEditorContext, GROUP_HEADER_HEIGHT, type FlowGroup } from './groupEditor.svelte'
import { getGroupEditorContext, type FlowGroup } from './groupEditor.svelte'
import { NoteColor } from './noteColors'
import GroupNodeCard from './GroupNodeCard.svelte'
import GroupActionBar from './GroupActionBar.svelte'
import type { FlowModule } from '$lib/gen'
import StepCountTab from './StepCountTab.svelte'
import type { CollapsedSubflowN } from './graphBuilder.svelte'
@@ -13,10 +11,9 @@
hoveredNodeId: string | null
allNodes: (Node & { type: string })[]
editMode: boolean
showNotes: boolean
}
let { hoveredNodeId, allNodes, editMode, showNotes }: Props = $props()
let { hoveredNodeId, allNodes, editMode }: Props = $props()
const groupEditorContext = getGroupEditorContext()
@@ -54,19 +51,16 @@
// All groups for always-visible labels
let allGroups = $derived(groupEditorContext?.groupEditor.getGroups() ?? [])
// Compute bounds for each group (outline extends up to include header)
// Compute bounds for each group (no header card when expanded)
function computeGroupBounds(group: FlowGroup) {
if (group.module_ids.length === 0) return null
const { minX, minY, maxX, maxY } = calculateNodesBoundsWithOffset(group.module_ids, allNodes)
const padding = 16
const headerGap = 24 // space between header bottom and first node
const noteHeight = groupEditorContext?.groupEditor.getNoteHeights()[group.id] ?? 0
const topPadding = headerGap + GROUP_HEADER_HEIGHT + noteHeight
return {
x: minX - padding,
y: minY - topPadding,
y: minY - padding,
width: maxX - minX + 2 * padding,
height: maxY - minY + padding + topPadding
height: maxY - minY + 2 * padding
}
}
@@ -123,15 +117,6 @@
groupEditorContext?.groupEditor.toggleRuntimeCollapse(groupId)
}
function getGroupModules(group: FlowGroup): FlowModule[] {
return group.module_ids
.map(
(id) =>
allNodes.find((n) => n.id === id)?.data?.module as FlowModule | undefined
)
.filter((m): m is FlowModule => m != null)
}
// Expanded subflows — derive from allNodes
let expandedSubflowNodes = $derived(
allNodes.filter(
@@ -176,12 +161,12 @@
></div>
</ViewportPortal>
<!-- Header card (above nodes) -->
<!-- StepCountTab + ellipsis menu (above nodes) -->
<ViewportPortal target="front">
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="absolute"
style="pointer-events: auto; transform: translate({bounds.x + 16}px, {bounds.y}px);"
style="pointer-events: auto; transform: translate({bounds.x + 16}px, {bounds.y - 24}px);"
style:z-index="4"
onpointerenter={() => {
actionBarHovered = true
@@ -195,41 +180,30 @@
actionBarHovered = false
}}
>
<StepCountTab
stepCount={group.module_ids.length}
color={group.color}
collapsed={false}
onExpand={() => toggleCollapse(group.id)}
/>
<GroupNodeCard
summary={group.summary}
stepCount={group.module_ids.length}
color={group.color}
note={group.note}
showNote={showNotes && group.note != null}
{editMode}
modules={getGroupModules(group)}
onExpand={() => toggleCollapse(group.id)}
onSummaryUpdate={(text) => groupEditorContext?.groupEditor.updateSummary(group.id, text)}
onNoteUpdate={(text) => groupEditorContext?.groupEditor.updateNote(group.id, text)}
onHeightChange={(h) => groupEditorContext?.groupEditor.setNoteHeight(group.id, h)}
/>
{#if editMode && visibleGroup?.id === group.id}
<GroupActionBar
note={group.note}
<div class="relative flex items-center">
<StepCountTab
stepCount={group.module_ids.length}
color={group.color}
collapsedByDefault={group.collapsed_by_default ?? false}
bind:menuOpen
onAddNote={() => groupEditorContext?.groupEditor.addNote(group.id)}
onRemoveNote={() => groupEditorContext?.groupEditor.removeNote(group.id)}
onUpdateColor={(c) => groupEditorContext?.groupEditor.updateColor(group.id, c)}
onUpdateCollapsedDefault={(v) => groupEditorContext?.groupEditor.updateCollapsedDefault(group.id, v)}
onDeleteGroup={() => {
groupEditorContext?.groupEditor.deleteGroup(group.id)
visibleGroup = undefined
}}
collapsed={false}
onExpand={() => toggleCollapse(group.id)}
/>
{/if}
{#if editMode && visibleGroup?.id === group.id}
<GroupActionBar
note={group.note}
color={group.color}
collapsedByDefault={group.collapsed_by_default ?? false}
bind:menuOpen
onAddNote={() => groupEditorContext?.groupEditor.addNote(group.id)}
onRemoveNote={() => groupEditorContext?.groupEditor.removeNote(group.id)}
onUpdateColor={(c) => groupEditorContext?.groupEditor.updateColor(group.id, c)}
onUpdateCollapsedDefault={(v) => groupEditorContext?.groupEditor.updateCollapsedDefault(group.id, v)}
onDeleteGroup={() => {
groupEditorContext?.groupEditor.deleteGroup(group.id)
visibleGroup = undefined
}}
/>
{/if}
</div>
</div>
</ViewportPortal>
{/if}
@@ -258,8 +258,8 @@ export function getGroupEditorContext(): GroupEditorContext | undefined {
return getContext<GroupEditorContext | undefined>(CONTEXT_KEY)
}
/** Extra vertical space pushed above the topmost node of each group for the header card */
export const GROUP_HEADER_HEIGHT = 40
/** Extra vertical space pushed above the topmost node of each group for the tab */
export const GROUP_HEADER_HEIGHT = 24
const GROUP_TOP_MARGIN = 24
@@ -314,8 +314,7 @@ export function computeCollapsedGroupNoteSpacing(
*/
export function computeGroupSpacing(
groups: FlowGroup[],
nodes: Array<{ id: string; position: { x: number; y: number } }>,
groupNoteHeights?: Record<string, number>
nodes: Array<{ id: string; position: { x: number; y: number } }>
): Record<string, { x: number; y: number }> {
if (groups.length === 0) {
return Object.fromEntries(nodes.map((n) => [n.id, { ...n.position }]))
@@ -336,9 +335,7 @@ export function computeGroupSpacing(
}
if (topY < Infinity) {
const noteHeight = groupNoteHeights?.[group.id] ?? 0
const totalHeader = GROUP_HEADER_HEIGHT + noteHeight
yPosMap[topY] = Math.max(yPosMap[topY] || 0, totalHeader) + GROUP_TOP_MARGIN
yPosMap[topY] = Math.max(yPosMap[topY] || 0, GROUP_HEADER_HEIGHT) + GROUP_TOP_MARGIN
}
}