chore: import upstream snapshot with attribution
CI / build (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:09:51 +08:00
commit 0232b4e2bb
3528 changed files with 291404 additions and 0 deletions
+104
View File
@@ -0,0 +1,104 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
.flowgram-logo-container {
position: absolute;
top: 50px;
width: calc(30vw - 64px);
height: 550px;
opacity: 0;
// Mobile responsive: move to top on small screens to prevent text overlap
@media (max-width: 768px) {
top: 0;
right: 0;
width: 100%;
height: 300px;
margin-bottom: 20px;
pointer-events: none;
}
// Tablet responsive: adjust size and position
@media (min-width: 769px) and (max-width: 1024px) {
width: calc(45vw - 32px);
right: 16px;
}
// Ensure minimum width to prevent squashing
@media (min-width: 1025px) and (max-width: 1300px) {
width: calc(45vw - 32px);
right: 0;
}
// Ensure minimum width to prevent squashing
@media (min-width: 1301) {
width: calc(30vw - 64px);
right: 15vw;
}
}
.flowgram-logo-mask {
background-image: conic-gradient(from 180deg at 50% 50%, #4161a6, #5681bd, #4a9da3, #479590, #4161a6);
filter: blur(120px);
opacity: 0.4;
z-index: 0;
border-radius: 100%;
width: 80%;
height: 80%;
animation: flowgram-logo-spin 2s linear infinite;
}
@keyframes flowgram-logo-spin {
from {
transform: translateX(20%) translateY(20%) rotate(0deg) scale(0.8);
}
50% {
transform: translateX(20%) translateY(20%) rotate(180deg) scale(1);
}
to {
transform: translateX(20%) translateY(20%) rotate(360deg) scale(0.8);
}
}
.gedit-playground {
background: transparent !important;
}
.gedit-playground-scroll-right-block {
display: none;
}
.gedit-playground-scroll-bottom-block {
display: none;
}
.flowgram-logo-node {
width: 60px;
min-height: 150px;
height: auto;
border-radius: 16px;
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.04), 0 4px 12px 0 rgba(0, 0, 0, 0.02);
display: flex;
flex-direction: column;
justify-content: center;
position: relative;
padding: 12px;
cursor: move;
transition: box-shadow 0.3s ease-in-out, transform 0.2s ease-in-out;
&:hover {
box-shadow:
0 2px 6px 0 rgba(0, 0, 0, 0.04),
0 4px 12px 0 rgba(0, 0, 0, 0.02),
0 0 16px 2px rgba(var(--glow-color, 59, 130, 246), 0.6),
0 0 32px 6px rgba(var(--glow-color, 59, 130, 246), 0.4),
0 0 48px 12px rgba(var(--glow-color, 59, 130, 246), 0.25),
0 0 64px 16px rgba(var(--glow-color, 59, 130, 246), 0.15);
transform: scale(1.05);
}
}
+25
View File
@@ -0,0 +1,25 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import '@flowgram.ai/free-layout-editor/index.css';
import { FreeLayoutEditorProvider, EditorRenderer } from '@flowgram.ai/free-layout-editor';
import './index.less';
import { useEditorProps } from './use-editor-props';
import { FlowGramLogoMask } from './musk';
export const FlowGramLogo = () => {
const editorProps = useEditorProps();
return (
<div className="flowgram-logo-container">
<FlowGramLogoMask />
<FreeLayoutEditorProvider {...editorProps}>
<EditorRenderer />
</FreeLayoutEditorProvider>
</div>
);
};
+53
View File
@@ -0,0 +1,53 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { WorkflowJSON } from '@flowgram.ai/free-layout-editor';
export const initialData: WorkflowJSON = {
nodes: [
{
id: '1',
type: 'start',
meta: {
position: { x: 0, y: 0 },
},
},
{
id: '2',
type: 'custom',
meta: {
position: { x: 110, y: 0 },
},
},
{
id: '3',
type: 'custom',
meta: {
position: { x: 220, y: 0 },
},
},
{
id: '4',
type: 'end',
meta: {
position: { x: 330, y: 0 },
},
},
],
edges: [
{
sourceNodeID: '1',
targetNodeID: '2',
},
{
sourceNodeID: '2',
targetNodeID: '3',
},
{
sourceNodeID: '3',
targetNodeID: '4',
},
],
};
+20
View File
@@ -0,0 +1,20 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { useDark } from '@rspress/core/runtime';
export const FlowGramLogoMask = () => {
const isDark = useDark();
return (
<div
className="flowgram-logo-mask"
style={{
backgroundImage: isDark
? 'conic-gradient(from 180deg at 50% 50%, #0095ff 0deg, 180deg, #42d392 1turn)'
: 'conic-gradient(from 180deg at 50% 50%, #3473fb 0deg, 180deg, #46cbc2 1turn)',
}}
/>
);
};
+25
View File
@@ -0,0 +1,25 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
export const NodeColorMap: Record<string, string> = {
'1': '#4161A6',
'2': '#5681BD',
'3': '#5DBBC1',
'4': '#8BC9C5',
};
export const NodeBorderColorMap: Record<string, string> = {
'1': '#355397',
'2': '#426ca7',
'3': '#46a6ac',
'4': '#7cbab6',
};
export const NodeGlowColorMap: Record<string, string> = {
'1': '141, 178, 254',
'2': '145, 190, 254',
'3': '155, 248, 254',
'4': '191, 255, 250',
};
+31
View File
@@ -0,0 +1,31 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { WorkflowNodeRegistry } from '@flowgram.ai/free-layout-editor';
export const nodeRegistries: WorkflowNodeRegistry[] = [
{
type: 'start',
meta: {
isStart: true,
deleteDisable: true,
copyDisable: true,
defaultPorts: [{ type: 'output' }],
},
},
{
type: 'end',
meta: {
deleteDisable: true,
copyDisable: true,
defaultPorts: [{ type: 'input' }],
},
},
{
type: 'custom',
meta: {},
defaultPorts: [{ type: 'output' }, { type: 'input' }],
},
];
+43
View File
@@ -0,0 +1,43 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import '@flowgram.ai/free-layout-editor/index.css';
import {
useNodeRender,
WorkflowNodeProps,
WorkflowNodeRenderer,
} from '@flowgram.ai/free-layout-editor';
import { PortRender } from './port';
import { NodeBorderColorMap, NodeColorMap, NodeGlowColorMap } from './node-color';
export const NodeRender = (props: WorkflowNodeProps) => {
const { selected, node, ports } = useNodeRender();
const nodeColor = NodeColorMap[node.id] ?? '#fff';
const borderColor = NodeBorderColorMap[node.id] ?? '#fff';
const glowColor = NodeGlowColorMap[node.id] ?? '59, 130, 246';
return (
<WorkflowNodeRenderer
className="flowgram-logo-node"
style={
{
background: nodeColor,
border: selected ? `2px solid ${borderColor}` : `2px solid ${nodeColor}`,
'--glow-color': glowColor,
} as React.CSSProperties & { '--glow-color': string }
}
portStyle={{
display: 'none',
}}
node={props.node}
>
{ports.map((p) => (
<PortRender key={p.id} entity={p} />
))}
</WorkflowNodeRenderer>
);
};
+117
View File
@@ -0,0 +1,117 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React, { useEffect, useState } from 'react';
import {
useService,
WorkflowHoverService,
WorkflowLinesManager,
WorkflowPortEntity,
} from '@flowgram.ai/free-layout-editor';
import { NodeColorMap } from './node-color';
export interface WorkflowPortRenderProps {
entity: WorkflowPortEntity;
className?: string;
style?: React.CSSProperties;
onClick?: (e: React.MouseEvent<HTMLDivElement>, port: WorkflowPortEntity) => void;
/** 激活状态颜色 (linked/hovered) */
primaryColor?: string;
/** 默认状态颜色 */
secondaryColor?: string;
/** 错误状态颜色 */
errorColor?: string;
/** 背景颜色 */
backgroundColor?: string;
}
export const PortRender: React.FC<WorkflowPortRenderProps> =
// eslint-disable-next-line react/display-name
React.memo<WorkflowPortRenderProps>((props: WorkflowPortRenderProps) => {
const hoverService = useService<WorkflowHoverService>(WorkflowHoverService);
const { entity } = props;
const { relativePosition } = entity;
const [targetElement, setTargetElement] = useState(entity.targetElement);
const [posX, updatePosX] = useState(relativePosition.x);
const [posY, updatePosY] = useState(relativePosition.y);
const [hovered, setHovered] = useState(false);
useEffect(() => {
// useEffect 时序问题可能导致 port.hasError 非最新,需重新触发一次 validate
entity.validate();
const dispose = entity.onEntityChange(() => {
// 如果有挂载的节点,不需要更新位置信息
if (entity.targetElement) {
if (entity.targetElement !== targetElement) {
setTargetElement(entity.targetElement);
}
return;
}
const newPos = entity.relativePosition;
// 加上 round 避免点位抖动
updatePosX(Math.round(newPos.x));
updatePosY(Math.round(newPos.y));
});
const dispose2 = hoverService.onHoveredChange((id) => {
setHovered(hoverService.isHovered(entity.id));
});
return () => {
dispose.dispose();
dispose2.dispose();
};
}, [hoverService, entity, targetElement]);
// 构建 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 content = (
<div
style={{
width: '24px',
height: '24px',
borderRadius: '50%',
marginTop: '-12px',
marginLeft: '-12px',
position: 'absolute',
background: '#fff',
border: 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
left: posX,
top: posY,
}}
data-port-entity-id={entity.id}
data-port-entity-type={entity.portType}
data-testid="sdk.workflow.canvas.node.port"
>
<div
style={{
width: hovered ? '20px' : '16px',
height: hovered ? '20px' : '16px',
borderRadius: '50%',
background: NodeColorMap[entity.node.id] ?? '#fff',
transition: 'width 0.2s ease, height 0.2s ease',
}}
/>
</div>
);
return content;
});
+192
View File
@@ -0,0 +1,192 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import type { PositionSchema } from '@flowgram.ai/free-layout-editor';
export interface PositionGroup {
[key: string]: PositionSchema;
}
const originPosition: PositionGroup = {
'1': {
x: 0,
y: 0,
},
'2': {
x: 110,
y: 0,
},
'3': {
x: 220,
y: 0,
},
'4': {
x: 330,
y: 0,
},
};
export const positionGroups: PositionGroup[] = [
// 水平线形态
{
'1': {
x: 60,
y: 0,
},
'2': {
x: 120,
y: 0,
},
'3': {
x: 180,
y: 0,
},
'4': {
x: 240,
y: 0,
},
},
originPosition,
// 锯齿形态
{
'1': {
x: 0,
y: -40,
},
'2': {
x: 110,
y: 40,
},
'3': {
x: 220,
y: -40,
},
'4': {
x: 330,
y: 40,
},
},
originPosition,
// 弧形形态
{
'1': {
x: 40,
y: -30,
},
'2': {
x: 120,
y: -50,
},
'3': {
x: 200,
y: -50,
},
'4': {
x: 280,
y: -30,
},
},
originPosition,
// 散布形态
{
'1': {
x: 30,
y: 60,
},
'2': {
x: 180,
y: -40,
},
'3': {
x: 80,
y: -60,
},
'4': {
x: 280,
y: 40,
},
},
originPosition,
// 对角上升形态
{
'1': {
x: 0,
y: 75,
},
'2': {
x: 110,
y: 25,
},
'3': {
x: 220,
y: -25,
},
'4': {
x: 330,
y: -75,
},
},
originPosition,
// 波浪形态
{
'1': {
x: 0,
y: 0,
},
'2': {
x: 110,
y: 80,
},
'3': {
x: 220,
y: 40,
},
'4': {
x: 330,
y: -20,
},
},
originPosition,
// 垂直堆叠形态
{
'1': {
x: 165,
y: -60,
},
'2': {
x: 165,
y: -20,
},
'3': {
x: 165,
y: 20,
},
'4': {
x: 165,
y: 60,
},
},
originPosition,
// 钻石形态
{
'1': {
x: 165,
y: -40,
},
'2': {
x: 110,
y: 0,
},
'3': {
x: 165,
y: 40,
},
'4': {
x: 220,
y: 0,
},
},
originPosition,
];
+60
View File
@@ -0,0 +1,60 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { delay, FreeLayoutPluginContext, startTween } from '@flowgram.ai/free-layout-editor';
import { PositionGroup, positionGroups } from './position-groups';
const updateNodePosition = async (params: {
ctx: FreeLayoutPluginContext;
positionGroup: PositionGroup;
}) => {
const { ctx, positionGroup } = params;
return new Promise<void>((resolve) => {
startTween({
from: { d: 0 },
to: { d: 100 },
duration: 1000,
onUpdate: (v) => {
ctx.document.getAllNodes().forEach((node) => {
const { transform } = node.transform;
const targetPosition = positionGroup[node.id] ?? {
x: transform.position.x,
y: transform.position.y,
};
transform.update({
position: {
x: transform.position.x + ((targetPosition.x - transform.position.x) * v.d) / 100,
y: transform.position.y + ((targetPosition.y - transform.position.y) * v.d) / 100,
},
});
});
},
onComplete: () => {
resolve();
},
});
});
};
export const updatePosition = async (ctx: FreeLayoutPluginContext) => {
// Cycle through position groups every 2 seconds
let currentGroupIndex = 0;
// Use while loop instead of recursion to avoid stack overflow
while (true) {
// Wait for 2 seconds before next update
await delay(2000);
// Update to current position group
await updateNodePosition({
ctx,
positionGroup: positionGroups[currentGroupIndex],
});
// Move to next position group (cycle back to 0 when reaching the end)
currentGroupIndex = (currentGroupIndex + 1) % positionGroups.length;
}
};
+76
View File
@@ -0,0 +1,76 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { useMemo } from 'react';
import {
Field,
FreeLayoutProps,
PlaygroundConfigEntity,
WorkflowLinesManager,
} from '@flowgram.ai/free-layout-editor';
import { updatePosition } from './update-position';
import { NodeRender } from './node-render';
import { nodeRegistries } from './node-registries';
import { NodeColorMap } from './node-color';
import { initialData } from './initial-data';
export const useEditorProps = () =>
useMemo<FreeLayoutProps>(
() => ({
background: false,
materials: {
renderDefaultNode: NodeRender,
},
isHideArrowLine: (ctx, line) => {
if (line.from && line.to) {
return true;
}
return false;
},
nodeRegistries,
initialData,
onInit: (ctx) => {
const linesManager = ctx.get(WorkflowLinesManager);
linesManager.getLineColor = (line) => {
const lineColor = NodeColorMap[line.from?.id ?? line.to?.id ?? ''] ?? '#000';
return lineColor;
};
},
onAllLayersRendered: async (ctx) => {
await ctx.tools.fitView(false);
// disable playground operations
const playgroundConfig = ctx.get(PlaygroundConfigEntity);
playgroundConfig.updateConfig = () => {};
// display logo container
const containerDOM = window.document.querySelector('.flowgram-logo-container');
if (containerDOM instanceof HTMLDivElement) {
containerDOM.style.opacity = '1';
}
// update nodes position
updatePosition(ctx);
},
getNodeDefaultRegistry(type) {
return {
type,
meta: {
defaultExpanded: true,
},
formMeta: {
/**
* Render form
*/
render: () => (
<>
<Field<string> name="title">{({ field }) => <div>{field.value}</div>}</Field>
</>
),
},
};
},
}),
[]
);