ui: align node sockets and widen resource cards

This commit is contained in:
Lucio Lelii 2026-09-29 11:14:11 +02:00
parent 97b174ac6e
commit 67d6bc6b71
6 changed files with 86 additions and 25 deletions

View File

@ -2,16 +2,28 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Attribution term under AGPL-3.0 section 7(b): see LICENSE-ADDENDUM.
import { ChangeDetectionStrategy, Component, HostBinding, Input } from "@angular/core";
import {
AfterViewInit,
ChangeDetectionStrategy,
Component,
ElementRef,
HostBinding,
inject,
Input,
OnDestroy
} from "@angular/core";
@Component({
template: ``,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CustomSocket {
export class CustomSocket implements AfterViewInit, OnDestroy {
@Input() data!: any;
@Input() emit!: any;
@Input() rendered!: any;
private readonly element = inject<ElementRef<HTMLElement>>(ElementRef);
private resizeObserver: ResizeObserver | null = null;
private renderFrame: number | null = null;
private side: "input" | "output" = "input";
private kind: "data" | "dependency" | "resource" = "data";
@ -19,10 +31,18 @@ export class CustomSocket {
return this.data.name;
}
@HostBinding("style.width") w = "15px";
@HostBinding("style.height") h = "15px";
@HostBinding("style.width")
get width() {
return this.kind === "resource" ? "9px" : "8px";
}
@HostBinding("style.height")
get height() {
return this.kind === "resource" ? "100%" : "26px";
}
@HostBinding("style.display") d = "block";
@HostBinding("style.borderRadius") br = "4px";
@HostBinding("style.borderRadius") br = "999px";
@HostBinding("style.border") border = "1px solid rgba(255,255,255,0.9)";
@HostBinding("style.cursor") get cursor() {
return this.data?.__readonly === true ? "default" : "crosshair";
@ -73,6 +93,29 @@ export class CustomSocket {
ngOnChanges(): void {
this.side = this.resolveSocketSide();
this.kind = this.resolveSocketKind();
requestAnimationFrame(() => this.rendered());
this.schedulePositionRefresh();
}
ngAfterViewInit(): void {
if (typeof ResizeObserver === "undefined") return;
this.resizeObserver = new ResizeObserver(() => this.schedulePositionRefresh());
this.resizeObserver.observe(this.element.nativeElement);
}
ngOnDestroy(): void {
this.resizeObserver?.disconnect();
if (this.renderFrame !== null) cancelAnimationFrame(this.renderFrame);
}
/**
* Rete stores the socket's DOM centre. Resource sockets change height when their card opens, so
* that position must be published again after every real size change.
*/
private schedulePositionRefresh(): void {
if (this.renderFrame !== null) cancelAnimationFrame(this.renderFrame);
this.renderFrame = requestAnimationFrame(() => {
this.renderFrame = null;
this.rendered?.();
});
}
}

View File

@ -425,7 +425,7 @@
.container-node__port-row {
display: flex;
align-items: center;
gap: 8px;
gap: 0;
margin-bottom: 8px;
}
@ -433,6 +433,19 @@
justify-content: flex-end;
}
.container-node__socket {
position: relative;
flex: 0 0 0;
width: 0;
left: -18px;
z-index: 20;
}
.container-node__socket--right {
left: auto;
right: -18px;
}
.container-node__port-label {
display: inline-flex;
flex-direction: column;

View File

@ -6,7 +6,7 @@
.llm-node {
position: relative;
width: 280px;
width: 340px;
user-select: none;
-webkit-user-select: none;
border-radius: 16px;
@ -30,7 +30,7 @@
inset: 0;
margin: auto;
z-index: 9991;
width: 280px;
width: 340px;
max-width: calc(100vw - 32px);
height: fit-content;
max-height: calc(100vh - 32px);
@ -586,21 +586,24 @@
}
.llm-row-input {
grid-template-columns: 16px 1fr;
gap: 6px;
grid-template-columns: 0 minmax(0, 1fr);
gap: 0;
}
.llm-row-output {
grid-template-columns: 1fr 16px;
gap: 6px;
grid-template-columns: minmax(0, 1fr) 0;
gap: 0;
}
.llm-socket {
position: relative;
left: -16px;
z-index: 20;
}
.llm-socket-right {
left: auto;
right: -16px;
justify-self: end;
}

View File

@ -14,7 +14,8 @@
.resource-card {
position: relative;
width: 250px;
width: 340px;
box-sizing: border-box;
padding: 12px 16px 12px 12px;
border: 1.5px solid #99f6e4;
border-left: 5px solid #0d9488;
@ -26,7 +27,7 @@
}
.resource-card--open {
width: 320px;
width: 340px;
}
/*

View File

@ -6,7 +6,7 @@
.llm-node {
position: relative;
width: 280px;
width: 340px;
user-select: none;
-webkit-user-select: none;
border-radius: 16px;
@ -495,21 +495,24 @@ button.llm-node-bias-summary:focus-visible {
}
.llm-row-input {
grid-template-columns: 16px 1fr;
gap: 6px;
grid-template-columns: 0 minmax(0, 1fr);
gap: 0;
}
.llm-row-output {
grid-template-columns: 1fr 16px;
gap: 6px;
grid-template-columns: minmax(0, 1fr) 0;
gap: 0;
}
.llm-socket {
position: relative;
left: -16px;
z-index: 20;
}
.llm-socket-right {
left: auto;
right: -16px;
justify-self: end;
}

View File

@ -124,12 +124,10 @@ export async function createEditor(
render.addPreset(
Presets.classic.setup({
// Rete pushes an output's anchor 12px past its socket, for a small handle on a node's side.
// A resource's handle is a bar along its right edge: its links start from the bar itself.
// Every socket is a bar centred on the node border, so the connection starts from the bar's
// actual DOM centre. The Rete default would push the anchor another 12px past it.
socketPositionWatcher: getDOMSocketPosition<HFSchemes, any>({
offset: (position, _nodeId, side, key) => key === FLOW_RESOURCE_PORT_KEY
? position
: { x: position.x + 12 * (side === 'input' ? -1 : 1), y: position.y }
offset: (position) => position
}),
customize: {
node(context: any) {