From c8085dad5aa55375327bd72cd620d30c7e6f5e8e Mon Sep 17 00:00:00 2001 From: Lucio Lelii Date: Wed, 30 Sep 2026 07:52:12 +0200 Subject: [PATCH] feat(editor): highlight selected connection endpoints --- .../graph-selection/graph-selection.ts | 17 +++++++-- .../custom-connection/custom-connection.ts | 25 ++++++++++--- src/app/shared/custom-socket/custom-socket.ts | 36 ++++++++++++++++++- 3 files changed, 70 insertions(+), 8 deletions(-) diff --git a/src/app/services/graph-selection/graph-selection.ts b/src/app/services/graph-selection/graph-selection.ts index 7de9e48..ba233ac 100644 --- a/src/app/services/graph-selection/graph-selection.ts +++ b/src/app/services/graph-selection/graph-selection.ts @@ -4,17 +4,30 @@ import { Injectable, signal } from '@angular/core'; +export type SelectedConnection = { + id: string; + sourceId: string; + sourceOutput: string; + targetId: string; + targetInput: string; +}; + @Injectable({ providedIn: 'root' }) export class GraphSelectionService { readonly selectedConnectionId = signal(null); + /** The two endpoints let their socket components light up with the selected wire. */ + readonly selectedConnection = signal(null); readonly deleteConnectionRequestTick = signal(0); - selectConnection(connectionId: string | null | undefined) { - this.selectedConnectionId.set(connectionId ? String(connectionId) : null); + selectConnection(connectionId: string | null | undefined, endpoints?: Omit) { + const id = connectionId ? String(connectionId) : null; + this.selectedConnectionId.set(id); + this.selectedConnection.set(id && endpoints ? { id, ...endpoints } : null); } clearConnectionSelection() { this.selectedConnectionId.set(null); + this.selectedConnection.set(null); } requestDeleteSelectedConnection() { diff --git a/src/app/shared/custom-connection/custom-connection.ts b/src/app/shared/custom-connection/custom-connection.ts index 91db8cb..b9a5dac 100644 --- a/src/app/shared/custom-connection/custom-connection.ts +++ b/src/app/shared/custom-connection/custom-connection.ts @@ -20,7 +20,9 @@ import { BiasComparisonViewStateService } from '@services/bias/bias-comparison-v selector: 'app-custom-connection', standalone: true, template: ` - + >(ElementRef); + private readonly graphSelection = inject(GraphSelectionService); private resizeObserver: ResizeObserver | null = null; private renderFrame: number | null = null; private side: "input" | "output" = "input"; @@ -33,6 +35,7 @@ export class CustomSocket implements AfterViewInit, OnDestroy { @HostBinding("style.width") get width() { + if (this.isSelectedEndpoint) return this.kind === "resource" ? "11px" : "12px"; return this.kind === "resource" ? "7px" : "8px"; } @@ -45,7 +48,13 @@ export class CustomSocket implements AfterViewInit, OnDestroy { @HostBinding("style.borderRadius") get borderRadius() { return this.kind === "resource" ? "4px" : "999px"; } - @HostBinding("style.border") border = "1px solid rgba(255,255,255,0.9)"; + @HostBinding("style.border") get border() { + return this.isSelectedEndpoint ? "2px solid #fff" : "1px solid rgba(255,255,255,0.9)"; + } + + @HostBinding('attr.data-connection-endpoint-selected') get selectedEndpointAttribute() { + return this.isSelectedEndpoint || null; + } @HostBinding("style.cursor") get cursor() { return this.data?.__readonly === true ? "default" : "crosshair"; } @@ -56,6 +65,13 @@ export class CustomSocket implements AfterViewInit, OnDestroy { @HostBinding("style.background") get bg() { + if (this.isSelectedEndpoint) { + return this.kind === 'resource' + ? 'linear-gradient(145deg, #5eead4 0%, #0f9f8d 100%)' + : this.kind === 'dependency' + ? 'linear-gradient(145deg, #a5b4fc 0%, #4f46e5 100%)' + : 'linear-gradient(145deg, #fdba74 0%, #ea580c 100%)'; + } if (this.kind === "resource") { return "linear-gradient(180deg, #5eead4 0%, #0f766e 100%)"; } @@ -71,6 +87,12 @@ export class CustomSocket implements AfterViewInit, OnDestroy { @HostBinding("style.boxShadow") get sh() { + if (this.isSelectedEndpoint) { + const color = this.kind === 'resource' ? 'rgba(15,159,141,0.78)' + : this.kind === 'dependency' ? 'rgba(79,70,229,0.72)' + : 'rgba(234,88,12,0.72)'; + return `0 0 0 4px ${color}, 0 5px 14px ${color}`; + } if (this.kind === "resource") { return "0 1px 4px rgba(15,118,110,0.32), 0 0 0 1px rgba(13,148,136,0.42)"; } @@ -92,6 +114,18 @@ export class CustomSocket implements AfterViewInit, OnDestroy { return kind === "dependency" || kind === "resource" ? kind : "data"; } + /** True for either end of the selected data, dependency or resource connection. */ + get isSelectedEndpoint(): boolean { + const selected = this.graphSelection.selectedConnection(); + if (!selected) return false; + const nodeId = this.data?.nodeId ?? this.data?.payload?.nodeId; + const key = this.data?.key ?? this.data?.payload?.key; + if (nodeId == null || key == null) return false; + return this.side === 'output' + ? String(nodeId) === selected.sourceId && String(key) === selected.sourceOutput + : String(nodeId) === selected.targetId && String(key) === selected.targetInput; + } + ngOnChanges(): void { this.side = this.resolveSocketSide(); this.kind = this.resolveSocketKind();