From 77c3ef6abfc590f26a7595706df44d2a658330df Mon Sep 17 00:00:00 2001 From: Guilhem Lemouel Date: Tue, 10 Mar 2026 20:12:54 +0100 Subject: [PATCH] refactor: unify group node headers with shared GroupHeader component Co-Authored-By: Claude Opus 4.6 --- .../lib/components/graph/GroupHeader.svelte | 104 ++++++++++++ .../components/graph/GroupModuleIcons.svelte | 6 +- .../lib/components/graph/GroupOverlay.svelte | 149 +++--------------- .../components/graph/groupEditor.svelte.ts | 7 +- .../src/lib/components/graph/noteColors.ts | 11 ++ .../renderers/nodes/CollapsedGroupNode.svelte | 71 ++++++--- 6 files changed, 195 insertions(+), 153 deletions(-) create mode 100644 frontend/src/lib/components/graph/GroupHeader.svelte 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}