feat: add inline-editable summary field to group header

Click the group name in edit mode to rename it inline. The input
replaces the label, saves on blur/Enter, and cancels on Escape.
Wired from both GroupOverlay and CollapsedGroupNode.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Guilhem
2026-02-24 17:26:29 +00:00
parent f86ca0ef4f
commit 8c5e3f6896
3 changed files with 58 additions and 1 deletions
@@ -3,6 +3,7 @@
import { getNodeColorClasses } from '$lib/components/graph'
import { NOTE_COLORS, NoteColor } from './noteColors'
import { twMerge } from 'tailwind-merge'
import { preventDefault, stopPropagation } from 'svelte/legacy'
import GroupNoteArea from './GroupNoteArea.svelte'
interface Props {
@@ -14,6 +15,7 @@
note?: string
showNote?: boolean
editMode?: boolean
onSummaryUpdate?: (text: string) => void
onNoteUpdate?: (text: string) => void
onHeightChange?: (height: number) => void
}
@@ -27,6 +29,7 @@
note,
showNote = false,
editMode = false,
onSummaryUpdate,
onNoteUpdate,
onHeightChange
}: Props = $props()
@@ -37,6 +40,37 @@
let defaultColorClasses = $derived(getNodeColorClasses(undefined, selected))
// Inline summary editing
let editingSummary = $state(false)
let summaryInput = $state('')
let summaryInputElement: HTMLInputElement | undefined = $state(undefined)
function startEditingSummary() {
if (!editMode) return
editingSummary = true
summaryInput = summary ?? ''
requestAnimationFrame(() => {
summaryInputElement?.focus()
summaryInputElement?.select()
})
}
function saveSummary() {
editingSummary = false
const trimmed = summaryInput.trim()
if (trimmed !== (summary ?? '')) {
onSummaryUpdate?.(trimmed)
}
}
function handleSummaryKeydown(event: KeyboardEvent) {
if (event.key === 'Enter') {
saveSummary()
} else if (event.key === 'Escape') {
editingSummary = false
}
}
// Reset height to 0 when note is hidden
$effect(() => {
if (!showNote) {
@@ -63,7 +97,27 @@
></div>
<div class="flex items-center w-full gap-1.5 px-2 h-[34px] relative z-1">
<Group size={14} />
<span class="text-2xs font-medium truncate">{summary || 'Group'}</span>
{#if editingSummary}
<input
bind:this={summaryInputElement}
bind:value={summaryInput}
class="text-2xs font-medium bg-transparent border-none p-0 m-0 outline-none min-w-0 flex-1 nodrag nowheel"
placeholder="Group"
onblur={saveSummary}
onkeydown={handleSummaryKeydown}
onclick={stopPropagation(preventDefault(() => {}))}
onpointerdown={stopPropagation(() => {})}
spellcheck="false"
/>
{:else}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<span
class="text-2xs font-medium truncate {editMode ? 'cursor-text hover:opacity-80' : ''}"
onclick={editMode ? stopPropagation(preventDefault(startEditingSummary)) : undefined}
onpointerdown={editMode ? stopPropagation(preventDefault(() => {})) : undefined}
>{summary || 'Group'}</span>
{/if}
{#if stepCount != null}
<span class="text-2xs opacity-60">{stepCount} node{stepCount !== 1 ? 's' : ''}</span>
{/if}
@@ -148,6 +148,7 @@
note={group.note}
showNote={showNotes && group.note != null}
{editMode}
onSummaryUpdate={(text) => groupEditorContext?.groupEditor.updateSummary(group.id, text)}
onNoteUpdate={(text) => groupEditorContext?.groupEditor.updateNote(group.id, text)}
onHeightChange={(h) => groupEditorContext?.groupEditor.setNoteHeight(group.id, h)}
/>
@@ -38,6 +38,8 @@
note={data.note}
showNote={data.showNotes && data.note != null}
editMode={data.editMode}
onSummaryUpdate={(text) =>
groupEditorContext?.groupEditor.updateSummary(data.groupId, text)}
onNoteUpdate={(text) =>
groupEditorContext?.groupEditor.updateNote(data.groupId, text)}
onHeightChange={(h) =>