Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/node-icon-accent-ui.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@workflowbuilder/ui': minor
---

`NodeIcon` takes an optional `accent` that tints its container and colors the glyph: `'blue' | 'green' | 'orange' | 'violet' | 'gray' | 'violet-gradient'`, or a custom name read from `--wb-public-node-icon-color-<name>` and `--wb-public-node-icon-container-background-color-<name>`; `NodeIconAccent` is exported. The icon is 36px with an 18px glyph (46px and 24px before): `--wb-public-node-icon-padding` defaults to 8px and the new `--wb-public-node-icon-glyph-size` sets the glyph through `font-size`.

Breaking changes:

- Render the icon inside `NodeIcon` sized in `em`, for example a Phosphor icon, otherwise it keeps its own size and the header ports are not centred on it.
5 changes: 5 additions & 0 deletions .changeset/node-icon-accent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@workflowbuilder/sdk': minor
---

Node definitions take an optional `accent` that colors the node's icon on the canvas and in the palette: `'blue' | 'green' | 'orange' | 'violet' | 'gray' | 'violet-gradient'`, or a custom name backed by `--wb-public-node-icon-*-<name>` variables. It is read by node type and never saved into the diagram; `WorkflowNodeTemplateProps` gains `accent`, `NodeIconAccent` is exported, and node definitions resolve as soon as `WorkflowBuilder.Root` mounts, so a layout without a Palette also shows node accents and the node Properties form.
9 changes: 9 additions & 0 deletions .changeset/node-icon-size.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@workflowbuilder/sdk': minor
---

Node icons are a 36px square with an 18px glyph (46px and 24px before): `--wb-public-node-icon-padding` defaults to 8px and the new `--wb-public-node-icon-glyph-size` sets the glyph. The AI Agent icon uses the `violet-gradient` accent, and `Icon` takes `size="inherit"` to follow the surrounding font size.

Breaking changes:

- In custom node templates render the icon inside `NodeIcon` with `size="inherit"` instead of `size="large"`, otherwise the glyph stays 24px.
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export const HumanDecisionNodeTemplate = defineNodeTemplate<HumanDecisionPropert
({
id,
icon,
accent,
label,
description,
data,
Expand All @@ -43,7 +44,7 @@ export const HumanDecisionNodeTemplate = defineNodeTemplate<HumanDecisionPropert
showHandles = true,
isValid,
}: WorkflowNodeTemplateProps<HumanDecisionProperties>) => {
const iconElement = useMemo(() => <Icon name={icon} size="large" />, [icon]);
const iconElement = useMemo(() => <Icon name={icon} size="inherit" />, [icon]);
const decisionRequest = data?.properties.decisionRequest;
const actions = useMemo(() => routedActions(decisionRequest), [decisionRequest]);

Expand All @@ -53,7 +54,7 @@ export const HumanDecisionNodeTemplate = defineNodeTemplate<HumanDecisionPropert
return (
<NodePanel.Root selected={selected} disabled={disabled}>
<NodePanel.Header>
<NodeIcon icon={iconElement} disabled={disabled} />
<NodeIcon icon={iconElement} accent={accent} disabled={disabled} />
<NodeDescription label={label} description={description} disabled={disabled} />
</NodePanel.Header>
<NodePanel.Content isVisible={isCanvasNode}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const MultiPortNodeTemplate = defineNodeTemplate<MultiPortProperties>(
memo(
({
icon,
accent,
label,
description,
selected = false,
Expand All @@ -30,7 +31,7 @@ export const MultiPortNodeTemplate = defineNodeTemplate<MultiPortProperties>(
}: WorkflowNodeTemplateProps<MultiPortProperties>) => {
const status = data?.properties.status ?? statusOptions.active.value;

const iconElement = useMemo(() => <Icon name={icon} size="large" />, [icon]);
const iconElement = useMemo(() => <Icon name={icon} size="inherit" />, [icon]);
const barClassName = clsx(styles['status-bar'], statusClass[status] ?? styles['status-draft']);

const handleTargetTopId = getHandleId({ handleType: 'target', innerId: 'top' });
Expand All @@ -43,7 +44,7 @@ export const MultiPortNodeTemplate = defineNodeTemplate<MultiPortProperties>(
<div className={barClassName} />
<NodePanel.Root selected={selected} disabled={disabled}>
<NodePanel.Header>
<NodeIcon icon={iconElement} disabled={disabled} />
<NodeIcon icon={iconElement} accent={accent} disabled={disabled} />
<NodeDescription label={label} description={description} disabled={disabled} />
</NodePanel.Header>
<NodePanel.Handles isVisible={showHandles}>
Expand Down
14 changes: 13 additions & 1 deletion apps/docs/src/components/ui-examples/node-icon.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { User } from '@phosphor-icons/react';
import { Sparkle, User } from '@phosphor-icons/react';
import { NodeIcon, NodePanel } from '@workflowbuilder/ui';

import { ComponentPreview } from './component-preview';
Expand All @@ -12,6 +12,18 @@ export function NodeIconExample() {
Node with Icon
</NodePanel.Header>
</NodePanel.Root>
<NodePanel.Root selected={false}>
<NodePanel.Header>
<NodeIcon icon={<User />} accent="violet" />
Violet accent
</NodePanel.Header>
</NodePanel.Root>
<NodePanel.Root selected={false}>
<NodePanel.Header>
<NodeIcon icon={<Sparkle />} accent="violet-gradient" />
AI accent
</NodePanel.Header>
</NodePanel.Root>
</ComponentPreview>
);
}
31 changes: 21 additions & 10 deletions apps/docs/src/content/docs/guides/add-a-custom-node.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ Custom nodes let you extend Workflow Builder with node types that match your spe

A node is a [`PaletteItem`](/api/types/paletteitem/) made of four pieces. File organisation is a suggestion — collapse them into one file if you prefer.

| Piece | Defined in | Purpose |
| ----------------------- | ---------------------------- | --------------------------------------------------------------------- |
| `schema` | `schema.ts` | Shape and validation of the node's properties. |
| `uischema` | `uischema.ts` | How those properties render in the property panel. |
| `defaultPropertiesData` | `default-properties-data.ts` | Initial values applied when the node is dropped. |
| Top-level fields | `<node-name>.ts` | `type`, `label`, `description`, `icon` — plus optional `isStartNode`. |
| Piece | Defined in | Purpose |
| ----------------------- | ---------------------------- | ---------------------------------------------------------------------------------- |
| `schema` | `schema.ts` | Shape and validation of the node's properties. |
| `uischema` | `uischema.ts` | How those properties render in the property panel. |
| `defaultPropertiesData` | `default-properties-data.ts` | Initial values applied when the node is dropped. |
| Top-level fields | `<node-name>.ts` | `type`, `label`, `description`, `icon` — plus optional `accent` and `isStartNode`. |

## 1. JSON Schema — `webhook/schema.ts`

Expand Down Expand Up @@ -106,12 +106,15 @@ export const webhookNode: PaletteItem<WebhookNodeSchema> = {
label: 'Webhook',
description: 'Send data to an external HTTP endpoint',
icon: 'Globe', // see the WBIcon name union for valid icon names
accent: 'blue',
defaultPropertiesData,
schema,
uischema,
};
```

`accent` colors the node's icon: `'blue'`, `'green'`, `'orange'`, `'violet'`, `'gray'`, `'violet-gradient'`, or a name of your own (see [`NodeIcon`](/ui-library/diagram-components/node-icon/#custom-accent)). The editor reads it from the palette item by node type every time it renders, and never saves it into the diagram, so changing it restyles existing diagrams too. Without it the icon keeps the default color, except on the built-in AI Agent template, which defaults to `'violet-gradient'`.

If this node is where a run begins, add `isStartNode: true`. The editor copies the flag onto every node dropped from this palette item, so it travels with the saved diagram as `data.isStartNode` and your execution engine can find the entry point directly:

```ts
Expand Down Expand Up @@ -224,8 +227,16 @@ import { Handle, Position } from '@xyflow/react';
import { memo, useMemo } from 'react';

export const MyNodeTemplate = memo(
({ icon, label, description, selected = false, disabled = false, showHandles = true }: WorkflowNodeTemplateProps) => {
const iconElement = useMemo(() => <Icon name={icon} size="large" />, [icon]);
({
icon,
accent,
label,
description,
selected = false,
disabled = false,
showHandles = true,
}: WorkflowNodeTemplateProps) => {
const iconElement = useMemo(() => <Icon name={icon} size="inherit" />, [icon]);

const handleTargetTopId = getHandleId({ handleType: 'target', innerId: 'top' });
const handleTargetLeftId = getHandleId({ handleType: 'target', innerId: 'left' });
Expand All @@ -235,7 +246,7 @@ export const MyNodeTemplate = memo(
return (
<NodePanel.Root selected={selected} disabled={disabled}>
<NodePanel.Header>
<NodeIcon icon={iconElement} disabled={disabled} />
<NodeIcon icon={iconElement} accent={accent} disabled={disabled} />
<NodeDescription label={label} description={description} disabled={disabled} />
</NodePanel.Header>
<NodePanel.Handles isVisible={showHandles}>
Expand All @@ -252,7 +263,7 @@ export const MyNodeTemplate = memo(

The example composes the node from `@workflowbuilder/ui` primitives (`NodePanel.Root`, `NodePanel.Header`, `NodePanel.Handles`) — the same building blocks Workflow Builder uses for its own node renderers, so the result matches the editor's visual language out of the box.

The component receives [`WorkflowNodeTemplateProps`](/api/components/workflownodetemplateprops/). Use [`getHandleId`](/api/utilities/gethandleid/) for handle IDs and pass `innerId` when a node has more than one handle of the same type. If your template needs typed access to `data.properties`, wrap the component in [`defineNodeTemplate`](/api/components/definenodetemplate/) to bind a schema-derived properties type.
The component receives [`WorkflowNodeTemplateProps`](/api/components/workflownodetemplateprops/), including the palette item's `accent`; `NodeIcon` sets the glyph size, so render the icon with `size="inherit"`. Use [`getHandleId`](/api/utilities/gethandleid/) for handle IDs and pass `innerId` when a node has more than one handle of the same type. If your template needs typed access to `data.properties`, wrap the component in [`defineNodeTemplate`](/api/components/definenodetemplate/) to bind a schema-derived properties type.

Wire it through the `nodeTemplates` prop on `<WorkflowBuilder.Root>`:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,47 @@ function NodeHeader({ label, description }) {
}
```

## Accent

`accent` tints the container, hides its border and colors the glyph. `blue`, `green`,
`orange`, `violet` and `gray` are named by hue; `violet-gradient` is a gradient with a white glyph.
Without `accent` the icon keeps the default color. A `disabled` icon keeps its border and the
disabled colors, whatever its accent.

```tsx
<NodeIcon icon={<User />} accent="violet" />
```

## Custom accent

Any other lowercase name works too: the icon reads `--wb-public-node-icon-color-<name>` for the
glyph and `--wb-public-node-icon-container-background-color-<name>` for the container. Define
both for each theme; a name without them falls back to the default color.

```css
:root {
--wb-public-node-icon-color-teal: #0f766e;
--wb-public-node-icon-container-background-color-teal: #ccfbf1;
}

html[data-theme='dark'] {
--wb-public-node-icon-color-teal: #5eead4;
--wb-public-node-icon-container-background-color-teal: #134e4a;
}
```

```tsx
<NodeIcon icon={<User />} accent="teal" />
```

## Size

The container pads the icon with `--wb-public-node-icon-padding` and sets `font-size` to
`--wb-public-node-icon-glyph-size`, so an icon sized in `em` - such as a Phosphor icon, or
the SDK's `Icon` with `size="inherit"` - follows it. To render a larger icon, set the
variables on `NodePanel.Root` or an element above it, not on `NodePanel.Header` or the
icon: the header ports read both to stay centred on the icon.

## Disabled

`disabled` mutes the glyph and the container with the Node Disabled colors.
Expand Down
4 changes: 2 additions & 2 deletions apps/icons/assets/ai-agent.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion apps/icons/src/icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { type WBIcon, iconMap } from '../dist';

type IconProps = {
name: WBIcon;
/** `inherit` follows the surrounding font-size, so a container such as `NodeIcon` sets the glyph size. */
size?: Size;
color?: string;
} & React.SVGProps<SVGSVGElement>;
Expand Down Expand Up @@ -88,10 +89,11 @@ function IconFallback({ size = 'medium' }: Pick<IconProps, 'size'>) {
return <svg style={{ width: computedSize, height: computedSize }} />;
}

type Size = 'extra-large' | 'large' | 'medium' | 'small';
type Size = 'extra-large' | 'large' | 'medium' | 'small' | 'inherit';
const iconSizeMap: Record<Size, string> = {
small: '0.5rem',
medium: '1rem',
large: '1.5rem',
'extra-large': '2rem',
inherit: '1em',
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ type HandlesAlignment = NonNullable<ComponentProps<typeof NodePanel.Handles>['al
// being re-derived per template.
//
// This helper alone does NOT make ports align across nodes. The visual
// stability of the resulting port Y depends on a companion global CSS rule in
// `packages/sdk/src/index.css` (search WB-192): the formula picks 'header' for
// horizontal flow, then the CSS pin anchors the port to the NodeIcon's
// stability of the resulting port Y depends on the header port `top` rule in
// `packages/ui/src/components/node/node-panel/handle.module.css`: the formula picks
// 'header' for horizontal flow, then the CSS pin anchors the port to the NodeIcon's
// vertical center so multi-line descriptions don't shift it. Both layers must
// stay in sync; removing either reintroduces the bug.
export function getHandlesAlignment({ layoutDirection }: { layoutDirection: LayoutDirection }): HandlesAlignment {
Expand Down
6 changes: 6 additions & 0 deletions packages/sdk/src/features/diagram/hooks/use-node-accent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { NodeIconAccent } from '../../../node/common';
import { useStore } from '../../../store/store';

export function useNodeAccent(nodeType: string): NodeIconAccent | undefined {
return useStore((store) => store.getNodeDefinition(nodeType)?.accent);
}
Loading
Loading