diff --git a/frontend/src/lib/components/graph/GroupHeader.svelte b/frontend/src/lib/components/graph/GroupHeader.svelte
new file mode 100644
index 0000000000..a5eaf22944
--- /dev/null
+++ b/frontend/src/lib/components/graph/GroupHeader.svelte
@@ -0,0 +1,104 @@
+
+
+
+
+
{}))}
+ title={collapsed ? 'Expand group' : 'Collapse group'}
+>
+
+
+
+
+ {#if editingSummary}
+
+ {:else}
+ {})) : undefined}
+ >{summary || 'Group'}
+ {/if}
+
+
diff --git a/frontend/src/lib/components/graph/GroupModuleIcons.svelte b/frontend/src/lib/components/graph/GroupModuleIcons.svelte
index 4700fea5f3..1cfc233295 100644
--- a/frontend/src/lib/components/graph/GroupModuleIcons.svelte
+++ b/frontend/src/lib/components/graph/GroupModuleIcons.svelte
@@ -40,12 +40,10 @@
})
-
+
{#each displayModules as mod, i (mod.id)}
0}
+ class="w-6 h-6 rounded-full overflow-hidden bg-surface-tertiary flex items-center justify-center shrink-0 shadow-sm"
>
diff --git a/frontend/src/lib/components/graph/GroupOverlay.svelte b/frontend/src/lib/components/graph/GroupOverlay.svelte
index fedaac3848..869b28d69d 100644
--- a/frontend/src/lib/components/graph/GroupOverlay.svelte
+++ b/frontend/src/lib/components/graph/GroupOverlay.svelte
@@ -4,9 +4,8 @@
import { getGroupEditorContext, type FlowGroup } from './groupEditor.svelte'
import { NoteColor, NOTE_COLORS } from './noteColors'
import GroupActionBar from './GroupActionBar.svelte'
- import StepCountTab from './StepCountTab.svelte'
+ import GroupHeader from './GroupHeader.svelte'
import type { CollapsedSubflowN } from './graphBuilder.svelte'
- import { stopPropagation, preventDefault } from 'svelte/legacy'
interface Props {
hoveredNodeId: string | null
@@ -21,35 +20,6 @@
// Menu open state
let menuOpen = $state(false)
- // Inline summary editing
- let editingGroupId = $state
(null)
- let summaryInput = $state('')
- let summaryInputEl = $state(undefined)
-
- function startEditingSummary(groupId: string, current: string) {
- if (!editMode) return
- editingGroupId = groupId
- summaryInput = current
- requestAnimationFrame(() => {
- summaryInputEl?.focus()
- summaryInputEl?.select()
- })
- }
-
- function saveSummary(groupId: string) {
- editingGroupId = null
- const trimmed = summaryInput.trim()
- groupEditorContext?.groupEditor.updateSummary(groupId, trimmed)
- }
-
- function handleSummaryKeydown(event: KeyboardEvent, groupId: string) {
- if (event.key === 'Enter') {
- saveSummary(groupId)
- } else if (event.key === 'Escape') {
- editingGroupId = null
- }
- }
-
// Action bar hover state to prevent flicker
let actionBarHovered = $state(false)
@@ -86,63 +56,27 @@
if (group.module_ids.length === 0) return null
const { minX, minY, maxX, maxY } = calculateNodesBoundsWithOffset(group.module_ids, allNodes)
const padding = 16
- const topPadding = 28
+ const topPadding = 34
+ const headerHeight = 22
+ const halfHeader = headerHeight / 2
return {
x: minX - padding,
- y: minY - topPadding,
+ y: minY - topPadding + halfHeader,
width: maxX - minX + 2 * padding,
- height: maxY - minY + topPadding + padding
+ height: maxY - minY + topPadding - halfHeader + padding,
+ headerY: minY - topPadding
}
}
- // Outline color mapping — default uses /60 opacity, hover uses full opacity
- const GROUP_OUTLINE_COLORS: Record = {
- [NoteColor.YELLOW]: 'outline-yellow-400/60 dark:outline-yellow-600/60',
- [NoteColor.BLUE]: 'outline-blue-400/60 dark:outline-blue-600/60',
- [NoteColor.GREEN]: 'outline-green-400/60 dark:outline-green-600/60',
- [NoteColor.PURPLE]: 'outline-purple-400/60 dark:outline-purple-600/60',
- [NoteColor.PINK]: 'outline-pink-400/60 dark:outline-pink-600/60',
- [NoteColor.ORANGE]: 'outline-orange-400/60 dark:outline-orange-600/60',
- [NoteColor.RED]: 'outline-red-400/60 dark:outline-red-600/60',
- [NoteColor.CYAN]: 'outline-cyan-400/60 dark:outline-cyan-600/60',
- [NoteColor.LIME]: 'outline-lime-400/60 dark:outline-lime-600/60',
- [NoteColor.GRAY]: 'outline-gray-400/60 dark:outline-gray-600/60'
- }
-
- const GROUP_OUTLINE_COLORS_HOVER: Record = {
- [NoteColor.YELLOW]: 'outline-yellow-400 dark:outline-yellow-600',
- [NoteColor.BLUE]: 'outline-blue-400 dark:outline-blue-600',
- [NoteColor.GREEN]: 'outline-green-400 dark:outline-green-600',
- [NoteColor.PURPLE]: 'outline-purple-400 dark:outline-purple-600',
- [NoteColor.PINK]: 'outline-pink-400 dark:outline-pink-600',
- [NoteColor.ORANGE]: 'outline-orange-400 dark:outline-orange-600',
- [NoteColor.RED]: 'outline-red-400 dark:outline-red-600',
- [NoteColor.CYAN]: 'outline-cyan-400 dark:outline-cyan-600',
- [NoteColor.LIME]: 'outline-lime-400 dark:outline-lime-600',
- [NoteColor.GRAY]: 'outline-gray-400 dark:outline-gray-600'
- }
-
function getOutlineColorClass(color?: string, hovered?: boolean): string {
- const map = hovered ? GROUP_OUTLINE_COLORS_HOVER : GROUP_OUTLINE_COLORS
- return map[(color as NoteColor) ?? NoteColor.BLUE] ?? map[NoteColor.BLUE]
- }
-
- const GROUP_BG_COLORS: Record = {
- [NoteColor.YELLOW]: 'bg-yellow-400/5 dark:bg-yellow-600/5',
- [NoteColor.BLUE]: 'bg-blue-400/5 dark:bg-blue-600/5',
- [NoteColor.GREEN]: 'bg-green-400/5 dark:bg-green-600/5',
- [NoteColor.PURPLE]: 'bg-purple-400/5 dark:bg-purple-600/5',
- [NoteColor.PINK]: 'bg-pink-400/5 dark:bg-pink-600/5',
- [NoteColor.ORANGE]: 'bg-orange-400/5 dark:bg-orange-600/5',
- [NoteColor.RED]: 'bg-red-400/5 dark:bg-red-600/5',
- [NoteColor.CYAN]: 'bg-cyan-400/5 dark:bg-cyan-600/5',
- [NoteColor.LIME]: 'bg-lime-400/5 dark:bg-lime-600/5',
- [NoteColor.GRAY]: 'bg-gray-400/5 dark:bg-gray-600/5'
+ const config = NOTE_COLORS[(color as NoteColor) ?? NoteColor.BLUE] ?? NOTE_COLORS[NoteColor.BLUE]
+ return hovered ? config.outline : config.outlineHover
}
function getBgColorClass(color?: string): string {
return (
- GROUP_BG_COLORS[(color as NoteColor) ?? NoteColor.BLUE] ?? GROUP_BG_COLORS[NoteColor.BLUE]
+ NOTE_COLORS[(color as NoteColor) ?? NoteColor.BLUE]?.backgroundLight ??
+ NOTE_COLORS[NoteColor.BLUE].backgroundLight
)
}
@@ -184,7 +118,7 @@
-
+
-
-
+ toggleCollapse(group.id)}
+ {editMode}
+ onToggleCollapse={() => toggleCollapse(group.id)}
+ onSummaryUpdate={(text) =>
+ groupEditorContext?.groupEditor.updateSummary(group.id, text)}
/>
- {#if editingGroupId === group.id}
- {@const textColorClass =
- NOTE_COLORS[(group.color as NoteColor) ?? NoteColor.BLUE]?.text ?? ''}
- saveSummary(group.id)}
- onkeydown={(e) => handleSummaryKeydown(e, group.id)}
- onclick={stopPropagation(preventDefault(() => {}))}
- onpointerdown={stopPropagation(preventDefault(() => {}))}
- spellcheck={false}
- />
- {:else}
- {@const textColorClass =
- NOTE_COLORS[(group.color as NoteColor) ?? NoteColor.BLUE]?.text ?? ''}
-
-
- startEditingSummary(group.id, group.summary ?? ''))
- )
- : undefined}
- onpointerdown={editMode ? stopPropagation(preventDefault(() => {})) : undefined}
- >{group.summary || (editMode ? 'Group' : '')}
- {/if}
-
- {#if editMode}
-
+ {#if editMode}
-
- {/if}
+ {/if}
+
{/if}
diff --git a/frontend/src/lib/components/graph/groupEditor.svelte.ts b/frontend/src/lib/components/graph/groupEditor.svelte.ts
index 7bbc65fc4c..e7ee2d8130 100644
--- a/frontend/src/lib/components/graph/groupEditor.svelte.ts
+++ b/frontend/src/lib/components/graph/groupEditor.svelte.ts
@@ -258,10 +258,11 @@ export function getGroupEditorContext(): GroupEditorContext | undefined {
return getContext
(CONTEXT_KEY)
}
-/** Extra vertical space pushed above the topmost node of each group for the tab */
-export const GROUP_HEADER_HEIGHT = 0
+/** Height of the group header bar */
+export const GROUP_HEADER_HEIGHT = 22
-const GROUP_TOP_MARGIN = 28
+/** Extra margin between the header and the first node */
+const GROUP_TOP_MARGIN = 12
/**
* Compute adjusted node positions for collapsed groups whose note is visible.
diff --git a/frontend/src/lib/components/graph/noteColors.ts b/frontend/src/lib/components/graph/noteColors.ts
index f9024adf7e..2b06e43b33 100644
--- a/frontend/src/lib/components/graph/noteColors.ts
+++ b/frontend/src/lib/components/graph/noteColors.ts
@@ -14,6 +14,7 @@ export enum NoteColor {
export interface NoteColorConfig {
background: string
+ backgroundLight: string
outline: string
outlineHover: string
text: string
@@ -24,6 +25,7 @@ export interface NoteColorConfig {
export const NOTE_COLORS: Record = {
[NoteColor.YELLOW]: {
background: 'bg-yellow-200 dark:bg-yellow-900',
+ backgroundLight: 'bg-yellow-400/5 dark:bg-yellow-600/5',
outline: 'outline-yellow-300 dark:outline-yellow-600',
outlineHover: 'outline-yellow-300/60 dark:outline-yellow-600/60',
text: 'text-yellow-900 dark:text-yellow-100',
@@ -31,6 +33,7 @@ export const NOTE_COLORS: Record = {
},
[NoteColor.BLUE]: {
background: 'bg-blue-100 dark:bg-blue-950',
+ backgroundLight: 'bg-blue-400/5 dark:bg-blue-600/5',
outline: 'outline-blue-300 dark:outline-blue-600',
outlineHover: 'outline-blue-300/60 dark:outline-blue-600/60',
text: 'text-blue-900 dark:text-blue-100',
@@ -38,6 +41,7 @@ export const NOTE_COLORS: Record = {
},
[NoteColor.GREEN]: {
background: 'bg-green-200 dark:bg-green-900',
+ backgroundLight: 'bg-green-400/5 dark:bg-green-600/5',
outline: 'outline-green-300 dark:outline-green-600',
outlineHover: 'outline-green-300/60 dark:outline-green-600/60',
text: 'text-green-900 dark:text-green-100',
@@ -45,6 +49,7 @@ export const NOTE_COLORS: Record = {
},
[NoteColor.PURPLE]: {
background: 'bg-purple-200 dark:bg-purple-900',
+ backgroundLight: 'bg-purple-400/5 dark:bg-purple-600/5',
outline: 'outline-purple-300 dark:outline-purple-600',
outlineHover: 'outline-purple-300/60 dark:outline-purple-600/60',
text: 'text-purple-900 dark:text-purple-100',
@@ -52,6 +57,7 @@ export const NOTE_COLORS: Record = {
},
[NoteColor.PINK]: {
background: 'bg-pink-200 dark:bg-pink-900',
+ backgroundLight: 'bg-pink-400/5 dark:bg-pink-600/5',
outline: 'outline-pink-300 dark:outline-pink-600',
outlineHover: 'outline-pink-300/60 dark:outline-pink-600/60',
text: 'text-pink-900 dark:text-pink-100',
@@ -59,6 +65,7 @@ export const NOTE_COLORS: Record = {
},
[NoteColor.ORANGE]: {
background: 'bg-orange-200 dark:bg-orange-900',
+ backgroundLight: 'bg-orange-400/5 dark:bg-orange-600/5',
outline: 'outline-orange-300 dark:outline-orange-600',
outlineHover: 'outline-orange-300/60 dark:outline-orange-600/60',
text: 'text-orange-900 dark:text-orange-100',
@@ -66,6 +73,7 @@ export const NOTE_COLORS: Record = {
},
[NoteColor.RED]: {
background: 'bg-red-200 dark:bg-red-900',
+ backgroundLight: 'bg-red-400/5 dark:bg-red-600/5',
outline: 'outline-red-300 dark:outline-red-600',
outlineHover: 'outline-red-300/60 dark:outline-red-600/60',
text: 'text-red-900 dark:text-red-100',
@@ -73,6 +81,7 @@ export const NOTE_COLORS: Record = {
},
[NoteColor.CYAN]: {
background: 'bg-cyan-200 dark:bg-cyan-900',
+ backgroundLight: 'bg-cyan-400/5 dark:bg-cyan-600/5',
outline: 'outline-cyan-300 dark:outline-cyan-600',
outlineHover: 'outline-cyan-300/60 dark:outline-cyan-600/60',
text: 'text-cyan-900 dark:text-cyan-100',
@@ -80,6 +89,7 @@ export const NOTE_COLORS: Record = {
},
[NoteColor.LIME]: {
background: 'bg-lime-200 dark:bg-lime-900',
+ backgroundLight: 'bg-lime-400/5 dark:bg-lime-600/5',
outline: 'outline-lime-300 dark:outline-lime-600',
outlineHover: 'outline-lime-300/60 dark:outline-lime-600/60',
text: 'text-lime-900 dark:text-lime-100',
@@ -87,6 +97,7 @@ export const NOTE_COLORS: Record = {
},
[NoteColor.GRAY]: {
background: 'bg-gray-200 dark:bg-gray-800',
+ backgroundLight: 'bg-gray-400/5 dark:bg-gray-600/5',
outline: 'outline-gray-300 dark:outline-gray-600',
outlineHover: 'outline-gray-300/60 dark:outline-gray-600/60',
text: 'text-gray-900 dark:text-gray-100',
diff --git a/frontend/src/lib/components/graph/renderers/nodes/CollapsedGroupNode.svelte b/frontend/src/lib/components/graph/renderers/nodes/CollapsedGroupNode.svelte
index 386b31eb88..affdbe722f 100644
--- a/frontend/src/lib/components/graph/renderers/nodes/CollapsedGroupNode.svelte
+++ b/frontend/src/lib/components/graph/renderers/nodes/CollapsedGroupNode.svelte
@@ -2,10 +2,13 @@
import NodeWrapper from './NodeWrapper.svelte'
import type { CollapsedGroupN } from '../../graphBuilder.svelte'
import { getGraphContext } from '../../graphContext'
- import GroupNodeCard from '../../GroupNodeCard.svelte'
+ import GroupHeader from '../../GroupHeader.svelte'
import GroupActionBar from '../../GroupActionBar.svelte'
+ import GroupModuleIcons from '../../GroupModuleIcons.svelte'
+ import GroupNoteArea from '../../GroupNoteArea.svelte'
import { getGroupEditorContext } from '../../groupEditor.svelte'
- import StepCountTab from '../../StepCountTab.svelte'
+ import { NOTE_COLORS, NoteColor } from '../../noteColors'
+ import { twMerge } from 'tailwind-merge'
interface Props {
data: CollapsedGroupN['data']
@@ -23,6 +26,10 @@
groupEditorContext?.groupEditor.getGroups().find((g) => g.id === data.groupId)
)
+ let noteColorConfig = $derived(
+ NOTE_COLORS[(data.color as NoteColor) ?? NoteColor.BLUE] ?? NOTE_COLORS[NoteColor.BLUE]
+ )
+
let hover = $state(false)
let menuOpen = $state(false)
@@ -31,27 +38,47 @@
{#snippet children({ darkMode })}
(hover = true)} onmouseleave={() => (hover = false)}>
-
data.eventHandlers.expandGroup(data.groupId)}
- />
+
+
+
data.eventHandlers.expandGroup(data.groupId)}
+ onSummaryUpdate={(text) =>
+ groupEditorContext?.groupEditor.updateSummary(data.groupId, text)}
+ />
+ {#if data.modules && data.modules.length > 0}
+
+
+
+ {/if}
+
-
- groupEditorContext?.groupEditor.updateSummary(data.groupId, text)}
- onNoteUpdate={(text) => groupEditorContext?.groupEditor.updateNote(data.groupId, text)}
- onHeightChange={(h) => {
- groupEditorContext?.groupEditor.setNoteHeight(data.groupId, h)
- }}
- />
+ {#if data.showNotes && data.note != null}
+
+ {
+ groupEditorContext?.groupEditor.setNoteHeight(data.groupId, h)
+ }}
+ onNoteUpdate={(text) =>
+ groupEditorContext?.groupEditor.updateNote(data.groupId, text)}
+ />
+
+ {/if}
+
{#if data.editMode}