refactor: extract shared GroupNodeCard component for group header

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Guilhem
2026-02-23 17:41:51 +00:00
parent e866a85542
commit cb7d87a1bb
3 changed files with 52 additions and 106 deletions

View File

@@ -0,0 +1,35 @@
<script lang="ts">
import FlowModuleSchemaItemViewer from '$lib/components/flows/map/FlowModuleSchemaItemViewer.svelte'
import BarsStaggered from '$lib/components/icons/BarsStaggered.svelte'
import { getNodeColorClasses } from '$lib/components/graph'
import { twMerge } from 'tailwind-merge'
interface Props {
summary?: string
selected?: boolean
}
let { summary, selected = false }: Props = $props()
let colorClasses = $derived(getNodeColorClasses(undefined, selected))
</script>
<div
class={twMerge(
'w-full module flex rounded-md cursor-pointer max-w-full drop-shadow-base',
colorClasses.bg
)}
style="width: 275px; height: 34px;"
>
<div
class={twMerge('absolute z-0 rounded-md outline-offset-0', colorClasses.outline)}
style="width: 275px; height: 34px;"
></div>
<div class="flex flex-col w-full">
<FlowModuleSchemaItemViewer label={summary || 'Group'} {colorClasses}>
{#snippet icon()}
<BarsStaggered size={16} />
{/snippet}
</FlowModuleSchemaItemViewer>
</div>
</div>

View File

@@ -1,11 +1,12 @@
<script lang="ts">
import { ViewportPortal, type Node } from '@xyflow/svelte'
import { calculateNodesBoundsWithOffset } from './util'
import { ChevronDown, Pen, X } from 'lucide-svelte'
import { calculateNodesBoundsWithOffset, NODE } from './util'
import { ChevronDown, X } from 'lucide-svelte'
import { getGroupEditorContext, type FlowGroup } from './groupEditor.svelte'
import { NoteColor, NOTE_COLORS } from './noteColors'
import { NoteColor } from './noteColors'
import NoteColorPicker from './NoteColorPicker.svelte'
import Button from '../common/button/Button.svelte'
import GroupNodeCard from './GroupNodeCard.svelte'
interface Props {
hoveredNodeId: string | null
@@ -41,7 +42,7 @@
hideTimeout = undefined
}
visibleGroup = activeGroup
} else if (!colorPickerOpen && !actionBarHovered && !editingSummary) {
} else if (!colorPickerOpen && !actionBarHovered) {
hideTimeout = setTimeout(() => {
visibleGroup = undefined
}, 150)
@@ -51,7 +52,7 @@
// All groups for always-visible labels
let allGroups = $derived(groupEditorContext?.groupEditor.getGroups() ?? [])
// Compute bounds for each group (for labels)
// Compute bounds for each group (with extra top padding for header card)
function computeGroupBounds(group: FlowGroup) {
if (group.module_ids.length === 0) return null
const { minX, minY, maxX, maxY } = calculateNodesBoundsWithOffset(
@@ -59,11 +60,12 @@
allNodes
)
const padding = 16
const topPadding = padding + 42 + NODE.gap.vertical / 2 // 34px header + 8px gap + insertion button
return {
x: minX - padding,
y: minY - padding,
y: minY - topPadding,
width: maxX - minX + 2 * padding,
height: maxY - minY + 2 * padding
height: maxY - minY + padding + topPadding
}
}
@@ -88,13 +90,6 @@
)
}
function getTextColorClass(color?: string): string {
return (
NOTE_COLORS[(color as NoteColor) ?? NoteColor.BLUE]?.text ??
NOTE_COLORS[NoteColor.BLUE].text
)
}
function toggleCollapse(groupId: string) {
const current =
groupEditorContext?.groupEditor
@@ -102,25 +97,6 @@
.find((g) => g.id === groupId)?.collapsed ?? false
groupEditorContext?.groupEditor.updateCollapsedDefault(groupId, !current)
}
// Label hover state (for showing pen button)
let hoveredLabelGroupId = $state<string | null>(null)
// Inline summary editing (tracks which group is being edited)
let editingGroupId = $state<string | null>(null)
let summaryInput = $state('')
let editingSummary = $derived(editingGroupId !== null)
function startEditSummary(group: FlowGroup) {
editingGroupId = group.id
summaryInput = group.summary ?? ''
}
function commitSummary(groupId: string) {
editingGroupId = null
groupEditorContext?.groupEditor.updateSummary(groupId, summaryInput)
}
</script>
{#each allGroups as group (group.id)}
@@ -136,60 +112,15 @@
style:height="{bounds.height}px"
style:z-index="4"
>
<!-- Label (top-left, above the border) -->
<div
class="absolute -top-6 left-0 flex items-center gap-1 h-5"
style="pointer-events: auto; cursor: default;"
onpointerenter={() => {
if (hideTimeout) {
clearTimeout(hideTimeout)
hideTimeout = undefined
}
hoveredLabelGroupId = group.id
visibleGroup = group
}}
onpointerleave={() => {
hoveredLabelGroupId = null
if (!colorPickerOpen && !actionBarHovered && !editingSummary) {
hideTimeout = setTimeout(() => {
visibleGroup = undefined
}, 150)
}
}}
>
{#if editingGroupId === group.id}
<input
class="text-xs font-medium bg-transparent border-none outline-none {getTextColorClass(group.color)} w-24"
bind:value={summaryInput}
onblur={() => commitSummary(group.id)}
onkeydown={(e) => {
if (e.key === 'Enter') commitSummary(group.id)
if (e.key === 'Escape') {
editingGroupId = null
}
}}
autofocus
/>
{:else}
<span class="text-xs font-medium {getTextColorClass(group.color)}">
{group.summary || 'Group'}
</span>
{#if editMode && hoveredLabelGroupId === group.id}
<button
class="flex items-center justify-center w-4 h-4 rounded hover:bg-surface-hover {getTextColorClass(group.color)} opacity-60 hover:opacity-100"
onclick={() => startEditSummary(group)}
title="Edit group name"
>
<Pen size={10} />
</button>
{/if}
{/if}
<!-- Header card (top-left, inside the border) -->
<div class="absolute top-2 left-2" style="pointer-events: auto;">
<GroupNodeCard summary={group.summary} />
</div>
<!-- Action bar (top-right, hover only) — matches group note style -->
<!-- Action bar (top-right, inside the border, hover only) -->
{#if editMode && visibleGroup?.id === group.id}
<div
class="absolute -top-7 right-0 p-1 h-7 group flex justify-end"
class="absolute top-2 right-2 p-1 h-7 group flex justify-end"
style="pointer-events: auto;"
onpointerenter={() => {
actionBarHovered = true

View File

@@ -1,13 +1,11 @@
<script lang="ts">
import { preventDefault, stopPropagation } from 'svelte/legacy'
import FlowModuleSchemaItemViewer from '$lib/components/flows/map/FlowModuleSchemaItemViewer.svelte'
import NodeWrapper from './NodeWrapper.svelte'
import { Maximize2 } from 'lucide-svelte'
import BarsStaggered from '$lib/components/icons/BarsStaggered.svelte'
import type { CollapsedGroupN } from '../../graphBuilder.svelte'
import { getGraphContext } from '../../graphContext'
import { getNodeColorClasses } from '$lib/components/graph'
import { twMerge } from 'tailwind-merge'
import GroupNodeCard from '../../GroupNodeCard.svelte'
interface Props {
data: CollapsedGroupN['data']
@@ -19,7 +17,6 @@
const { selectionManager } = getGraphContext()
let selected = $derived(!!(selectionManager && selectionManager.isNodeSelected(id)))
let colorClasses = $derived(getNodeColorClasses(undefined, selected))
let hover = $state(false)
</script>
@@ -27,28 +24,11 @@
{#snippet children({ darkMode })}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class={twMerge(
'w-full module flex rounded-md cursor-pointer max-w-full drop-shadow-base',
colorClasses.bg
)}
style="width: 275px; height: 34px;"
class="relative"
onmouseenter={() => (hover = true)}
onmouseleave={() => (hover = false)}
>
<div
class={twMerge('absolute z-0 rounded-md outline-offset-0', colorClasses.outline)}
style="width: 275px; height: 34px;"
></div>
<div class="flex flex-col w-full">
<FlowModuleSchemaItemViewer
label={data.summary || 'Group'}
{colorClasses}
>
{#snippet icon()}
<BarsStaggered size={16} />
{/snippet}
</FlowModuleSchemaItemViewer>
</div>
<GroupNodeCard summary={data.summary} {selected} />
<div class="absolute -translate-y-[100%] top-2 right-10 h-7 p-1">
<button