Преглед изворни кода

feat: add customizable port colors to WorkflowPortRender (#360)

* feat(workflow-port): add customizable port colors

* fix(workflow-colors): unify CSS variable naming to --g-workflow-* format - Update LineColors enum and demo configuration - Add CSS variable definitions - Ensure consistent naming across all workflow-related variables

---------

Co-authored-by: husky-dot <xiaozhi@xiaozhideMacBook-Pro.local>
Co-authored-by: husky-dot <xiaozhi@172-0-8-36.lightspeed.rcsntx.sbcglobal.net>
小智 пре 7 месеци
родитељ
комит
a6d61d347e

+ 6 - 6
apps/demo-free-layout/src/hooks/use-editor-props.tsx

@@ -77,12 +77,12 @@ export function useEditorProps(
         return json;
       },
       lineColor: {
-        hidden: 'var(--g-line-color-hidden,transparent)',
-        default: 'var(--g-line-color-default,#4d53e8)',
-        drawing: 'var(--g-line-color-drawing, #5DD6E3)',
-        hovered: 'var(--g-line-color-hover,#37d0ff)',
-        selected: 'var(--g-line-color-selected,#37d0ff)',
-        error: 'var(--g-line-color-hover,red)',
+        hidden: 'var(--g-workflow-line-color-hidden,transparent)',
+        default: 'var(--g-workflow-line-color-default,#4d53e8)',
+        drawing: 'var(--g-workflow-line-color-drawing, #5DD6E3)',
+        hovered: 'var(--g-workflow-line-color-hover,#37d0ff)',
+        selected: 'var(--g-workflow-line-color-selected,#37d0ff)',
+        error: 'var(--g-workflow-line-color-error,red)',
       },
       /*
        * Check whether the line can be added

+ 52 - 43
apps/demo-free-layout/src/styles/index.css

@@ -1,71 +1,80 @@
 :root {
-    --g-workflow-port-color-primary: #4d53e8;
-    --g-workflow-port-color-secondary: #9197f1;
-    --g-workflow-port-color-error: #ff0000;
-    --g-workflow-port-color-background: #ffffff;
+  /* Port colors */
+  --g-workflow-port-color-primary: #4d53e8;
+  --g-workflow-port-color-secondary: #9197f1;
+  --g-workflow-port-color-error: #ff0000;
+  --g-workflow-port-color-background: #ffffff;
+
+  /* Line colors */
+  --g-workflow-line-color-hidden: transparent;
+  --g-workflow-line-color-default: #4d53e8;
+  --g-workflow-line-color-drawing: #5dd6e3;
+  --g-workflow-line-color-hover: #37d0ff;
+  --g-workflow-line-color-selected: #37d0ff;
+  --g-workflow-line-color-error: red;
 }
 
 .gedit-selector-bounds-background {
-    cursor: move;
-    display: none !important;
+  cursor: move;
+  display: none !important;
 }
 
 .gedit-selector-bounds-foreground {
-    cursor: move;
-    position: absolute;
-    left: 0;
-    top: 0;
-    width: 0;
-    height: 0;
-    outline: 1px solid var(--g-playground-selectBox-outline);
-    z-index: 33;
-    background-color: var(--g-playground-selectBox-background);
+  cursor: move;
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 0;
+  height: 0;
+  outline: 1px solid var(--g-playground-selectBox-outline);
+  z-index: 33;
+  background-color: var(--g-playground-selectBox-background);
 }
 
 @keyframes blink {
-    0% {
-        opacity: 1;
-    }
-    50% {
-        opacity: 0;
-    }
-    100% {
-        opacity: 1;
-    }
+  0% {
+    opacity: 1;
+  }
+  50% {
+    opacity: 0;
+  }
+  100% {
+    opacity: 1;
+  }
 }
 
 .node-running {
-    border: 1px dashed rgb(78, 64, 229) !important;
-    border-radius: 8px;
+  border: 1px dashed rgb(78, 64, 229) !important;
+  border-radius: 8px;
 }
 .demo-editor {
-    flex-grow: 1;
-    position: relative;
-    height: 100%;
+  flex-grow: 1;
+  position: relative;
+  height: 100%;
 }
 
 .demo-container {
-    position: absolute;
-    left: 0px;
-    top: 0px;
-    display: flex;
-    width: 100%;
-    height: 100%;
-    flex-direction: column;
+  position: absolute;
+  left: 0px;
+  top: 0px;
+  display: flex;
+  width: 100%;
+  height: 100%;
+  flex-direction: column;
 }
 
 .demo-tools {
-    padding: 10px;
-    display: flex;
-    justify-content: space-between;
+  padding: 10px;
+  display: flex;
+  justify-content: space-between;
 }
 
 .demo-tools-group > * {
-    margin-right: 8px;
+  margin-right: 8px;
 }
 
 .mouse-pad-option-icon {
-    display: flex;
-    justify-content: center;
-    align-items: center;
+  display: flex;
+  justify-content: center;
+  align-items: center;
 }

+ 9 - 1
apps/docs/src/en/api/components/workflow-node-renderer.mdx

@@ -17,7 +17,15 @@ export const BaseNode = () => {
    * https://github.com/bytedance/flowgram.ai/blob/main/packages/client/free-layout-editor/src/components/workflow-node-renderer.tsx
    */
   return (
-    <WorkflowNodeRenderer className="demo-free-node" node={props.node}>
+    <WorkflowNodeRenderer
+      className="demo-free-node"
+      node={props.node}
+      // Optional port color customization
+      portPrimaryColor="#4d53e8"        // Active state color (linked/hovered)
+      portSecondaryColor="#9197f1"      // Default state color
+      portErrorColor="#ff4444"          // Error state color
+      portBackgroundColor="#ffffff"     // Background color
+    >
       {
         // Form rendering through formMeta generation
         form?.render()

+ 20 - 1
apps/docs/src/en/guide/advanced/free-layout/port.mdx

@@ -56,6 +56,15 @@ function BaseNode() {
 
 Ports are ultimately rendered through the `WorkflowPortRender` component, supporting custom styles, or businesses can reimplement this component based on the source code, see [Free Layout Best Practices - Node Rendering](https://github.com/bytedance/flowgram.ai/blob/main/apps/demo-free-layout/src/components/base-node/node-wrapper.tsx)
 
+### Custom Port Colors
+
+You can customize port colors by passing color props to `WorkflowPortRender`:
+
+- `primaryColor` - Active state color (when linked or hovered)
+- `secondaryColor` - Default state color
+- `errorColor` - Error state color
+- `backgroundColor` - Background color
+
 ```tsx pure
 
 import { WorkflowPortRender, useNodeRender } from '@flowgram.ai/free-layout-editor';
@@ -67,7 +76,17 @@ function BaseNode() {
       <div data-port-id="condition-if-0" data-port-type="output"></div>
       <div data-port-id="condition-if-1" data-port-type="output"></div>
       {ports.map((p) => (
-        <WorkflowPortRender key={p.id} entity={p} className="xxx" style={{ /* custom style */}}/>
+        <WorkflowPortRender
+          key={p.id}
+          entity={p}
+          className="xxx"
+          style={{ /* custom style */}}
+          // Custom port colors
+          primaryColor="#4d53e8"        // Active state color (linked/hovered)
+          secondaryColor="#9197f1"      // Default state color
+          errorColor="#ff4444"          // Error state color
+          backgroundColor="#ffffff"     // Background color
+        />
       ))}
     </div>
   )

+ 9 - 1
apps/docs/src/zh/api/components/workflow-node-renderer.mdx

@@ -17,7 +17,15 @@ export const BaseNode = () => {
    * https://github.com/bytedance/flowgram.ai/blob/main/packages/client/free-layout-editor/src/components/workflow-node-renderer.tsx
    */
   return (
-    <WorkflowNodeRenderer className="demo-free-node" node={props.node}>
+    <WorkflowNodeRenderer
+      className="demo-free-node"
+      node={props.node}
+      // 可选的端口颜色自定义
+      portPrimaryColor="#4d53e8"        // 激活状态颜色 (linked/hovered)
+      portSecondaryColor="#9197f1"      // 默认状态颜色
+      portErrorColor="#ff4444"          // 错误状态颜色
+      portBackgroundColor="#ffffff"     // 背景颜色
+    >
       {
         // 表单渲染通过 formMeta 生成
         form?.render()

+ 20 - 1
apps/docs/src/zh/guide/advanced/free-layout/port.mdx

@@ -56,6 +56,15 @@ function BaseNode() {
 
 端口最终通过 `WorkflowPortRender` 组件渲染,支持自定义 style, 或者业务基于源码重新实现该组件, 参考 [自由布局最佳实践 - 节点渲染](https://github.com/bytedance/flowgram.ai/blob/main/apps/demo-free-layout/src/components/base-node/node-wrapper.tsx)
 
+### 自定义端口颜色
+
+可以通过向 `WorkflowPortRender` 传递颜色 props 来自定义端口颜色:
+
+- `primaryColor` - 激活状态颜色(linked/hovered)
+- `secondaryColor` - 默认状态颜色
+- `errorColor` - 错误状态颜色
+- `backgroundColor` - 背景颜色
+
 ```tsx pure
 
 import { WorkflowPortRender, useNodeRender } from '@flowgram.ai/free-layout-editor';
@@ -67,7 +76,17 @@ function BaseNode() {
       <div data-port-id="condition-if-0" data-port-type="output"></div>
       <div data-port-id="condition-if-1" data-port-type="output"></div>
       {ports.map((p) => (
-        <WorkflowPortRender key={p.id} entity={p} className="xxx" style={{ /* custom style */}}/>
+        <WorkflowPortRender
+          key={p.id}
+          entity={p}
+          className="xxx"
+          style={{ /* custom style */}}
+          // 自定义端口颜色
+          primaryColor="#4d53e8"        // 激活状态颜色(linked/hovered)
+          secondaryColor="#9197f1"      // 默认状态颜色
+          errorColor="#ff4444"          // 错误状态颜色
+          backgroundColor="#ffffff"     // 背景颜色
+        />
       ))}
     </div>
   )

+ 6 - 6
packages/canvas-engine/free-layout-core/src/typings/workflow-line.ts

@@ -24,12 +24,12 @@ export interface LineColor {
 }
 
 export enum LineColors {
-  HIDDEN = 'var(--g-line-color-hidden,transparent)', // 隐藏线条
-  DEFUALT = 'var(--g-line-color-default,#4d53e8)',
-  DRAWING = 'var(--g-line-color-drawing, #5DD6E3)', // '#b5bbf8', // '#9197F1',
-  HOVER = 'var(--g-line-color-hover,#37d0ff)',
-  SELECTED = 'var(--g-line-color-selected,#37d0ff)',
-  ERROR = 'var(--g-line-color-error,red)',
+  HIDDEN = 'var(--g-workflow-line-color-hidden,transparent)', // 隐藏线条
+  DEFUALT = 'var(--g-workflow-line-color-default,#4d53e8)',
+  DRAWING = 'var(--g-workflow-line-color-drawing, #5DD6E3)', // '#b5bbf8', // '#9197F1',
+  HOVER = 'var(--g-workflow-line-color-hover,#37d0ff)',
+  SELECTED = 'var(--g-workflow-line-color-selected,#37d0ff)',
+  ERROR = 'var(--g-workflow-line-color-error,red)',
 }
 
 export interface WorkflowLineRenderContribution {

+ 12 - 0
packages/client/free-layout-editor/src/components/workflow-node-renderer.tsx

@@ -19,6 +19,14 @@ export interface WorkflowNodeProps {
     port: WorkflowPortEntity,
     e: React.MouseEvent<HTMLDivElement> | React.MouseEventHandler<HTMLDivElement>
   ) => void;
+  /** 端口激活状态颜色 (linked/hovered) */
+  portPrimaryColor?: string;
+  /** 端口默认状态颜色 */
+  portSecondaryColor?: string;
+  /** 端口错误状态颜色 */
+  portErrorColor?: string;
+  /** 端口背景颜色 */
+  portBackgroundColor?: string;
 }
 
 export const WorkflowNodeRenderer: React.FC<WorkflowNodeProps> = (props) => {
@@ -50,6 +58,10 @@ export const WorkflowNodeRenderer: React.FC<WorkflowNodeProps> = (props) => {
           onClick={props.onPortClick ? (e) => props.onPortClick!(p, e) : undefined}
           className={props.portClassName}
           style={props.portStyle}
+          primaryColor={props.portPrimaryColor}
+          secondaryColor={props.portSecondaryColor}
+          errorColor={props.portErrorColor}
+          backgroundColor={props.portBackgroundColor}
         />
       ))}
     </>

+ 29 - 1
packages/plugins/free-lines-plugin/src/components/workflow-port-render/index.tsx

@@ -19,6 +19,14 @@ export interface WorkflowPortRenderProps {
   className?: string;
   style?: React.CSSProperties;
   onClick?: React.MouseEventHandler<HTMLDivElement>;
+  /** 激活状态颜色 (linked/hovered) */
+  primaryColor?: string;
+  /** 默认状态颜色 */
+  secondaryColor?: string;
+  /** 错误状态颜色 */
+  errorColor?: string;
+  /** 背景颜色 */
+  backgroundColor?: string;
 }
 
 export const WorkflowPortRender: React.FC<WorkflowPortRenderProps> =
@@ -79,10 +87,30 @@ export const WorkflowPortRender: React.FC<WorkflowPortRenderProps> =
       // 有线条链接的时候深蓝色小圆点
       linked,
     });
+
+    // 构建 CSS 自定义属性用于颜色覆盖
+    const colorStyles: Record<string, string> = {};
+    if (props.primaryColor) {
+      colorStyles['--g-workflow-port-color-primary'] = props.primaryColor;
+    }
+    if (props.secondaryColor) {
+      colorStyles['--g-workflow-port-color-secondary'] = props.secondaryColor;
+    }
+    if (props.errorColor) {
+      colorStyles['--g-workflow-port-color-error'] = props.errorColor;
+    }
+    if (props.backgroundColor) {
+      colorStyles['--g-workflow-port-color-background'] = props.backgroundColor;
+    }
+
+    const combinedStyle = targetElement
+      ? { ...props.style, ...colorStyles }
+      : { ...props.style, ...colorStyles, left: posX, top: posY };
+
     const content = (
       <WorkflowPointStyle
         className={className}
-        style={targetElement ? props.style : { ...props.style, left: posX, top: posY }}
+        style={combinedStyle}
         onClick={onClick}
         data-port-entity-id={entity.id}
         data-port-entity-type={entity.portType}