Skip to content

Commit 8410a99

Browse files
WB-192: misaligned port positions (#20)
* fix(sdk): fix hardcoded source handle position * fix(sdk): proper port positions for long descriptions * chore(sdk): tighten WB-192 fix scope, drop duplicate type, cross-ref icon size, add changeset
1 parent 27bfefd commit 8410a99

6 files changed

Lines changed: 41 additions & 12 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@workflowbuilder/sdk': patch
3+
---
4+
5+
fix: stabilize horizontal port Y on built-in node templates so multi-line descriptions no longer shift the port and bend edges between adjacent nodes. Unifies `<NodePanel.Handles alignment>` selection through one helper across all four built-in templates and pins the resulting port to the NodeIcon's vertical center via a global CSS rule scoped to a SDK-owned anchor class. Also fixes a separate latent bug where `DecisionNodeTemplate` hardcoded `Position.Right` on the source handle instead of honoring `layoutDirection` (broke decision nodes in `DOWN` layout).
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { NodePanel } from '@synergycodes/overflow-ui';
2+
import type { ComponentProps } from 'react';
3+
4+
import type { LayoutDirection } from '../../../node/common';
5+
6+
// Source-of-truth: overflow-ui's `<NodePanel.Handles>` prop, so adding a new
7+
// alignment in overflow-ui surfaces here as a type error instead of silently
8+
// drifting.
9+
type HandlesAlignment = NonNullable<ComponentProps<typeof NodePanel.Handles>['alignment']>;
10+
11+
// Unifies how built-in node templates choose the `alignment` prop they pass to
12+
// `<NodePanel.Handles>`, so the formula has one place to evolve instead of
13+
// being re-derived per template.
14+
//
15+
// This helper alone does NOT make ports align across nodes. The visual
16+
// stability of the resulting port Y depends on a companion global CSS rule in
17+
// `packages/sdk/src/index.css` (search WB-192): the formula picks 'header' for
18+
// horizontal flow, then the CSS pin anchors the port to the NodeIcon's
19+
// vertical center so multi-line descriptions don't shift it. Both layers must
20+
// stay in sync; removing either reintroduces the bug.
21+
export function getHandlesAlignment({ layoutDirection }: { layoutDirection: LayoutDirection }): HandlesAlignment {
22+
return layoutDirection === 'RIGHT' ? 'header' : 'center';
23+
}

packages/sdk/src/features/diagram/nodes/ai-agent-node-template/ai-agent-node-template.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type { ItemOption } from '../../../../node/node-schema';
1212
import type { AiAgentTool } from '../../../json-form/types/controls';
1313
import { getHandleId } from '../../handles/get-handle-id';
1414
import { getHandlePosition } from '../../handles/get-handle-position';
15+
import { getHandlesAlignment } from '../../handles/get-handles-alignment';
1516
import { ConnectableItem } from '../components/connectable-item/connectable-item';
1617
import { SettingInfo } from './components/setting-info/setting-info';
1718
import { ToolInfo } from './components/tool-info/tool-info';
@@ -55,7 +56,7 @@ export const AiAgentNodeTemplate = memo(
5556

5657
const iconElement = useMemo(() => <Icon name={icon} size="large" />, [icon]);
5758

58-
const handlesAlignment = layoutDirection === 'RIGHT' ? 'header' : 'center';
59+
const handlesAlignment = getHandlesAlignment({ layoutDirection });
5960

6061
return (
6162
<Collapsible>

packages/sdk/src/features/diagram/nodes/decision-node-template/decision-node-template.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { NodeDescription, NodeIcon, NodePanel, Status } from '@synergycodes/overflow-ui';
2-
import { Handle, Position } from '@xyflow/react';
2+
import { Handle } from '@xyflow/react';
33
import { memo, useMemo } from 'react';
44

55
import { Icon } from '@workflow-builder/icons';
@@ -12,6 +12,7 @@ import type { DecisionBranch } from '../../../json-form/types/controls';
1212
import { OptionalNodeContent } from '../../../plugins-core/components/diagram/optional-node-content';
1313
import { getHandleId } from '../../handles/get-handle-id';
1414
import { getHandlePosition } from '../../handles/get-handle-position';
15+
import { getHandlesAlignment } from '../../handles/get-handles-alignment';
1516
import { BranchesContainer } from './components/branches-container';
1617

1718
type Props = {
@@ -47,10 +48,11 @@ export const DecisionNodeTemplate = memo(
4748
const handleSourceId = getHandleId({ handleType: 'source' });
4849

4950
const handleTargetPosition = getHandlePosition({ direction: layoutDirection, handleType: 'target' });
51+
const handleSourcePosition = getHandlePosition({ direction: layoutDirection, handleType: 'source' });
5052

5153
const isCanvasNode = showHandles;
5254

53-
const handlesAlignment = layoutDirection === 'RIGHT' ? 'header' : 'center';
55+
const handlesAlignment = getHandlesAlignment({ layoutDirection });
5456

5557
return (
5658
<NodePanel.Root selected={selected} className={styles['decision-node']}>
@@ -70,7 +72,7 @@ export const DecisionNodeTemplate = memo(
7072
</NodePanel.Content>
7173
<NodePanel.Handles isVisible={isCanvasNode} alignment={handlesAlignment}>
7274
<Handle id={handleTargetId} position={handleTargetPosition} type="target" />
73-
<Handle id={handleSourceId} position={Position.Right} type="source" />
75+
<Handle id={handleSourceId} position={handleSourcePosition} type="source" />
7476
</NodePanel.Handles>
7577
</NodePanel.Root>
7678
);

packages/sdk/src/features/diagram/nodes/start-node-template/start-node-template.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { withOptionalComponentPlugins } from '../../../plugins-core/adapters/ada
1212
import { OptionalNodeContent } from '../../../plugins-core/components/diagram/optional-node-content';
1313
import { getHandleId } from '../../handles/get-handle-id';
1414
import { getHandlePosition } from '../../handles/get-handle-position';
15+
import { getHandlesAlignment } from '../../handles/get-handles-alignment';
1516

1617
type StartNodeTemplateProps = {
1718
id: string;
@@ -47,17 +48,15 @@ const StartNodeTemplateComponent = memo(
4748

4849
const iconElement = useMemo(() => <Icon name={icon} size="large" />, [icon]);
4950

50-
const hasContent = !!children;
51-
52-
const handlesAlignment = hasContent && layoutDirection === 'RIGHT' ? 'header' : 'center';
51+
const handlesAlignment = getHandlesAlignment({ layoutDirection });
5352

5453
return (
5554
<Collapsible>
5655
<NodePanel.Root selected={selected} className={styles['content']}>
5756
<NodePanel.Header>
5857
<NodeIcon icon={iconElement} />
5958
<NodeDescription label={label} description={description} />
60-
{hasContent && <Collapsible.Button />}
59+
{!!children && <Collapsible.Button />}
6160
</NodePanel.Header>
6261
<NodePanel.Content isVisible={isCanvasNode}>
6362
<OptionalNodeContent nodeId={id}>

packages/sdk/src/features/diagram/nodes/workflow-node-template/workflow-node-template.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { withOptionalComponentPlugins } from '../../../plugins-core/adapters/ada
1313
import { OptionalNodeContent } from '../../../plugins-core/components/diagram/optional-node-content';
1414
import { getHandleId } from '../../handles/get-handle-id';
1515
import { getHandlePosition } from '../../handles/get-handle-position';
16+
import { getHandlesAlignment } from '../../handles/get-handles-alignment';
1617

1718
/**
1819
* Props for the editor's default workflow-node template. A custom node
@@ -73,17 +74,15 @@ const WorkflowNodeTemplateComponent = memo(
7374

7475
const iconElement = useMemo(() => <Icon name={icon} size="large" />, [icon]);
7576

76-
const hasContent = !!children;
77-
78-
const handlesAlignment = hasContent && layoutDirection === 'RIGHT' ? 'header' : 'center';
77+
const handlesAlignment = getHandlesAlignment({ layoutDirection });
7978

8079
return (
8180
<Collapsible>
8281
<NodePanel.Root selected={selected} className={styles['content']}>
8382
<NodePanel.Header>
8483
<NodeIcon icon={iconElement} />
8584
<NodeDescription label={label} description={description} />
86-
{hasContent && <Collapsible.Button />}
85+
{!!children && <Collapsible.Button />}
8786
</NodePanel.Header>
8887
<NodePanel.Content isVisible={isCanvasNode}>
8988
<OptionalNodeContent nodeId={id}>

0 commit comments

Comments
 (0)