diff --git a/packages/sdk/src/features/diagram/nodes/workflow-node-template/workflow-node-template.tsx b/packages/sdk/src/features/diagram/nodes/workflow-node-template/workflow-node-template.tsx
index 9eb025dbd..13608bb7f 100644
--- a/packages/sdk/src/features/diagram/nodes/workflow-node-template/workflow-node-template.tsx
+++ b/packages/sdk/src/features/diagram/nodes/workflow-node-template/workflow-node-template.tsx
@@ -46,6 +46,8 @@ export type WorkflowNodeTemplateProps;
selected?: boolean;
+ /** Render the Node Disabled variant (palette entries that cannot be added). */
+ disabled?: boolean;
layoutDirection?: LayoutDirection;
isConnecting?: boolean;
showHandles?: boolean;
@@ -61,6 +63,7 @@ const WorkflowNodeTemplateComponent = memo(
description,
layoutDirection = 'RIGHT',
selected = false,
+ disabled = false,
showHandles = true,
isValid,
children,
@@ -80,10 +83,10 @@ const WorkflowNodeTemplateComponent = memo(
return (
-
+
-
-
+
+
{!!children && }
diff --git a/packages/sdk/src/features/palette/components/items/palette-item.module.css b/packages/sdk/src/features/palette/components/items/palette-item.module.css
index 3cc503c2d..a5238a53e 100644
--- a/packages/sdk/src/features/palette/components/items/palette-item.module.css
+++ b/packages/sdk/src/features/palette/components/items/palette-item.module.css
@@ -1,12 +1,8 @@
.item {
- border-radius: 0.75rem;
cursor: grab;
- outline-offset: -1px;
- outline: 1px solid transparent;
}
.disabled {
cursor: default;
user-select: none;
- opacity: 0.5;
}
diff --git a/packages/sdk/src/features/palette/components/items/palette-item.tsx b/packages/sdk/src/features/palette/components/items/palette-item.tsx
index ecf337864..cf898cc33 100644
--- a/packages/sdk/src/features/palette/components/items/palette-item.tsx
+++ b/packages/sdk/src/features/palette/components/items/palette-item.tsx
@@ -24,7 +24,7 @@ export function PaletteItem({ item, onDragStart, onMouseDown, isDisabled = false
onMouseDown={() => onMouseDown(item.type)}
onDragStart={onDragStart}
>
-
+
);
}
diff --git a/packages/sdk/src/features/palette/node-preview-container.spec.tsx b/packages/sdk/src/features/palette/node-preview-container.spec.tsx
index e5349dae4..16d7ead61 100644
--- a/packages/sdk/src/features/palette/node-preview-container.spec.tsx
+++ b/packages/sdk/src/features/palette/node-preview-container.spec.tsx
@@ -15,7 +15,11 @@ vi.mock('../diagram/nodes/start-node-template/start-node-template', () => ({
StartNodeTemplate: () => null,
}));
vi.mock('../diagram/nodes/workflow-node-template/workflow-node-template', () => ({
- WorkflowNodeTemplate: (props: WorkflowNodeTemplateProps) => {props.label}
,
+ WorkflowNodeTemplate: (props: WorkflowNodeTemplateProps) => (
+
+ {props.label}
+
+ ),
}));
let mockNodeDefinition: PaletteItem | undefined;
@@ -78,6 +82,14 @@ describe('NodePreviewContainer', () => {
expect(screen.queryByTestId('custom-template')).toBeNull();
});
+ it('passes the selected and disabled node states to the template', () => {
+ render();
+
+ const element = screen.getByTestId('built-in-template');
+ expect(element.dataset.selected).toBe('true');
+ expect(element.dataset.disabled).toBe('true');
+ });
+
it('renders nothing when the palette type is unknown', () => {
mockNodeDefinition = undefined;
diff --git a/packages/sdk/src/features/palette/node-preview-container.tsx b/packages/sdk/src/features/palette/node-preview-container.tsx
index 0fc22636b..f9e7e1fa6 100644
--- a/packages/sdk/src/features/palette/node-preview-container.tsx
+++ b/packages/sdk/src/features/palette/node-preview-container.tsx
@@ -27,11 +27,18 @@ const BUILT_IN_TEMPLATES: NodeTemplateRegistry = {
[NodeType.DecisionNode]: DecisionNodeTemplate,
};
-type NodePreviewContainerProps = {
+type NodeStateProps = {
+ /** Drag preview: the Node Active state (outline and ring). */
+ selected?: boolean;
+ /** Unavailable palette entry: the Node Disabled state. */
+ disabled?: boolean;
+};
+
+type NodePreviewContainerProps = NodeStateProps & {
type: string;
};
-export function NodePreviewContainer({ type }: NodePreviewContainerProps) {
+export function NodePreviewContainer({ type, selected, disabled }: NodePreviewContainerProps) {
const getNodeDefinition = useStore((state) => state.getNodeDefinition);
const nodeDefinition = getNodeDefinition(type);
@@ -39,14 +46,14 @@ export function NodePreviewContainer({ type }: NodePreviewContainerProps) {
return;
}
- return ;
+ return ;
}
-type NodePreviewProps = {
+type NodePreviewProps = NodeStateProps & {
nodeDefinition: PaletteItem;
};
-function NodePreview({ nodeDefinition }: NodePreviewProps) {
+function NodePreview({ nodeDefinition, selected, disabled }: NodePreviewProps) {
const { type, icon, label, description, templateType = NodeType.Node } = nodeDefinition;
const translateIfPossible = useTranslateIfPossible();
@@ -58,5 +65,15 @@ function NodePreview({ nodeDefinition }: NodePreviewProps) {
const templateKey = resolveReactFlowNodeType(type, templateType, custom);
const TemplateComponent = custom[templateKey] ?? BUILT_IN_TEMPLATES[templateKey] ?? BUILT_IN_TEMPLATES[NodeType.Node];
- return ;
+ return (
+
+ );
}
diff --git a/packages/sdk/src/features/palette/palette-container.tsx b/packages/sdk/src/features/palette/palette-container.tsx
index 78544ac4c..de2d69532 100644
--- a/packages/sdk/src/features/palette/palette-container.tsx
+++ b/packages/sdk/src/features/palette/palette-container.tsx
@@ -1,7 +1,6 @@
import { useEffect } from 'react';
import styles from './palette-container.module.css';
-import './variables.css';
import { Sidebar } from '../../components/sidebar/sidebar';
import { useStore } from '../../store/store';
@@ -50,7 +49,7 @@ export function PaletteContainer() {
/>
{draggedItem && (
-
+
)}
diff --git a/packages/sdk/src/features/palette/variables.css b/packages/sdk/src/features/palette/variables.css
deleted file mode 100644
index 2915411e0..000000000
--- a/packages/sdk/src/features/palette/variables.css
+++ /dev/null
@@ -1,4 +0,0 @@
-:root {
- --wb-sdk-palette-item-outline-color-hover: var(--wb-ds-canvas-node-stroke-hover);
- --wb-sdk-palette-item-shad-color-hover: rgba(21, 21, 22, 0.1);
-}
diff --git a/packages/ui/src/components/node/node-description/node-description.module.css b/packages/ui/src/components/node/node-description/node-description.module.css
index 9981c910b..bb2ab5e9d 100644
--- a/packages/ui/src/components/node/node-description/node-description.module.css
+++ b/packages/ui/src/components/node/node-description/node-description.module.css
@@ -1,6 +1,8 @@
:root {
--wb-public-node-title-color: var(--wb-ds-ui-text-default);
--wb-public-node-title-subtitle: var(--wb-ds-ui-text-subtle-default);
+ --wb-public-node-title-color-disabled: var(--wb-ds-canvas-node-text-disabled);
+ --wb-public-node-subtitle-color-disabled: var(--wb-ds-canvas-node-text-disabled);
}
@layer ui.component {
@@ -29,5 +31,15 @@
.subtitle {
color: var(--wb-public-node-title-subtitle);
}
+
+ &.disabled {
+ .title {
+ color: var(--wb-public-node-title-color-disabled);
+ }
+
+ .subtitle {
+ color: var(--wb-public-node-subtitle-color-disabled);
+ }
+ }
}
}
diff --git a/packages/ui/src/components/node/node-description/node-description.spec.tsx b/packages/ui/src/components/node/node-description/node-description.spec.tsx
index 30cdd515d..3fea8e0b3 100644
--- a/packages/ui/src/components/node/node-description/node-description.spec.tsx
+++ b/packages/ui/src/components/node/node-description/node-description.spec.tsx
@@ -30,6 +30,12 @@ describe('NodeDescription', () => {
expect(subtitle.getAttribute('title')).toBe('Sends the ticket to the right responder.');
});
+ it('marks the block as disabled', () => {
+ act(() => root.render());
+
+ expect(container.firstElementChild?.className).toMatch(/disabled/);
+ });
+
it('renders no tooltip attribute when there is no description', () => {
act(() => root.render());
diff --git a/packages/ui/src/components/node/node-description/node-description.tsx b/packages/ui/src/components/node/node-description/node-description.tsx
index 80a8a9bd5..263707348 100644
--- a/packages/ui/src/components/node/node-description/node-description.tsx
+++ b/packages/ui/src/components/node/node-description/node-description.tsx
@@ -5,12 +5,14 @@ import styles from './node-description.module.css';
export type NodeDescriptionProps = {
label: string;
description?: string;
+ /** Muted title and subtitle of the Node Disabled variant. */
+ disabled?: boolean;
className?: string;
};
-export function NodeDescription({ label, description, className }: NodeDescriptionProps) {
+export function NodeDescription({ label, description, disabled = false, className }: NodeDescriptionProps) {
return (
-
+
{label}
diff --git a/packages/ui/src/components/node/node-icon/node-icon.module.css b/packages/ui/src/components/node/node-icon/node-icon.module.css
index 1f42a5734..f2e5e123a 100644
--- a/packages/ui/src/components/node/node-icon/node-icon.module.css
+++ b/packages/ui/src/components/node/node-icon/node-icon.module.css
@@ -6,6 +6,8 @@
--wb-public-node-icon-color: var(--wb-ds-canvas-node-icon-primary);
--wb-public-node-icon-container-border-color: var(--wb-ds-canvas-node-stroke-default);
--wb-public-node-icon-container-background-color: var(--wb-ds-canvas-node-bg-secondary-default);
+ --wb-public-node-icon-color-disabled: var(--wb-ds-ui-icon-disabled);
+ --wb-public-node-icon-container-background-color-disabled: var(--wb-ds-canvas-node-bg-primary-disabled);
}
@layer ui.component {
@@ -19,5 +21,10 @@
border-radius: var(--wb-public-node-icon-border-radius);
border: var(--wb-public-node-icon-border-size) solid var(--wb-public-node-icon-container-border-color);
background: var(--wb-public-node-icon-container-background-color);
+
+ &.disabled {
+ color: var(--wb-public-node-icon-color-disabled);
+ background: var(--wb-public-node-icon-container-background-color-disabled);
+ }
}
}
diff --git a/packages/ui/src/components/node/node-icon/node-icon.tsx b/packages/ui/src/components/node/node-icon/node-icon.tsx
index 7060949c7..f2e818557 100644
--- a/packages/ui/src/components/node/node-icon/node-icon.tsx
+++ b/packages/ui/src/components/node/node-icon/node-icon.tsx
@@ -5,9 +5,11 @@ import styles from './node-icon.module.css';
export type NodeIconProps = {
icon: ReactNode;
+ /** Muted glyph and container of the Node Disabled variant. */
+ disabled?: boolean;
className?: string;
};
-export function NodeIcon({ icon, className }: NodeIconProps) {
- return
{icon}
;
+export function NodeIcon({ icon, disabled = false, className }: NodeIconProps) {
+ return
{icon}
;
}
diff --git a/packages/ui/src/components/node/node-panel/node-panel.module.css b/packages/ui/src/components/node/node-panel/node-panel.module.css
index 4a8e962f0..cffac5194 100644
--- a/packages/ui/src/components/node/node-panel/node-panel.module.css
+++ b/packages/ui/src/components/node/node-panel/node-panel.module.css
@@ -12,6 +12,8 @@
--wb-public-node-hover-background-color: var(--wb-ds-canvas-node-bg-primary-default);
--wb-public-node-border-hover-color: var(--wb-ds-canvas-node-stroke-hover);
+ --wb-public-node-background-color-disabled: var(--wb-ds-canvas-node-bg-primary-disabled);
+
--wb-public-node-border-color-selected: var(--wb-ds-canvas-node-stroke-hover);
--wb-public-node-box-shadow-selected: var(--wb-ds-shadow-canvas-focus-node-x) var(--wb-ds-shadow-canvas-focus-node-y)
var(--wb-ds-shadow-canvas-focus-node-blur) var(--wb-ds-shadow-canvas-focus-node-spread)
@@ -19,7 +21,7 @@
}
@layer ui.component {
- .node-panel-wrapper:hover .container {
+ .node-panel-wrapper:hover .container:not(.disabled) {
background: var(--wb-public-node-hover-background-color);
border-color: var(--wb-public-node-border-hover-color);
}
@@ -42,6 +44,10 @@
box-shadow: var(--wb-public-node-box-shadow-selected);
}
+ &.disabled {
+ background: var(--wb-public-node-background-color-disabled);
+ }
+
&:not(:hover) {
transition:
border-color var(--wb-public-transition),
diff --git a/packages/ui/src/components/node/node-panel/node-panel.spec.tsx b/packages/ui/src/components/node/node-panel/node-panel.spec.tsx
new file mode 100644
index 000000000..4e16fd058
--- /dev/null
+++ b/packages/ui/src/components/node/node-panel/node-panel.spec.tsx
@@ -0,0 +1,39 @@
+import { act } from 'react';
+import { createRoot } from 'react-dom/client';
+import { afterEach, describe, expect, it } from 'vitest';
+
+import { NodePanel } from './node-panel';
+
+const containers: HTMLDivElement[] = [];
+
+function renderRoot(props: { selected: boolean; disabled?: boolean }) {
+ const container = document.createElement('div');
+ document.body.append(container);
+ containers.push(container);
+ act(() => {
+ createRoot(container).render(
+
+ header
+ ,
+ );
+ });
+ return container.firstElementChild!.firstElementChild as HTMLElement;
+}
+
+afterEach(() => {
+ for (const container of containers.splice(0)) container.remove();
+});
+
+describe('NodePanel.Root states', () => {
+ it('marks the shell as selected', () => {
+ const shell = renderRoot({ selected: true });
+ expect(shell.className).toMatch(/selected/);
+ expect(shell.className).not.toMatch(/disabled/);
+ });
+
+ it('marks the shell as disabled', () => {
+ const shell = renderRoot({ selected: false, disabled: true });
+ expect(shell.className).toMatch(/disabled/);
+ expect(shell.className).not.toMatch(/selected/);
+ });
+});
diff --git a/packages/ui/src/components/node/node-panel/node-panel.tsx b/packages/ui/src/components/node/node-panel/node-panel.tsx
index c579a2b9d..6c8652a68 100644
--- a/packages/ui/src/components/node/node-panel/node-panel.tsx
+++ b/packages/ui/src/components/node/node-panel/node-panel.tsx
@@ -7,6 +7,12 @@ import nodeStyles from './node-panel.module.css';
type Props = {
/** Whether the node panel is selected */
selected: boolean;
+ /**
+ * Renders the node in its disabled state (muted surface, text and icon,
+ * no hover). Used for node types that cannot be added right now, for
+ * example palette entries in read-only mode.
+ */
+ disabled?: boolean;
/** The content of the node panel */
children?: React.ReactNode;
/** css className of the node panel */
@@ -64,7 +70,7 @@ const Handles = memo(function Handles({
return <>{isVisible && children}>;
});
-const Root = memo(function Root({ selected, children, className }: Props) {
+const Root = memo(function Root({ selected, disabled = false, children, className }: Props) {
const { headerComponent, contentComponent, handlesComponent, handlesAlignment, hasHandles } = useMemo(() => {
const childrenArray = Children.toArray(children);
@@ -91,6 +97,7 @@ const Root = memo(function Root({ selected, children, className }: Props) {