Explorar o código

feat: fixed layout animation demo (#981)

* chore: init demo vibe flow

* feat: vibe flow example data init

* feat: vibe flow update schema

* refactor: vibe flow

* feat: vibe flow package

* feat(vibeflow): loading & thinking render

* feat(vibeflow): loading status render

* feat(vibeflow): load schema animation

* perf(vibeflow): smooth animatino

* feat(vibeflow): add block animation

* feat(vibeflow): glowing light up thinking node

* refactor: demo-vibe-flow rename to demo-fixed-layout-animation

* feat(demo): long relative file path replace to absolute path

* fix(core): inline block without children set padding to 0

* perf(demo): smooth branch create animation

* feat(demo): remove node motion

* feat(demo): fitview motion
Louis Young hai 2 meses
pai
achega
5ebdf6d20c
Modificáronse 40 ficheiros con 2342 adicións e 8 borrados
  1. 20 0
      apps/demo-fixed-layout-animation/.eslintrc.js
  2. 12 0
      apps/demo-fixed-layout-animation/index.html
  3. 65 0
      apps/demo-fixed-layout-animation/package.json
  4. 34 0
      apps/demo-fixed-layout-animation/rsbuild.config.ts
  5. 28 0
      apps/demo-fixed-layout-animation/src/app.tsx
  6. 14 0
      apps/demo-fixed-layout-animation/src/components/form-render/index.tsx
  7. 29 0
      apps/demo-fixed-layout-animation/src/components/loading-dots/index.less
  8. 14 0
      apps/demo-fixed-layout-animation/src/components/loading-dots/index.tsx
  9. 118 0
      apps/demo-fixed-layout-animation/src/components/node-render/index.less
  10. 34 0
      apps/demo-fixed-layout-animation/src/components/node-render/index.tsx
  11. 90 0
      apps/demo-fixed-layout-animation/src/components/thinking-node/index.less
  12. 20 0
      apps/demo-fixed-layout-animation/src/components/thinking-node/index.tsx
  13. 90 0
      apps/demo-fixed-layout-animation/src/components/tools/index.tsx
  14. 35 0
      apps/demo-fixed-layout-animation/src/components/tools/minimap.tsx
  15. 294 0
      apps/demo-fixed-layout-animation/src/components/update-schema/example-schemas.ts
  16. 419 0
      apps/demo-fixed-layout-animation/src/components/update-schema/example.py
  17. 64 0
      apps/demo-fixed-layout-animation/src/components/update-schema/index.less
  18. 49 0
      apps/demo-fixed-layout-animation/src/components/update-schema/index.tsx
  19. 10 0
      apps/demo-fixed-layout-animation/src/fields/content-field/index.less
  20. 13 0
      apps/demo-fixed-layout-animation/src/fields/content-field/index.tsx
  21. 73 0
      apps/demo-fixed-layout-animation/src/fields/thinking-text-field/index.less
  22. 59 0
      apps/demo-fixed-layout-animation/src/fields/thinking-text-field/index.tsx
  23. 15 0
      apps/demo-fixed-layout-animation/src/fields/title-field/index.less
  24. 28 0
      apps/demo-fixed-layout-animation/src/fields/title-field/index.tsx
  25. 96 0
      apps/demo-fixed-layout-animation/src/hooks/use-editor-props.tsx
  26. 55 0
      apps/demo-fixed-layout-animation/src/hooks/use-node-loading.tsx
  27. 43 0
      apps/demo-fixed-layout-animation/src/nodes/condition/index.ts
  28. 21 0
      apps/demo-fixed-layout-animation/src/nodes/custom/index.ts
  29. 21 0
      apps/demo-fixed-layout-animation/src/nodes/index.ts
  30. 30 0
      apps/demo-fixed-layout-animation/src/nodes/thinking/index.tsx
  31. 6 0
      apps/demo-fixed-layout-animation/src/services/index.ts
  32. 170 0
      apps/demo-fixed-layout-animation/src/services/load-schema-service/index.ts
  33. 19 0
      apps/demo-fixed-layout-animation/src/services/load-schema-service/type.ts
  34. 92 0
      apps/demo-fixed-layout-animation/src/services/load-schema-service/utils.ts
  35. 38 0
      apps/demo-fixed-layout-animation/tsconfig.json
  36. 12 3
      apps/demo-fixed-layout/tsconfig.json
  37. 8 0
      common/config/rush/command-line.json
  38. 79 0
      common/config/rush/pnpm-lock.yaml
  39. 15 5
      packages/canvas-engine/fixed-layout-core/src/activities/inline-blocks.ts
  40. 10 0
      rush.json

+ 20 - 0
apps/demo-fixed-layout-animation/.eslintrc.js

@@ -0,0 +1,20 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+const { defineConfig } = require('@flowgram.ai/eslint-config');
+
+module.exports = defineConfig({
+  preset: 'web',
+  packageRoot: __dirname,
+  rules: {
+    'no-console': 'off',
+    'react/prop-types': 'off',
+  },
+  settings: {
+    react: {
+      version: 'detect', // 自动检测 React 版本
+    },
+  },
+});

+ 12 - 0
apps/demo-fixed-layout-animation/index.html

@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html lang="en" data-bundler="rspack">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Flow FixedLayoutEditor Demo</title>
+  </head>
+  <body>
+    <div id="root"></div>
+  </body>
+</html>

+ 65 - 0
apps/demo-fixed-layout-animation/package.json

@@ -0,0 +1,65 @@
+{
+  "name": "@flowgram.ai/demo-fixed-layout-animation",
+  "version": "0.1.0",
+  "description": "",
+  "keywords": [],
+  "license": "MIT",
+  "main": "./src/app.ts",
+  "files": [
+    "src/",
+    ".eslintrc.js",
+    ".gitignore",
+    "index.html",
+    "package.json",
+    "rsbuild.config.ts",
+    "tsconfig.json"
+  ],
+  "scripts": {
+    "build": "exit 0",
+    "build:fast": "exit 0",
+    "build:watch": "exit 0",
+    "build:prod": "cross-env MODE=app NODE_ENV=production rsbuild build",
+    "clean": "rimraf dist",
+    "dev": "cross-env MODE=app NODE_ENV=development rsbuild dev --open",
+    "lint": "eslint ./src --cache",
+    "lint:fix": "eslint ./src --fix",
+    "ts-check": "tsc --noEmit",
+    "start": "cross-env NODE_ENV=development rsbuild dev --open",
+    "test": "exit",
+    "test:cov": "exit",
+    "watch": "exit 0"
+  },
+  "dependencies": {
+    "@flowgram.ai/fixed-layout-editor": "workspace:*",
+    "@flowgram.ai/fixed-semi-materials": "workspace:*",
+    "@flowgram.ai/minimap-plugin": "workspace:*",
+    "lodash-es": "^4.17.21",
+    "nanoid": "^5.0.9",
+    "react": "^18",
+    "react-dom": "^18",
+    "classnames": "^2.5.1",
+    "styled-components": "^5"
+  },
+  "devDependencies": {
+    "@flowgram.ai/ts-config": "workspace:*",
+    "@flowgram.ai/eslint-config": "workspace:*",
+    "@rsbuild/core": "^1.2.16",
+    "@rsbuild/plugin-react": "^1.1.1",
+    "@rsbuild/plugin-less": "^1.1.1",
+    "@types/lodash-es": "^4.17.12",
+    "@types/node": "^18",
+    "@types/react": "^18",
+    "@types/react-dom": "^18",
+    "@types/styled-components": "^5",
+    "@typescript-eslint/parser": "^6.10.0",
+    "typescript": "^5.8.3",
+    "eslint": "^8.54.0",
+    "less": "^4.1.2",
+    "less-loader": "^6",
+    "cross-env": "~7.0.3"
+  },
+  "publishConfig": {
+    "access": "public",
+    "registry": "https://registry.npmjs.org/"
+  }
+}

+ 34 - 0
apps/demo-fixed-layout-animation/rsbuild.config.ts

@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { pluginReact } from '@rsbuild/plugin-react';
+import { pluginLess } from '@rsbuild/plugin-less';
+import { defineConfig } from '@rsbuild/core';
+
+export default defineConfig({
+  plugins: [pluginReact(), pluginLess()],
+  source: {
+    entry: {
+      index: './src/app.tsx',
+    },
+    /**
+     * support inversify @injectable() and @inject decorators
+     */
+    decorators: {
+      version: 'legacy',
+    },
+  },
+  html: {
+    title: 'demo-fixed-layout-animation',
+  },
+  tools: {
+    rspack: {
+      /**
+       * ignore warnings from @coze-editor/editor/language-typescript
+       */
+      ignoreWarnings: [/Critical dependency: the request of a dependency is an expression/],
+    },
+  },
+});

+ 28 - 0
apps/demo-fixed-layout-animation/src/app.tsx

@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import '@flowgram.ai/fixed-layout-editor/index.css';
+
+import { createRoot } from 'react-dom/client';
+import { FixedLayoutEditorProvider, EditorRenderer } from '@flowgram.ai/fixed-layout-editor';
+
+import { useEditorProps } from '@/hooks/use-editor-props';
+import { UpdateSchema } from '@/components/update-schema';
+import { Tools } from '@/components/tools';
+
+const FlowGramApp = () => {
+  const editorProps = useEditorProps();
+  return (
+    <FixedLayoutEditorProvider {...editorProps}>
+      <EditorRenderer />
+      <Tools />
+      <UpdateSchema />
+    </FixedLayoutEditorProvider>
+  );
+};
+
+const app = createRoot(document.getElementById('root')!);
+
+app.render(<FlowGramApp />);

+ 14 - 0
apps/demo-fixed-layout-animation/src/components/form-render/index.tsx

@@ -0,0 +1,14 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { TitleField } from '@/fields/title-field';
+import { ContentField } from '@/fields/content-field';
+
+export const FormRender = () => (
+  <>
+    <TitleField />
+    <ContentField />
+  </>
+);

+ 29 - 0
apps/demo-fixed-layout-animation/src/components/loading-dots/index.less

@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+.loading-dots {
+  display: flex;
+  gap: 4px;
+
+  .dot {
+    width: 6px;
+    height: 6px;
+    background: #3b82f6;
+    border-radius: 50%;
+    animation: bounce 1.4s ease-in-out infinite both;
+
+    &:nth-child(1) {
+      animation-delay: -0.32s;
+    }
+
+    &:nth-child(2) {
+      animation-delay: -0.16s;
+    }
+
+    &:nth-child(3) {
+      animation-delay: 0s;
+    }
+  }
+}

+ 14 - 0
apps/demo-fixed-layout-animation/src/components/loading-dots/index.tsx

@@ -0,0 +1,14 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import './index.less';
+
+export const LoadingDots = () => (
+  <div className="loading-dots">
+    <span className="dot"></span>
+    <span className="dot"></span>
+    <span className="dot"></span>
+  </div>
+);

+ 118 - 0
apps/demo-fixed-layout-animation/src/components/node-render/index.less

@@ -0,0 +1,118 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+.node-render {
+  background: #fff;
+  border: 1px solid rgba(6, 7, 9, 0.15);
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  position: relative;
+  cursor: pointer;
+  padding: 16px;
+  background-color: #ffffff;
+  border-radius: 8px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+  width: auto;
+  min-width: 200px;
+
+  // Activated state - when node is selected/focused
+  &-activated {
+    border-color: #82a7fc;
+  }
+
+  // Dragging state - when node is being dragged
+  &-dragging {
+    opacity: 0.3;
+  }
+
+  // Block icon states - when showing order or regular block icons
+  &-block-icon,
+  &-block-order-icon {
+    width: 260px;
+  }
+
+  // Hover effects for better UX
+  &:hover {
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+  }
+
+  // Focus state for accessibility
+  &:focus-within {
+    outline: 2px solid #82a7fc;
+    outline-offset: 2px;
+  }
+
+  .node-form {
+    transition: opacity 1s ease-in-out;
+  }
+}
+
+.node-render-before-render {
+  max-height: 1px;
+
+  .node-form {
+    opacity: 0;
+  }
+}
+
+.node-render-rendered {
+  max-height: 1px;
+  animation: node-rendered-transition 1s ease-out forwards;
+
+  .node-form {
+    opacity: 1;
+  }
+}
+
+@keyframes node-rendered-transition {
+  0% {
+    max-height: 1px;
+  }
+
+  100% {
+    max-height: 500px;
+  }
+}
+
+.node-render-removed {
+  max-height: 500px;
+  animation: node-removed-transition 0.3s ease-out forwards;
+  overflow: hidden;
+  padding: 0 16px;
+  transition: opacity 0.3s ease-out;
+  opacity: 0;
+}
+
+@keyframes node-removed-transition {
+  0% {
+    max-height: 500px;
+    padding: 16px;
+  }
+
+  100% {
+    max-height: 1px;
+    padding: 0 16px;
+  }
+}
+
+.node-render-border-transition {
+  outline: 2px solid transparent;
+  animation: node-border-appear-hide 0.8s ease-in-out forwards;
+}
+
+@keyframes node-border-appear-hide {
+  0% {
+    outline: 2px solid transparent;
+  }
+
+  50% {
+    outline: 2px solid #82a7fc;
+  }
+
+  100% {
+    outline: 2px solid transparent;
+  }
+}

+ 34 - 0
apps/demo-fixed-layout-animation/src/components/node-render/index.tsx

@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import '@flowgram.ai/fixed-layout-editor/index.css';
+import './index.less';
+
+import classNames from 'classnames';
+import { FlowNodeEntity, useNodeRender } from '@flowgram.ai/fixed-layout-editor';
+
+import { useNodeStatus } from '@/hooks/use-node-loading';
+
+export const NodeRender = ({ node }: { node: FlowNodeEntity }) => {
+  const { onMouseEnter, onMouseLeave, form, dragging, isBlockOrderIcon, isBlockIcon, activated } =
+    useNodeRender();
+
+  const { className } = useNodeStatus();
+
+  return (
+    <div
+      className={classNames('node-render', className, {
+        'node-render-activated': activated,
+        'node-render-dragging': dragging,
+        'node-render-block-order-icon': isBlockOrderIcon,
+        'node-render-block-icon': isBlockIcon,
+      })}
+      onMouseEnter={onMouseEnter}
+      onMouseLeave={onMouseLeave}
+    >
+      <div className="node-form">{form?.render()}</div>
+    </div>
+  );
+};

+ 90 - 0
apps/demo-fixed-layout-animation/src/components/thinking-node/index.less

@@ -0,0 +1,90 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+.thinking-node {
+  background: #fff;
+  border: 1px solid oklch(80.9% .105 251.813);
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  position: relative;
+  cursor: pointer;
+  padding: 16px;
+  background-color: oklch(97% .014 254.604);
+  border-radius: 8px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+  width: auto;
+  min-width: 200px;
+
+  .node-form {
+    transition: opacity 1s ease-in-out;
+  }
+}
+
+.thinking-node-loading {
+  &::before {
+    content: '';
+    position: absolute;
+    top: -3px;
+    left: -3px;
+    right: -3px;
+    bottom: -3px;
+    background: linear-gradient(90deg,
+        transparent,
+        transparent,
+        transparent,
+        transparent,
+        rgba(59, 130, 246, 0.6),
+        rgba(96, 165, 250, 0.6),
+        transparent,
+        transparent,
+        transparent,
+        transparent,
+        transparent,
+        transparent);
+    background-size: 400% 100%;
+    border-radius: 8px;
+    z-index: -1;
+    animation: flowing-border 5s linear infinite;
+    pointer-events: none;
+  }
+
+  &::after {
+    content: '';
+    position: absolute;
+    top: -3px;
+    left: -3px;
+    right: -3px;
+    bottom: -3px;
+    background: linear-gradient(90deg,
+        transparent,
+        transparent,
+        transparent,
+        transparent,
+        rgba(59, 130, 246, 0.08),
+        rgba(96, 165, 250, 0.08),
+        transparent,
+        transparent,
+        transparent,
+        transparent,
+        transparent,
+        transparent);
+    background-size: 400% 100%;
+    border-radius: 8px;
+    z-index: 1;
+    animation: flowing-border 5s linear infinite;
+    pointer-events: none;
+  }
+}
+
+@keyframes flowing-border {
+  0% {
+    background-position: 0% 0;
+  }
+
+  100% {
+    background-position: 400% 0;
+  }
+}

+ 20 - 0
apps/demo-fixed-layout-animation/src/components/thinking-node/index.tsx

@@ -0,0 +1,20 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import classNames from 'classnames';
+import { useNodeRender } from '@flowgram.ai/fixed-layout-editor';
+import './index.less';
+
+import { useNodeStatus } from '@/hooks/use-node-loading';
+
+export const ThinkingNode = () => {
+  const { form } = useNodeRender();
+  const { className } = useNodeStatus();
+  return (
+    <div className={classNames('thinking-node', 'thinking-node-loading', className)}>
+      <div className="node-form">{form?.render()}</div>
+    </div>
+  );
+};

+ 90 - 0
apps/demo-fixed-layout-animation/src/components/tools/index.tsx

@@ -0,0 +1,90 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { CSSProperties, useEffect, useState } from 'react';
+
+import { usePlaygroundTools, useClientContext } from '@flowgram.ai/fixed-layout-editor';
+
+import { Minimap } from './minimap';
+
+export const Tools = () => {
+  const { history } = useClientContext();
+  const tools = usePlaygroundTools();
+  const [canUndo, setCanUndo] = useState(false);
+  const [canRedo, setCanRedo] = useState(false);
+
+  const buttonStyle: CSSProperties = {
+    border: '1px solid #e0e0e0',
+    borderRadius: '4px',
+    cursor: 'pointer',
+    padding: '4px',
+    color: '#141414',
+    background: '#e1e3e4',
+  };
+
+  useEffect(() => {
+    const disposable = history.undoRedoService.onChange(() => {
+      setCanUndo(history.canUndo());
+      setCanRedo(history.canRedo());
+    });
+    return () => disposable.dispose();
+  }, [history]);
+
+  return (
+    <>
+      <Minimap />
+      <div
+        style={{ position: 'absolute', zIndex: 10, bottom: 16, left: 16, display: 'flex', gap: 8 }}
+      >
+        <button style={buttonStyle} onClick={() => tools.zoomin()}>
+          ZoomIn
+        </button>
+        <button style={buttonStyle} onClick={() => tools.zoomout()}>
+          ZoomOut
+        </button>
+        <span
+          style={{
+            ...buttonStyle,
+            display: 'flex',
+            alignItems: 'center',
+            justifyContent: 'center',
+            cursor: 'default',
+            width: 40,
+          }}
+        >
+          {Math.floor(tools.zoom * 100)}%
+        </span>
+        <button style={buttonStyle} onClick={() => tools.fitView()}>
+          FitView
+        </button>
+        <button style={buttonStyle} onClick={() => tools.changeLayout()}>
+          ChangeLayout
+        </button>
+        <button
+          style={{
+            ...buttonStyle,
+            cursor: canUndo ? 'pointer' : 'not-allowed',
+            color: canUndo ? '#141414' : '#b1b1b1',
+          }}
+          onClick={() => history.undo()}
+          disabled={!canUndo}
+        >
+          Undo
+        </button>
+        <button
+          style={{
+            ...buttonStyle,
+            cursor: canRedo ? 'pointer' : 'not-allowed',
+            color: canRedo ? '#141414' : '#b1b1b1',
+          }}
+          onClick={() => history.redo()}
+          disabled={!canRedo}
+        >
+          Redo
+        </button>
+      </div>
+    </>
+  );
+};

+ 35 - 0
apps/demo-fixed-layout-animation/src/components/tools/minimap.tsx

@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { MinimapRender } from '@flowgram.ai/minimap-plugin';
+
+export const Minimap = () => (
+  <div
+    style={{
+      position: 'absolute',
+      left: 16,
+      bottom: 58,
+      zIndex: 100,
+      width: 218,
+    }}
+  >
+    <MinimapRender
+      containerStyles={{
+        pointerEvents: 'auto',
+        position: 'relative',
+        top: 'unset',
+        right: 'unset',
+        bottom: 'unset',
+        left: 'unset',
+      }}
+      inactiveStyle={{
+        opacity: 1,
+        scale: 1,
+        translateX: 0,
+        translateY: 0,
+      }}
+    />
+  </div>
+);

+ 294 - 0
apps/demo-fixed-layout-animation/src/components/update-schema/example-schemas.ts

@@ -0,0 +1,294 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { FlowDocumentJSON } from '@flowgram.ai/fixed-layout-editor';
+
+const initSchema = {
+  nodes: [
+    {
+      id: 'start_0',
+      type: 'start',
+      data: {
+        title: '开始',
+      },
+    },
+  ],
+};
+
+const processStartSchema = {
+  nodes: [
+    {
+      id: 'start_0',
+      type: 'start',
+      data: {
+        title: '开始',
+        content: '天气穿衣建议工作流',
+      },
+    },
+    {
+      id: 'thinking_0',
+      type: 'thinking',
+      data: {
+        text: '正在生成天气穿衣建议工作流...业务流程:1.进行输入处理 2.获取天气数据 3.生成穿衣建议 4.整理输出。我需要根据这些步骤来生成天气穿衣建议工作流核心节点...',
+      },
+    },
+  ],
+};
+
+const addCoreNodesSchema = {
+  nodes: [
+    {
+      id: 'start_0',
+      type: 'start',
+      data: {
+        title: '开始',
+        content: '天气穿衣建议工作流',
+      },
+    },
+    {
+      id: 'validate_input_0',
+      type: 'custom',
+      data: {
+        title: '输入处理节点',
+        content: '验证并清理城市名称输入 - validate_city_input()',
+      },
+    },
+    {
+      id: 'thinking_1',
+      type: 'thinking',
+      data: {
+        text: '正在生成错误检查节点与天气检查节点...',
+      },
+    },
+    {
+      id: 'fetch_weather_0',
+      type: 'custom',
+      data: {
+        title: '天气数据获取',
+        content: '调用wttr.in API获取天气信息 - fetch_weather_data()',
+      },
+    },
+    {
+      id: 'generate_suggestion_0',
+      type: 'custom',
+      data: {
+        title: '穿衣建议生成',
+        content: '基于天气数据生成穿衣建议 - generate_clothing_suggestion()',
+      },
+    },
+    {
+      id: 'format_response_0',
+      type: 'custom',
+      data: {
+        title: '输出整理节点',
+        content: '格式化最终回答 - format_final_response()',
+      },
+    },
+    {
+      id: 'end_0',
+      type: 'end',
+      data: {
+        title: '结束',
+        content: '返回格式化的穿衣建议',
+      },
+    },
+  ],
+};
+
+const completeWorkflowLoadingSchema = {
+  nodes: [
+    {
+      id: 'start_0',
+      type: 'start',
+      data: {
+        title: '开始',
+        content: '天气穿衣建议工作流',
+      },
+    },
+    {
+      id: 'validate_input_0',
+      type: 'custom',
+      data: {
+        title: '输入处理节点',
+        content: '验证并清理城市名称输入 - validate_city_input()',
+      },
+    },
+    {
+      id: 'condition_0',
+      type: 'condition',
+      data: {
+        title: '输入验证',
+        content: '检查输入验证是否有错误',
+      },
+      blocks: [
+        {
+          id: 'thinking_2',
+          type: 'thinking',
+          data: {
+            text: '天气数据获取节点生成中',
+          },
+        },
+        {
+          id: 'thinking_3',
+          type: 'thinking',
+          data: {
+            text: '格式化错误节点生成中',
+          },
+        },
+      ],
+    },
+    {
+      id: 'condition_1',
+      type: 'condition',
+      data: {
+        title: '天气数据检查',
+        content: '检查天气数据获取是否成功',
+      },
+      blocks: [
+        {
+          id: 'thinking_4',
+          type: 'thinking',
+          data: {
+            text: '穿衣建议生成节点生成中',
+          },
+        },
+        {
+          id: 'thinking_5',
+          type: 'thinking',
+          data: {
+            text: '格式化错误节点生成中',
+          },
+        },
+      ],
+    },
+    {
+      id: 'format_response_0',
+      type: 'custom',
+      data: {
+        title: '输出整理节点',
+        content: '格式化最终回答 - format_final_response()',
+      },
+    },
+    {
+      id: 'end_0',
+      type: 'end',
+      data: {
+        title: '结束',
+        content: '返回格式化的穿衣建议',
+      },
+    },
+  ],
+};
+
+const completeWorkflowSchema = {
+  nodes: [
+    {
+      id: 'start_0',
+      type: 'start',
+      data: {
+        title: '开始',
+        content: '天气穿衣建议工作流',
+      },
+    },
+    {
+      id: 'validate_input_0',
+      type: 'custom',
+      data: {
+        title: '输入处理节点',
+        content: '验证并清理城市名称输入 - validate_city_input()',
+      },
+    },
+    {
+      id: 'condition_0',
+      type: 'condition',
+      data: {
+        title: '输入验证',
+        content: '检查输入验证是否有错误',
+      },
+      blocks: [
+        {
+          id: 'block_0',
+          type: 'block',
+          blocks: [
+            {
+              id: 'fetch_weather_0',
+              type: 'custom',
+              data: {
+                title: '天气数据获取',
+                content: '调用wttr.in API获取天气信息 - fetch_weather_data()',
+              },
+            },
+            {
+              id: 'format_data_0',
+              type: 'custom',
+              data: {
+                title: '格式化数据',
+                content: '天气数据提取并进行处理格式化',
+              },
+            },
+          ],
+        },
+        {
+          id: 'format_error_0',
+          type: 'custom',
+          data: {
+            title: '格式化错误',
+            content: '直接跳转到输出格式化',
+          },
+        },
+      ],
+    },
+    {
+      id: 'condition_1',
+      type: 'condition',
+      data: {
+        title: '天气数据检查',
+        content: '检查天气数据获取是否成功',
+      },
+      blocks: [
+        {
+          id: 'generate_suggestion_0',
+          type: 'custom',
+          data: {
+            title: '穿衣建议生成',
+            content: '基于天气数据生成穿衣建议 - generate_clothing_suggestion()',
+          },
+        },
+        {
+          id: 'format_error_1',
+          type: 'custom',
+          data: {
+            title: '格式化错误',
+            content: '跳转到输出格式化',
+          },
+        },
+      ],
+    },
+    {
+      id: 'format_response_0',
+      type: 'custom',
+      data: {
+        title: '输出整理节点',
+        content: '格式化最终回答 - format_final_response()',
+      },
+    },
+    {
+      id: 'end_0',
+      type: 'end',
+      data: {
+        title: '结束',
+        content: '返回格式化的穿衣建议',
+      },
+    },
+  ],
+};
+
+export const exampleSchemas: FlowDocumentJSON[] = [
+  initSchema,
+  processStartSchema,
+  addCoreNodesSchema,
+  completeWorkflowLoadingSchema,
+  completeWorkflowSchema,
+];

+ 419 - 0
apps/demo-fixed-layout-animation/src/components/update-schema/example.py

@@ -0,0 +1,419 @@
+#!/usr/bin/env python3
+"""
+Weather-based Clothing Advisor using LangGraph
+A workflow that fetches weather data and provides clothing recommendations.
+"""
+
+import json
+import re
+import requests
+from typing import Dict, Any, Optional, TypedDict
+from dataclasses import dataclass
+from langgraph.graph import Graph, StateGraph, END
+from langchain_core.messages import HumanMessage, SystemMessage
+from langchain_openai import ChatOpenAI
+import os
+
+
+# State definition for the workflow
+class WorkflowState(TypedDict):
+    """State structure for the weather clothing advisor workflow"""
+    city_name: str
+    validated_city: str
+    weather_data: Dict[str, Any]
+    temperature: float
+    weather_condition: str
+    clothing_suggestion: str
+    final_response: str
+    error_message: Optional[str]
+
+
+@dataclass
+class WeatherInfo:
+    """Weather information data structure"""
+    temperature: float
+    condition: str
+    humidity: int
+    wind_speed: float
+    description: str
+
+
+class WeatherClothingAdvisor:
+    """Main class for the weather-based clothing advisor workflow"""
+
+    def __init__(self, openai_api_key: Optional[str] = None):
+        """
+        Initialize the advisor with OpenAI API key
+
+        Args:
+            openai_api_key: OpenAI API key for LLM calls
+        """
+        self.openai_api_key = openai_api_key or os.getenv("OPENAI_API_KEY")
+        if self.openai_api_key:
+            self.llm = ChatOpenAI(
+                api_key=self.openai_api_key,
+                model="gpt-3.5-turbo",
+                temperature=0.7
+            )
+        else:
+            self.llm = None
+            print("Warning: No OpenAI API key provided. Using rule-based suggestions.")
+
+    def validate_city_input(self, state: WorkflowState) -> WorkflowState:
+        """
+        Node 1: Input processing and validation
+        Validates and cleans the city name input
+
+        Args:
+            state: Current workflow state
+
+        Returns:
+            Updated state with validated city name
+        """
+        city_name = state.get("city_name", "").strip()
+
+        # Basic validation
+        if not city_name:
+            state["error_message"] = "城市名称不能为空"
+            return state
+
+        # Remove special characters and normalize
+        validated_city = re.sub(r'[^\w\s-]', '', city_name)
+        validated_city = validated_city.strip()
+
+        if len(validated_city) < 2:
+            state["error_message"] = "请输入有效的城市名称"
+            return state
+
+        state["validated_city"] = validated_city
+        state["error_message"] = None
+
+        print(f"✓ 城市名称验证通过: {validated_city}")
+        return state
+
+    def fetch_weather_data(self, state: WorkflowState) -> WorkflowState:
+        """
+        Node 2: Weather data retrieval
+        Fetches weather information from wttr.in API
+
+        Args:
+            state: Current workflow state
+
+        Returns:
+            Updated state with weather data
+        """
+        if state.get("error_message"):
+            return state
+
+        validated_city = state["validated_city"]
+
+        try:
+            # Use wttr.in API for weather data
+            url = f"http://wttr.in/{validated_city}?format=j1"
+            headers = {
+                'User-Agent': 'WeatherClothingAdvisor/1.0'
+            }
+
+            print(f"🌤️  正在获取 {validated_city} 的天气数据...")
+            response = requests.get(url, headers=headers, timeout=10)
+            response.raise_for_status()
+
+            weather_data = response.json()
+
+            # Extract current weather information
+            current_condition = weather_data["current_condition"][0]
+            temperature_c = float(current_condition["temp_C"])
+            weather_desc = current_condition["weatherDesc"][0]["value"]
+            humidity = int(current_condition["humidity"])
+            wind_speed = float(current_condition["windspeedKmph"])
+
+            state["weather_data"] = weather_data
+            state["temperature"] = temperature_c
+            state["weather_condition"] = weather_desc
+
+            weather_info = WeatherInfo(
+                temperature=temperature_c,
+                condition=weather_desc,
+                humidity=humidity,
+                wind_speed=wind_speed,
+                description=f"{temperature_c}°C, {weather_desc}, 湿度 {humidity}%, 风速 {wind_speed}km/h"
+            )
+
+            print(f"✓ 天气数据获取成功: {weather_info.description}")
+
+        except requests.exceptions.RequestException as e:
+            state["error_message"] = f"获取天气数据失败: {str(e)}"
+            print(f"❌ 天气数据获取失败: {str(e)}")
+        except (KeyError, ValueError, IndexError) as e:
+            state["error_message"] = f"天气数据解析失败: {str(e)}"
+            print(f"❌ 天气数据解析失败: {str(e)}")
+
+        return state
+
+    def generate_clothing_suggestion(self, state: WorkflowState) -> WorkflowState:
+        """
+        Node 3: Clothing suggestion generation
+        Generates clothing recommendations based on weather data
+
+        Args:
+            state: Current workflow state
+
+        Returns:
+            Updated state with clothing suggestions
+        """
+        if state.get("error_message"):
+            return state
+
+        temperature = state["temperature"]
+        weather_condition = state["weather_condition"]
+        city_name = state["validated_city"]
+
+        print(f"🧥 正在生成穿衣建议...")
+
+        if self.llm:
+            # Use LLM for intelligent suggestions
+            try:
+                prompt = f"""
+                作为一个专业的穿衣顾问,请根据以下天气信息为用户提供详细的穿衣建议:
+
+                城市:{city_name}
+                温度:{temperature}°C
+                天气状况:{weather_condition}
+
+                请提供:
+                1. 上身穿着建议
+                2. 下身穿着建议
+                3. 外套建议
+                4. 配饰建议(如帽子、围巾等)
+                5. 鞋子建议
+                6. 特别注意事项
+
+                请用简洁明了的中文回答,语气友好自然。
+                """
+
+                messages = [
+                    SystemMessage(content="你是一个专业的穿衣顾问,擅长根据天气情况提供实用的穿衣建议。"),
+                    HumanMessage(content=prompt)
+                ]
+
+                response = self.llm.invoke(messages)
+                state["clothing_suggestion"] = response.content
+
+            except Exception as e:
+                print(f"⚠️  LLM调用失败,使用规则建议: {str(e)}")
+                state["clothing_suggestion"] = self._get_rule_based_suggestion(temperature, weather_condition)
+        else:
+            # Use rule-based suggestions
+            state["clothing_suggestion"] = self._get_rule_based_suggestion(temperature, weather_condition)
+
+        print("✓ 穿衣建议生成完成")
+        return state
+
+    def _get_rule_based_suggestion(self, temperature: float, weather_condition: str) -> str:
+        """
+        Generate rule-based clothing suggestions
+
+        Args:
+            temperature: Temperature in Celsius
+            weather_condition: Weather condition description
+
+        Returns:
+            Clothing suggestion string
+        """
+        suggestions = []
+
+        # Temperature-based suggestions
+        if temperature < 0:
+            suggestions.append("🧥 上身:保暖内衣 + 毛衣 + 厚外套")
+            suggestions.append("👖 下身:保暖裤 + 厚裤子")
+            suggestions.append("🧤 配饰:帽子、围巾、手套必备")
+        elif temperature < 10:
+            suggestions.append("🧥 上身:长袖衬衫 + 毛衣 + 外套")
+            suggestions.append("👖 下身:长裤")
+            suggestions.append("🧣 配饰:围巾、帽子")
+        elif temperature < 20:
+            suggestions.append("👔 上身:长袖衬衫 + 薄外套")
+            suggestions.append("👖 下身:长裤或牛仔裤")
+            suggestions.append("🧢 配饰:可选择轻薄围巾")
+        elif temperature < 25:
+            suggestions.append("👕 上身:长袖T恤或薄衬衫")
+            suggestions.append("👖 下身:长裤或休闲裤")
+        else:
+            suggestions.append("👕 上身:短袖T恤或薄衬衫")
+            suggestions.append("🩳 下身:短裤或薄长裤")
+            suggestions.append("🧴 注意:防晒和补水")
+
+        # Weather condition adjustments
+        weather_lower = weather_condition.lower()
+        if any(word in weather_lower for word in ['rain', 'shower', '雨', '阵雨']):
+            suggestions.append("☔ 特别提醒:携带雨伞或穿防水外套")
+        elif any(word in weather_lower for word in ['snow', '雪']):
+            suggestions.append("❄️ 特别提醒:穿防滑鞋,注意保暖")
+        elif any(word in weather_lower for word in ['wind', '风']):
+            suggestions.append("💨 特别提醒:选择防风外套")
+
+        # Shoe suggestions
+        if temperature < 5:
+            suggestions.append("👢 鞋子:保暖靴子或厚底鞋")
+        elif temperature > 25:
+            suggestions.append("👟 鞋子:透气运动鞋或凉鞋")
+        else:
+            suggestions.append("👟 鞋子:舒适的运动鞋或休闲鞋")
+
+        return "\n".join(suggestions)
+
+    def format_final_response(self, state: WorkflowState) -> WorkflowState:
+        """
+        Node 4: Output formatting
+        Formats the final response with weather info and clothing suggestions
+
+        Args:
+            state: Current workflow state
+
+        Returns:
+            Updated state with formatted final response
+        """
+        if state.get("error_message"):
+            state["final_response"] = f"❌ 错误:{state['error_message']}"
+            return state
+
+        city_name = state["validated_city"]
+        temperature = state["temperature"]
+        weather_condition = state["weather_condition"]
+        clothing_suggestion = state["clothing_suggestion"]
+
+        final_response = f"""
+🌍 {city_name} 天气穿衣建议
+
+📊 当前天气情况:
+• 温度:{temperature}°C
+• 天气:{weather_condition}
+
+👔 穿衣建议:
+{clothing_suggestion}
+
+💡 温馨提示:
+建议出门前再次确认天气变化,根据个人体感适当调整穿着。
+        """.strip()
+
+        state["final_response"] = final_response
+        print("✓ 最终回答格式化完成")
+
+        return state
+
+    def create_workflow(self) -> StateGraph:
+        """
+        Create and configure the LangGraph workflow
+
+        Returns:
+            Configured StateGraph workflow
+        """
+        # Create the graph
+        workflow = StateGraph(WorkflowState)
+
+        # Add nodes
+        workflow.add_node("validate_input", self.validate_city_input)
+        workflow.add_node("fetch_weather", self.fetch_weather_data)
+        workflow.add_node("generate_suggestion", self.generate_clothing_suggestion)
+        workflow.add_node("format_response", self.format_final_response)
+
+        # Define the flow
+        workflow.set_entry_point("validate_input")
+
+        # Add conditional edges
+        workflow.add_conditional_edges(
+            "validate_input",
+            lambda state: "fetch_weather" if not state.get("error_message") else "format_response"
+        )
+
+        workflow.add_conditional_edges(
+            "fetch_weather",
+            lambda state: "generate_suggestion" if not state.get("error_message") else "format_response"
+        )
+
+        workflow.add_conditional_edges(
+            "generate_suggestion",
+            lambda state: "format_response" if not state.get("error_message") else "format_response"
+        )
+
+        workflow.add_edge("format_response", END)
+
+        return workflow.compile()
+
+    def get_clothing_advice(self, city_name: str) -> str:
+        """
+        Main method to get clothing advice for a city
+
+        Args:
+            city_name: Name of the city to get weather and clothing advice for
+
+        Returns:
+            Formatted clothing advice string
+        """
+        print(f"🚀 开始为 '{city_name}' 生成穿衣建议...")
+
+        # Create and run the workflow
+        workflow = self.create_workflow()
+
+        # Initial state
+        initial_state = WorkflowState(
+            city_name=city_name,
+            validated_city="",
+            weather_data={},
+            temperature=0.0,
+            weather_condition="",
+            clothing_suggestion="",
+            final_response="",
+            error_message=None
+        )
+
+        # Execute the workflow
+        result = workflow.invoke(initial_state)
+
+        return result["final_response"]
+
+
+def main():
+    """Main function to demonstrate the weather clothing advisor"""
+    print("🌤️ 天气穿衣建议助手")
+    print("=" * 50)
+
+    # Initialize the advisor
+    advisor = WeatherClothingAdvisor()
+
+    # Example usage
+    cities = ["北京", "上海", "广州", "深圳"]
+
+    for city in cities:
+        print(f"\n{'='*20} {city} {'='*20}")
+        try:
+            advice = advisor.get_clothing_advice(city)
+            print(advice)
+        except Exception as e:
+            print(f"❌ 处理 {city} 时出错: {str(e)}")
+        print("\n" + "-" * 60)
+
+    # Interactive mode
+    print("\n🎯 交互模式 (输入 'quit' 退出)")
+    while True:
+        try:
+            city_input = input("\n请输入城市名称: ").strip()
+            if city_input.lower() in ['quit', 'exit', '退出', 'q']:
+                print("👋 再见!")
+                break
+
+            if city_input:
+                advice = advisor.get_clothing_advice(city_input)
+                print(f"\n{advice}")
+            else:
+                print("❌ 请输入有效的城市名称")
+
+        except KeyboardInterrupt:
+            print("\n👋 再见!")
+            break
+        except Exception as e:
+            print(f"❌ 出现错误: {str(e)}")
+
+
+if __name__ == "__main__":
+    main()

+ 64 - 0
apps/demo-fixed-layout-animation/src/components/update-schema/index.less

@@ -0,0 +1,64 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+.update-schema-button-container {
+  // Position and layout
+  position: absolute;
+  top: 50px;
+  right: 50px;
+  z-index: 100;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.update-schema-button {
+  // Size and spacing
+  width: auto;
+  min-width: 120px;
+  height: 44px;
+  padding: 12px 24px;
+
+  // Typography
+  font-size: 14px;
+  font-weight: 600;
+  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+  color: #ffffff;
+
+  // Background and borders
+  background: #667eea;
+  border: none;
+  border-radius: 8px;
+
+  // Shadow and effects
+  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4), 0 2px 4px rgba(0, 0, 0, 0.1);
+
+  // Interaction states
+  cursor: pointer;
+  transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+
+  // Prevent text selection
+  user-select: none;
+  -webkit-user-select: none;
+
+  // Hover state - subtle enhancement
+  &:hover {
+    background: #5a6fd8;
+    box-shadow: 0 6px 16px rgba(102, 126, 234, 0.5), 0 3px 6px rgba(0, 0, 0, 0.15);
+  }
+
+  // Active/Click state - gentle press effect
+  &:active {
+    background: #4c5bc4;
+    box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3), 0 1px 2px rgba(0, 0, 0, 0.1);
+  }
+
+  // Button content styling
+  .button-content {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+  }
+}

+ 49 - 0
apps/demo-fixed-layout-animation/src/components/update-schema/index.tsx

@@ -0,0 +1,49 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { useState } from 'react';
+
+import { FlowDocumentJSON, useService } from '@flowgram.ai/fixed-layout-editor';
+
+import './index.less';
+import { WorkflowLoadSchemaService } from '@/services';
+
+import { exampleSchemas } from './example-schemas';
+
+export const UpdateSchema = () => {
+  const loadSchemaService = useService(WorkflowLoadSchemaService);
+  const [currentSchemaIndex, setCurrentSchemaIndex] = useState<number>(0);
+
+  const handleUpdateSchema = (): void => {
+    const currentSchema: FlowDocumentJSON = exampleSchemas[currentSchemaIndex];
+
+    // Update the document with current schema
+    loadSchemaService.load(currentSchema);
+
+    // Move to next schema index, cycle back to 0 when reaching the end
+    setCurrentSchemaIndex((currentSchemaIndex + 1) % exampleSchemas.length);
+  };
+
+  const handleForceUpdateSchema = (): void => {
+    const currentSchema: FlowDocumentJSON = exampleSchemas[currentSchemaIndex];
+
+    // Update the document with current schema
+    loadSchemaService.forceLoad(currentSchema);
+
+    // Move to next schema index, cycle back to 0 when reaching the end
+    setCurrentSchemaIndex((currentSchemaIndex + 1) % exampleSchemas.length);
+  };
+
+  return (
+    <div className="update-schema-button-container">
+      <button onClick={handleUpdateSchema} className="update-schema-button">
+        <span className="button-content">{`更新 ${currentSchemaIndex}/${exampleSchemas.length}`}</span>
+      </button>
+      <button onClick={handleForceUpdateSchema} className="update-schema-button">
+        <span className="button-content">{`强制更新 ${currentSchemaIndex}/${exampleSchemas.length}`}</span>
+      </button>
+    </div>
+  );
+};

+ 10 - 0
apps/demo-fixed-layout-animation/src/fields/content-field/index.less

@@ -0,0 +1,10 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+.form-render-content {
+  font-size: 14px;
+  line-height: 1.6;
+  color: #666666;
+}

+ 13 - 0
apps/demo-fixed-layout-animation/src/fields/content-field/index.tsx

@@ -0,0 +1,13 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { Field } from '@flowgram.ai/fixed-layout-editor';
+import './index.less';
+
+export const ContentField = () => (
+  <Field<string> name="content">
+    {({ field }) => <div className="form-render-content">{field.value}</div>}
+  </Field>
+);

+ 73 - 0
apps/demo-fixed-layout-animation/src/fields/thinking-text-field/index.less

@@ -0,0 +1,73 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+.thinking-title {
+  font-size: 14px;
+  font-weight: 500;
+  color: #8d8d8d;
+}
+
+.thinking-text {
+  display: flex;
+  align-items: flex-start;
+  flex-direction: column;
+  gap: 4px;
+  border-radius: 8px;
+  line-height: 1.5;
+  font-size: 14px;
+
+  .thinking-content {
+    flex: 1;
+    word-break: break-word;
+    color: #8d8d8d;
+  }
+
+  .cursor {
+    font-weight: bold;
+    animation: blink 1s infinite;
+  }
+}
+
+@keyframes pulse {
+
+  0%,
+  100% {
+    transform: scale(1);
+    opacity: 1;
+  }
+
+  50% {
+    transform: scale(1.1);
+    opacity: 0.8;
+  }
+}
+
+@keyframes bounce {
+
+  0%,
+  80%,
+  100% {
+    transform: scale(0);
+    opacity: 0.5;
+  }
+
+  40% {
+    transform: scale(1);
+    opacity: 1;
+  }
+}
+
+@keyframes blink {
+
+  0%,
+  50% {
+    opacity: 1;
+  }
+
+  51%,
+  100% {
+    opacity: 0;
+  }
+}

+ 59 - 0
apps/demo-fixed-layout-animation/src/fields/thinking-text-field/index.tsx

@@ -0,0 +1,59 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import React, { useState, useEffect } from 'react';
+
+import { Field } from '@flowgram.ai/fixed-layout-editor';
+import './index.less';
+
+interface ThinkingTextProps {
+  thinking?: string;
+}
+
+// ThinkingText component with typewriter effect
+const ThinkingText: React.FC<ThinkingTextProps> = ({ thinking }) => {
+  const [displayedText, setDisplayedText] = useState<string>('');
+  const [currentIndex, setCurrentIndex] = useState<number>(0);
+
+  // Reset animation when thinking text changes
+  useEffect(() => {
+    setDisplayedText('');
+    setCurrentIndex(0);
+  }, [thinking]);
+
+  // Typewriter effect for thinking text
+  useEffect(() => {
+    if (!thinking || currentIndex >= thinking.length) {
+      return;
+    }
+
+    const timer = setTimeout(() => {
+      setDisplayedText((prev: string) => prev + (thinking?.[currentIndex] || ''));
+      setCurrentIndex((prev: number) => prev + 1);
+    }, 50); // 50ms delay between each character
+
+    return () => clearTimeout(timer);
+  }, [thinking, currentIndex]);
+
+  if (!thinking) {
+    return null;
+  }
+
+  return (
+    <div className="thinking-text">
+      <div className="thinking-title">思考:</div>
+      <div>
+        <span className="thinking-content">{displayedText}</span>
+        <span className="cursor">
+          {currentIndex < (thinking?.length || 0) && <span className="cursor">|</span>}
+        </span>
+      </div>
+    </div>
+  );
+};
+
+export const ThinkingTextField = () => (
+  <Field<string> name="text">{({ field }) => <ThinkingText thinking={field.value} />}</Field>
+);

+ 15 - 0
apps/demo-fixed-layout-animation/src/fields/title-field/index.less

@@ -0,0 +1,15 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+.form-render-title {
+  font-size: 18px;
+  font-weight: bold;
+  margin-bottom: 12px;
+  color: #333333;
+  display: flex;
+  gap: 8px;
+  justify-content: flex-start;
+  align-items: center;
+}

+ 28 - 0
apps/demo-fixed-layout-animation/src/fields/title-field/index.tsx

@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { Field } from '@flowgram.ai/fixed-layout-editor';
+
+import { useNodeStatus } from '@/hooks/use-node-loading';
+import { LoadingDots } from '@/components/loading-dots';
+import './index.less';
+
+export const TitleField = () => {
+  const { loading } = useNodeStatus();
+  return (
+    <Field<string> name="title">
+      {({ field }) => (
+        <div className="form-render-title">
+          <span>{field.value}</span>
+          {loading && (
+            <span>
+              <LoadingDots />
+            </span>
+          )}
+        </div>
+      )}
+    </Field>
+  );
+};

+ 96 - 0
apps/demo-fixed-layout-animation/src/hooks/use-editor-props.tsx

@@ -0,0 +1,96 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import '@flowgram.ai/fixed-layout-editor/index.css';
+
+import { useMemo } from 'react';
+
+import { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';
+import { defaultFixedSemiMaterials } from '@flowgram.ai/fixed-semi-materials';
+import { FixedLayoutProps, FlowRendererKey } from '@flowgram.ai/fixed-layout-editor';
+
+import { WorkflowLoadSchemaService } from '@/services';
+import { nodeRegistries } from '@/nodes';
+import { ThinkingNode } from '@/components/thinking-node';
+import { NodeRender } from '@/components/node-render';
+import { FormRender } from '@/components/form-render';
+
+export function useEditorProps(): FixedLayoutProps {
+  return useMemo<FixedLayoutProps>(
+    () => ({
+      plugins: () => [
+        createMinimapPlugin({
+          disableLayer: true,
+          enableDisplayAllNodes: true,
+          canvasStyle: {
+            canvasWidth: 200,
+            canvasHeight: 100,
+            canvasPadding: 50,
+          },
+        }),
+      ],
+      nodeRegistries,
+      initialData: {
+        nodes: [],
+      },
+      materials: {
+        renderDefaultNode: NodeRender,
+        components: {
+          ...defaultFixedSemiMaterials,
+          [FlowRendererKey.DRAG_NODE]: () => <></>,
+          [FlowRendererKey.BRANCH_ADDER]: () => <></>,
+          [FlowRendererKey.ADDER]: () => <></>,
+        },
+        renderNodes: {
+          ThinkingNode,
+        },
+      },
+      onAllLayersRendered: (ctx) => {
+        setTimeout(() => {
+          ctx.playground.config.fitView(ctx.document.root.bounds.pad(30));
+        }, 10);
+      },
+      onBind: ({ bind }) => {
+        bind(WorkflowLoadSchemaService).toSelf().inSingletonScope();
+      },
+      /**
+       * Get the default node registry, which will be merged with the 'nodeRegistries'
+       * 提供默认的节点注册,这个会和 nodeRegistries 做合并
+       */
+      getNodeDefaultRegistry(type) {
+        return {
+          type,
+          meta: {
+            defaultExpanded: true,
+          },
+          formMeta: {
+            /**
+             * Render form
+             */
+            render: FormRender,
+          },
+        };
+      },
+      /**
+       * Redo/Undo enable
+       */
+      history: {
+        enable: true,
+        enableChangeNode: true, // Listen Node engine data change
+        onApply: (ctx) => {
+          if (ctx.document.disposed) return;
+          // Listen change to trigger auto save
+          // console.log('auto save: ', ctx.document.toJSON());
+        },
+      },
+      /**
+       * Node engine enable, you can configure formMeta in the FlowNodeRegistry
+       */ nodeEngine: {
+        enable: true,
+      },
+    }),
+    []
+  );
+}

+ 55 - 0
apps/demo-fixed-layout-animation/src/hooks/use-node-loading.tsx

@@ -0,0 +1,55 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { useEffect, useState } from 'react';
+
+import { FlowNodeFormData, FormModelV2, useNodeRender } from '@flowgram.ai/fixed-layout-editor';
+
+interface NodeStatus {
+  loading: boolean;
+  className: string;
+}
+
+const NodeStatusKey = 'status';
+
+export const useNodeStatus = () => {
+  const { node } = useNodeRender();
+  const formModel = node.getData(FlowNodeFormData).getFormModel<FormModelV2>();
+  const formStatus = formModel.getValueIn<NodeStatus>(NodeStatusKey);
+
+  const [loading, setLoading] = useState(formStatus?.loading ?? false);
+  const [className, setClassName] = useState(formStatus?.className ?? '');
+
+  // 初始化表单值
+  useEffect(() => {
+    const initSize = formModel.getValueIn<{ width: number; height: number }>(NodeStatusKey);
+    if (!initSize) {
+      formModel.setValueIn(NodeStatusKey, {
+        loading: false,
+      });
+    }
+  }, [formModel]);
+
+  // 同步表单外部值变化:初始化/undo/redo/协同
+  useEffect(() => {
+    const disposer = formModel.onFormValuesChange(({ name }) => {
+      if (name !== NodeStatusKey && name !== '') {
+        return;
+      }
+      const newStatus = formModel.getValueIn<NodeStatus>(NodeStatusKey);
+      if (!newStatus) {
+        return;
+      }
+      setLoading(newStatus.loading);
+      setClassName(newStatus.className);
+    });
+    return () => disposer.dispose();
+  }, [formModel]);
+
+  return {
+    loading,
+    className,
+  };
+};

+ 43 - 0
apps/demo-fixed-layout-animation/src/nodes/condition/index.ts

@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+import {
+  FlowNodeBaseType,
+  FlowNodeEntity,
+  FlowNodeJSON,
+  FlowNodeMeta,
+  FlowNodeRegistry,
+  FlowNodeSplitType,
+} from '@flowgram.ai/fixed-layout-editor';
+
+export const ConditionNodeRegistry: FlowNodeRegistry<FlowNodeMeta> = {
+  type: 'condition',
+  extend: FlowNodeSplitType.DYNAMIC_SPLIT,
+  onBlockChildCreate(
+    originParent: FlowNodeEntity,
+    blockData: FlowNodeJSON,
+    addedNodes: FlowNodeEntity[] = [] // 新创建的节点都要存在这里
+  ) {
+    const { document } = originParent;
+    const parent = document.getNode(`$inlineBlocks$${originParent.id}`);
+    const blockNode = document.addNode(
+      {
+        id: `$block$${blockData.id}`,
+        type: FlowNodeBaseType.BLOCK,
+        parent,
+      },
+      addedNodes
+    );
+    const createdNode = document.addNode(
+      {
+        ...blockData,
+        type: blockData.type || FlowNodeBaseType.BLOCK,
+        parent: blockNode,
+      },
+      addedNodes
+    );
+    addedNodes.push(blockNode, createdNode);
+    return createdNode;
+  },
+};

+ 21 - 0
apps/demo-fixed-layout-animation/src/nodes/custom/index.ts

@@ -0,0 +1,21 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { FlowNodeMeta, FlowNodeRegistry } from '@flowgram.ai/fixed-layout-editor';
+
+export const CustomNodeRegistry: FlowNodeRegistry<FlowNodeMeta> = {
+  type: 'custom',
+  meta: {},
+  // onAdd() {
+  //   return {
+  //     id: `custom_${nanoid(5)}`,
+  //     type: 'custom',
+  //     data: {
+  //       title: 'Custom',
+  //       content: 'this is custom content',
+  //     },
+  //   };
+  // },
+};

+ 21 - 0
apps/demo-fixed-layout-animation/src/nodes/index.ts

@@ -0,0 +1,21 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+/**
+ * Copyright (c) 202 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import type { FlowNodeMeta, FlowNodeRegistry } from '@flowgram.ai/fixed-layout-editor';
+
+import { ThinkingNodeRegistry } from './thinking';
+import { CustomNodeRegistry } from './custom';
+import { ConditionNodeRegistry } from './condition';
+
+export const nodeRegistries: FlowNodeRegistry<FlowNodeMeta>[] = [
+  ConditionNodeRegistry,
+  CustomNodeRegistry,
+  ThinkingNodeRegistry,
+];

+ 30 - 0
apps/demo-fixed-layout-animation/src/nodes/thinking/index.tsx

@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { FlowNodeMeta, FlowNodeRegistry } from '@flowgram.ai/fixed-layout-editor';
+
+import { ThinkingTextField } from '@/fields/thinking-text-field';
+import { LoadingDots } from '@/components/loading-dots';
+
+export const ThinkingNodeRegistry: FlowNodeRegistry<FlowNodeMeta> = {
+  type: 'thinking',
+  meta: {
+    renderKey: 'ThinkingNode',
+  },
+  formMeta: {
+    render: () => (
+      <>
+        <div
+          style={{
+            marginBottom: 16,
+          }}
+        >
+          <LoadingDots />
+        </div>
+        <ThinkingTextField />
+      </>
+    ),
+  },
+};

+ 6 - 0
apps/demo-fixed-layout-animation/src/services/index.ts

@@ -0,0 +1,6 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+export { WorkflowLoadSchemaService } from './load-schema-service';

+ 170 - 0
apps/demo-fixed-layout-animation/src/services/load-schema-service/index.ts

@@ -0,0 +1,170 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import {
+  delay,
+  EntityManager,
+  FlowDocument,
+  FlowDocumentJSON,
+  FlowNodeBaseType,
+  FlowNodeEntity,
+  FlowNodeFormData,
+  FlowOperationBaseService,
+  FormModelV2,
+  inject,
+  injectable,
+  Playground,
+} from '@flowgram.ai/fixed-layout-editor';
+
+import { WorkflowLoadSchemaUtils } from './utils';
+import { SchemaPatch, SchemaPatchData } from './type';
+
+@injectable()
+export class WorkflowLoadSchemaService {
+  @inject(FlowDocument) private document: FlowDocument;
+
+  @inject(EntityManager) private entityManager: EntityManager;
+
+  @inject(FlowOperationBaseService) private operationService: FlowOperationBaseService;
+
+  @inject(Playground) private playground: Playground;
+
+  private currentSchema: FlowDocumentJSON = {
+    nodes: [],
+  };
+
+  // constructor() {
+  //   (window as any).WorkflowLoadSchemaService = this;
+  // }
+
+  public async load(schema: FlowDocumentJSON): Promise<void> {
+    const schemaPatch: SchemaPatch = WorkflowLoadSchemaUtils.createSchemaPatch(
+      this.currentSchema,
+      schema
+    );
+    this.currentSchema = schema;
+    await this.applySchemaPatch(schemaPatch);
+    this.document.fromJSON(schema);
+  }
+
+  public forceLoad(schema: FlowDocumentJSON): void {
+    this.currentSchema = schema;
+    this.document.fromJSON(schema);
+  }
+
+  private async applySchemaPatch(schemaPatch: SchemaPatch): Promise<void> {
+    await this.applyRemovePatch(schemaPatch.remove);
+    await delay(300);
+    await this.applyCreatePatch(schemaPatch.create);
+    await this.playground.config.fitView(this.document.root.bounds.pad(30));
+  }
+
+  private async applyCreatePatch(createSchemaPatchData: SchemaPatchData[]): Promise<void> {
+    const skipNodeIDs: Set<string> = new Set();
+    for (const nodePatchData of createSchemaPatchData) {
+      // 跳过 block 节点
+      if (skipNodeIDs.has(nodePatchData.nodeID)) {
+        continue;
+      }
+      const parentNode = this.getNode(nodePatchData.parentID);
+      // 特殊处理 condition 节点
+      if (parentNode?.flowNodeType === 'condition') {
+        const blocksSchema = createSchemaPatchData
+          .filter((item) => item.parentID === parentNode.id)
+          .map((item) => {
+            skipNodeIDs.add(item.nodeID);
+            return item.schema;
+          });
+        const blocks = this.document.addInlineBlocks(parentNode, blocksSchema);
+        await Promise.all(blocks.map((block) => this.createNodeMotion(block)));
+        continue;
+      }
+      // 更新节点数据
+      const isExist = Boolean(this.getNode(nodePatchData.nodeID));
+      const node = this.createNode(nodePatchData);
+      if (!isExist) {
+        // 新增节点动画
+        await this.createNodeMotion(node);
+      }
+    }
+  }
+
+  private createNode(patchData: SchemaPatchData): FlowNodeEntity {
+    const parent = this.getNode(patchData.parentID) ?? this.document.root;
+    if (parent?.flowNodeType === 'condition') {
+      // 特殊处理 condition 节点
+      const blocks = this.document.addInlineBlocks(parent, [patchData.schema]);
+      return blocks.find((block) => block.flowNodeType === patchData.schema.type) ?? blocks[0];
+    } else if (patchData.fromNodeID) {
+      return this.operationService.addFromNode(patchData.fromNodeID, patchData.schema);
+    } else {
+      return this.document.addNode({
+        ...patchData.schema,
+        parent,
+      });
+    }
+  }
+
+  private getNode(id?: string): FlowNodeEntity | undefined {
+    if (!id) {
+      return undefined;
+    }
+    return this.document.getNode(id);
+  }
+
+  private async createNodeMotion(node: FlowNodeEntity): Promise<void> {
+    // 隐藏节点
+    this.setNodeStatus(node, { loading: true, className: 'node-render-before-render' });
+    this.document.fireRender();
+    await delay(20);
+    // 展示节点动画
+    this.setNodeStatus(node, { loading: true, className: 'node-render-rendered' });
+    await delay(180);
+    // 滚动到节点位置
+    this.playground.scrollToView({
+      bounds: node.bounds,
+      scrollToCenter: true,
+    });
+    // 高亮节点边框
+    this.setNodeStatus(node, { loading: true, className: 'node-render-border-transition' });
+    await delay(800);
+    // 移除节点边框高亮
+    this.setNodeStatus(node, { loading: false, className: '' });
+  }
+
+  private async removeNodeMotion(node: FlowNodeEntity): Promise<void> {
+    // 隐藏节点
+    this.setNodeStatus(node, { loading: false, className: 'node-render-removed' });
+    this.document.fireRender();
+    await delay(300);
+  }
+
+  private async applyRemovePatch(removeNodeIDs: string[]): Promise<void> {
+    await Promise.all(
+      removeNodeIDs.map(async (nodeID) => {
+        const node = this.entityManager.getEntityById<FlowNodeEntity>(nodeID);
+        const parent = node?.parent;
+        if (node) {
+          await this.removeNodeMotion(node);
+          node.dispose();
+        }
+        if (parent?.flowNodeType === FlowNodeBaseType.BLOCK && !parent.blocks.length) {
+          parent.dispose();
+        }
+      })
+    );
+  }
+
+  private setNodeStatus(
+    node: FlowNodeEntity,
+    status: {
+      loading: boolean;
+      className: string;
+    }
+  ): void {
+    const formModel = node.getData(FlowNodeFormData)?.getFormModel<FormModelV2>();
+    formModel?.setValueIn('status', status);
+  }
+}

+ 19 - 0
apps/demo-fixed-layout-animation/src/services/load-schema-service/type.ts

@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { FlowNodeJSON } from '@flowgram.ai/fixed-layout-editor';
+
+export interface SchemaPatchData {
+  nodeID: string;
+  schema: FlowNodeJSON;
+  parentID?: string;
+  index?: number;
+  fromNodeID?: string;
+}
+
+export interface SchemaPatch {
+  create: SchemaPatchData[];
+  remove: string[];
+}

+ 92 - 0
apps/demo-fixed-layout-animation/src/services/load-schema-service/utils.ts

@@ -0,0 +1,92 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { FlowDocumentJSON, FlowNodeJSON } from '@flowgram.ai/fixed-layout-editor';
+
+import { SchemaPatch, SchemaPatchData } from './type';
+
+export namespace WorkflowLoadSchemaUtils {
+  const createSchemaPatchDataMap = (params: {
+    nodeSchemas: FlowNodeJSON[];
+    parentID?: string;
+    schemaPatchDataMap?: Map<string, SchemaPatchData>;
+  }): Map<string, SchemaPatchData> => {
+    const { nodeSchemas, parentID, schemaPatchDataMap = new Map() } = params;
+    nodeSchemas.forEach((nodeSchema: FlowNodeJSON, index: number) => {
+      const prevNodeSchema = nodeSchemas[index - 1];
+      const processedSchema: FlowNodeJSON = {
+        ...nodeSchema,
+        blocks: [],
+      };
+      const schemaPatchData: SchemaPatchData = {
+        nodeID: nodeSchema.id,
+        schema: processedSchema,
+        parentID,
+        index,
+        fromNodeID: prevNodeSchema?.id,
+      };
+      schemaPatchDataMap.set(nodeSchema.id, schemaPatchData);
+      if (nodeSchema.blocks) {
+        createSchemaPatchDataMap({
+          nodeSchemas: nodeSchema.blocks,
+          parentID: nodeSchema.id,
+          schemaPatchDataMap,
+        });
+      }
+    });
+    return schemaPatchDataMap;
+  };
+
+  export const createSchemaPatch = (
+    prevSchema: FlowDocumentJSON,
+    schema: FlowDocumentJSON
+  ): SchemaPatch => {
+    const prevSchemaPatchDataMap = createSchemaPatchDataMap({
+      nodeSchemas: prevSchema.nodes,
+    });
+    const currentSchemaPatchDataMap = createSchemaPatchDataMap({
+      nodeSchemas: schema.nodes,
+    });
+    const prevNodeIDs: string[] = Array.from(prevSchemaPatchDataMap.keys());
+    const currentNodeIDs: string[] = Array.from(currentSchemaPatchDataMap.keys());
+
+    const createNodeIDs: string[] = currentNodeIDs.filter((id) => {
+      if (!prevSchemaPatchDataMap.has(id)) {
+        return true;
+      }
+      const prevSchemaPatchData = prevSchemaPatchDataMap.get(id)!;
+      const currentSchemaPatchData = currentSchemaPatchDataMap.get(id)!;
+      return (
+        prevSchemaPatchData.parentID !== currentSchemaPatchData.parentID ||
+        prevSchemaPatchData.fromNodeID !== currentSchemaPatchData.fromNodeID
+      );
+    });
+
+    const removeNodeIDs: string[] = prevNodeIDs.filter((id) => {
+      if (!currentSchemaPatchDataMap.has(id)) {
+        return true;
+      }
+      const prevSchemaPatchData = prevSchemaPatchDataMap.get(id)!;
+      const currentSchemaPatchData = currentSchemaPatchDataMap.get(id)!;
+      return (
+        prevSchemaPatchData.parentID !== currentSchemaPatchData.parentID ||
+        prevSchemaPatchData.fromNodeID !== currentSchemaPatchData.fromNodeID
+      );
+    });
+
+    const createSchemaPatches: SchemaPatchData[] = createNodeIDs
+      .map((id) => currentSchemaPatchDataMap.get(id)!)
+      .filter(Boolean);
+
+    const schemaPatch: SchemaPatch = {
+      create: createSchemaPatches,
+      remove: removeNodeIDs,
+    };
+
+    console.log('@debug schemaPatch', schemaPatch);
+
+    return schemaPatch;
+  };
+}

+ 38 - 0
apps/demo-fixed-layout-animation/tsconfig.json

@@ -0,0 +1,38 @@
+{
+  "extends": "@flowgram.ai/ts-config/tsconfig.flow.path.json",
+  "compilerOptions": {
+    "rootDir": "./src",
+    "outDir": "./dist",
+    "experimentalDecorators": true,
+    "target": "es2020",
+    "module": "esnext",
+    "strictPropertyInitialization": false,
+    "strict": true,
+    "esModuleInterop": true,
+    "moduleResolution": "node",
+    "skipLibCheck": true,
+    "noUnusedLocals": true,
+    "noImplicitAny": true,
+    "allowJs": true,
+    "resolveJsonModule": true,
+    "types": [
+      "node"
+    ],
+    "jsx": "react-jsx",
+    "lib": [
+      "es6",
+      "dom",
+      "es2020",
+      "es2019.Array"
+    ],
+    "baseUrl": ".",
+    "paths": {
+      "@/*": [
+        "src/*"
+      ],
+    }
+  },
+  "include": [
+    "./src"
+  ],
+}

+ 12 - 3
apps/demo-fixed-layout/tsconfig.json

@@ -15,9 +15,18 @@
     "noImplicitAny": true,
     "allowJs": true,
     "resolveJsonModule": true,
-    "types": ["node"],
+    "types": [
+      "node"
+    ],
     "jsx": "react-jsx",
-    "lib": ["es6", "dom", "es2020", "es2019.Array"]
+    "lib": [
+      "es6",
+      "dom",
+      "es2020",
+      "es2019.Array"
+    ]
   },
-  "include": ["./src"],
+  "include": [
+    "./src",
+  ],
 }

+ 8 - 0
common/config/rush/command-line.json

@@ -304,6 +304,14 @@
 			"safeForSimultaneousRushProcesses": true,
 			"shellCommand": "concurrently --kill-others --raw --prefix \"{name}\" --names [watch],[demo] -c white,blue \"rush build:watch --to-except @flowgram.ai/demo-fixed-layout\" \"cd apps/demo-fixed-layout && rushx ts-check && rushx dev\""
 		},
+		{
+			"name": "dev:demo-fixed-layout-animation",
+			"commandKind": "global",
+			"summary": "⭐️️ run dev in app/demo-fixed-layout-animation",
+			"autoinstallerName": "rush-commands",
+			"safeForSimultaneousRushProcesses": true,
+			"shellCommand": "concurrently --kill-others --raw --prefix \"{name}\" --names [watch],[demo] -c white,blue \"rush build:watch --to-except @flowgram.ai/demo-fixed-layout-animation\" \"cd apps/demo-fixed-layout-animation && rushx ts-check && rushx dev\""
+		},
 		{
 			"name": "dev:demo-fixed-layout-simple",
 			"commandKind": "global",

+ 79 - 0
common/config/rush/pnpm-lock.yaml

@@ -190,6 +190,85 @@ importers:
         specifier: ^5.8.3
         version: 5.9.2
 
+  ../../apps/demo-fixed-layout-animation:
+    dependencies:
+      '@flowgram.ai/fixed-layout-editor':
+        specifier: workspace:*
+        version: link:../../packages/client/fixed-layout-editor
+      '@flowgram.ai/fixed-semi-materials':
+        specifier: workspace:*
+        version: link:../../packages/materials/fixed-semi-materials
+      '@flowgram.ai/minimap-plugin':
+        specifier: workspace:*
+        version: link:../../packages/plugins/minimap-plugin
+      classnames:
+        specifier: ^2.5.1
+        version: 2.5.1
+      lodash-es:
+        specifier: ^4.17.21
+        version: 4.17.21
+      nanoid:
+        specifier: ^5.0.9
+        version: 5.1.5
+      react:
+        specifier: ^18
+        version: 18.3.1
+      react-dom:
+        specifier: ^18
+        version: 18.3.1(react@18.3.1)
+      styled-components:
+        specifier: ^5
+        version: 5.3.11(@babel/core@7.28.4)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)
+    devDependencies:
+      '@flowgram.ai/eslint-config':
+        specifier: workspace:*
+        version: link:../../config/eslint-config
+      '@flowgram.ai/ts-config':
+        specifier: workspace:*
+        version: link:../../config/ts-config
+      '@rsbuild/core':
+        specifier: ^1.2.16
+        version: 1.5.6
+      '@rsbuild/plugin-less':
+        specifier: ^1.1.1
+        version: 1.5.0(@rsbuild/core@1.5.6)
+      '@rsbuild/plugin-react':
+        specifier: ^1.1.1
+        version: 1.4.0(@rsbuild/core@1.5.6)
+      '@types/lodash-es':
+        specifier: ^4.17.12
+        version: 4.17.12
+      '@types/node':
+        specifier: ^18
+        version: 18.19.124
+      '@types/react':
+        specifier: ^18
+        version: 18.3.24
+      '@types/react-dom':
+        specifier: ^18
+        version: 18.3.7(@types/react@18.3.24)
+      '@types/styled-components':
+        specifier: ^5
+        version: 5.1.34
+      '@typescript-eslint/parser':
+        specifier: ^6.10.0
+        version: 6.21.0(eslint@8.57.1)(typescript@5.9.2)
+      cross-env:
+        specifier: ~7.0.3
+        version: 7.0.3
+      eslint:
+        specifier: ^8.54.0
+        version: 8.57.1
+      less:
+        specifier: ^4.1.2
+        version: 4.4.1
+      less-loader:
+        specifier: ^6
+        version: 6.2.0(webpack@5.101.3)
+      typescript:
+        specifier: ^5.8.3
+        version: 5.9.2
+
   ../../apps/demo-fixed-layout-simple:
     dependencies:
       '@douyinfe/semi-icons':

+ 15 - 5
packages/canvas-engine/fixed-layout-core/src/activities/inline-blocks.ts

@@ -27,11 +27,21 @@ export const InlineBlocksRegistry: FlowNodeRegistry = {
     hidden: true,
     spacing: (node) => getDefaultSpacing(node.entity, ConstantKeys.NODE_SPACING),
     isInlineBlocks: true,
-    inlineSpacingPre: (node) =>
-      getDefaultSpacing(node.entity, ConstantKeys.INLINE_BLOCKS_PADDING_TOP) ||
-      DEFAULT_SPACING.INLINE_BLOCKS_PADDING_TOP,
-    inlineSpacingAfter: (node) =>
-      getDefaultSpacing(node.entity, ConstantKeys.INLINE_BLOCKS_PADDING_BOTTOM),
+    inlineSpacingPre: (transform) => {
+      if (transform.entity.blocks.length === 0) {
+        return 0;
+      }
+      return (
+        getDefaultSpacing(transform.entity, ConstantKeys.INLINE_BLOCKS_PADDING_TOP) ||
+        DEFAULT_SPACING.INLINE_BLOCKS_PADDING_TOP
+      );
+    },
+    inlineSpacingAfter: (transform) => {
+      if (transform.entity.blocks.length === 0) {
+        return 0;
+      }
+      return getDefaultSpacing(transform.entity, ConstantKeys.INLINE_BLOCKS_PADDING_BOTTOM);
+    },
   },
   /**
    * 控制子分支的间距

+ 10 - 0
rush.json

@@ -472,6 +472,16 @@
             ],
             "versionPolicyName": "appPolicy"
         },
+        {
+            "packageName": "@flowgram.ai/demo-fixed-layout-animation",
+            "projectFolder": "apps/demo-fixed-layout-animation",
+            "tags": [
+                "level-1",
+                "team-flow",
+                "demo"
+            ],
+            "versionPolicyName": "appPolicy"
+        },
         {
             "packageName": "@flowgram.ai/utils",
             "projectFolder": "packages/common/utils",